Better drawing and added more icons
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
from PySide6.QtCore import QPointF
|
||||
from PySide6.QtGui import QUndoCommand
|
||||
|
||||
from bedit.core.model import Component, Connection, Port
|
||||
from bedit.core.model import Annotation, Component, Connection, Port
|
||||
|
||||
|
||||
class AddComponentCommand(QUndoCommand):
|
||||
@@ -76,6 +76,55 @@ class AddConnectionCommand(QUndoCommand):
|
||||
self.controller._remove_connection(self.owner_id, self.connection.id)
|
||||
|
||||
|
||||
class AddAnnotationCommand(QUndoCommand):
|
||||
def __init__(self, controller, owner_id: str, annotation: Annotation) -> None:
|
||||
super().__init__(f"Draw {annotation.kind}")
|
||||
self.controller, self.owner_id, self.annotation = controller, owner_id, annotation
|
||||
|
||||
def redo(self) -> None:
|
||||
self.controller._insert_annotation(self.owner_id, self.annotation)
|
||||
|
||||
def undo(self) -> None:
|
||||
self.controller._remove_annotation(self.owner_id, self.annotation.id)
|
||||
|
||||
|
||||
class EditGraphItemCommand(QUndoCommand):
|
||||
def __init__(
|
||||
self,
|
||||
controller,
|
||||
owner_id: str,
|
||||
item_kind: str,
|
||||
item_id: str,
|
||||
old: dict,
|
||||
new: dict,
|
||||
text: str,
|
||||
) -> None:
|
||||
super().__init__(text)
|
||||
self.controller, self.owner_id = controller, owner_id
|
||||
self.item_kind, self.item_id = item_kind, item_id
|
||||
self.old, self.new = old, new
|
||||
|
||||
def redo(self) -> None:
|
||||
self.controller._set_graph_item_data(self.owner_id, self.item_kind, self.item_id, self.new)
|
||||
|
||||
def undo(self) -> None:
|
||||
self.controller._set_graph_item_data(self.owner_id, self.item_kind, self.item_id, self.old)
|
||||
|
||||
|
||||
class DeleteAnnotationsCommand(QUndoCommand):
|
||||
def __init__(self, controller, owner_id: str, annotations: dict[str, Annotation]) -> None:
|
||||
super().__init__("Delete annotations")
|
||||
self.controller, self.owner_id, self.annotations = controller, owner_id, annotations
|
||||
|
||||
def redo(self) -> None:
|
||||
for annotation_id in self.annotations:
|
||||
self.controller._remove_annotation(self.owner_id, annotation_id)
|
||||
|
||||
def undo(self) -> None:
|
||||
for annotation in self.annotations.values():
|
||||
self.controller._insert_annotation(self.owner_id, annotation)
|
||||
|
||||
|
||||
class ReplaceComponentCommand(QUndoCommand):
|
||||
def __init__(self, controller, old: Component, new: Component) -> None:
|
||||
super().__init__("Apply JSON changes")
|
||||
|
||||
@@ -8,8 +8,11 @@ from PySide6.QtGui import QUndoStack
|
||||
from bedit.gui.controllers.commands import (
|
||||
AddComponentCommand,
|
||||
AddConnectionCommand,
|
||||
AddAnnotationCommand,
|
||||
AddInterfacePortCommand,
|
||||
DeleteSelectionCommand,
|
||||
DeleteAnnotationsCommand,
|
||||
EditGraphItemCommand,
|
||||
EditTextDefinitionCommand,
|
||||
EditComponentAppearanceCommand,
|
||||
MoveComponentCommand,
|
||||
@@ -21,6 +24,7 @@ from bedit.gui.controllers.commands import (
|
||||
RotateComponentsCommand,
|
||||
)
|
||||
from bedit.core.model import (
|
||||
Annotation,
|
||||
Component,
|
||||
Connection,
|
||||
Endpoint,
|
||||
@@ -43,6 +47,9 @@ class DocumentController(QObject):
|
||||
componentRotated = Signal(str, float)
|
||||
connectionAdded = Signal(str)
|
||||
connectionRemoved = Signal(str)
|
||||
graphItemChanged = Signal(str, str)
|
||||
annotationAdded = Signal(str)
|
||||
annotationRemoved = Signal(str)
|
||||
interfaceChanged = Signal()
|
||||
filePathChanged = Signal(object)
|
||||
modifiedChanged = Signal(bool)
|
||||
@@ -221,16 +228,14 @@ class DocumentController(QObject):
|
||||
if (component := self.active_component.graph.blocks.get(component_id)) is not None
|
||||
}
|
||||
if rotations:
|
||||
self.undo_stack.push(
|
||||
RotateComponentsCommand(self, self.active_component_id, rotations)
|
||||
)
|
||||
self.undo_stack.push(RotateComponentsCommand(self, self.active_component_id, rotations))
|
||||
|
||||
def connect(
|
||||
self,
|
||||
source: Endpoint,
|
||||
target: Endpoint,
|
||||
*,
|
||||
routing: str = "spline",
|
||||
routing: str = "angled",
|
||||
waypoints: list[QPointF] | None = None,
|
||||
) -> str:
|
||||
if self.active_component_id is None:
|
||||
@@ -240,9 +245,7 @@ class DocumentController(QObject):
|
||||
if source_port is None or target_port is None:
|
||||
raise ValueError("A connection endpoint no longer exists")
|
||||
if not PortTypeRegistry.compatible(source_port.type, target_port.type):
|
||||
raise ValueError(
|
||||
f"Cannot connect {source_port.type!r} to {target_port.type!r}"
|
||||
)
|
||||
raise ValueError(f"Cannot connect {source_port.type!r} to {target_port.type!r}")
|
||||
if routing not in {"direct", "angled", "spline"}:
|
||||
raise ValueError(f"Unknown connection routing: {routing}")
|
||||
connection = Connection(
|
||||
@@ -251,16 +254,154 @@ class DocumentController(QObject):
|
||||
target,
|
||||
properties={
|
||||
"routing": routing,
|
||||
"waypoints": [
|
||||
{"x": point.x(), "y": point.y()} for point in (waypoints or [])
|
||||
],
|
||||
"waypoints": [{"x": point.x(), "y": point.y()} for point in (waypoints or [])],
|
||||
},
|
||||
)
|
||||
self.undo_stack.push(
|
||||
AddConnectionCommand(self, self.active_component_id, connection)
|
||||
)
|
||||
self.undo_stack.push(AddConnectionCommand(self, self.active_component_id, connection))
|
||||
return connection.id
|
||||
|
||||
def add_annotation(
|
||||
self,
|
||||
kind: str,
|
||||
start: QPointF,
|
||||
end: QPointF,
|
||||
*,
|
||||
text: str = "",
|
||||
routing: str = "angled",
|
||||
waypoints: list[QPointF] | None = None,
|
||||
) -> str:
|
||||
if self.active_component_id is None:
|
||||
raise ValueError("There is no active graph")
|
||||
if kind != "line":
|
||||
left, right = sorted((start.x(), end.x()))
|
||||
top, bottom = sorted((start.y(), end.y()))
|
||||
start, end = QPointF(left, top), QPointF(right, bottom)
|
||||
style = {
|
||||
"stroke": "#303030",
|
||||
"lineWidth": 1.5,
|
||||
"lineStyle": "solid",
|
||||
"fill": "none" if kind in {"line", "text"} else "#dbeafe",
|
||||
}
|
||||
if kind == "box":
|
||||
style["cornerRadius"] = 0.0
|
||||
if kind == "text":
|
||||
style.update({"fontSize": 12.0, "color": "#202020"})
|
||||
annotation = Annotation(
|
||||
id=str(uuid4()),
|
||||
kind=kind,
|
||||
x=start.x(),
|
||||
y=start.y(),
|
||||
width=end.x() - start.x(),
|
||||
height=end.y() - start.y(),
|
||||
text=text,
|
||||
layer=-1,
|
||||
properties={
|
||||
**style,
|
||||
"routing": routing,
|
||||
"waypoints": [{"x": point.x(), "y": point.y()} for point in (waypoints or [])],
|
||||
},
|
||||
)
|
||||
self.undo_stack.push(AddAnnotationCommand(self, self.active_component_id, annotation))
|
||||
return annotation.id
|
||||
|
||||
def set_route_waypoints(
|
||||
self, item_kind: str, item_id: str, waypoints: list[QPointF], routing: str | None = None
|
||||
) -> None:
|
||||
item = (
|
||||
self.active_graph.connections
|
||||
if item_kind == "connection"
|
||||
else self.active_graph.annotations
|
||||
).get(item_id)
|
||||
if item is None or self.active_component_id is None:
|
||||
return
|
||||
old = deepcopy(item.properties)
|
||||
new = deepcopy(old)
|
||||
if routing is not None:
|
||||
new["routing"] = routing
|
||||
new["waypoints"] = [{"x": point.x(), "y": point.y()} for point in waypoints]
|
||||
if old != new:
|
||||
self.undo_stack.push(
|
||||
EditGraphItemCommand(
|
||||
self, self.active_component_id, item_kind, item_id, old, new, "Edit line nodes"
|
||||
)
|
||||
)
|
||||
|
||||
def set_annotation_geometry(self, annotation_id: str, old: dict, new: dict) -> None:
|
||||
if old != new and self.active_component_id is not None:
|
||||
self.undo_stack.push(
|
||||
EditGraphItemCommand(
|
||||
self,
|
||||
self.active_component_id,
|
||||
"annotation_geometry",
|
||||
annotation_id,
|
||||
old,
|
||||
new,
|
||||
"Move annotation",
|
||||
)
|
||||
)
|
||||
|
||||
def edit_annotation(self, annotation_id: str, values: dict) -> None:
|
||||
annotation = self.active_graph.annotations.get(annotation_id)
|
||||
if annotation is None or self.active_component_id is None:
|
||||
return
|
||||
old = annotation.to_dict()
|
||||
if old != values:
|
||||
self.undo_stack.push(
|
||||
EditGraphItemCommand(
|
||||
self,
|
||||
self.active_component_id,
|
||||
"annotation_data",
|
||||
annotation_id,
|
||||
old,
|
||||
deepcopy(values),
|
||||
"Edit shape",
|
||||
)
|
||||
)
|
||||
|
||||
def reorder_annotations(self, annotation_ids: set[str], operation: str) -> None:
|
||||
if not annotation_ids or self.active_component_id is None:
|
||||
return
|
||||
graph = self.active_graph
|
||||
layers = [item.layer for item in graph.annotations.values()]
|
||||
minimum, maximum = min(layers, default=-1), max(layers, default=1)
|
||||
for annotation_id in annotation_ids:
|
||||
item = graph.annotations.get(annotation_id)
|
||||
if item is None:
|
||||
continue
|
||||
old = {"layer": item.layer}
|
||||
forward = item.layer + 1
|
||||
backward = item.layer - 1
|
||||
if forward == 0:
|
||||
forward = 1
|
||||
if backward == 0:
|
||||
backward = -1
|
||||
layer = {
|
||||
"forward": forward,
|
||||
"backward": backward,
|
||||
"front": max(1, maximum + 1),
|
||||
"back": min(-1, minimum - 1),
|
||||
}[operation]
|
||||
self.undo_stack.push(
|
||||
EditGraphItemCommand(
|
||||
self,
|
||||
self.active_component_id,
|
||||
"annotation_layer",
|
||||
annotation_id,
|
||||
old,
|
||||
{"layer": layer},
|
||||
"Reorder annotation",
|
||||
)
|
||||
)
|
||||
|
||||
def delete_annotations(self, annotation_ids: set[str]) -> None:
|
||||
items = {
|
||||
key: self.active_graph.annotations[key]
|
||||
for key in annotation_ids
|
||||
if key in self.active_graph.annotations
|
||||
}
|
||||
if items and self.active_component_id is not None:
|
||||
self.undo_stack.push(DeleteAnnotationsCommand(self, self.active_component_id, items))
|
||||
|
||||
def _port_for_endpoint(self, endpoint: Endpoint, role: str) -> Port | None:
|
||||
owner = self.active_component
|
||||
if owner is None:
|
||||
@@ -272,7 +413,9 @@ class DocumentController(QObject):
|
||||
if component is None:
|
||||
return None
|
||||
ports = component.outputs if role == "source" else component.inputs
|
||||
return next((port for port in ports if port.id == (endpoint.interface or endpoint.port)), None)
|
||||
return next(
|
||||
(port for port in ports if port.id == (endpoint.interface or endpoint.port)), None
|
||||
)
|
||||
|
||||
def connection_port_type(self, connection: Connection) -> str:
|
||||
port = self._port_for_endpoint(connection.source, "source")
|
||||
@@ -289,9 +432,7 @@ class DocumentController(QObject):
|
||||
x=position.x(),
|
||||
y=position.y(),
|
||||
)
|
||||
self.undo_stack.push(
|
||||
AddInterfacePortCommand(self, component.id, direction, port)
|
||||
)
|
||||
self.undo_stack.push(AddInterfacePortCommand(self, component.id, direction, port))
|
||||
return port.id
|
||||
|
||||
def move_interface_port(self, port_id: str, old: QPointF, new: QPointF) -> None:
|
||||
@@ -350,11 +491,17 @@ class DocumentController(QObject):
|
||||
parent = self.document.find_parent(component.id)
|
||||
if parent is not None:
|
||||
for connection in parent.graph.connections.values():
|
||||
if connection.target.block == component.id and connection.target.port not in input_ids:
|
||||
if (
|
||||
connection.target.block == component.id
|
||||
and connection.target.port not in input_ids
|
||||
):
|
||||
raise ValueError(
|
||||
f"Input {connection.target.port!r} is still connected in the containing graph"
|
||||
)
|
||||
if connection.source.block == component.id and connection.source.port not in output_ids:
|
||||
if (
|
||||
connection.source.block == component.id
|
||||
and connection.source.port not in output_ids
|
||||
):
|
||||
raise ValueError(
|
||||
f"Output {connection.source.port!r} is still connected in the containing graph"
|
||||
)
|
||||
@@ -422,9 +569,15 @@ class DocumentController(QObject):
|
||||
parent = self.document.find_parent(component_id)
|
||||
if parent is not None:
|
||||
for connection in parent.graph.connections.values():
|
||||
if connection.target.block == component_id and connection.target.port not in input_ids:
|
||||
if (
|
||||
connection.target.block == component_id
|
||||
and connection.target.port not in input_ids
|
||||
):
|
||||
raise ValueError("An input cannot be removed or reoriented while connected")
|
||||
if connection.source.block == component_id and connection.source.port not in output_ids:
|
||||
if (
|
||||
connection.source.block == component_id
|
||||
and connection.source.port not in output_ids
|
||||
):
|
||||
raise ValueError("An output cannot be removed or reoriented while connected")
|
||||
for connection in component.graph.connections.values():
|
||||
if connection.source.interface and connection.source.interface not in input_ids:
|
||||
@@ -471,7 +624,9 @@ class DocumentController(QObject):
|
||||
or connection.target.interface in output_ids
|
||||
):
|
||||
all_connection_ids.add(connection.id)
|
||||
blocks = {block_id: graph.blocks[block_id] for block_id in block_ids if block_id in graph.blocks}
|
||||
blocks = {
|
||||
block_id: graph.blocks[block_id] for block_id in block_ids if block_id in graph.blocks
|
||||
}
|
||||
connections = {
|
||||
connection_id: graph.connections[connection_id]
|
||||
for connection_id in all_connection_ids
|
||||
@@ -526,9 +681,7 @@ class DocumentController(QObject):
|
||||
)
|
||||
connections[connection.id] = connection
|
||||
if blocks:
|
||||
self.undo_stack.push(
|
||||
PasteSelectionCommand(self, owner.id, blocks, connections)
|
||||
)
|
||||
self.undo_stack.push(PasteSelectionCommand(self, owner.id, blocks, connections))
|
||||
return list(blocks)
|
||||
|
||||
def _graph_for(self, owner_id: str):
|
||||
@@ -571,9 +724,7 @@ class DocumentController(QObject):
|
||||
self.componentMoved.emit(component_id, position)
|
||||
self.documentReset.emit()
|
||||
|
||||
def _rotate_component(
|
||||
self, owner_id: str, component_id: str, rotation: float
|
||||
) -> None:
|
||||
def _rotate_component(self, owner_id: str, component_id: str, rotation: float) -> None:
|
||||
component = self._graph_for(owner_id).blocks.get(component_id)
|
||||
if component is None:
|
||||
return
|
||||
@@ -593,6 +744,46 @@ class DocumentController(QObject):
|
||||
self.connectionRemoved.emit(connection_id)
|
||||
self.documentReset.emit()
|
||||
|
||||
def _insert_annotation(self, owner_id: str, annotation: Annotation) -> None:
|
||||
self._graph_for(owner_id).annotations[annotation.id] = annotation
|
||||
if owner_id == self.active_component_id:
|
||||
self.annotationAdded.emit(annotation.id)
|
||||
self.documentReset.emit()
|
||||
|
||||
def _remove_annotation(self, owner_id: str, annotation_id: str) -> None:
|
||||
self._graph_for(owner_id).annotations.pop(annotation_id, None)
|
||||
if owner_id == self.active_component_id:
|
||||
self.annotationRemoved.emit(annotation_id)
|
||||
self.documentReset.emit()
|
||||
|
||||
def _set_graph_item_data(
|
||||
self, owner_id: str, item_kind: str, item_id: str, values: dict
|
||||
) -> None:
|
||||
graph = self._graph_for(owner_id)
|
||||
if item_kind == "connection":
|
||||
item = graph.connections.get(item_id)
|
||||
if item is not None:
|
||||
item.properties = deepcopy(values)
|
||||
else:
|
||||
item = graph.annotations.get(item_id)
|
||||
if item is not None:
|
||||
if item_kind == "annotation_geometry":
|
||||
item.x, item.y = float(values["x"]), float(values["y"])
|
||||
item.width, item.height = float(values["width"]), float(values["height"])
|
||||
elif item_kind == "annotation_layer":
|
||||
item.layer = int(values["layer"])
|
||||
elif item_kind == "annotation_data":
|
||||
replacement = Annotation.from_dict(values)
|
||||
item.kind = replacement.kind
|
||||
item.x, item.y = replacement.x, replacement.y
|
||||
item.width, item.height = replacement.width, replacement.height
|
||||
item.text, item.layer = replacement.text, replacement.layer
|
||||
item.properties = replacement.properties
|
||||
else:
|
||||
item.properties = deepcopy(values)
|
||||
if owner_id == self.active_component_id:
|
||||
self.graphItemChanged.emit(item_kind, item_id)
|
||||
|
||||
def _replace_component(self, old_id: str, replacement: Component) -> None:
|
||||
if self.document is None:
|
||||
return
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -31,6 +31,13 @@ class Ui_IconEditorDialog(object):
|
||||
self.dialogLayout.setObjectName(u"dialogLayout")
|
||||
self.shapeToolbarLayout = QHBoxLayout()
|
||||
self.shapeToolbarLayout.setObjectName(u"shapeToolbarLayout")
|
||||
self.pointerButton = QToolButton(IconEditorDialog)
|
||||
self.pointerButton.setObjectName(u"pointerButton")
|
||||
self.pointerButton.setCheckable(True)
|
||||
self.pointerButton.setChecked(True)
|
||||
|
||||
self.shapeToolbarLayout.addWidget(self.pointerButton)
|
||||
|
||||
self.addShapeLabel = QLabel(IconEditorDialog)
|
||||
self.addShapeLabel.setObjectName(u"addShapeLabel")
|
||||
|
||||
@@ -38,31 +45,37 @@ class Ui_IconEditorDialog(object):
|
||||
|
||||
self.addRectangleButton = QToolButton(IconEditorDialog)
|
||||
self.addRectangleButton.setObjectName(u"addRectangleButton")
|
||||
self.addRectangleButton.setCheckable(True)
|
||||
|
||||
self.shapeToolbarLayout.addWidget(self.addRectangleButton)
|
||||
|
||||
self.addCircleButton = QToolButton(IconEditorDialog)
|
||||
self.addCircleButton.setObjectName(u"addCircleButton")
|
||||
self.addCircleButton.setCheckable(True)
|
||||
|
||||
self.shapeToolbarLayout.addWidget(self.addCircleButton)
|
||||
|
||||
self.addEllipseButton = QToolButton(IconEditorDialog)
|
||||
self.addEllipseButton.setObjectName(u"addEllipseButton")
|
||||
self.addEllipseButton.setCheckable(True)
|
||||
|
||||
self.shapeToolbarLayout.addWidget(self.addEllipseButton)
|
||||
|
||||
self.addLineButton = QToolButton(IconEditorDialog)
|
||||
self.addLineButton.setObjectName(u"addLineButton")
|
||||
self.addLineButton.setCheckable(True)
|
||||
|
||||
self.shapeToolbarLayout.addWidget(self.addLineButton)
|
||||
|
||||
self.addTriangleButton = QToolButton(IconEditorDialog)
|
||||
self.addTriangleButton.setObjectName(u"addTriangleButton")
|
||||
self.addTriangleButton.setCheckable(True)
|
||||
|
||||
self.shapeToolbarLayout.addWidget(self.addTriangleButton)
|
||||
|
||||
self.addTextButton = QToolButton(IconEditorDialog)
|
||||
self.addTextButton.setObjectName(u"addTextButton")
|
||||
self.addTextButton.setCheckable(True)
|
||||
|
||||
self.shapeToolbarLayout.addWidget(self.addTextButton)
|
||||
|
||||
@@ -70,6 +83,21 @@ class Ui_IconEditorDialog(object):
|
||||
|
||||
self.shapeToolbarLayout.addItem(self.toolbarSpacer)
|
||||
|
||||
self.zoomInButton = QToolButton(IconEditorDialog)
|
||||
self.zoomInButton.setObjectName(u"zoomInButton")
|
||||
|
||||
self.shapeToolbarLayout.addWidget(self.zoomInButton)
|
||||
|
||||
self.zoomOutButton = QToolButton(IconEditorDialog)
|
||||
self.zoomOutButton.setObjectName(u"zoomOutButton")
|
||||
|
||||
self.shapeToolbarLayout.addWidget(self.zoomOutButton)
|
||||
|
||||
self.centerButton = QToolButton(IconEditorDialog)
|
||||
self.centerButton.setObjectName(u"centerButton")
|
||||
|
||||
self.shapeToolbarLayout.addWidget(self.centerButton)
|
||||
|
||||
self.deleteSelectedButton = QPushButton(IconEditorDialog)
|
||||
self.deleteSelectedButton.setObjectName(u"deleteSelectedButton")
|
||||
|
||||
@@ -106,6 +134,7 @@ class Ui_IconEditorDialog(object):
|
||||
|
||||
def retranslateUi(self, IconEditorDialog):
|
||||
IconEditorDialog.setWindowTitle(QCoreApplication.translate("IconEditorDialog", u"Icon Editor", None))
|
||||
self.pointerButton.setText(QCoreApplication.translate("IconEditorDialog", u"Pointer", None))
|
||||
self.addShapeLabel.setText(QCoreApplication.translate("IconEditorDialog", u"Add:", None))
|
||||
self.addRectangleButton.setText(QCoreApplication.translate("IconEditorDialog", u"Rectangle", None))
|
||||
self.addCircleButton.setText(QCoreApplication.translate("IconEditorDialog", u"Circle", None))
|
||||
@@ -113,6 +142,18 @@ class Ui_IconEditorDialog(object):
|
||||
self.addLineButton.setText(QCoreApplication.translate("IconEditorDialog", u"Line", None))
|
||||
self.addTriangleButton.setText(QCoreApplication.translate("IconEditorDialog", u"Triangle", None))
|
||||
self.addTextButton.setText(QCoreApplication.translate("IconEditorDialog", u"Text", None))
|
||||
self.zoomInButton.setText(QCoreApplication.translate("IconEditorDialog", u"+", None))
|
||||
#if QT_CONFIG(tooltip)
|
||||
self.zoomInButton.setToolTip(QCoreApplication.translate("IconEditorDialog", u"Zoom in", None))
|
||||
#endif // QT_CONFIG(tooltip)
|
||||
self.zoomOutButton.setText(QCoreApplication.translate("IconEditorDialog", u"\u2212", None))
|
||||
#if QT_CONFIG(tooltip)
|
||||
self.zoomOutButton.setToolTip(QCoreApplication.translate("IconEditorDialog", u"Zoom out", None))
|
||||
#endif // QT_CONFIG(tooltip)
|
||||
self.centerButton.setText(QCoreApplication.translate("IconEditorDialog", u"Fit", None))
|
||||
#if QT_CONFIG(tooltip)
|
||||
self.centerButton.setToolTip(QCoreApplication.translate("IconEditorDialog", u"Fit and center the icon canvas", None))
|
||||
#endif // QT_CONFIG(tooltip)
|
||||
self.deleteSelectedButton.setText(QCoreApplication.translate("IconEditorDialog", u"Delete selected", None))
|
||||
self.portHintLabel.setText(QCoreApplication.translate("IconEditorDialog", u"Green points are inputs; red points are outputs. Drag them to place connection anchors.", None))
|
||||
# retranslateUi
|
||||
|
||||
@@ -29,7 +29,7 @@ class Ui_MainWindow(object):
|
||||
def setupUi(self, MainWindow):
|
||||
if not MainWindow.objectName():
|
||||
MainWindow.setObjectName(u"MainWindow")
|
||||
MainWindow.resize(1000, 700)
|
||||
MainWindow.resize(1209, 777)
|
||||
self.actionNew = QAction(MainWindow)
|
||||
self.actionNew.setObjectName(u"actionNew")
|
||||
icon = QIcon()
|
||||
@@ -42,54 +42,63 @@ class Ui_MainWindow(object):
|
||||
self.actionRotateClockwise.setIcon(icon1)
|
||||
self.actionZoomIn = QAction(MainWindow)
|
||||
self.actionZoomIn.setObjectName(u"actionZoomIn")
|
||||
icon2 = QIcon()
|
||||
icon2.addFile(u":/icons/icons/zoom-in.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
||||
self.actionZoomIn.setIcon(icon2)
|
||||
self.actionZoomOut = QAction(MainWindow)
|
||||
self.actionZoomOut.setObjectName(u"actionZoomOut")
|
||||
icon3 = QIcon()
|
||||
icon3.addFile(u":/icons/icons/zoom-out.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
||||
self.actionZoomOut.setIcon(icon3)
|
||||
self.actionCenterView = QAction(MainWindow)
|
||||
self.actionCenterView.setObjectName(u"actionCenterView")
|
||||
icon4 = QIcon()
|
||||
icon4.addFile(u":/icons/icons/zoom-original.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
||||
self.actionCenterView.setIcon(icon4)
|
||||
self.actionOpen = QAction(MainWindow)
|
||||
self.actionOpen.setObjectName(u"actionOpen")
|
||||
icon2 = QIcon()
|
||||
icon2.addFile(u":/icons/icons/document-open.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
||||
self.actionOpen.setIcon(icon2)
|
||||
icon5 = QIcon()
|
||||
icon5.addFile(u":/icons/icons/document-open.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
||||
self.actionOpen.setIcon(icon5)
|
||||
self.actionSave = QAction(MainWindow)
|
||||
self.actionSave.setObjectName(u"actionSave")
|
||||
icon3 = QIcon()
|
||||
icon3.addFile(u":/icons/icons/document-save.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
||||
self.actionSave.setIcon(icon3)
|
||||
icon6 = QIcon()
|
||||
icon6.addFile(u":/icons/icons/document-save.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
||||
self.actionSave.setIcon(icon6)
|
||||
self.actionSaveAs = QAction(MainWindow)
|
||||
self.actionSaveAs.setObjectName(u"actionSaveAs")
|
||||
icon4 = QIcon()
|
||||
icon4.addFile(u":/icons/icons/document-save-as.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
||||
self.actionSaveAs.setIcon(icon4)
|
||||
icon7 = QIcon()
|
||||
icon7.addFile(u":/icons/icons/document-save-as.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
||||
self.actionSaveAs.setIcon(icon7)
|
||||
self.actionExit = QAction(MainWindow)
|
||||
self.actionExit.setObjectName(u"actionExit")
|
||||
self.actionClose = QAction(MainWindow)
|
||||
self.actionClose.setObjectName(u"actionClose")
|
||||
self.actionUndo = QAction(MainWindow)
|
||||
self.actionUndo.setObjectName(u"actionUndo")
|
||||
icon5 = QIcon()
|
||||
icon5.addFile(u":/icons/icons/edit-undo.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
||||
self.actionUndo.setIcon(icon5)
|
||||
icon8 = QIcon()
|
||||
icon8.addFile(u":/icons/icons/edit-undo.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
||||
self.actionUndo.setIcon(icon8)
|
||||
self.actionRedo = QAction(MainWindow)
|
||||
self.actionRedo.setObjectName(u"actionRedo")
|
||||
icon6 = QIcon()
|
||||
icon6.addFile(u":/icons/icons/edit-redo.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
||||
self.actionRedo.setIcon(icon6)
|
||||
icon9 = QIcon()
|
||||
icon9.addFile(u":/icons/icons/edit-redo.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
||||
self.actionRedo.setIcon(icon9)
|
||||
self.actionCut = QAction(MainWindow)
|
||||
self.actionCut.setObjectName(u"actionCut")
|
||||
icon7 = QIcon()
|
||||
icon7.addFile(u":/icons/icons/edit-cut.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
||||
self.actionCut.setIcon(icon7)
|
||||
icon10 = QIcon()
|
||||
icon10.addFile(u":/icons/icons/edit-cut.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
||||
self.actionCut.setIcon(icon10)
|
||||
self.actionCopy = QAction(MainWindow)
|
||||
self.actionCopy.setObjectName(u"actionCopy")
|
||||
icon8 = QIcon()
|
||||
icon8.addFile(u":/icons/icons/edit-copy.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
||||
self.actionCopy.setIcon(icon8)
|
||||
icon11 = QIcon()
|
||||
icon11.addFile(u":/icons/icons/edit-copy.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
||||
self.actionCopy.setIcon(icon11)
|
||||
self.actionPaste = QAction(MainWindow)
|
||||
self.actionPaste.setObjectName(u"actionPaste")
|
||||
icon9 = QIcon()
|
||||
icon9.addFile(u":/icons/icons/edit-paste.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
||||
self.actionPaste.setIcon(icon9)
|
||||
icon12 = QIcon()
|
||||
icon12.addFile(u":/icons/icons/edit-paste.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
||||
self.actionPaste.setIcon(icon12)
|
||||
self.actionSelectAll = QAction(MainWindow)
|
||||
self.actionSelectAll.setObjectName(u"actionSelectAll")
|
||||
self.actionDelete = QAction(MainWindow)
|
||||
@@ -129,9 +138,9 @@ class Ui_MainWindow(object):
|
||||
self.librariesLayout.setContentsMargins(0, 0, 0, 0)
|
||||
self.treeView = QTreeView(self.dockWidgetContents)
|
||||
self.treeView.setObjectName(u"treeView")
|
||||
self.treeView.setIconSize(QSize(28, 28))
|
||||
self.treeView.setStyleSheet(u"QTreeView::item { height: 32px; }")
|
||||
self.treeView.setAlternatingRowColors(True)
|
||||
self.treeView.setIconSize(QSize(28, 28))
|
||||
self.treeView.setUniformRowHeights(True)
|
||||
|
||||
self.librariesLayout.addWidget(self.treeView)
|
||||
@@ -149,8 +158,8 @@ class Ui_MainWindow(object):
|
||||
self.documentPanelLayout.setContentsMargins(0, 0, 0, 0)
|
||||
self.documentTreeView = QTreeView(self.documentDockContents)
|
||||
self.documentTreeView.setObjectName(u"documentTreeView")
|
||||
self.documentTreeView.setIconSize(QSize(16, 16))
|
||||
self.documentTreeView.setAlternatingRowColors(True)
|
||||
self.documentTreeView.setIconSize(QSize(16, 16))
|
||||
self.documentTreeView.setUniformRowHeights(True)
|
||||
|
||||
self.documentPanelLayout.addWidget(self.documentTreeView)
|
||||
@@ -179,12 +188,18 @@ class Ui_MainWindow(object):
|
||||
self.workspaceHeaderLayout.setContentsMargins(6, 2, 6, 2)
|
||||
self.navigateUpButton = QToolButton(self.workspaceHeader)
|
||||
self.navigateUpButton.setObjectName(u"navigateUpButton")
|
||||
icon13 = QIcon()
|
||||
icon13.addFile(u":/icons/icons/arrow-up.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
||||
self.navigateUpButton.setIcon(icon13)
|
||||
|
||||
self.workspaceHeaderLayout.addWidget(self.navigateUpButton)
|
||||
|
||||
self.navigateDownButton = QToolButton(self.workspaceHeader)
|
||||
self.navigateDownButton.setObjectName(u"navigateDownButton")
|
||||
self.navigateDownButton.setEnabled(False)
|
||||
icon14 = QIcon()
|
||||
icon14.addFile(u":/icons/icons/arrow-down.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
||||
self.navigateDownButton.setIcon(icon14)
|
||||
|
||||
self.workspaceHeaderLayout.addWidget(self.navigateDownButton)
|
||||
|
||||
@@ -210,6 +225,9 @@ class Ui_MainWindow(object):
|
||||
|
||||
self.pointerToolButton = QToolButton(self.workspaceHeader)
|
||||
self.pointerToolButton.setObjectName(u"pointerToolButton")
|
||||
icon15 = QIcon()
|
||||
icon15.addFile(u":/icons/icons/edit-select.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
||||
self.pointerToolButton.setIcon(icon15)
|
||||
self.pointerToolButton.setCheckable(True)
|
||||
self.pointerToolButton.setChecked(True)
|
||||
|
||||
@@ -221,6 +239,39 @@ class Ui_MainWindow(object):
|
||||
|
||||
self.workspaceHeaderLayout.addWidget(self.connectToolButton)
|
||||
|
||||
self.boxToolButton = QToolButton(self.workspaceHeader)
|
||||
self.boxToolButton.setObjectName(u"boxToolButton")
|
||||
icon16 = QIcon()
|
||||
icon16.addFile(u":/icons/icons/draw-rectangle.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
||||
self.boxToolButton.setIcon(icon16)
|
||||
self.boxToolButton.setCheckable(True)
|
||||
|
||||
self.workspaceHeaderLayout.addWidget(self.boxToolButton)
|
||||
|
||||
self.lineToolButton = QToolButton(self.workspaceHeader)
|
||||
self.lineToolButton.setObjectName(u"lineToolButton")
|
||||
icon17 = QIcon()
|
||||
icon17.addFile(u":/icons/icons/draw-bezier-curves.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
||||
self.lineToolButton.setIcon(icon17)
|
||||
self.lineToolButton.setCheckable(True)
|
||||
|
||||
self.workspaceHeaderLayout.addWidget(self.lineToolButton)
|
||||
|
||||
self.textToolButton = QToolButton(self.workspaceHeader)
|
||||
self.textToolButton.setObjectName(u"textToolButton")
|
||||
icon18 = QIcon()
|
||||
icon18.addFile(u":/icons/icons/draw-text.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
||||
self.textToolButton.setIcon(icon18)
|
||||
self.textToolButton.setCheckable(True)
|
||||
|
||||
self.workspaceHeaderLayout.addWidget(self.textToolButton)
|
||||
|
||||
self.rotateToolButton = QToolButton(self.workspaceHeader)
|
||||
self.rotateToolButton.setObjectName(u"rotateToolButton")
|
||||
self.rotateToolButton.setIcon(icon1)
|
||||
|
||||
self.workspaceHeaderLayout.addWidget(self.rotateToolButton)
|
||||
|
||||
self.routingLabel = QLabel(self.workspaceHeader)
|
||||
self.routingLabel.setObjectName(u"routingLabel")
|
||||
|
||||
@@ -235,13 +286,13 @@ class Ui_MainWindow(object):
|
||||
self.angledRoutingButton = QToolButton(self.workspaceHeader)
|
||||
self.angledRoutingButton.setObjectName(u"angledRoutingButton")
|
||||
self.angledRoutingButton.setCheckable(True)
|
||||
self.angledRoutingButton.setChecked(True)
|
||||
|
||||
self.workspaceHeaderLayout.addWidget(self.angledRoutingButton)
|
||||
|
||||
self.splineRoutingButton = QToolButton(self.workspaceHeader)
|
||||
self.splineRoutingButton.setObjectName(u"splineRoutingButton")
|
||||
self.splineRoutingButton.setCheckable(True)
|
||||
self.splineRoutingButton.setChecked(True)
|
||||
|
||||
self.workspaceHeaderLayout.addWidget(self.splineRoutingButton)
|
||||
|
||||
@@ -296,7 +347,7 @@ class Ui_MainWindow(object):
|
||||
MainWindow.setCentralWidget(self.centralwidget)
|
||||
self.menubar = QMenuBar(MainWindow)
|
||||
self.menubar.setObjectName(u"menubar")
|
||||
self.menubar.setGeometry(QRect(0, 0, 1000, 24))
|
||||
self.menubar.setGeometry(QRect(0, 0, 1209, 24))
|
||||
self.menuFile = QMenu(self.menubar)
|
||||
self.menuFile.setObjectName(u"menuFile")
|
||||
self.menuEdit = QMenu(self.menubar)
|
||||
@@ -324,9 +375,6 @@ class Ui_MainWindow(object):
|
||||
self.editToolbar = QToolBar(MainWindow)
|
||||
self.editToolbar.setObjectName(u"editToolbar")
|
||||
MainWindow.addToolBar(Qt.ToolBarArea.TopToolBarArea, self.editToolbar)
|
||||
self.transformToolbar = QToolBar(MainWindow)
|
||||
self.transformToolbar.setObjectName(u"transformToolbar")
|
||||
MainWindow.addToolBar(Qt.ToolBarArea.TopToolBarArea, self.transformToolbar)
|
||||
self.cameraToolbar = QToolBar(MainWindow)
|
||||
self.cameraToolbar.setObjectName(u"cameraToolbar")
|
||||
MainWindow.addToolBar(Qt.ToolBarArea.TopToolBarArea, self.cameraToolbar)
|
||||
@@ -366,7 +414,6 @@ class Ui_MainWindow(object):
|
||||
self.editToolbar.addAction(self.actionCopy)
|
||||
self.editToolbar.addAction(self.actionCut)
|
||||
self.editToolbar.addAction(self.actionPaste)
|
||||
self.transformToolbar.addAction(self.actionRotateClockwise)
|
||||
self.cameraToolbar.addAction(self.actionZoomIn)
|
||||
self.cameraToolbar.addAction(self.actionZoomOut)
|
||||
self.cameraToolbar.addAction(self.actionCenterView)
|
||||
@@ -469,19 +516,35 @@ class Ui_MainWindow(object):
|
||||
self.actionAboutQt.setText(QCoreApplication.translate("MainWindow", u"About &Qt", None))
|
||||
self.panel_libraries.setWindowTitle(QCoreApplication.translate("MainWindow", u"Libraries", None))
|
||||
self.panel_document.setWindowTitle(QCoreApplication.translate("MainWindow", u"Document", None))
|
||||
self.navigateUpButton.setText(QCoreApplication.translate("MainWindow", u"Up", None))
|
||||
#if QT_CONFIG(tooltip)
|
||||
self.navigateUpButton.setToolTip(QCoreApplication.translate("MainWindow", u"Open the containing graph", None))
|
||||
#endif // QT_CONFIG(tooltip)
|
||||
self.navigateDownButton.setText(QCoreApplication.translate("MainWindow", u"Down", None))
|
||||
self.navigateUpButton.setText(QCoreApplication.translate("MainWindow", u"Up", None))
|
||||
#if QT_CONFIG(tooltip)
|
||||
self.navigateDownButton.setToolTip(QCoreApplication.translate("MainWindow", u"Open the selected block", None))
|
||||
#endif // QT_CONFIG(tooltip)
|
||||
self.navigateDownButton.setText(QCoreApplication.translate("MainWindow", u"Down", None))
|
||||
self.graphBreadcrumbLabel.setText(QCoreApplication.translate("MainWindow", u"Untitled", None))
|
||||
self.workspaceModeLabel.setText(QCoreApplication.translate("MainWindow", u"Graph", None))
|
||||
self.applyJsonButton.setText(QCoreApplication.translate("MainWindow", u"Apply JSON", None))
|
||||
self.pointerToolButton.setText(QCoreApplication.translate("MainWindow", u"Pointer", None))
|
||||
self.connectToolButton.setText(QCoreApplication.translate("MainWindow", u"Connect", None))
|
||||
#if QT_CONFIG(tooltip)
|
||||
self.boxToolButton.setToolTip(QCoreApplication.translate("MainWindow", u"Draw a box annotation", None))
|
||||
#endif // QT_CONFIG(tooltip)
|
||||
self.boxToolButton.setText(QCoreApplication.translate("MainWindow", u"Box", None))
|
||||
#if QT_CONFIG(tooltip)
|
||||
self.lineToolButton.setToolTip(QCoreApplication.translate("MainWindow", u"Draw a line annotation", None))
|
||||
#endif // QT_CONFIG(tooltip)
|
||||
self.lineToolButton.setText(QCoreApplication.translate("MainWindow", u"Line", None))
|
||||
#if QT_CONFIG(tooltip)
|
||||
self.textToolButton.setToolTip(QCoreApplication.translate("MainWindow", u"Add a text annotation", None))
|
||||
#endif // QT_CONFIG(tooltip)
|
||||
self.textToolButton.setText(QCoreApplication.translate("MainWindow", u"Text", None))
|
||||
#if QT_CONFIG(tooltip)
|
||||
self.rotateToolButton.setToolTip(QCoreApplication.translate("MainWindow", u"Rotate selected blocks clockwise", None))
|
||||
#endif // QT_CONFIG(tooltip)
|
||||
self.rotateToolButton.setText(QCoreApplication.translate("MainWindow", u"Rotate", None))
|
||||
self.routingLabel.setText(QCoreApplication.translate("MainWindow", u"Line:", None))
|
||||
self.directRoutingButton.setText(QCoreApplication.translate("MainWindow", u"Direct", None))
|
||||
self.angledRoutingButton.setText(QCoreApplication.translate("MainWindow", u"Angled", None))
|
||||
@@ -496,7 +559,6 @@ class Ui_MainWindow(object):
|
||||
self.menuHelp.setTitle(QCoreApplication.translate("MainWindow", u"&Help", None))
|
||||
self.fileToolbar.setWindowTitle(QCoreApplication.translate("MainWindow", u"File", None))
|
||||
self.editToolbar.setWindowTitle(QCoreApplication.translate("MainWindow", u"Edit", None))
|
||||
self.transformToolbar.setWindowTitle(QCoreApplication.translate("MainWindow", u"Transform", None))
|
||||
self.cameraToolbar.setWindowTitle(QCoreApplication.translate("MainWindow", u"Camera", None))
|
||||
# retranslateUi
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from PySide6.QtCore import QPointF, QRectF
|
||||
from PySide6.QtGui import QColor, QPainter, QPen
|
||||
from PySide6.QtCore import Qt
|
||||
from PySide6.QtGui import QColor, QPainter, QPen, QWheelEvent
|
||||
from PySide6.QtWidgets import QGraphicsView
|
||||
|
||||
from bedit.gui.preferences import application_settings
|
||||
@@ -22,3 +23,25 @@ class IconCanvasView(QGraphicsView):
|
||||
painter.drawLine(QPointF(x, rect.top()), QPointF(x, rect.bottom()))
|
||||
for y in range(top, int(rect.bottom()) + grid, grid):
|
||||
painter.drawLine(QPointF(rect.left(), y), QPointF(rect.right(), y))
|
||||
|
||||
def _zoom(self, factor: float) -> None:
|
||||
target = self.transform().m11() * factor
|
||||
if 0.25 <= target <= 16.0:
|
||||
self.scale(factor, factor)
|
||||
|
||||
def zoom_in(self) -> None:
|
||||
self._zoom(1.2)
|
||||
|
||||
def zoom_out(self) -> None:
|
||||
self._zoom(1 / 1.2)
|
||||
|
||||
def center_icon(self) -> None:
|
||||
if self.scene() is not None:
|
||||
self.fitInView(
|
||||
self.scene().sceneRect().adjusted(-10, -10, 10, 10),
|
||||
Qt.AspectRatioMode.KeepAspectRatio,
|
||||
)
|
||||
|
||||
def wheelEvent(self, event: QWheelEvent) -> None: # noqa: N802
|
||||
self._zoom(1.2 if event.angleDelta().y() > 0 else 1 / 1.2)
|
||||
event.accept()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -13,14 +13,14 @@ def icon_bounds(icon: Icon) -> QRectF:
|
||||
rect = QRectF(
|
||||
float(element.get("x", 0)),
|
||||
float(element.get("y", 0)),
|
||||
max(0.0, float(element.get("width", 0))),
|
||||
max(0.0, float(element.get("height", 0))),
|
||||
)
|
||||
float(element.get("width", 0)),
|
||||
float(element.get("height", 0)),
|
||||
).normalized()
|
||||
bounds = rect if bounds.isNull() else bounds.united(rect)
|
||||
return bounds if not bounds.isNull() else QRectF(32, 32, 64, 64)
|
||||
|
||||
|
||||
def _pen(element: dict) -> QPen:
|
||||
def shape_pen(element: dict) -> QPen:
|
||||
styles = {
|
||||
"solid": Qt.PenStyle.SolidLine,
|
||||
"dash": Qt.PenStyle.DashLine,
|
||||
@@ -51,12 +51,16 @@ def paint_icon(
|
||||
for element in icon.elements:
|
||||
kind = element.get("type", "rectangle")
|
||||
rect = QRectF(
|
||||
float(element.get("x", 0)), float(element.get("y", 0)),
|
||||
float(element.get("width", 20)), float(element.get("height", 20)),
|
||||
float(element.get("x", 0)),
|
||||
float(element.get("y", 0)),
|
||||
float(element.get("width", 20)),
|
||||
float(element.get("height", 20)),
|
||||
)
|
||||
painter.setPen(_pen(element))
|
||||
painter.setPen(shape_pen(element))
|
||||
fill = element.get("fill", "#ffffff")
|
||||
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)
|
||||
)
|
||||
if kind == "rectangle":
|
||||
radius = float(element.get("cornerRadius", 0))
|
||||
painter.drawRoundedRect(rect, radius, radius)
|
||||
@@ -65,7 +69,11 @@ def paint_icon(
|
||||
elif kind == "line":
|
||||
painter.drawLine(rect.topLeft(), rect.bottomRight())
|
||||
elif kind == "triangle":
|
||||
painter.drawPolygon(QPolygonF([QPointF(rect.center().x(), rect.top()), rect.bottomRight(), rect.bottomLeft()]))
|
||||
painter.drawPolygon(
|
||||
QPolygonF(
|
||||
[QPointF(rect.center().x(), rect.top()), rect.bottomRight(), rect.bottomLeft()]
|
||||
)
|
||||
)
|
||||
elif kind == "text":
|
||||
painter.setPen(QColor(element.get("color", element.get("stroke", "#202020"))))
|
||||
font = QFont()
|
||||
|
||||
@@ -24,15 +24,18 @@ from PySide6.QtWidgets import (
|
||||
QGraphicsView,
|
||||
QApplication,
|
||||
QMenu,
|
||||
QInputDialog,
|
||||
QStyleOptionGraphicsItem,
|
||||
QToolTip,
|
||||
QWidget,
|
||||
)
|
||||
|
||||
from bedit.core.model import Component, Connection, Endpoint, Port
|
||||
from bedit.core.model import Annotation, Component, Connection, Endpoint, Port
|
||||
from bedit.gui.controllers.document import DocumentController
|
||||
from bedit.gui.models.library_tree import COMPONENT_MIME_TYPE
|
||||
from bedit.gui.graphics.icon_renderer import icon_bounds, paint_icon
|
||||
from bedit.gui.graphics.icon_editor import ShapeOptionsDialog
|
||||
from bedit.gui.graphics.icon_renderer import shape_pen
|
||||
from bedit.gui.graphics.connection_styles import ConnectionStyle, connection_style
|
||||
from bedit.gui.preferences import application_settings
|
||||
|
||||
@@ -94,7 +97,9 @@ class ComponentGraphicsItem(QGraphicsObject):
|
||||
position = port.properties.get("iconPosition", {})
|
||||
item.setPos(
|
||||
float(position.get("x", x)) * self.WIDTH / self.component.icon.width,
|
||||
float(position.get("y", spacing * index)) * self.HEIGHT / self.component.icon.height,
|
||||
float(position.get("y", spacing * index))
|
||||
* self.HEIGHT
|
||||
/ self.component.icon.height,
|
||||
)
|
||||
result[port.id] = item
|
||||
return result
|
||||
@@ -212,6 +217,9 @@ class InterfaceTerminalItem(QGraphicsObject):
|
||||
super().mousePressEvent(event)
|
||||
|
||||
def contextMenuEvent(self, event: QGraphicsSceneContextMenuEvent) -> None: # noqa: N802
|
||||
if self.index < 0:
|
||||
event.accept()
|
||||
return
|
||||
menu = QMenu()
|
||||
options_action = menu.addAction(f"{self.direction.title()} Options…")
|
||||
if menu.exec(event.screenPos()) is options_action:
|
||||
@@ -253,13 +261,24 @@ class ConnectionGraphicsItem(QGraphicsPathItem):
|
||||
self.end_direction = QPointF(1, 0)
|
||||
self.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsSelectable)
|
||||
self._update_pen()
|
||||
self.setZValue(-1)
|
||||
self.setZValue(0)
|
||||
self.setToolTip(name or "Connection")
|
||||
self.handles: list[WaypointHandle] = []
|
||||
|
||||
def contextMenuEvent(self, event: QGraphicsSceneContextMenuEvent) -> None: # noqa: N802
|
||||
if not self.isSelected():
|
||||
self.scene().clearSelection()
|
||||
self.setSelected(True)
|
||||
menu = QMenu()
|
||||
add_node_action = menu.addAction("Add Node")
|
||||
menu.addSeparator()
|
||||
options_action = menu.addAction("Connection Options…")
|
||||
if menu.exec(event.screenPos()) is options_action:
|
||||
selected = menu.exec(event.screenPos())
|
||||
if selected is add_node_action:
|
||||
scene = self.scene()
|
||||
if isinstance(scene, GraphScene):
|
||||
scene.add_route_node("connection", self.connection_id, event.scenePos())
|
||||
elif selected is options_action:
|
||||
scene = self.scene()
|
||||
if isinstance(scene, GraphScene):
|
||||
scene.connectionOptionsRequested.emit(self.connection_id)
|
||||
@@ -269,6 +288,7 @@ class ConnectionGraphicsItem(QGraphicsPathItem):
|
||||
result = super().itemChange(change, value)
|
||||
if change == QGraphicsItem.GraphicsItemChange.ItemSelectedHasChanged:
|
||||
self._update_pen()
|
||||
self._update_handles()
|
||||
return result
|
||||
|
||||
def _update_pen(self) -> None:
|
||||
@@ -291,6 +311,24 @@ class ConnectionGraphicsItem(QGraphicsPathItem):
|
||||
self.start_direction = points[1] - points[0]
|
||||
self.end_direction = points[-1] - points[-2]
|
||||
|
||||
def set_waypoints(self, points: list[QPointF]) -> None:
|
||||
for handle in self.handles:
|
||||
if handle.scene() is not None:
|
||||
handle.scene().removeItem(handle)
|
||||
self.handles = [
|
||||
WaypointHandle("connection", self.connection_id, index, point)
|
||||
for index, point in enumerate(points)
|
||||
]
|
||||
scene = self.scene()
|
||||
if scene is not None:
|
||||
for handle in self.handles:
|
||||
scene.addItem(handle)
|
||||
self._update_handles()
|
||||
|
||||
def _update_handles(self) -> None:
|
||||
for handle in self.handles:
|
||||
handle.setVisible(self.isSelected())
|
||||
|
||||
@staticmethod
|
||||
def _arrow(end: QPointF, direction: QPointF, size: float) -> QPolygonF:
|
||||
length = max(0.001, (direction.x() ** 2 + direction.y() ** 2) ** 0.5)
|
||||
@@ -306,7 +344,211 @@ class ConnectionGraphicsItem(QGraphicsPathItem):
|
||||
if self.style.arrow_at_target:
|
||||
painter.drawPolygon(self._arrow(self.end, self.end_direction, self.style.arrow_size))
|
||||
if self.style.arrow_at_source:
|
||||
painter.drawPolygon(self._arrow(self.start, -self.start_direction, self.style.arrow_size))
|
||||
painter.drawPolygon(
|
||||
self._arrow(self.start, -self.start_direction, self.style.arrow_size)
|
||||
)
|
||||
|
||||
|
||||
class WaypointHandle(QGraphicsEllipseItem):
|
||||
def __init__(self, item_kind: str, item_id: str, index: int, position: QPointF) -> None:
|
||||
super().__init__(-5, -5, 10, 10)
|
||||
self.item_kind, self.item_id, self.index = item_kind, item_id, index
|
||||
self.setPos(position)
|
||||
self.setBrush(QColor("#ffffff"))
|
||||
self.setPen(QPen(QColor("#2563eb"), 1.5))
|
||||
self.setZValue(1000)
|
||||
self.setFlags(
|
||||
QGraphicsItem.GraphicsItemFlag.ItemIsMovable
|
||||
| QGraphicsItem.GraphicsItemFlag.ItemSendsGeometryChanges
|
||||
)
|
||||
self.setCursor(Qt.CursorShape.SizeAllCursor)
|
||||
|
||||
def itemChange(self, change, value): # noqa: N802
|
||||
if change == QGraphicsItem.GraphicsItemChange.ItemPositionChange:
|
||||
snapped = _snapped(value)
|
||||
scene = self.scene()
|
||||
if isinstance(scene, GraphScene):
|
||||
scene.preview_route_node(self.item_kind, self.item_id, self.index, snapped)
|
||||
return snapped
|
||||
return super().itemChange(change, value)
|
||||
|
||||
def mouseReleaseEvent(self, event: QGraphicsSceneMouseEvent) -> None: # noqa: N802
|
||||
super().mouseReleaseEvent(event)
|
||||
scene = self.scene()
|
||||
if isinstance(scene, GraphScene):
|
||||
scene.move_route_node(self.item_kind, self.item_id, self.index, self.pos())
|
||||
|
||||
def contextMenuEvent(self, event: QGraphicsSceneContextMenuEvent) -> None: # noqa: N802
|
||||
menu = QMenu()
|
||||
delete_action = menu.addAction("Delete Node")
|
||||
if menu.exec(event.screenPos()) is delete_action:
|
||||
scene = self.scene()
|
||||
if isinstance(scene, GraphScene):
|
||||
scene.delete_route_node(self.item_kind, self.item_id, self.index)
|
||||
event.accept()
|
||||
|
||||
|
||||
class AnnotationGraphicsItem(QGraphicsObject):
|
||||
def __init__(self, annotation: Annotation, controller: DocumentController) -> None:
|
||||
super().__init__()
|
||||
self.annotation = annotation
|
||||
self.annotation_id = annotation.id
|
||||
self.controller = controller
|
||||
self.drag_start = QPointF(annotation.x, annotation.y)
|
||||
self.setPos(annotation.x, annotation.y)
|
||||
self.setZValue(annotation.layer)
|
||||
self.setFlags(
|
||||
QGraphicsItem.GraphicsItemFlag.ItemIsMovable
|
||||
| QGraphicsItem.GraphicsItemFlag.ItemIsSelectable
|
||||
| QGraphicsItem.GraphicsItemFlag.ItemSendsGeometryChanges
|
||||
)
|
||||
|
||||
def boundingRect(self) -> QRectF: # noqa: N802
|
||||
width, height = self.annotation.width, self.annotation.height
|
||||
return QRectF(min(0, width), min(0, height), abs(width), abs(height)).adjusted(-4, -4, 4, 4)
|
||||
|
||||
def paint(self, painter: QPainter, option, widget=None) -> None:
|
||||
del option, widget
|
||||
rect = QRectF(0, 0, self.annotation.width, self.annotation.height).normalized()
|
||||
painter.setPen(shape_pen(self.annotation.properties))
|
||||
fill = self.annotation.properties.get("fill", "none")
|
||||
painter.setBrush(
|
||||
Qt.BrushStyle.NoBrush if fill in {"none", "transparent", ""} else QColor(fill)
|
||||
)
|
||||
if self.annotation.kind == "box":
|
||||
radius = float(self.annotation.properties.get("cornerRadius", 0))
|
||||
painter.drawRoundedRect(rect, radius, radius)
|
||||
else:
|
||||
painter.setPen(
|
||||
QColor(
|
||||
self.annotation.properties.get(
|
||||
"color", self.annotation.properties.get("stroke", "#202020")
|
||||
)
|
||||
)
|
||||
)
|
||||
font = painter.font()
|
||||
font.setPointSizeF(float(self.annotation.properties.get("fontSize", 12)))
|
||||
painter.setFont(font)
|
||||
painter.drawText(
|
||||
rect, Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignTop, self.annotation.text
|
||||
)
|
||||
if self.isSelected():
|
||||
painter.setBrush(Qt.BrushStyle.NoBrush)
|
||||
painter.setPen(QPen(QColor("#2563eb"), 1, Qt.PenStyle.DashLine))
|
||||
painter.drawRect(self.boundingRect())
|
||||
|
||||
def mousePressEvent(self, event: QGraphicsSceneMouseEvent) -> None: # noqa: N802
|
||||
self.drag_start = self.pos()
|
||||
super().mousePressEvent(event)
|
||||
|
||||
def mouseReleaseEvent(self, event: QGraphicsSceneMouseEvent) -> None: # noqa: N802
|
||||
super().mouseReleaseEvent(event)
|
||||
new_pos = _snapped(self.pos())
|
||||
self.setPos(new_pos)
|
||||
old = {
|
||||
"x": self.drag_start.x(),
|
||||
"y": self.drag_start.y(),
|
||||
"width": self.annotation.width,
|
||||
"height": self.annotation.height,
|
||||
}
|
||||
new = {**old, "x": new_pos.x(), "y": new_pos.y()}
|
||||
self.controller.set_annotation_geometry(self.annotation_id, old, new)
|
||||
|
||||
def itemChange(self, change, value): # noqa: N802
|
||||
if change == QGraphicsItem.GraphicsItemChange.ItemPositionChange:
|
||||
return _snapped(value)
|
||||
return super().itemChange(change, value)
|
||||
|
||||
def contextMenuEvent(self, event: QGraphicsSceneContextMenuEvent) -> None: # noqa: N802
|
||||
scene = self.scene()
|
||||
if not isinstance(scene, GraphScene):
|
||||
return
|
||||
if not self.isSelected():
|
||||
scene.clearSelection()
|
||||
self.setSelected(True)
|
||||
menu = QMenu()
|
||||
options_action = menu.addAction("Shape Options…")
|
||||
menu.addSeparator()
|
||||
actions = {
|
||||
menu.addAction("Bring Forward"): "forward",
|
||||
menu.addAction("Send Backward"): "backward",
|
||||
menu.addAction("Bring to Front"): "front",
|
||||
menu.addAction("Send to Back"): "back",
|
||||
}
|
||||
selected = menu.exec(event.screenPos())
|
||||
if selected is options_action:
|
||||
scene.edit_annotation_options(self.annotation_id)
|
||||
elif selected in actions:
|
||||
scene.reorder_selected_annotations(actions[selected])
|
||||
event.accept()
|
||||
|
||||
|
||||
class LineAnnotationGraphicsItem(QGraphicsPathItem):
|
||||
def __init__(self, annotation: Annotation) -> None:
|
||||
super().__init__()
|
||||
self.annotation = annotation
|
||||
self.annotation_id = annotation.id
|
||||
self.handles: list[WaypointHandle] = []
|
||||
self.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsSelectable)
|
||||
self.setZValue(annotation.layer)
|
||||
self._update_pen()
|
||||
|
||||
def _update_pen(self) -> None:
|
||||
pen = shape_pen(self.annotation.properties)
|
||||
if self.isSelected():
|
||||
pen.setColor(QColor("#2563eb"))
|
||||
pen.setWidthF(max(3.0, pen.widthF()))
|
||||
self.setPen(pen)
|
||||
|
||||
def set_waypoints(self, points: list[QPointF], start: QPointF, end: QPointF) -> None:
|
||||
for handle in self.handles:
|
||||
if handle.scene() is not None:
|
||||
handle.scene().removeItem(handle)
|
||||
self.handles = [WaypointHandle("annotation", self.annotation_id, -1, start)]
|
||||
self.handles.extend(
|
||||
WaypointHandle("annotation", self.annotation_id, index, point)
|
||||
for index, point in enumerate(points)
|
||||
)
|
||||
self.handles.append(WaypointHandle("annotation", self.annotation_id, -2, end))
|
||||
scene = self.scene()
|
||||
if scene is not None:
|
||||
for handle in self.handles:
|
||||
scene.addItem(handle)
|
||||
for handle in self.handles:
|
||||
handle.setVisible(self.isSelected())
|
||||
|
||||
def itemChange(self, change, value): # noqa: N802
|
||||
result = super().itemChange(change, value)
|
||||
if change == QGraphicsItem.GraphicsItemChange.ItemSelectedHasChanged:
|
||||
for handle in self.handles:
|
||||
handle.setVisible(self.isSelected())
|
||||
self._update_pen()
|
||||
return result
|
||||
|
||||
def contextMenuEvent(self, event: QGraphicsSceneContextMenuEvent) -> None: # noqa: N802
|
||||
if not self.isSelected():
|
||||
self.scene().clearSelection()
|
||||
self.setSelected(True)
|
||||
menu = QMenu()
|
||||
add_action = menu.addAction("Add Node")
|
||||
options_action = menu.addAction("Shape Options…")
|
||||
menu.addSeparator()
|
||||
actions = {
|
||||
menu.addAction("Bring Forward"): "forward",
|
||||
menu.addAction("Send Backward"): "backward",
|
||||
menu.addAction("Bring to Front"): "front",
|
||||
menu.addAction("Send to Back"): "back",
|
||||
}
|
||||
selected = menu.exec(event.screenPos())
|
||||
scene = self.scene()
|
||||
if isinstance(scene, GraphScene):
|
||||
if selected is add_action:
|
||||
scene.add_route_node("annotation", self.annotation_id, event.scenePos())
|
||||
elif selected is options_action:
|
||||
scene.edit_annotation_options(self.annotation_id)
|
||||
elif selected in actions:
|
||||
scene.reorder_selected_annotations(actions[selected])
|
||||
event.accept()
|
||||
|
||||
|
||||
class GraphScene(QGraphicsScene):
|
||||
@@ -322,17 +564,21 @@ class GraphScene(QGraphicsScene):
|
||||
self.input_items: dict[str, InterfaceTerminalItem] = {}
|
||||
self.output_items: dict[str, InterfaceTerminalItem] = {}
|
||||
self.connection_items: dict[str, ConnectionGraphicsItem] = {}
|
||||
self.annotation_items: dict[str, QGraphicsItem] = {}
|
||||
self.pending_source: ConnectionPortItem | None = None
|
||||
self.pending_waypoints: list[QPointF] = []
|
||||
self.pending_preview: QGraphicsPathItem | None = None
|
||||
self.interaction_mode = "pointer"
|
||||
self.connection_routing = "spline"
|
||||
self.connection_routing = "angled"
|
||||
self.drawing_start: QPointF | None = None
|
||||
self.drawing_waypoints: list[QPointF] = []
|
||||
self.setSceneRect(-2000, -2000, 4000, 4000)
|
||||
|
||||
controller.documentReset.connect(self.rebuild)
|
||||
controller.activeGraphChanged.connect(self.rebuild)
|
||||
controller.componentMoved.connect(self.set_component_position)
|
||||
controller.componentRotated.connect(self.set_component_rotation)
|
||||
controller.graphItemChanged.connect(self.refresh_graph_item)
|
||||
self.rebuild()
|
||||
|
||||
def rebuild(self) -> None:
|
||||
@@ -341,6 +587,7 @@ class GraphScene(QGraphicsScene):
|
||||
self.input_items.clear()
|
||||
self.output_items.clear()
|
||||
self.connection_items.clear()
|
||||
self.annotation_items.clear()
|
||||
self.pending_source = None
|
||||
self.pending_waypoints.clear()
|
||||
self.pending_preview = None
|
||||
@@ -371,6 +618,16 @@ class GraphScene(QGraphicsScene):
|
||||
self.addItem(item)
|
||||
self.connection_items[connection.id] = item
|
||||
self.update_connection(connection.id)
|
||||
for annotation in owner.graph.annotations.values():
|
||||
if annotation.kind == "line":
|
||||
item = LineAnnotationGraphicsItem(annotation)
|
||||
self.addItem(item)
|
||||
self.annotation_items[annotation.id] = item
|
||||
self.update_annotation(annotation.id)
|
||||
else:
|
||||
item = AnnotationGraphicsItem(annotation, self.controller)
|
||||
self.addItem(item)
|
||||
self.annotation_items[annotation.id] = item
|
||||
|
||||
def set_component_position(self, component_id: str, position: QPointF) -> None:
|
||||
item = self.component_items.get(component_id)
|
||||
@@ -383,6 +640,52 @@ class GraphScene(QGraphicsScene):
|
||||
item.setRotation(rotation)
|
||||
self.update_connections_for_block(component_id)
|
||||
|
||||
def refresh_graph_item(self, item_kind: str, item_id: str) -> None:
|
||||
if item_kind == "connection":
|
||||
self.update_connection(item_id)
|
||||
return
|
||||
annotation = self.controller.active_graph.annotations.get(item_id)
|
||||
graphics = self.annotation_items.get(item_id)
|
||||
if annotation is None or graphics is None:
|
||||
return
|
||||
graphics.setZValue(annotation.layer)
|
||||
if isinstance(graphics, LineAnnotationGraphicsItem):
|
||||
graphics._update_pen()
|
||||
self.update_annotation(item_id)
|
||||
elif isinstance(graphics, AnnotationGraphicsItem):
|
||||
graphics.prepareGeometryChange()
|
||||
graphics.setPos(annotation.x, annotation.y)
|
||||
graphics.update()
|
||||
|
||||
def edit_annotation_options(self, annotation_id: str) -> None:
|
||||
annotation = self.controller.active_graph.annotations.get(annotation_id)
|
||||
if annotation is None:
|
||||
return
|
||||
element = {
|
||||
"type": {"box": "rectangle", "line": "line", "text": "text"}[annotation.kind],
|
||||
"width": abs(annotation.width),
|
||||
"height": abs(annotation.height),
|
||||
"text": annotation.text,
|
||||
**annotation.properties,
|
||||
}
|
||||
dialog = ShapeOptionsDialog(element)
|
||||
if dialog.exec() != dialog.DialogCode.Accepted:
|
||||
return
|
||||
values = annotation.to_dict()
|
||||
values["size"] = {
|
||||
"width": dialog.element["width"] * (-1 if annotation.width < 0 else 1),
|
||||
"height": dialog.element["height"] * (-1 if annotation.height < 0 else 1),
|
||||
}
|
||||
values["text"] = str(dialog.element.get("text", annotation.text))
|
||||
values["properties"].update(
|
||||
{
|
||||
key: value
|
||||
for key, value in dialog.element.items()
|
||||
if key not in {"type", "x", "y", "width", "height", "text"}
|
||||
}
|
||||
)
|
||||
self.controller.edit_annotation(annotation_id, values)
|
||||
|
||||
def update_connections_for_block(self, component_id: str) -> None:
|
||||
for connection in self.controller.active_graph.connections.values():
|
||||
if component_id in (connection.source.block, connection.target.block):
|
||||
@@ -418,7 +721,7 @@ class GraphScene(QGraphicsScene):
|
||||
if source is None or target is None:
|
||||
return
|
||||
start, end = source.scenePos(), target.scenePos()
|
||||
routing = str(connection.properties.get("routing", "spline"))
|
||||
routing = str(connection.properties.get("routing", "angled"))
|
||||
waypoints = [
|
||||
QPointF(float(point["x"]), float(point["y"]))
|
||||
for point in connection.properties.get("waypoints", [])
|
||||
@@ -426,6 +729,35 @@ class GraphScene(QGraphicsScene):
|
||||
]
|
||||
path, direction_points = self._route_path(start, end, routing, waypoints)
|
||||
graphics.set_connection_path(path, direction_points)
|
||||
graphics.set_waypoints(waypoints)
|
||||
|
||||
def update_annotation(self, annotation_id: str) -> None:
|
||||
annotation = self.controller.active_graph.annotations.get(annotation_id)
|
||||
graphics = self.annotation_items.get(annotation_id)
|
||||
if annotation is None or not isinstance(graphics, LineAnnotationGraphicsItem):
|
||||
return
|
||||
start = QPointF(annotation.x, annotation.y)
|
||||
end = start + QPointF(annotation.width, annotation.height)
|
||||
waypoints = [
|
||||
QPointF(float(p["x"]), float(p["y"]))
|
||||
for p in annotation.properties.get("waypoints", [])
|
||||
]
|
||||
routing = str(annotation.properties.get("routing", "angled"))
|
||||
path, points = self._route_path(start, end, routing, waypoints)
|
||||
graphics.setPath(path)
|
||||
graphics.set_waypoints(waypoints, start, end)
|
||||
|
||||
@staticmethod
|
||||
def _orthogonal_points(start: QPointF, end: QPointF, waypoints: list[QPointF]) -> list[QPointF]:
|
||||
points = [start]
|
||||
for waypoint in [*waypoints, end]:
|
||||
previous = points[-1]
|
||||
if previous.x() != waypoint.x() and previous.y() != waypoint.y():
|
||||
elbow = QPointF(waypoint.x(), previous.y())
|
||||
points.append(elbow)
|
||||
if waypoint != points[-1]:
|
||||
points.append(waypoint)
|
||||
return points
|
||||
|
||||
@staticmethod
|
||||
def _route_path(
|
||||
@@ -437,16 +769,23 @@ class GraphScene(QGraphicsScene):
|
||||
waypoints = waypoints or []
|
||||
path = QPainterPath(start)
|
||||
if routing == "angled":
|
||||
points = [start, *waypoints, end]
|
||||
points = GraphScene._orthogonal_points(start, end, waypoints)
|
||||
for point in points[1:]:
|
||||
path.lineTo(point)
|
||||
return path, points
|
||||
if routing == "direct":
|
||||
path.lineTo(end)
|
||||
return path, [start, end]
|
||||
if waypoints:
|
||||
points = [start, *waypoints, end]
|
||||
for index, point in enumerate(points[1:-1], start=1):
|
||||
following = points[index + 1]
|
||||
midpoint = QPointF((point.x() + following.x()) / 2, (point.y() + following.y()) / 2)
|
||||
path.quadTo(point, midpoint)
|
||||
path.quadTo(points[-2], end)
|
||||
return path, points
|
||||
distance = max(50.0, abs(end.x() - start.x()) * 0.5)
|
||||
first_control = start + QPointF(distance, 0)
|
||||
second_control = end - QPointF(distance, 0)
|
||||
first_control, second_control = start + QPointF(distance, 0), end - QPointF(distance, 0)
|
||||
path.cubicTo(first_control, second_control, end)
|
||||
return path, [start, first_control, second_control, end]
|
||||
|
||||
@@ -454,12 +793,16 @@ class GraphScene(QGraphicsScene):
|
||||
self.interaction_mode = mode
|
||||
if mode != "connect":
|
||||
self._clear_pending_source()
|
||||
self._clear_drawing()
|
||||
|
||||
def set_connection_routing(self, routing: str) -> None:
|
||||
self.connection_routing = routing
|
||||
self._clear_pending_source()
|
||||
|
||||
def mousePressEvent(self, event: QGraphicsSceneMouseEvent) -> None: # noqa: N802
|
||||
if self.interaction_mode in {"box", "text", "line"}:
|
||||
self._drawing_press(event)
|
||||
return
|
||||
if self.interaction_mode != "connect":
|
||||
self._clear_pending_source()
|
||||
super().mousePressEvent(event)
|
||||
@@ -483,7 +826,15 @@ class GraphScene(QGraphicsScene):
|
||||
self.pending_source.endpoint,
|
||||
item.endpoint,
|
||||
routing=self.connection_routing,
|
||||
waypoints=self.pending_waypoints,
|
||||
waypoints=(
|
||||
self._completed_angled_waypoints(
|
||||
self.pending_source.scenePos(),
|
||||
item.scenePos(),
|
||||
self.pending_waypoints,
|
||||
)
|
||||
if self.connection_routing == "angled"
|
||||
else self.pending_waypoints
|
||||
),
|
||||
)
|
||||
except ValueError as error:
|
||||
QToolTip.showText(event.screenPos(), str(error))
|
||||
@@ -500,8 +851,222 @@ class GraphScene(QGraphicsScene):
|
||||
self._update_preview(event.scenePos())
|
||||
event.accept()
|
||||
return
|
||||
if self.interaction_mode in {"box", "line"} and self.drawing_start is not None:
|
||||
self._update_drawing_preview(event.scenePos())
|
||||
event.accept()
|
||||
return
|
||||
super().mouseMoveEvent(event)
|
||||
|
||||
def mouseReleaseEvent(self, event: QGraphicsSceneMouseEvent) -> None: # noqa: N802
|
||||
if self.interaction_mode == "box" and self.drawing_start is not None:
|
||||
end = _snapped(event.scenePos())
|
||||
if end != self.drawing_start:
|
||||
self.controller.add_annotation("box", self.drawing_start, end)
|
||||
self._clear_drawing()
|
||||
event.accept()
|
||||
return
|
||||
super().mouseReleaseEvent(event)
|
||||
|
||||
def _drawing_press(self, event: QGraphicsSceneMouseEvent) -> None:
|
||||
if event.button() == Qt.MouseButton.RightButton:
|
||||
self._clear_drawing()
|
||||
event.accept()
|
||||
return
|
||||
point = _snapped(event.scenePos())
|
||||
if self.interaction_mode == "text":
|
||||
text_value, accepted = QInputDialog.getText(None, "Add Text", "Text:")
|
||||
if accepted and text_value:
|
||||
self.controller.add_annotation(
|
||||
"text", point, point + QPointF(160, 48), text=text_value
|
||||
)
|
||||
event.accept()
|
||||
return
|
||||
if self.drawing_start is None:
|
||||
self.drawing_start = point
|
||||
self.drawing_waypoints = []
|
||||
self._create_preview()
|
||||
elif self.interaction_mode == "line" and self.connection_routing == "angled":
|
||||
self.drawing_waypoints.append(point)
|
||||
self._update_drawing_preview(point)
|
||||
elif self.interaction_mode == "line":
|
||||
self.controller.add_annotation(
|
||||
"line", self.drawing_start, point, routing=self.connection_routing
|
||||
)
|
||||
self._clear_drawing()
|
||||
event.accept()
|
||||
|
||||
def mouseDoubleClickEvent(self, event: QGraphicsSceneMouseEvent) -> None: # noqa: N802
|
||||
if self.interaction_mode == "line" and self.drawing_start is not None:
|
||||
end = _snapped(event.scenePos())
|
||||
waypoints = (
|
||||
self.drawing_waypoints[:-1]
|
||||
if self.drawing_waypoints and self.drawing_waypoints[-1] == end
|
||||
else self.drawing_waypoints
|
||||
)
|
||||
completed = self._completed_angled_waypoints(self.drawing_start, end, waypoints)
|
||||
self.controller.add_annotation(
|
||||
"line", self.drawing_start, end, routing="angled", waypoints=completed
|
||||
)
|
||||
self._clear_drawing()
|
||||
event.accept()
|
||||
return
|
||||
super().mouseDoubleClickEvent(event)
|
||||
|
||||
@staticmethod
|
||||
def _completed_angled_waypoints(
|
||||
start: QPointF, end: QPointF, waypoints: list[QPointF]
|
||||
) -> list[QPointF]:
|
||||
return GraphScene._orthogonal_points(start, end, waypoints)[1:-1]
|
||||
|
||||
def _update_drawing_preview(self, cursor: QPointF) -> None:
|
||||
if self.drawing_start is None or self.pending_preview is None:
|
||||
return
|
||||
end = _snapped(cursor)
|
||||
if self.interaction_mode == "box":
|
||||
path = QPainterPath()
|
||||
path.addRect(QRectF(self.drawing_start, end).normalized())
|
||||
else:
|
||||
path, _ = self._route_path(
|
||||
self.drawing_start, end, self.connection_routing, self.drawing_waypoints
|
||||
)
|
||||
self.pending_preview.setPath(path)
|
||||
|
||||
def _clear_drawing(self) -> None:
|
||||
if self.pending_preview is not None and self.pending_source is None:
|
||||
self.removeItem(self.pending_preview)
|
||||
self.pending_preview = None
|
||||
self.drawing_start = None
|
||||
self.drawing_waypoints.clear()
|
||||
|
||||
def _route_values(self, item_kind: str, item_id: str):
|
||||
item = (
|
||||
self.controller.active_graph.connections
|
||||
if item_kind == "connection"
|
||||
else self.controller.active_graph.annotations
|
||||
).get(item_id)
|
||||
if item is None:
|
||||
return None, []
|
||||
points = [
|
||||
QPointF(float(p["x"]), float(p["y"])) for p in item.properties.get("waypoints", [])
|
||||
]
|
||||
return item, points
|
||||
|
||||
def add_route_node(self, item_kind: str, item_id: str, position: QPointF) -> None:
|
||||
item, points = self._route_values(item_kind, item_id)
|
||||
if item is None:
|
||||
return
|
||||
snapped = _snapped(position)
|
||||
if item_kind == "annotation":
|
||||
start = QPointF(item.x, item.y)
|
||||
end = QPointF(item.x + item.width, item.y + item.height)
|
||||
else:
|
||||
graphics = self.connection_items[item_id]
|
||||
start, end = graphics.start, graphics.end
|
||||
anchors = [start, *points, end]
|
||||
|
||||
def segment_distance(point: QPointF, first: QPointF, second: QPointF) -> float:
|
||||
delta = second - first
|
||||
length_squared = delta.x() ** 2 + delta.y() ** 2
|
||||
if length_squared == 0:
|
||||
return (point - first).manhattanLength()
|
||||
ratio = max(
|
||||
0.0,
|
||||
min(
|
||||
1.0,
|
||||
((point.x() - first.x()) * delta.x() + (point.y() - first.y()) * delta.y())
|
||||
/ length_squared,
|
||||
),
|
||||
)
|
||||
nearest = first + delta * ratio
|
||||
return (point.x() - nearest.x()) ** 2 + (point.y() - nearest.y()) ** 2
|
||||
|
||||
insertion = min(
|
||||
range(len(anchors) - 1),
|
||||
key=lambda index: segment_distance(snapped, anchors[index], anchors[index + 1]),
|
||||
)
|
||||
points.insert(insertion, snapped)
|
||||
routing = "angled" if item.properties.get("routing") == "direct" else None
|
||||
self.controller.set_route_waypoints(item_kind, item_id, points, routing)
|
||||
|
||||
def move_route_node(self, item_kind: str, item_id: str, index: int, position: QPointF) -> None:
|
||||
item, points = self._route_values(item_kind, item_id)
|
||||
if item is None:
|
||||
return
|
||||
position = _snapped(position)
|
||||
if item_kind == "annotation" and index in {-1, -2}:
|
||||
old = {
|
||||
"x": item.x,
|
||||
"y": item.y,
|
||||
"width": item.width,
|
||||
"height": item.height,
|
||||
}
|
||||
start = position if index == -1 else QPointF(item.x, item.y)
|
||||
end = position if index == -2 else QPointF(item.x + item.width, item.y + item.height)
|
||||
new = {
|
||||
"x": start.x(),
|
||||
"y": start.y(),
|
||||
"width": end.x() - start.x(),
|
||||
"height": end.y() - start.y(),
|
||||
}
|
||||
self.controller.set_annotation_geometry(item_id, old, new)
|
||||
return
|
||||
if index < len(points):
|
||||
points[index] = position
|
||||
self.controller.set_route_waypoints(item_kind, item_id, points)
|
||||
|
||||
def preview_route_node(
|
||||
self, item_kind: str, item_id: str, index: int, position: QPointF
|
||||
) -> None:
|
||||
item, points = self._route_values(item_kind, item_id)
|
||||
if item is None:
|
||||
return
|
||||
if item_kind == "annotation" and index in {-1, -2}:
|
||||
graphics = self.annotation_items.get(item_id)
|
||||
if not isinstance(graphics, LineAnnotationGraphicsItem):
|
||||
return
|
||||
start = position if index == -1 else QPointF(item.x, item.y)
|
||||
end = position if index == -2 else QPointF(item.x + item.width, item.y + item.height)
|
||||
path, _direction_points = self._route_path(
|
||||
start,
|
||||
end,
|
||||
str(item.properties.get("routing", "angled")),
|
||||
points,
|
||||
)
|
||||
graphics.setPath(path)
|
||||
return
|
||||
if index >= len(points):
|
||||
return
|
||||
points[index] = position
|
||||
routing = str(item.properties.get("routing", "angled"))
|
||||
if item_kind == "connection":
|
||||
graphics = self.connection_items.get(item_id)
|
||||
if graphics is None:
|
||||
return
|
||||
path, direction_points = self._route_path(graphics.start, graphics.end, routing, points)
|
||||
graphics.set_connection_path(path, direction_points)
|
||||
return
|
||||
graphics = self.annotation_items.get(item_id)
|
||||
if not isinstance(graphics, LineAnnotationGraphicsItem):
|
||||
return
|
||||
start = QPointF(item.x, item.y)
|
||||
end = QPointF(item.x + item.width, item.y + item.height)
|
||||
path, _direction_points = self._route_path(start, end, routing, points)
|
||||
graphics.setPath(path)
|
||||
|
||||
def delete_route_node(self, item_kind: str, item_id: str, index: int) -> None:
|
||||
_item, points = self._route_values(item_kind, item_id)
|
||||
if 0 <= index < len(points):
|
||||
points.pop(index)
|
||||
self.controller.set_route_waypoints(item_kind, item_id, points)
|
||||
|
||||
def reorder_selected_annotations(self, operation: str) -> None:
|
||||
ids = {
|
||||
item.annotation_id
|
||||
for item in self.selectedItems()
|
||||
if isinstance(item, (AnnotationGraphicsItem, LineAnnotationGraphicsItem))
|
||||
}
|
||||
self.controller.reorder_annotations(ids, operation)
|
||||
|
||||
def _create_preview(self) -> None:
|
||||
self.pending_preview = QGraphicsPathItem()
|
||||
self.pending_preview.setPen(QPen(QColor("#64748b"), 1.5, Qt.PenStyle.DashLine))
|
||||
@@ -511,9 +1076,10 @@ class GraphScene(QGraphicsScene):
|
||||
def _update_preview(self, cursor: QPointF) -> None:
|
||||
if self.pending_source is None or self.pending_preview is None:
|
||||
return
|
||||
preview_cursor = _snapped(cursor) if self.connection_routing == "angled" else cursor
|
||||
path, _points = self._route_path(
|
||||
self.pending_source.scenePos(),
|
||||
cursor,
|
||||
preview_cursor,
|
||||
self.connection_routing,
|
||||
self.pending_waypoints,
|
||||
)
|
||||
@@ -622,6 +1188,7 @@ class GraphWorkspaceView(QGraphicsView):
|
||||
connections: set[str] = set()
|
||||
inputs: set[str] = set()
|
||||
outputs: set[str] = set()
|
||||
annotations: set[str] = set()
|
||||
for item in self.scene().selectedItems():
|
||||
if isinstance(item, ComponentGraphicsItem):
|
||||
blocks.add(item.component_id)
|
||||
@@ -629,32 +1196,29 @@ class GraphWorkspaceView(QGraphicsView):
|
||||
connections.add(item.connection_id)
|
||||
elif isinstance(item, InterfaceTerminalItem):
|
||||
(inputs if item.direction == "input" else outputs).add(item.port.id)
|
||||
elif isinstance(item, (AnnotationGraphicsItem, LineAnnotationGraphicsItem)):
|
||||
annotations.add(item.annotation_id)
|
||||
self.controller.delete_selection(blocks, connections, inputs, outputs)
|
||||
self.controller.delete_annotations(annotations)
|
||||
|
||||
def has_selected_components(self) -> bool:
|
||||
scene = self.scene()
|
||||
return bool(
|
||||
scene
|
||||
and any(isinstance(item, ComponentGraphicsItem) for item in scene.selectedItems())
|
||||
scene and any(isinstance(item, ComponentGraphicsItem) for item in scene.selectedItems())
|
||||
)
|
||||
|
||||
def has_single_selected_component(self) -> bool:
|
||||
scene = self.scene()
|
||||
return bool(
|
||||
scene
|
||||
and sum(
|
||||
isinstance(item, ComponentGraphicsItem) for item in scene.selectedItems()
|
||||
)
|
||||
== 1
|
||||
and sum(isinstance(item, ComponentGraphicsItem) for item in scene.selectedItems()) == 1
|
||||
)
|
||||
|
||||
def open_selected_component(self) -> bool:
|
||||
if self.controller is None or self.scene() is None:
|
||||
return False
|
||||
selected = [
|
||||
item
|
||||
for item in self.scene().selectedItems()
|
||||
if isinstance(item, ComponentGraphicsItem)
|
||||
item for item in self.scene().selectedItems() if isinstance(item, ComponentGraphicsItem)
|
||||
]
|
||||
if len(selected) != 1:
|
||||
return False
|
||||
|
||||
@@ -4,7 +4,14 @@ from pathlib import Path
|
||||
|
||||
from PySide6.QtCore import Qt, Slot
|
||||
from PySide6.QtGui import QCloseEvent
|
||||
from PySide6.QtWidgets import QButtonGroup, QFileDialog, QMainWindow, QMenu, QMessageBox
|
||||
from PySide6.QtWidgets import (
|
||||
QButtonGroup,
|
||||
QFileDialog,
|
||||
QMainWindow,
|
||||
QMenu,
|
||||
QMessageBox,
|
||||
QTabWidget,
|
||||
)
|
||||
|
||||
from bedit.core.model import Component, Port
|
||||
from bedit.core.serializer import JsonDocumentSerializer
|
||||
@@ -51,11 +58,11 @@ class MainWindow(QMainWindow):
|
||||
self.ui.leftDockHost.show()
|
||||
self.ui.panel_libraries.show()
|
||||
self.ui.panel_document.show()
|
||||
self.ui.leftDockHost.splitDockWidget(
|
||||
self.ui.panel_document,
|
||||
self.ui.panel_libraries,
|
||||
Qt.Orientation.Vertical,
|
||||
self.ui.leftDockHost.setTabPosition(
|
||||
Qt.DockWidgetArea.LeftDockWidgetArea, QTabWidget.TabPosition.North
|
||||
)
|
||||
self.ui.leftDockHost.tabifyDockWidget(self.ui.panel_document, self.ui.panel_libraries)
|
||||
self.ui.panel_document.raise_()
|
||||
self.ui.workspaceSplitter.setSizes([280, 720])
|
||||
self.reload_libraries()
|
||||
self._active_graph_changed()
|
||||
@@ -67,22 +74,14 @@ class MainWindow(QMainWindow):
|
||||
self.ui.treeView.setDragEnabled(True)
|
||||
self.ui.treeView.setDragDropMode(self.ui.treeView.DragDropMode.DragOnly)
|
||||
self.ui.treeView.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
|
||||
self.ui.treeView.customContextMenuRequested.connect(
|
||||
self.show_external_library_context_menu
|
||||
)
|
||||
self.ui.treeView.customContextMenuRequested.connect(self.show_external_library_context_menu)
|
||||
self.library_tree_model.rebuilt.connect(self.ui.treeView.expandAll)
|
||||
self.ui.documentTreeView.setModel(self.document_tree_model)
|
||||
self.ui.documentTreeView.setHeaderHidden(True)
|
||||
self.ui.documentTreeView.setDragEnabled(True)
|
||||
self.ui.documentTreeView.setDragDropMode(
|
||||
self.ui.documentTreeView.DragDropMode.DragOnly
|
||||
)
|
||||
self.ui.documentTreeView.setContextMenuPolicy(
|
||||
Qt.ContextMenuPolicy.CustomContextMenu
|
||||
)
|
||||
self.ui.documentTreeView.customContextMenuRequested.connect(
|
||||
self.show_library_context_menu
|
||||
)
|
||||
self.ui.documentTreeView.setDragDropMode(self.ui.documentTreeView.DragDropMode.DragOnly)
|
||||
self.ui.documentTreeView.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
|
||||
self.ui.documentTreeView.customContextMenuRequested.connect(self.show_library_context_menu)
|
||||
self.ui.documentTreeView.doubleClicked.connect(self.activate_tree_component)
|
||||
self.document_tree_model.rebuilt.connect(self.ui.documentTreeView.expandAll)
|
||||
self.ui.graphView.set_model(self.document_controller)
|
||||
@@ -97,6 +96,9 @@ class MainWindow(QMainWindow):
|
||||
self.mode_button_group.setExclusive(True)
|
||||
self.mode_button_group.addButton(self.ui.pointerToolButton)
|
||||
self.mode_button_group.addButton(self.ui.connectToolButton)
|
||||
self.mode_button_group.addButton(self.ui.boxToolButton)
|
||||
self.mode_button_group.addButton(self.ui.lineToolButton)
|
||||
self.mode_button_group.addButton(self.ui.textToolButton)
|
||||
self.routing_button_group = QButtonGroup(self)
|
||||
self.routing_button_group.setExclusive(True)
|
||||
for button in (
|
||||
@@ -109,15 +111,13 @@ class MainWindow(QMainWindow):
|
||||
self.ui.navigateDownButton.clicked.connect(self.navigate_down)
|
||||
self.ui.pointerToolButton.clicked.connect(lambda: self.set_graph_tool("pointer"))
|
||||
self.ui.connectToolButton.clicked.connect(lambda: self.set_graph_tool("connect"))
|
||||
self.ui.directRoutingButton.clicked.connect(
|
||||
lambda: self.set_connection_routing("direct")
|
||||
)
|
||||
self.ui.angledRoutingButton.clicked.connect(
|
||||
lambda: self.set_connection_routing("angled")
|
||||
)
|
||||
self.ui.splineRoutingButton.clicked.connect(
|
||||
lambda: self.set_connection_routing("spline")
|
||||
)
|
||||
self.ui.boxToolButton.clicked.connect(lambda: self.set_graph_tool("box"))
|
||||
self.ui.lineToolButton.clicked.connect(lambda: self.set_graph_tool("line"))
|
||||
self.ui.textToolButton.clicked.connect(lambda: self.set_graph_tool("text"))
|
||||
self.ui.rotateToolButton.clicked.connect(self.ui.graphView.rotate_selected)
|
||||
self.ui.directRoutingButton.clicked.connect(lambda: self.set_connection_routing("direct"))
|
||||
self.ui.angledRoutingButton.clicked.connect(lambda: self.set_connection_routing("angled"))
|
||||
self.ui.splineRoutingButton.clicked.connect(lambda: self.set_connection_routing("spline"))
|
||||
self.ui.applyJsonButton.clicked.connect(self.apply_json)
|
||||
self.document_controller.activeGraphChanged.connect(self._active_graph_changed)
|
||||
|
||||
@@ -160,7 +160,6 @@ class MainWindow(QMainWindow):
|
||||
for toolbar in (
|
||||
self.ui.fileToolbar,
|
||||
self.ui.editToolbar,
|
||||
self.ui.transformToolbar,
|
||||
self.ui.cameraToolbar,
|
||||
):
|
||||
self.ui.menuToolbars.addAction(toolbar.toggleViewAction())
|
||||
@@ -184,7 +183,11 @@ class MainWindow(QMainWindow):
|
||||
if self.document_controller.document is None:
|
||||
self.setWindowTitle("BEdit")
|
||||
return
|
||||
name = self.document_controller.file_path.name if self.document_controller.file_path else "Untitled"
|
||||
name = (
|
||||
self.document_controller.file_path.name
|
||||
if self.document_controller.file_path
|
||||
else "Untitled"
|
||||
)
|
||||
modified = "*" if not self.document_controller.undo_stack.isClean() else ""
|
||||
self.setWindowTitle(f"{modified}{name} — BEdit")
|
||||
|
||||
@@ -217,12 +220,17 @@ class MainWindow(QMainWindow):
|
||||
def _update_edit_actions(self) -> None:
|
||||
component = self.document_controller.active_component
|
||||
is_graph = component is not None and component.implementation_kind == "graph"
|
||||
has_selection = bool(self.ui.graphView.scene() and self.ui.graphView.scene().selectedItems())
|
||||
has_selection = bool(
|
||||
self.ui.graphView.scene() and self.ui.graphView.scene().selectedItems()
|
||||
)
|
||||
for action in (self.ui.actionCut, self.ui.actionCopy, self.ui.actionDelete):
|
||||
action.setEnabled(is_graph and has_selection)
|
||||
self.ui.actionRotateClockwise.setEnabled(
|
||||
is_graph and self.ui.graphView.has_selected_components()
|
||||
)
|
||||
self.ui.rotateToolButton.setEnabled(
|
||||
is_graph and self.ui.graphView.has_selected_components()
|
||||
)
|
||||
self.ui.actionSelectAll.setEnabled(is_graph)
|
||||
self.ui.actionPaste.setEnabled(is_graph)
|
||||
self.ui.navigateDownButton.setEnabled(
|
||||
@@ -243,6 +251,10 @@ class MainWindow(QMainWindow):
|
||||
for widget in (
|
||||
self.ui.pointerToolButton,
|
||||
self.ui.connectToolButton,
|
||||
self.ui.boxToolButton,
|
||||
self.ui.lineToolButton,
|
||||
self.ui.textToolButton,
|
||||
self.ui.rotateToolButton,
|
||||
self.ui.routingLabel,
|
||||
self.ui.directRoutingButton,
|
||||
self.ui.angledRoutingButton,
|
||||
@@ -252,7 +264,13 @@ class MainWindow(QMainWindow):
|
||||
|
||||
def set_graph_tool(self, mode: str) -> None:
|
||||
self.ui.graphView.set_tool_mode(mode)
|
||||
(self.ui.pointerToolButton if mode == "pointer" else self.ui.connectToolButton).setChecked(True)
|
||||
{
|
||||
"pointer": self.ui.pointerToolButton,
|
||||
"connect": self.ui.connectToolButton,
|
||||
"box": self.ui.boxToolButton,
|
||||
"line": self.ui.lineToolButton,
|
||||
"text": self.ui.textToolButton,
|
||||
}[mode].setChecked(True)
|
||||
|
||||
def set_connection_routing(self, routing: str) -> None:
|
||||
self.ui.graphView.set_connection_routing(routing)
|
||||
@@ -312,9 +330,7 @@ class MainWindow(QMainWindow):
|
||||
raise ValueError("'source' must be an object")
|
||||
inputs = [Port.from_dict(item) for item in data["inputs"]]
|
||||
outputs = [Port.from_dict(item) for item in data["outputs"]]
|
||||
self.document_controller.replace_active_text_definition(
|
||||
inputs, outputs, data["source"]
|
||||
)
|
||||
self.document_controller.replace_active_text_definition(inputs, outputs, data["source"])
|
||||
except (TypeError, ValueError, json.JSONDecodeError) as error:
|
||||
QMessageBox.critical(self, "Invalid text component JSON", str(error))
|
||||
return False
|
||||
@@ -497,9 +513,7 @@ class MainWindow(QMainWindow):
|
||||
try:
|
||||
if library is not None:
|
||||
library.document.validate()
|
||||
JsonDocumentSerializer.save(
|
||||
library.document, Path(library.source_path)
|
||||
)
|
||||
JsonDocumentSerializer.save(library.document, Path(library.source_path))
|
||||
except (OSError, ValueError) as error:
|
||||
component.inputs, component.outputs = old_inputs, old_outputs
|
||||
QMessageBox.warning(self, "Cannot change library ports", str(error))
|
||||
@@ -559,9 +573,7 @@ class MainWindow(QMainWindow):
|
||||
connection = owner.graph.connections.get(connection_id)
|
||||
if connection is None:
|
||||
return
|
||||
dialog = ItemOptionsDialog(
|
||||
"Connection Options", connection.name, self, name_required=False
|
||||
)
|
||||
dialog = ItemOptionsDialog("Connection Options", connection.name, self, name_required=False)
|
||||
if dialog.exec() == dialog.DialogCode.Accepted:
|
||||
self.document_controller.rename_connection(connection_id, dialog.name)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user