Better drawing and added more icons
This commit is contained in:
@@ -1,13 +1,15 @@
|
||||
from copy import deepcopy
|
||||
|
||||
from PySide6.QtCore import QPointF, QRectF, Qt
|
||||
from PySide6.QtGui import QColor, QPainter, QPen, QPolygonF
|
||||
from PySide6.QtCore import QPointF, QRectF, QSizeF, Qt
|
||||
from PySide6.QtGui import QColor, QPainter, QPainterPath, QPen, QPolygonF
|
||||
from PySide6.QtWidgets import (
|
||||
QColorDialog,
|
||||
QButtonGroup,
|
||||
QDialog,
|
||||
QGraphicsEllipseItem,
|
||||
QGraphicsItem,
|
||||
QGraphicsObject,
|
||||
QGraphicsPathItem,
|
||||
QGraphicsScene,
|
||||
QGraphicsSceneContextMenuEvent,
|
||||
QGraphicsSceneMouseEvent,
|
||||
@@ -19,7 +21,7 @@ from bedit.core.model import Component, Icon, Port
|
||||
from bedit.gui.generated.ui_icon_editor_dialog import Ui_IconEditorDialog
|
||||
from bedit.gui.generated.ui_shape_options_dialog import Ui_ShapeOptionsDialog
|
||||
from bedit.gui.graphics.icon_canvas import icon_grid_size
|
||||
from bedit.gui.graphics.icon_renderer import _pen
|
||||
from bedit.gui.graphics.icon_renderer import shape_pen
|
||||
|
||||
|
||||
def _icon_grid_size() -> int:
|
||||
@@ -49,6 +51,21 @@ class ResizeHandle(QGraphicsEllipseItem):
|
||||
if self.owner.scene()
|
||||
else QPointF(128, 128)
|
||||
)
|
||||
if self.owner.element.get("type") == "line":
|
||||
minimum = (
|
||||
self.owner.scene().sceneRect().topLeft() - self.owner.pos()
|
||||
if self.owner.scene()
|
||||
else QPointF(-128, -128)
|
||||
)
|
||||
value = QPointF(
|
||||
min(maximum.x(), max(minimum.x(), _snap(value.x()))),
|
||||
min(maximum.y(), max(minimum.y(), _snap(value.y()))),
|
||||
)
|
||||
self.owner.prepareGeometryChange()
|
||||
self.owner.element["width"] = value.x()
|
||||
self.owner.element["height"] = value.y()
|
||||
self.owner.update()
|
||||
return super().itemChange(change, value)
|
||||
value = QPointF(
|
||||
min(maximum.x(), max(_icon_grid_size(), _snap(value.x()))),
|
||||
min(maximum.y(), max(_icon_grid_size(), _snap(value.y()))),
|
||||
@@ -82,9 +99,17 @@ class ShapeOptionsDialog(QDialog):
|
||||
self.ui.cornerRadiusSpin.setValue(float(self.element.get("cornerRadius", 0)))
|
||||
self.ui.textEdit.setText(str(self.element.get("text", "Text")))
|
||||
self.ui.fontSizeSpin.setValue(float(self.element.get("fontSize", 12)))
|
||||
self._show_row(self.ui.fillTypeLabel, self.ui.fillTypeCombo, self.element.get("type") != "line")
|
||||
self._show_row(self.ui.fillColorLabel, self.ui.fillColorButton, self.element.get("type") != "line")
|
||||
self._show_row(self.ui.cornerRadiusLabel, self.ui.cornerRadiusSpin, self.element.get("type") == "rectangle")
|
||||
self._show_row(
|
||||
self.ui.fillTypeLabel, self.ui.fillTypeCombo, self.element.get("type") != "line"
|
||||
)
|
||||
self._show_row(
|
||||
self.ui.fillColorLabel, self.ui.fillColorButton, self.element.get("type") != "line"
|
||||
)
|
||||
self._show_row(
|
||||
self.ui.cornerRadiusLabel,
|
||||
self.ui.cornerRadiusSpin,
|
||||
self.element.get("type") == "rectangle",
|
||||
)
|
||||
is_text = self.element.get("type") == "text"
|
||||
self._show_row(self.ui.textLabel, self.ui.textEdit, is_text)
|
||||
self._show_row(self.ui.fontSizeLabel, self.ui.fontSizeSpin, is_text)
|
||||
@@ -99,7 +124,9 @@ class ShapeOptionsDialog(QDialog):
|
||||
|
||||
def _choose_color(self, current: str) -> str:
|
||||
color = QColorDialog.getColor(
|
||||
QColor(current), self, "Choose colour",
|
||||
QColor(current),
|
||||
self,
|
||||
"Choose colour",
|
||||
QColorDialog.ColorDialogOption.ShowAlphaChannel,
|
||||
)
|
||||
if not color.isValid():
|
||||
@@ -129,7 +156,9 @@ class ShapeOptionsDialog(QDialog):
|
||||
self.element["width"] = self.ui.widthSpin.value()
|
||||
self.element["height"] = self.ui.heightSpin.value()
|
||||
if self.element.get("type") != "line":
|
||||
self.element["fill"] = self.fill_color if self.ui.fillTypeCombo.currentText() == "solid" else "none"
|
||||
self.element["fill"] = (
|
||||
self.fill_color if self.ui.fillTypeCombo.currentText() == "solid" else "none"
|
||||
)
|
||||
if self.element.get("type") == "rectangle":
|
||||
self.element["cornerRadius"] = self.ui.cornerRadiusSpin.value()
|
||||
if self.element.get("type") == "text":
|
||||
@@ -144,21 +173,35 @@ class ShapeItem(QGraphicsObject):
|
||||
super().__init__()
|
||||
self.element = element
|
||||
self.setPos(float(element.get("x", 0)), float(element.get("y", 0)))
|
||||
self.setFlags(QGraphicsItem.GraphicsItemFlag.ItemIsMovable | QGraphicsItem.GraphicsItemFlag.ItemIsSelectable | QGraphicsItem.GraphicsItemFlag.ItemSendsGeometryChanges)
|
||||
self.setFlags(
|
||||
QGraphicsItem.GraphicsItemFlag.ItemIsMovable
|
||||
| QGraphicsItem.GraphicsItemFlag.ItemIsSelectable
|
||||
| QGraphicsItem.GraphicsItemFlag.ItemSendsGeometryChanges
|
||||
)
|
||||
self.resize_handle = ResizeHandle(self)
|
||||
self.resize_handle.setPos(float(element.get("width", 20)), float(element.get("height", 20)))
|
||||
self.resize_handle.hide()
|
||||
|
||||
def boundingRect(self) -> QRectF: # noqa: N802
|
||||
margin = max(3.0, float(self.element.get("lineWidth", 1.5)))
|
||||
return QRectF(0, 0, float(self.element.get("width", 20)), float(self.element.get("height", 20))).adjusted(-margin, -margin, margin, margin)
|
||||
return (
|
||||
QRectF(
|
||||
0, 0, float(self.element.get("width", 20)), float(self.element.get("height", 20))
|
||||
)
|
||||
.normalized()
|
||||
.adjusted(-margin, -margin, margin, margin)
|
||||
)
|
||||
|
||||
def paint(self, painter: QPainter, option, widget=None) -> None:
|
||||
del option, widget
|
||||
rect = QRectF(0, 0, float(self.element.get("width", 20)), float(self.element.get("height", 20)))
|
||||
painter.setPen(_pen(self.element))
|
||||
rect = QRectF(
|
||||
0, 0, float(self.element.get("width", 20)), float(self.element.get("height", 20))
|
||||
)
|
||||
painter.setPen(shape_pen(self.element))
|
||||
fill = self.element.get("fill", "none")
|
||||
painter.setBrush(Qt.BrushStyle.NoBrush if fill in {"none", "transparent", ""} else QColor(fill))
|
||||
painter.setBrush(
|
||||
Qt.BrushStyle.NoBrush if fill in {"none", "transparent", ""} else QColor(fill)
|
||||
)
|
||||
kind = self.element.get("type")
|
||||
if kind == "rectangle":
|
||||
radius = float(self.element.get("cornerRadius", 0))
|
||||
@@ -168,13 +211,17 @@ class ShapeItem(QGraphicsObject):
|
||||
elif kind == "line":
|
||||
painter.drawLine(rect.topLeft(), rect.bottomRight())
|
||||
elif kind == "triangle":
|
||||
painter.drawPolygon(QPolygonF([QPointF(rect.center().x(), 0), rect.bottomRight(), rect.bottomLeft()]))
|
||||
painter.drawPolygon(
|
||||
QPolygonF([QPointF(rect.center().x(), 0), rect.bottomRight(), rect.bottomLeft()])
|
||||
)
|
||||
elif kind == "text":
|
||||
painter.setPen(QColor(self.element.get("color", "#202020")))
|
||||
font = painter.font()
|
||||
font.setPointSizeF(float(self.element.get("fontSize", 12)))
|
||||
painter.setFont(font)
|
||||
painter.drawText(rect, Qt.AlignmentFlag.AlignCenter, str(self.element.get("text", "Text")))
|
||||
painter.drawText(
|
||||
rect, Qt.AlignmentFlag.AlignCenter, str(self.element.get("text", "Text"))
|
||||
)
|
||||
if self.isSelected():
|
||||
painter.setBrush(Qt.BrushStyle.NoBrush)
|
||||
painter.setPen(QPen(QColor("#2563eb"), 1, Qt.PenStyle.DashLine))
|
||||
@@ -182,23 +229,7 @@ class ShapeItem(QGraphicsObject):
|
||||
|
||||
def itemChange(self, change, value): # noqa: N802
|
||||
if change == QGraphicsItem.GraphicsItemChange.ItemPositionChange:
|
||||
bounds = self.scene().sceneRect() if self.scene() else QRectF(0, 0, 128, 128)
|
||||
value = QPointF(
|
||||
max(
|
||||
bounds.left(),
|
||||
min(
|
||||
bounds.right() - float(self.element.get("width", 20)),
|
||||
_snap(value.x()),
|
||||
),
|
||||
),
|
||||
max(
|
||||
bounds.top(),
|
||||
min(
|
||||
bounds.bottom() - float(self.element.get("height", 20)),
|
||||
_snap(value.y()),
|
||||
),
|
||||
),
|
||||
)
|
||||
value = self._bounded_position(value)
|
||||
elif change == QGraphicsItem.GraphicsItemChange.ItemPositionHasChanged:
|
||||
self.element["x"], self.element["y"] = value.x(), value.y()
|
||||
elif change == QGraphicsItem.GraphicsItemChange.ItemSelectedHasChanged:
|
||||
@@ -207,10 +238,17 @@ class ShapeItem(QGraphicsObject):
|
||||
|
||||
def mouseReleaseEvent(self, event: QGraphicsSceneMouseEvent) -> None: # noqa: N802
|
||||
super().mouseReleaseEvent(event)
|
||||
self.setPos(self._bounded_position(self.pos()))
|
||||
|
||||
def _bounded_position(self, position: QPointF) -> QPointF:
|
||||
bounds = self.scene().sceneRect() if self.scene() else QRectF(0, 0, 128, 128)
|
||||
self.setPos(
|
||||
max(bounds.left(), min(bounds.right() - float(self.element.get("width", 20)), _snap(self.pos().x()))),
|
||||
max(bounds.top(), min(bounds.bottom() - float(self.element.get("height", 20)), _snap(self.pos().y()))),
|
||||
width = float(self.element.get("width", 20))
|
||||
height = float(self.element.get("height", 20))
|
||||
minimum_x, maximum_x = min(0.0, width), max(0.0, width)
|
||||
minimum_y, maximum_y = min(0.0, height), max(0.0, height)
|
||||
return QPointF(
|
||||
max(bounds.left() - minimum_x, min(bounds.right() - maximum_x, _snap(position.x()))),
|
||||
max(bounds.top() - minimum_y, min(bounds.bottom() - maximum_y, _snap(position.y()))),
|
||||
)
|
||||
|
||||
def contextMenuEvent(self, event: QGraphicsSceneContextMenuEvent) -> None: # noqa: N802
|
||||
@@ -219,11 +257,19 @@ class ShapeItem(QGraphicsObject):
|
||||
delete = menu.addAction("Delete Shape")
|
||||
chosen = menu.exec(event.screenPos())
|
||||
if chosen is options:
|
||||
dialog = ShapeOptionsDialog(self.element)
|
||||
options_element = deepcopy(self.element)
|
||||
width_sign = -1 if float(options_element.get("width", 20)) < 0 else 1
|
||||
height_sign = -1 if float(options_element.get("height", 20)) < 0 else 1
|
||||
options_element["width"] = abs(float(options_element.get("width", 20)))
|
||||
options_element["height"] = abs(float(options_element.get("height", 20)))
|
||||
dialog = ShapeOptionsDialog(options_element)
|
||||
if dialog.exec() == dialog.DialogCode.Accepted:
|
||||
self.prepareGeometryChange()
|
||||
self.element.clear()
|
||||
self.element.update(dialog.element)
|
||||
if self.element.get("type") == "line":
|
||||
self.element["width"] *= width_sign
|
||||
self.element["height"] *= height_sign
|
||||
self.resize_handle.setPos(
|
||||
float(self.element.get("width", 20)),
|
||||
float(self.element.get("height", 20)),
|
||||
@@ -244,7 +290,11 @@ class PortHandle(QGraphicsEllipseItem):
|
||||
self.setPen(QPen(QColor("#ffffff"), 1.5))
|
||||
self.setToolTip(f"{direction.title()}: {port.name} (drag to position)")
|
||||
self.setZValue(10)
|
||||
self.setFlags(QGraphicsItem.GraphicsItemFlag.ItemIsMovable | QGraphicsItem.GraphicsItemFlag.ItemIsSelectable | QGraphicsItem.GraphicsItemFlag.ItemSendsGeometryChanges)
|
||||
self.setFlags(
|
||||
QGraphicsItem.GraphicsItemFlag.ItemIsMovable
|
||||
| QGraphicsItem.GraphicsItemFlag.ItemIsSelectable
|
||||
| QGraphicsItem.GraphicsItemFlag.ItemSendsGeometryChanges
|
||||
)
|
||||
|
||||
def itemChange(self, change, value): # noqa: N802
|
||||
if change == QGraphicsItem.GraphicsItemChange.ItemPositionChange:
|
||||
@@ -266,6 +316,81 @@ class PortHandle(QGraphicsEllipseItem):
|
||||
)
|
||||
|
||||
|
||||
class IconDrawingScene(QGraphicsScene):
|
||||
def __init__(self, editor: "IconEditorDialog", rect: QRectF) -> None:
|
||||
super().__init__(rect, editor)
|
||||
self.editor = editor
|
||||
self.mode = "pointer"
|
||||
self.start: QPointF | None = None
|
||||
self.preview: QGraphicsPathItem | None = None
|
||||
|
||||
def set_mode(self, mode: str) -> None:
|
||||
self.mode = mode
|
||||
self._clear_preview()
|
||||
|
||||
def mousePressEvent(self, event: QGraphicsSceneMouseEvent) -> None: # noqa: N802
|
||||
if self.mode == "pointer":
|
||||
super().mousePressEvent(event)
|
||||
return
|
||||
point = self._bounded(event.scenePos())
|
||||
if self.mode == "text":
|
||||
text, accepted = QInputDialog.getText(self.editor, "Add Text", "Text:", text="Text")
|
||||
if accepted:
|
||||
self.editor.create_shape("text", point, point + QPointF(55, 35), text)
|
||||
event.accept()
|
||||
return
|
||||
self.start = point
|
||||
self.preview = QGraphicsPathItem()
|
||||
self.preview.setPen(QPen(QColor("#64748b"), 1.5, Qt.PenStyle.DashLine))
|
||||
self.preview.setZValue(100)
|
||||
self.addItem(self.preview)
|
||||
event.accept()
|
||||
|
||||
def mouseMoveEvent(self, event: QGraphicsSceneMouseEvent) -> None: # noqa: N802
|
||||
if self.start is None or self.preview is None:
|
||||
super().mouseMoveEvent(event)
|
||||
return
|
||||
end = self._bounded(event.scenePos())
|
||||
path = QPainterPath()
|
||||
rect = QRectF(self.start, end).normalized()
|
||||
if self.mode == "line":
|
||||
path.moveTo(self.start)
|
||||
path.lineTo(end)
|
||||
elif self.mode in {"circle", "ellipse"}:
|
||||
if self.mode == "circle":
|
||||
side = min(rect.width(), rect.height())
|
||||
rect.setSize(QSizeF(side, side))
|
||||
path.addEllipse(rect)
|
||||
else:
|
||||
path.addRect(rect)
|
||||
self.preview.setPath(path)
|
||||
event.accept()
|
||||
|
||||
def mouseReleaseEvent(self, event: QGraphicsSceneMouseEvent) -> None: # noqa: N802
|
||||
if self.start is None:
|
||||
super().mouseReleaseEvent(event)
|
||||
return
|
||||
end = self._bounded(event.scenePos())
|
||||
start = self.start
|
||||
self._clear_preview()
|
||||
if start != end:
|
||||
self.editor.create_shape(self.mode, start, end)
|
||||
event.accept()
|
||||
|
||||
def _bounded(self, point: QPointF) -> QPointF:
|
||||
rect = self.sceneRect()
|
||||
return QPointF(
|
||||
min(rect.right(), max(rect.left(), _snap(point.x()))),
|
||||
min(rect.bottom(), max(rect.top(), _snap(point.y()))),
|
||||
)
|
||||
|
||||
def _clear_preview(self) -> None:
|
||||
if self.preview is not None:
|
||||
self.removeItem(self.preview)
|
||||
self.preview = None
|
||||
self.start = None
|
||||
|
||||
|
||||
class IconEditorDialog(QDialog):
|
||||
def __init__(self, component: Component, parent=None) -> None:
|
||||
super().__init__(parent)
|
||||
@@ -275,6 +400,10 @@ class IconEditorDialog(QDialog):
|
||||
self.icon = Icon.from_dict(component.icon.to_dict())
|
||||
self.inputs = deepcopy(component.inputs)
|
||||
self.outputs = deepcopy(component.outputs)
|
||||
self.tool_group = QButtonGroup(self)
|
||||
self.tool_group.setExclusive(True)
|
||||
self.tool_group.addButton(self.ui.pointerButton)
|
||||
self.ui.pointerButton.clicked.connect(lambda: self.set_draw_mode("pointer"))
|
||||
for button, kind in (
|
||||
(self.ui.addRectangleButton, "rectangle"),
|
||||
(self.ui.addCircleButton, "circle"),
|
||||
@@ -283,43 +412,70 @@ class IconEditorDialog(QDialog):
|
||||
(self.ui.addTriangleButton, "triangle"),
|
||||
(self.ui.addTextButton, "text"),
|
||||
):
|
||||
button.clicked.connect(
|
||||
lambda _checked=False, value=kind: self.add_shape(value)
|
||||
)
|
||||
self.tool_group.addButton(button)
|
||||
button.clicked.connect(lambda _checked=False, value=kind: self.set_draw_mode(value))
|
||||
self.ui.deleteSelectedButton.clicked.connect(self.delete_selected)
|
||||
self.scene = QGraphicsScene(0, 0, self.icon.width, self.icon.height, self)
|
||||
self.scene = IconDrawingScene(self, QRectF(0, 0, self.icon.width, self.icon.height))
|
||||
self.ui.iconView.setScene(self.scene)
|
||||
self.view = self.ui.iconView
|
||||
self.view.setRenderHint(QPainter.RenderHint.Antialiasing)
|
||||
self.ui.zoomInButton.clicked.connect(self.view.zoom_in)
|
||||
self.ui.zoomOutButton.clicked.connect(self.view.zoom_out)
|
||||
self.ui.centerButton.clicked.connect(self.view.center_icon)
|
||||
self.scene.addRect(self.scene.sceneRect(), QPen(QColor("#64748b"), 0)).setZValue(-100)
|
||||
for element in self.icon.elements:
|
||||
self.scene.addItem(ShapeItem(element))
|
||||
self._add_ports(self.inputs, "input", 0.0)
|
||||
self._add_ports(self.outputs, "output", self.icon.width)
|
||||
self.view.fitInView(self.scene.sceneRect().adjusted(-10, -10, 10, 10), Qt.AspectRatioMode.KeepAspectRatio)
|
||||
self.view.center_icon()
|
||||
|
||||
def _add_ports(self, ports: list[Port], direction: str, default_x: float) -> None:
|
||||
spacing = self.icon.height / (len(ports) + 1)
|
||||
for index, port in enumerate(ports, 1):
|
||||
saved = port.properties.get("iconPosition", {})
|
||||
position = QPointF(float(saved.get("x", default_x)), float(saved.get("y", spacing * index)))
|
||||
position = QPointF(
|
||||
float(saved.get("x", default_x)), float(saved.get("y", spacing * index))
|
||||
)
|
||||
self.scene.addItem(PortHandle(port, direction, position))
|
||||
|
||||
def add_shape(self, kind: str) -> None:
|
||||
count = len([item for item in self.scene.items() if isinstance(item, ShapeItem)])
|
||||
x, y = 15 + (count * 5) % 40, 15 + (count * 4) % 25
|
||||
element = {"type": kind, "x": x, "y": y, "width": 55, "height": 35, "fill": "#dbeafe", "stroke": "#303030", "lineWidth": 1.5, "lineStyle": "solid"}
|
||||
def set_draw_mode(self, mode: str) -> None:
|
||||
self.scene.set_mode(mode)
|
||||
self.view.setDragMode(
|
||||
self.view.DragMode.RubberBandDrag if mode == "pointer" else self.view.DragMode.NoDrag
|
||||
)
|
||||
|
||||
def create_shape(self, kind: str, start: QPointF, end: QPointF, text: str = "Text") -> None:
|
||||
left, right = sorted((start.x(), end.x()))
|
||||
top, bottom = sorted((start.y(), end.y()))
|
||||
element = {
|
||||
"type": kind,
|
||||
"x": left,
|
||||
"y": top,
|
||||
"width": max(_icon_grid_size(), right - left),
|
||||
"height": max(_icon_grid_size(), bottom - top),
|
||||
"fill": "#dbeafe",
|
||||
"stroke": "#303030",
|
||||
"lineWidth": 1.5,
|
||||
"lineStyle": "solid",
|
||||
}
|
||||
if kind == "circle":
|
||||
element["width"] = element["height"] = 35
|
||||
element["width"] = element["height"] = min(element["width"], element["height"])
|
||||
if kind == "line":
|
||||
element["x"], element["y"] = start.x(), start.y()
|
||||
element["width"], element["height"] = end.x() - start.x(), end.y() - start.y()
|
||||
element["fill"] = "none"
|
||||
if kind == "rectangle":
|
||||
element["cornerRadius"] = 0
|
||||
if kind == "text":
|
||||
text, accepted = QInputDialog.getText(self, "Add Text", "Text:", text="Text")
|
||||
if not accepted:
|
||||
return
|
||||
element.update({"text": text, "fontSize": 12, "color": "#202020", "fill": "none", "lineStyle": "none"})
|
||||
element.update(
|
||||
{
|
||||
"text": text,
|
||||
"fontSize": 12,
|
||||
"color": "#202020",
|
||||
"fill": "none",
|
||||
"lineStyle": "none",
|
||||
}
|
||||
)
|
||||
self.icon.elements.append(element)
|
||||
item = ShapeItem(element)
|
||||
self.scene.addItem(item)
|
||||
@@ -332,5 +488,7 @@ class IconEditorDialog(QDialog):
|
||||
item.element["_deleted"] = True
|
||||
|
||||
def accept(self) -> None:
|
||||
self.icon.elements = [element for element in self.icon.elements if not element.pop("_deleted", False)]
|
||||
self.icon.elements = [
|
||||
element for element in self.icon.elements if not element.pop("_deleted", False)
|
||||
]
|
||||
super().accept()
|
||||
|
||||
Reference in New Issue
Block a user