Line in icon tool
This commit is contained in:
@@ -24,6 +24,8 @@ class Shape:
|
||||
return Rectangle.from_data(data)
|
||||
if cls is Shape and data.get("type") == "text":
|
||||
return Text.from_data(data)
|
||||
if cls is Shape and data.get("type") == "line":
|
||||
return Line.from_data(data)
|
||||
pos = data.get("pos", [0, 0])
|
||||
return cls(layer=int(data.get("layer", 0)), type=data.get("type"), pos=(int(pos[0]), int(pos[1])))
|
||||
|
||||
@@ -39,6 +41,24 @@ class LineType(Enum):
|
||||
DASH_DOT = "dash_dot"
|
||||
|
||||
|
||||
@dataclass
|
||||
class Line(Shape):
|
||||
type: str = field(init=False, default="line")
|
||||
end: tuple[int, int] = (32, 0)
|
||||
line_type: LineType = LineType.SOLID
|
||||
line_thickness: float = 1.0
|
||||
line_color: str = "#000000ff"
|
||||
|
||||
@classmethod
|
||||
def from_data(cls, data: Mapping[str, Any]) -> Line:
|
||||
pos = data.get("pos", [0, 0])
|
||||
end = data.get("end", [32, 0])
|
||||
return cls(layer=int(data.get("layer", 0)), pos=(int(pos[0]), int(pos[1])), end=(int(end[0]), int(end[1])), line_type=LineType(data.get("line_type", "solid")), line_thickness=float(data.get("line_thickness", 1.0)), line_color=str(data.get("line_color", "#000000ff")))
|
||||
|
||||
def to_data(self) -> dict[str, Any]:
|
||||
return {**super().to_data(), "end": list(self.end), "line_type": self.line_type.value, "line_thickness": self.line_thickness, "line_color": self.line_color}
|
||||
|
||||
|
||||
@dataclass
|
||||
class Rectangle(Shape):
|
||||
type: str = field(init=False, default="rectangle")
|
||||
|
||||
@@ -81,6 +81,7 @@
|
||||
</attribute>
|
||||
<addaction name="actionAdd_Rectangle"/>
|
||||
<addaction name="actionAdd_Text"/>
|
||||
<addaction name="actionAdd_Line"/>
|
||||
</widget>
|
||||
<action name="actionUndo">
|
||||
<property name="icon">
|
||||
@@ -220,6 +221,21 @@
|
||||
<enum>QAction::MenuRole::NoRole</enum>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionAdd_Line">
|
||||
<property name="icon">
|
||||
<iconset resource="../../resources/resources.qrc">
|
||||
<normaloff>:/icons/icons/draw-path.png</normaloff>:/icons/icons/draw-path.png</iconset>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Add Line</string>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>Add a line</string>
|
||||
</property>
|
||||
<property name="menuRole">
|
||||
<enum>QAction::MenuRole::NoRole</enum>
|
||||
</property>
|
||||
</action>
|
||||
</widget>
|
||||
<resources>
|
||||
<include location="../../resources/resources.qrc"/>
|
||||
|
||||
@@ -11,7 +11,7 @@ from bedit_gui.models import Icon, Shape, ShapeID
|
||||
from bedit_gui.services import icon_files
|
||||
from bedit_gui.services.application_logging import get_logger
|
||||
from bedit_gui.ui.generated.ui_icon_editor_window import Ui_iconEditor
|
||||
from bedit_gui.views.icon_graphics_scene import IconGraphicsScene, RectangleCreationTool, TextCreationTool
|
||||
from bedit_gui.views.icon_graphics_scene import IconGraphicsScene, LineCreationTool, RectangleCreationTool, TextCreationTool
|
||||
from bedit_gui.views.shape_options_dialog import ShapeOptionsDialog
|
||||
|
||||
logger = get_logger(__name__)
|
||||
@@ -125,6 +125,8 @@ class IconEditorWindow(QMainWindow):
|
||||
self.ui.actionAdd_Rectangle.triggered.connect(self._start_rectangle_tool)
|
||||
self.ui.actionAdd_Text.setCheckable(True)
|
||||
self.ui.actionAdd_Text.triggered.connect(self._start_text_tool)
|
||||
self.ui.actionAdd_Line.setCheckable(True)
|
||||
self.ui.actionAdd_Line.triggered.connect(self._start_line_tool)
|
||||
self.cancel_tool_shortcut = QShortcut(QKeySequence(Qt.Key.Key_Escape), self)
|
||||
self.cancel_tool_shortcut.activated.connect(self.scene.cancel_creation_tool)
|
||||
self.delete_shortcut = QShortcut(QKeySequence(Qt.Key.Key_Delete), self)
|
||||
@@ -305,17 +307,29 @@ class IconEditorWindow(QMainWindow):
|
||||
def _start_rectangle_tool(self) -> None:
|
||||
layer = max((shape.layer for shape in self._icon.shapes.values()), default=-1) + 1
|
||||
self.ui.actionAdd_Text.setChecked(False)
|
||||
self.ui.actionAdd_Line.setChecked(False)
|
||||
self.scene.set_creation_tool(RectangleCreationTool(self.scene, layer))
|
||||
self.ui.actionAdd_Rectangle.setChecked(True)
|
||||
|
||||
def _start_text_tool(self) -> None:
|
||||
layer = max((shape.layer for shape in self._icon.shapes.values()), default=-1) + 1
|
||||
self.ui.actionAdd_Rectangle.setChecked(False)
|
||||
self.ui.actionAdd_Line.setChecked(False)
|
||||
self.scene.set_creation_tool(TextCreationTool(self.scene, layer))
|
||||
self.ui.actionAdd_Text.setChecked(True)
|
||||
|
||||
def _start_line_tool(self) -> None:
|
||||
layer = max((shape.layer for shape in self._icon.shapes.values()), default=-1) + 1
|
||||
self.ui.actionAdd_Rectangle.setChecked(False)
|
||||
self.ui.actionAdd_Text.setChecked(False)
|
||||
self.scene.set_creation_tool(LineCreationTool(self.scene, layer))
|
||||
self.ui.actionAdd_Line.setChecked(True)
|
||||
|
||||
def _tool_active_changed(self, active: bool) -> None:
|
||||
if not active:
|
||||
self.ui.actionAdd_Rectangle.setChecked(False)
|
||||
self.ui.actionAdd_Text.setChecked(False)
|
||||
self.ui.actionAdd_Line.setChecked(False)
|
||||
cursor = Qt.CursorShape.CrossCursor if active else Qt.CursorShape.ArrowCursor
|
||||
self.ui.graphicsView.viewport().setCursor(cursor)
|
||||
|
||||
|
||||
@@ -4,12 +4,12 @@ import math
|
||||
from copy import deepcopy
|
||||
from typing import Protocol
|
||||
|
||||
from PySide6.QtCore import QObject, QPointF, QRectF, Qt, Signal
|
||||
from PySide6.QtCore import QLineF, QObject, QPointF, QRectF, Qt, Signal
|
||||
from PySide6.QtGui import QBrush, QColor, QFont, QPainter, QPainterPath, QPen
|
||||
from PySide6.QtWidgets import QGraphicsItem, QGraphicsPathItem, QGraphicsRectItem, QGraphicsScene, QGraphicsSceneContextMenuEvent, QGraphicsSceneMouseEvent, QMenu, QStyleOptionGraphicsItem, QWidget
|
||||
from PySide6.QtWidgets import QGraphicsItem, QGraphicsLineItem, QGraphicsPathItem, QGraphicsRectItem, QGraphicsScene, QGraphicsSceneContextMenuEvent, QGraphicsSceneMouseEvent, QMenu, QStyleOptionGraphicsItem, QWidget
|
||||
|
||||
from bedit_core.models import Port, PortID, SignalDirection
|
||||
from bedit_gui.models import Icon, LineType, Rectangle, Shape, ShapeID, Text
|
||||
from bedit_gui.models import Icon, Line, LineType, Rectangle, Shape, ShapeID, Text
|
||||
|
||||
|
||||
class ShapeCreationTool(Protocol):
|
||||
@@ -67,6 +67,45 @@ class TextCreationTool(RectangleCreationTool):
|
||||
return Text(layer=self.layer, pos=(round(rect.x()), round(rect.y())), width=rect.width(), height=rect.height(), text="Text")
|
||||
|
||||
|
||||
class LineCreationTool:
|
||||
def __init__(self, scene: QGraphicsScene, layer: int) -> None:
|
||||
self.scene = scene
|
||||
self.layer = layer
|
||||
self.start: QPointF | None = None
|
||||
self.preview: QGraphicsLineItem | None = None
|
||||
|
||||
def begin(self, position: QPointF) -> None:
|
||||
position = self._bounded(position)
|
||||
self.start = position
|
||||
self.preview = self.scene.addLine(QLineF(position, position), QPen(Qt.PenStyle.DashLine))
|
||||
|
||||
def update(self, position: QPointF) -> None:
|
||||
if self.preview is not None and self.start is not None:
|
||||
self.preview.setLine(QLineF(self.start, self._bounded(position)))
|
||||
|
||||
def finish(self, position: QPointF) -> Shape | None:
|
||||
if self.start is None:
|
||||
return None
|
||||
start = self.start
|
||||
end = self._bounded(position)
|
||||
self.cancel()
|
||||
if start == end:
|
||||
return None
|
||||
return Line(layer=self.layer, pos=(round(start.x()), round(start.y())), end=(round(end.x()), round(end.y())))
|
||||
|
||||
def cancel(self) -> None:
|
||||
if self.preview is not None:
|
||||
self.scene.removeItem(self.preview)
|
||||
self.preview = None
|
||||
self.start = None
|
||||
|
||||
def _bounded(self, position: QPointF) -> QPointF:
|
||||
rect = self.scene.sceneRect()
|
||||
x = max(rect.left(), min(rect.right(), round(position.x())))
|
||||
y = max(rect.top(), min(rect.bottom(), round(position.y())))
|
||||
return QPointF(x, y)
|
||||
|
||||
|
||||
class ShapeGraphicsItem(QGraphicsPathItem):
|
||||
handle_size = 6.0
|
||||
|
||||
@@ -76,7 +115,7 @@ class ShapeGraphicsItem(QGraphicsPathItem):
|
||||
self.shape_model = deepcopy(shape)
|
||||
self.icon_scene = scene
|
||||
self._original_shape: Shape | None = None
|
||||
self._resizing = False
|
||||
self._resize_handle: str | None = None
|
||||
self.setFlags(QGraphicsItem.GraphicsItemFlag.ItemIsSelectable | QGraphicsItem.GraphicsItemFlag.ItemIsMovable | QGraphicsItem.GraphicsItemFlag.ItemSendsGeometryChanges)
|
||||
|
||||
def resize_handle_rect(self) -> QRectF:
|
||||
@@ -84,29 +123,32 @@ class ShapeGraphicsItem(QGraphicsPathItem):
|
||||
corner = self.path().boundingRect().bottomRight()
|
||||
return QRectF(corner.x() - size / 2, corner.y() - size / 2, size, size)
|
||||
|
||||
def resize_handles(self) -> dict[str, QRectF]:
|
||||
return {"size": self.resize_handle_rect()}
|
||||
|
||||
def boundingRect(self) -> QRectF:
|
||||
margin = self.handle_size / 2
|
||||
return super().boundingRect().adjusted(-margin, -margin, margin, margin)
|
||||
|
||||
def mousePressEvent(self, event: QGraphicsSceneMouseEvent) -> None:
|
||||
self._original_shape = self.current_shape()
|
||||
self._resizing = self.isSelected() and self.resize_handle_rect().contains(event.pos())
|
||||
if self._resizing:
|
||||
self._resize_handle = next((name for name, rect in self.resize_handles().items() if self.isSelected() and rect.contains(event.pos())), None)
|
||||
if self._resize_handle is not None:
|
||||
event.accept()
|
||||
return
|
||||
super().mousePressEvent(event)
|
||||
|
||||
def mouseMoveEvent(self, event: QGraphicsSceneMouseEvent) -> None:
|
||||
if self._resizing:
|
||||
self.resize_to(event.scenePos())
|
||||
if self._resize_handle is not None:
|
||||
self.resize_to(event.scenePos(), self._resize_handle)
|
||||
event.accept()
|
||||
return
|
||||
super().mouseMoveEvent(event)
|
||||
|
||||
def mouseReleaseEvent(self, event: QGraphicsSceneMouseEvent) -> None:
|
||||
if self._resizing:
|
||||
self.resize_to(event.scenePos())
|
||||
self._resizing = False
|
||||
if self._resize_handle is not None:
|
||||
self.resize_to(event.scenePos(), self._resize_handle)
|
||||
self._resize_handle = None
|
||||
event.accept()
|
||||
else:
|
||||
super().mouseReleaseEvent(event)
|
||||
@@ -130,9 +172,9 @@ class ShapeGraphicsItem(QGraphicsPathItem):
|
||||
position = value
|
||||
if isinstance(position, QPointF):
|
||||
bounds = self.icon_scene.sceneRect()
|
||||
size = self.path().boundingRect()
|
||||
x = max(bounds.left(), min(bounds.right() - size.width(), round(position.x())))
|
||||
y = max(bounds.top(), min(bounds.bottom() - size.height(), round(position.y())))
|
||||
shape_rect = self.path().boundingRect()
|
||||
x = max(bounds.left() - shape_rect.left(), min(bounds.right() - shape_rect.right(), round(position.x())))
|
||||
y = max(bounds.top() - shape_rect.top(), min(bounds.bottom() - shape_rect.bottom(), round(position.y())))
|
||||
return QPointF(x, y)
|
||||
return super().itemChange(change, value)
|
||||
|
||||
@@ -141,12 +183,13 @@ class ShapeGraphicsItem(QGraphicsPathItem):
|
||||
if self.isSelected():
|
||||
painter.setPen(QPen(QColor("#ffffff")))
|
||||
painter.setBrush(QBrush(QColor("#2675bf")))
|
||||
painter.drawRect(self.resize_handle_rect())
|
||||
for rect in self.resize_handles().values():
|
||||
painter.drawRect(rect)
|
||||
|
||||
def current_shape(self) -> Shape:
|
||||
raise NotImplementedError
|
||||
|
||||
def resize_to(self, position: QPointF) -> None:
|
||||
def resize_to(self, position: QPointF, handle: str) -> None:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
@@ -168,7 +211,7 @@ class RectangleGraphicsItem(ShapeGraphicsItem):
|
||||
shape.height = rect.height()
|
||||
return shape
|
||||
|
||||
def resize_to(self, position: QPointF) -> None:
|
||||
def resize_to(self, position: QPointF, _handle: str) -> None:
|
||||
bounds = self.icon_scene.sceneRect()
|
||||
width = max(1.0, min(bounds.right(), round(position.x())) - self.pos().x())
|
||||
height = max(1.0, min(bounds.bottom(), round(position.y())) - self.pos().y())
|
||||
@@ -199,7 +242,7 @@ class TextGraphicsItem(ShapeGraphicsItem):
|
||||
shape.height = rect.height()
|
||||
return shape
|
||||
|
||||
def resize_to(self, position: QPointF) -> None:
|
||||
def resize_to(self, position: QPointF, _handle: str) -> None:
|
||||
bounds = self.icon_scene.sceneRect()
|
||||
width = max(1.0, min(bounds.right(), round(position.x())) - self.pos().x())
|
||||
height = max(1.0, min(bounds.bottom(), round(position.y())) - self.pos().y())
|
||||
@@ -223,6 +266,45 @@ class TextGraphicsItem(ShapeGraphicsItem):
|
||||
self.setPath(path)
|
||||
|
||||
|
||||
class LineGraphicsItem(ShapeGraphicsItem):
|
||||
def __init__(self, shape_id: ShapeID, shape: Line, scene: IconGraphicsScene) -> None:
|
||||
super().__init__(shape_id, shape, scene)
|
||||
self.line = deepcopy(shape)
|
||||
self._set_points(QPointF(shape.pos[0], shape.pos[1]), QPointF(shape.end[0], shape.end[1]))
|
||||
self.setPen(scene._line_pen(shape.line_type, shape.line_thickness, shape.line_color))
|
||||
self.setZValue(shape.layer)
|
||||
|
||||
def resize_handles(self) -> dict[str, QRectF]:
|
||||
size = self.handle_size
|
||||
start = self.path().pointAtPercent(0)
|
||||
end = self.path().pointAtPercent(1)
|
||||
return {"start": QRectF(start.x() - size / 2, start.y() - size / 2, size, size), "end": QRectF(end.x() - size / 2, end.y() - size / 2, size, size)}
|
||||
|
||||
def current_shape(self) -> Line:
|
||||
shape = deepcopy(self.line)
|
||||
start = self.mapToScene(self.path().pointAtPercent(0))
|
||||
end = self.mapToScene(self.path().pointAtPercent(1))
|
||||
shape.pos = (round(start.x()), round(start.y()))
|
||||
shape.end = (round(end.x()), round(end.y()))
|
||||
return shape
|
||||
|
||||
def resize_to(self, position: QPointF, handle: str) -> None:
|
||||
bounds = self.icon_scene.sceneRect()
|
||||
position = QPointF(max(bounds.left(), min(bounds.right(), round(position.x()))), max(bounds.top(), min(bounds.bottom(), round(position.y()))))
|
||||
start = self.mapToScene(self.path().pointAtPercent(0))
|
||||
end = self.mapToScene(self.path().pointAtPercent(1))
|
||||
start = position if handle == "start" else start
|
||||
end = position if handle == "end" else end
|
||||
self.setPos(0, 0)
|
||||
self._set_points(start, end)
|
||||
|
||||
def _set_points(self, start: QPointF, end: QPointF) -> None:
|
||||
self.prepareGeometryChange()
|
||||
path = QPainterPath(start)
|
||||
path.lineTo(end)
|
||||
self.setPath(path)
|
||||
|
||||
|
||||
class PortGraphicsItem(QGraphicsRectItem):
|
||||
size = 16.0
|
||||
|
||||
@@ -358,13 +440,19 @@ class IconGraphicsScene(QGraphicsScene):
|
||||
self.addItem(RectangleGraphicsItem(shape_id, shape, self))
|
||||
elif isinstance(shape, Text):
|
||||
self.addItem(TextGraphicsItem(shape_id, shape, self))
|
||||
elif isinstance(shape, Line):
|
||||
self.addItem(LineGraphicsItem(shape_id, shape, self))
|
||||
|
||||
@staticmethod
|
||||
def _pen(shape: Rectangle) -> QPen:
|
||||
return IconGraphicsScene._line_pen(shape.line_type, shape.line_thickness, shape.line_color)
|
||||
|
||||
@staticmethod
|
||||
def _line_pen(line_type: LineType, thickness: float, color: str) -> QPen:
|
||||
styles = {LineType.SOLID: Qt.PenStyle.SolidLine, LineType.DASHED: Qt.PenStyle.DashLine, LineType.DOTTED: Qt.PenStyle.DotLine, LineType.DASH_DOT: Qt.PenStyle.DashDotLine}
|
||||
if shape.line_type is LineType.NONE:
|
||||
if line_type is LineType.NONE:
|
||||
return QPen(Qt.PenStyle.NoPen)
|
||||
return QPen(IconGraphicsScene._color(shape.line_color), shape.line_thickness, styles[shape.line_type])
|
||||
return QPen(IconGraphicsScene._color(color), thickness, styles[line_type])
|
||||
|
||||
@staticmethod
|
||||
def _color(value: str) -> QColor:
|
||||
|
||||
@@ -5,7 +5,7 @@ from copy import deepcopy
|
||||
from PySide6.QtCore import QRectF
|
||||
from PySide6.QtWidgets import QCheckBox, QComboBox, QDialog, QDialogButtonBox, QDoubleSpinBox, QFormLayout, QLabel, QLineEdit, QSpinBox, QVBoxLayout, QWidget
|
||||
|
||||
from bedit_gui.models import LineType, Rectangle, Shape, Text
|
||||
from bedit_gui.models import Line, LineType, Rectangle, Shape, Text
|
||||
from bedit_gui.views.color_button import ColorButton
|
||||
|
||||
|
||||
@@ -36,6 +36,8 @@ class ShapeOptionsDialog(QDialog):
|
||||
self._add_rectangle_fields(shape)
|
||||
elif isinstance(shape, Text):
|
||||
self._add_text_fields(shape)
|
||||
elif isinstance(shape, Line):
|
||||
self._add_line_fields(shape)
|
||||
|
||||
buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
|
||||
buttons.accepted.connect(self.accept)
|
||||
@@ -53,6 +55,8 @@ class ShapeOptionsDialog(QDialog):
|
||||
width = min(self.width.value(), self._scene_rect.right() - self.x.value())
|
||||
height = min(self.height.value(), self._scene_rect.bottom() - self.y.value())
|
||||
return Text(layer=self.layer.value(), pos=(self.x.value(), self.y.value()), width=width, height=height, color=self.color.color(), bold=self.bold.isChecked(), italic=self.italic.isChecked(), size=self.size.value(), text=self.text.text())
|
||||
if isinstance(self._shape, Line):
|
||||
return Line(layer=self.layer.value(), pos=(self.x.value(), self.y.value()), end=(self.end_x.value(), self.end_y.value()), line_type=self.line_type.currentData(), line_thickness=self.line_thickness.value(), line_color=self.line_color.color())
|
||||
shape = deepcopy(self._shape)
|
||||
shape.layer = self.layer.value()
|
||||
shape.pos = (self.x.value(), self.y.value())
|
||||
@@ -108,3 +112,26 @@ class ShapeOptionsDialog(QDialog):
|
||||
self.form.addRow("Italic", self.italic)
|
||||
self.form.addRow("Size", self.size)
|
||||
self.form.addRow("Text", self.text)
|
||||
|
||||
def _add_line_fields(self, shape: Line) -> None:
|
||||
self.x.setMaximum(round(self._scene_rect.right()))
|
||||
self.y.setMaximum(round(self._scene_rect.bottom()))
|
||||
self.end_x = QSpinBox()
|
||||
self.end_x.setRange(round(self._scene_rect.left()), round(self._scene_rect.right()))
|
||||
self.end_x.setValue(shape.end[0])
|
||||
self.end_y = QSpinBox()
|
||||
self.end_y.setRange(round(self._scene_rect.top()), round(self._scene_rect.bottom()))
|
||||
self.end_y.setValue(shape.end[1])
|
||||
self.line_type = QComboBox()
|
||||
for line_type in LineType:
|
||||
self.line_type.addItem(line_type.value.replace("_", " ").title(), line_type)
|
||||
self.line_type.setCurrentIndex(self.line_type.findData(shape.line_type))
|
||||
self.line_thickness = QDoubleSpinBox()
|
||||
self.line_thickness.setRange(0, 1000)
|
||||
self.line_thickness.setValue(shape.line_thickness)
|
||||
self.line_color = ColorButton(shape.line_color)
|
||||
self.form.addRow("End X", self.end_x)
|
||||
self.form.addRow("End Y", self.end_y)
|
||||
self.form.addRow("Line type", self.line_type)
|
||||
self.form.addRow("Line thickness", self.line_thickness)
|
||||
self.form.addRow("Line color", self.line_color)
|
||||
|
||||
Reference in New Issue
Block a user