Labels
This commit is contained in:
24
src/bedit_gui/commands/graph_label_command.py
Normal file
24
src/bedit_gui/commands/graph_label_command.py
Normal file
@@ -0,0 +1,24 @@
|
||||
from copy import deepcopy
|
||||
|
||||
from PySide6.QtGui import QUndoCommand
|
||||
|
||||
from bedit_core.models import ComponentID
|
||||
from bedit_gui.models import GraphComponentLabel
|
||||
|
||||
|
||||
class ChangeGraphComponentLabelCommand(QUndoCommand):
|
||||
def __init__(self, document: object, graph_id: ComponentID, component_id: ComponentID, label: GraphComponentLabel, text: str) -> None:
|
||||
super().__init__(text)
|
||||
self.document = document
|
||||
self.graph_id = graph_id
|
||||
self.component_id = component_id
|
||||
database = document._graph_database(False)
|
||||
graph = database.graphs.get(graph_id) if database is not None else None
|
||||
self.old_label = deepcopy(graph.component_labels.get(component_id)) if graph is not None and component_id in graph.component_labels else None
|
||||
self.new_label = deepcopy(label)
|
||||
|
||||
def redo(self) -> None:
|
||||
self.document._set_graph_component_label(self.graph_id, self.component_id, self.new_label)
|
||||
|
||||
def undo(self) -> None:
|
||||
self.document._set_graph_component_label(self.graph_id, self.component_id, self.old_label)
|
||||
@@ -9,7 +9,7 @@ from PySide6.QtWidgets import QAbstractItemView, QDialog, QHeaderView, QMenu
|
||||
from bedit_core.models import Component, ComponentID, ConnectionID, EquationImplementation, GraphImplementation, Port, PortID, Parameter, ParameterID
|
||||
from bedit_core.models import Document as CoreDocument
|
||||
from bedit_gui.documents import Document
|
||||
from bedit_gui.models import Graph, Icon
|
||||
from bedit_gui.models import Graph, GraphComponentLabel, Icon
|
||||
from bedit_gui.views.dialogs.interface_editor_dialog import InterfaceEditorDialog
|
||||
from bedit_gui.views.dialogs.param_editor_dialog import ParamEditorDialog
|
||||
from bedit_gui.views.icon_editor_window import IconEditorWindow
|
||||
@@ -63,11 +63,13 @@ class DocumentTreeController(QObject):
|
||||
document.model_changed.connect(self._on_document_changed)
|
||||
document.icon_changed.connect(self._on_icon_changed)
|
||||
document.graph_component_position_changed.connect(self._on_graph_component_position_changed)
|
||||
document.graph_component_label_changed.connect(self._on_graph_component_label_changed)
|
||||
document.graph_connection_points_changed.connect(self._on_graph_connection_points_changed)
|
||||
document.equation_text_changed.connect(self._on_equation_text_changed)
|
||||
self.model.rename_document_requested.connect(self.document.rename)
|
||||
self.model.rename_component_requested.connect(self.document.rename_component)
|
||||
window.graph_editor.component_move_requested.connect(self.document.move_graph_component)
|
||||
window.graph_editor.component_label_move_requested.connect(self.document.move_graph_component_label)
|
||||
window.graph_editor.component_context_menu_requested.connect(self._show_graph_component_context_menu)
|
||||
window.graph_editor.component_open_requested.connect(self._open_graph_component)
|
||||
window.graph_editor.connection_points_change_requested.connect(self.document.change_graph_connection_points)
|
||||
@@ -155,6 +157,11 @@ class DocumentTreeController(QObject):
|
||||
if graph_component is not None and self.document.component_id(graph_component) == graph_id:
|
||||
self.window.graph_editor.set_component_position(component_id, position)
|
||||
|
||||
def _on_graph_component_label_changed(self, graph_id: ComponentID, component_id: ComponentID, label: object) -> None:
|
||||
graph_component = self.window.graph_editor.component()
|
||||
if graph_component is not None and self.document.component_id(graph_component) == graph_id:
|
||||
self.window.graph_editor.set_component_label(component_id, label if isinstance(label, GraphComponentLabel) else None)
|
||||
|
||||
def _on_graph_connection_points_changed(self, graph_id: ComponentID, connection_id: ConnectionID, points: list[tuple[int, int]] | None) -> None:
|
||||
graph_component = self.window.graph_editor.component()
|
||||
if graph_component is not None and self.document.component_id(graph_component) == graph_id:
|
||||
@@ -177,7 +184,7 @@ class DocumentTreeController(QObject):
|
||||
def _show_graph_component_context_menu(self, component_id: ComponentID, global_position: QPoint) -> None:
|
||||
component = self._components.get(component_id)
|
||||
if component is not None:
|
||||
self._show_component_context_menu(component, global_position)
|
||||
self._show_component_context_menu(component, global_position, component_id)
|
||||
|
||||
def _open_graph_component(self, component_id: ComponentID) -> None:
|
||||
index = self.model.component_index(component_id)
|
||||
@@ -185,11 +192,16 @@ class DocumentTreeController(QObject):
|
||||
self.window.ui.documentTree.selectionModel().setCurrentIndex(index, QItemSelectionModel.SelectionFlag.ClearAndSelect | QItemSelectionModel.SelectionFlag.Rows)
|
||||
self.window.ui.documentTree.scrollTo(index)
|
||||
|
||||
def _show_component_context_menu(self, component: Component, global_position: QPoint) -> None:
|
||||
def _show_component_context_menu(self, component: Component, global_position: QPoint, graph_component_id: ComponentID | None = None) -> None:
|
||||
menu = QMenu(self.window.ui.documentTree)
|
||||
edit_interface = menu.addAction("Edit Interface")
|
||||
edit_params = menu.addAction("Edit Parameters")
|
||||
edit_icon = menu.addAction("Edit Icon")
|
||||
show_label = None
|
||||
if graph_component_id is not None:
|
||||
show_label = menu.addAction("Show Label")
|
||||
show_label.setCheckable(True)
|
||||
show_label.setChecked(self.window.graph_editor.component_label_visible(graph_component_id))
|
||||
menu.addSeparator()
|
||||
add_graph_component = None
|
||||
add_equation_component = None
|
||||
@@ -205,6 +217,10 @@ class DocumentTreeController(QObject):
|
||||
self._edit_params(component)
|
||||
elif selected is edit_icon:
|
||||
self._edit_icon(component)
|
||||
elif show_label is not None and selected is show_label:
|
||||
graph_component = self.window.graph_editor.component()
|
||||
if graph_component is not None:
|
||||
self.document.set_graph_component_label_visible(graph_component, graph_component_id, show_label.isChecked())
|
||||
elif add_graph_component is not None and selected is add_graph_component:
|
||||
self._add_graph_component(component)
|
||||
elif add_equation_component is not None and selected is add_equation_component:
|
||||
|
||||
@@ -15,13 +15,14 @@ from bedit_gui.commands.equation_text_command import ChangeEquationTextCommand
|
||||
from bedit_gui.commands.graph_position_command import MoveGraphComponentCommand
|
||||
from bedit_gui.commands.graph_connection_points_command import ChangeGraphConnectionPointsCommand
|
||||
from bedit_gui.commands.graph_connection_command import AddGraphConnectionCommand, DeleteGraphConnectionCommand
|
||||
from bedit_gui.commands.graph_label_command import ChangeGraphComponentLabelCommand
|
||||
from bedit_gui.commands.port_commands import AddPortCommand, ChangePortCommand, RemovePortCommand
|
||||
from bedit_gui.commands.param_commands import AddParamCommand, ChangeParamCommand, RemoveParamCommand
|
||||
from bedit_gui.commands.rename_component_command import RenameComponentCommand
|
||||
from bedit_gui.commands.rename_document_command import RenameDocumentCommand
|
||||
from bedit_gui.commands.simulation_database_command import ChangeSimulationDatabaseCommand
|
||||
from bedit_gui.commands.component_command import AddEmptyEquationComponent, AddEmptyGraphComponent, DeleteComponent, PasteComponents
|
||||
from bedit_gui.models import Graph, GraphConnection, GraphDatabase, Icon, IconDatabase, Simulation, SimulationDatabase
|
||||
from bedit_gui.models import Graph, GraphComponentLabel, GraphConnection, GraphDatabase, Icon, IconDatabase, Simulation, SimulationDatabase
|
||||
from bedit_gui.services import document_files
|
||||
|
||||
|
||||
@@ -35,6 +36,7 @@ class Document(QObject):
|
||||
equation_text_changed = Signal(object, str)
|
||||
simulation_database_changed = Signal(object)
|
||||
graph_component_position_changed = Signal(object, object, object)
|
||||
graph_component_label_changed = Signal(object, object, object)
|
||||
graph_connection_points_changed = Signal(object, object, object)
|
||||
|
||||
def __init__(self, parent: QObject | None = None) -> None:
|
||||
@@ -157,6 +159,36 @@ class Document(QObject):
|
||||
graph.component_positions[component_id] = position
|
||||
self.graph_component_position_changed.emit(graph_id, component_id, position)
|
||||
|
||||
def move_graph_component_label(self, graph_component: Component, component_id: ComponentID, relative_position: tuple[int, int]) -> None:
|
||||
graph_id = self.component_id(graph_component)
|
||||
database = self._graph_database(False)
|
||||
graph = database.graphs.get(graph_id) if database is not None else None
|
||||
current = deepcopy(graph.component_labels.get(component_id)) if graph is not None and component_id in graph.component_labels else GraphComponentLabel()
|
||||
if current.relative_position != relative_position:
|
||||
current.relative_position = relative_position
|
||||
self.undo_stack.push(ChangeGraphComponentLabelCommand(self, graph_id, component_id, current, "Move component label"))
|
||||
|
||||
def set_graph_component_label_visible(self, graph_component: Component, component_id: ComponentID, visible: bool) -> None:
|
||||
graph_id = self.component_id(graph_component)
|
||||
database = self._graph_database(False)
|
||||
graph = database.graphs.get(graph_id) if database is not None else None
|
||||
current = deepcopy(graph.component_labels.get(component_id)) if graph is not None and component_id in graph.component_labels else GraphComponentLabel()
|
||||
if current.visible != visible:
|
||||
current.visible = visible
|
||||
self.undo_stack.push(ChangeGraphComponentLabelCommand(self, graph_id, component_id, current, "Show component label" if visible else "Hide component label"))
|
||||
|
||||
def _set_graph_component_label(self, graph_id: ComponentID, component_id: ComponentID, label: GraphComponentLabel | None) -> None:
|
||||
if label is None:
|
||||
database = self._graph_database(False)
|
||||
graph = database.graphs.get(graph_id) if database is not None else None
|
||||
if graph is not None:
|
||||
graph.component_labels.pop(component_id, None)
|
||||
else:
|
||||
database = self._graph_database(True)
|
||||
graph = database.graphs.setdefault(graph_id, Graph())
|
||||
graph.component_labels[component_id] = deepcopy(label)
|
||||
self.graph_component_label_changed.emit(graph_id, component_id, deepcopy(label))
|
||||
|
||||
def _graph_database(self, create: bool) -> GraphDatabase | None:
|
||||
metadata = self.model.metadata
|
||||
value = metadata.get("graph_database") if metadata is not None else None
|
||||
|
||||
@@ -134,23 +134,39 @@ class GraphConnection:
|
||||
def to_data(self) -> dict[str, Any]:
|
||||
return {"points": [list(point) for point in self.points]}
|
||||
|
||||
@dataclass
|
||||
class GraphComponentLabel:
|
||||
relative_position: tuple[int, int] = (0, 8)
|
||||
visible: bool = True
|
||||
|
||||
@classmethod
|
||||
def from_data(cls, data: Mapping[str, Any]) -> GraphComponentLabel:
|
||||
position = data.get("relative_position", [0, 8])
|
||||
return cls(relative_position=(int(position[0]), int(position[1])), visible=bool(data.get("visible", True)))
|
||||
|
||||
def to_data(self) -> dict[str, Any]:
|
||||
return {"relative_position": list(self.relative_position), "visible": self.visible}
|
||||
|
||||
@dataclass
|
||||
class Graph:
|
||||
shapes: dict[ShapeID, Shape] = field(default_factory=dict)
|
||||
component_positions: dict[ComponentID, tuple[int, int]] = field(default_factory=dict)
|
||||
connections: dict[ConnectionID, GraphConnection] = field(default_factory=dict)
|
||||
component_labels: dict[ComponentID, GraphComponentLabel] = field(default_factory=dict)
|
||||
|
||||
@classmethod
|
||||
def from_data(cls, data: Mapping[str, Any]) -> Graph:
|
||||
shapes = {ShapeID(key): Shape.from_data(value) for key, value in data.get("shapes", {}).items()}
|
||||
component_positions = {ComponentID(key): (int(value[0]), int(value[1])) for key, value in data.get("component_positions", {}).items()}
|
||||
component_labels = {ComponentID(key): GraphComponentLabel.from_data(value) for key, value in data.get("component_labels", {}).items()}
|
||||
connections = {ConnectionID(key): GraphConnection.from_data(value) for key, value in data.get("connections", {}).items()}
|
||||
return cls(shapes=shapes, component_positions=component_positions, connections=connections)
|
||||
return cls(shapes=shapes, component_positions=component_positions, component_labels=component_labels, connections=connections)
|
||||
|
||||
def to_data(self) -> dict[str, Any]:
|
||||
return {
|
||||
"shapes": {str(key): shape.to_data() for key, shape in self.shapes.items()},
|
||||
"component_positions": {str(key): list(position) for key, position in self.component_positions.items()},
|
||||
"component_labels": {str(key): label.to_data() for key, label in self.component_labels.items()},
|
||||
"connections": {str(key): connection.to_data() for key, connection in self.connections.items()},
|
||||
}
|
||||
|
||||
|
||||
@@ -6,10 +6,10 @@ from math import hypot
|
||||
|
||||
from PySide6.QtCore import QEvent, QObject, QPointF, QRectF, QSize, QTimer, Qt, Signal
|
||||
from PySide6.QtGui import QActionGroup, QBrush, QColor, QCursor, QKeySequence, QMouseEvent, QPainter, QPainterPath, QPen, QShortcut, QWheelEvent
|
||||
from PySide6.QtWidgets import QGraphicsEllipseItem, QGraphicsItem, QGraphicsPathItem, QGraphicsPixmapItem, QGraphicsScene, QGraphicsSceneMouseEvent, QGraphicsView, QMenu, QWidget
|
||||
from PySide6.QtWidgets import QGraphicsEllipseItem, QGraphicsItem, QGraphicsPathItem, QGraphicsPixmapItem, QGraphicsScene, QGraphicsSceneMouseEvent, QGraphicsTextItem, QGraphicsView, QMenu, QWidget
|
||||
|
||||
from bedit_core.models import BondCausality, BondConnection, BondPort, Component, ComponentID, Connection, ConnectionID, GraphImplementation, Port, PortID, SignalConnection, SignalDirection, SignalPort
|
||||
from bedit_gui.models import Graph, GraphConnection, Icon
|
||||
from bedit_gui.models import Graph, GraphComponentLabel, GraphConnection, Icon
|
||||
from bedit_gui.ui.generated.ui_graph_editor_widget import Ui_graphEditorWidget
|
||||
from bedit_gui.utils.icon import get_pixmap_bounding_box, render_icon
|
||||
|
||||
@@ -20,6 +20,7 @@ MAX_ZOOM = 4.0
|
||||
ZOOM_STEP = 1.15
|
||||
ZOOM_TO_FIT_PADDING = 32.0
|
||||
COMPONENT_ICON_SIZE = QSize(128, 128)
|
||||
COMPONENT_LABEL_FONT_SIZE = 24.0
|
||||
FALLBACK_COMPONENT_SPACING = 128
|
||||
CONNECTION_WIDTH = 4.0
|
||||
CONNECTION_BOUNDING_BOX_SPACING = 16.0
|
||||
@@ -184,6 +185,58 @@ class GraphComponentItem(QGraphicsPixmapItem):
|
||||
event.accept()
|
||||
|
||||
|
||||
class GraphComponentLabelItem(QGraphicsTextItem):
|
||||
def __init__(self, component_id: ComponentID, text: str, label: GraphComponentLabel, component_item: GraphComponentItem, editor: GraphEditorWidget) -> None:
|
||||
super().__init__(text, component_item)
|
||||
self.component_id = component_id
|
||||
self.editor = editor
|
||||
self._dragging = False
|
||||
self._drag_start = label.relative_position
|
||||
font = self.font()
|
||||
font.setItalic(True)
|
||||
font.setPointSizeF(COMPONENT_LABEL_FONT_SIZE)
|
||||
self.setFont(font)
|
||||
self.setDefaultTextColor(QColor("#202020"))
|
||||
self.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsSelectable)
|
||||
self.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsMovable, editor.mode is GraphEditorMode.NORMAL)
|
||||
self.setFlag(QGraphicsItem.GraphicsItemFlag.ItemSendsGeometryChanges)
|
||||
self.set_relative_position(label.relative_position)
|
||||
|
||||
def relative_position(self) -> tuple[int, int]:
|
||||
component_bounds = self.editor._component_bounds[self.component_id]
|
||||
return round(self.pos().x() + self.boundingRect().width() / 2), round(self.pos().y() - component_bounds.bottom())
|
||||
|
||||
def set_relative_position(self, position: tuple[int, int]) -> None:
|
||||
component_bounds = self.editor._component_bounds[self.component_id]
|
||||
self.setPos(position[0] - self.boundingRect().width() / 2, component_bounds.bottom() + position[1])
|
||||
|
||||
def itemChange(self, change: QGraphicsItem.GraphicsItemChange, value: object) -> object:
|
||||
if change == QGraphicsItem.GraphicsItemChange.ItemPositionChange and self._dragging and isinstance(value, QPointF):
|
||||
component_bounds = self.editor._component_bounds[self.component_id]
|
||||
size = self.editor.snap_to_grid_size
|
||||
relative_x = value.x() + self.boundingRect().width() / 2
|
||||
relative_y = value.y() - component_bounds.bottom()
|
||||
value = QPointF(round(relative_x / size) * size - self.boundingRect().width() / 2, component_bounds.bottom() + round(relative_y / size) * size)
|
||||
return super().itemChange(change, value)
|
||||
|
||||
def mousePressEvent(self, event: QGraphicsSceneMouseEvent) -> None:
|
||||
self._drag_start = self.relative_position()
|
||||
self._dragging = True
|
||||
super().mousePressEvent(event)
|
||||
|
||||
def mouseReleaseEvent(self, event: QGraphicsSceneMouseEvent) -> None:
|
||||
super().mouseReleaseEvent(event)
|
||||
self._dragging = False
|
||||
position = self.relative_position()
|
||||
self.set_relative_position(position)
|
||||
if position != self._drag_start:
|
||||
self.editor.finish_component_label_move(self.component_id, position)
|
||||
|
||||
def contextMenuEvent(self, event) -> None:
|
||||
self.editor.component_context_menu_requested.emit(self.component_id, event.screenPos())
|
||||
event.accept()
|
||||
|
||||
|
||||
class GraphConnectionPointItem(QGraphicsEllipseItem):
|
||||
def __init__(self, connection_id: ConnectionID, index: int, position: tuple[int, int], editor: GraphEditorWidget) -> None:
|
||||
radius = CONNECTION_WIDTH
|
||||
@@ -234,6 +287,7 @@ class GraphEditorWidget(QWidget):
|
||||
component_move_requested = Signal(object, object, object)
|
||||
component_context_menu_requested = Signal(object, object)
|
||||
component_open_requested = Signal(object)
|
||||
component_label_move_requested = Signal(object, object, object)
|
||||
connection_points_change_requested = Signal(object, object, object, str)
|
||||
connection_add_requested = Signal(object, object)
|
||||
connections_delete_requested = Signal(object, object)
|
||||
@@ -251,6 +305,7 @@ class GraphEditorWidget(QWidget):
|
||||
self._connection_preview: QGraphicsPathItem | None = None
|
||||
self._component_items: dict[ComponentID, GraphComponentItem] = {}
|
||||
self._component_bounds: dict[ComponentID, QRectF] = {}
|
||||
self._component_label_items: dict[ComponentID, GraphComponentLabelItem] = {}
|
||||
self._connection_items: dict[ConnectionID, GraphConnectionItem] = {}
|
||||
self._connection_point_items: dict[ConnectionID, list[GraphConnectionPointItem]] = {}
|
||||
self.set_snap_to_grid_size(snap_to_grid_size)
|
||||
@@ -310,6 +365,7 @@ class GraphEditorWidget(QWidget):
|
||||
self._icons = icons or {}
|
||||
self._component_items = {}
|
||||
self._component_bounds = {}
|
||||
self._component_label_items = {}
|
||||
self._connection_items = {}
|
||||
self._connection_point_items = {}
|
||||
self.scene.clear()
|
||||
@@ -331,6 +387,11 @@ class GraphEditorWidget(QWidget):
|
||||
self._component_bounds[component_id] = bounds.translated(-pixmap.width() / 2, -pixmap.height() / 2)
|
||||
self.scene.addItem(item)
|
||||
|
||||
for component_id, child in component.implementation.graph.components.items():
|
||||
label = graph.component_labels.get(component_id, GraphComponentLabel())
|
||||
if label.visible:
|
||||
self._create_component_label_item(component_id, child.name, label)
|
||||
|
||||
for connection_id, connection in component.implementation.graph.connections.items():
|
||||
if not isinstance(connection, (SignalConnection, BondConnection)):
|
||||
continue
|
||||
@@ -459,6 +520,31 @@ class GraphEditorWidget(QWidget):
|
||||
if self._component is not None:
|
||||
self.component_move_requested.emit(self._component, component_id, position)
|
||||
|
||||
def finish_component_label_move(self, component_id: ComponentID, relative_position: tuple[int, int]) -> None:
|
||||
if self._component is not None:
|
||||
self.component_label_move_requested.emit(self._component, component_id, relative_position)
|
||||
|
||||
def component_label_visible(self, component_id: ComponentID) -> bool:
|
||||
return self._graph.component_labels.get(component_id, GraphComponentLabel()).visible
|
||||
|
||||
def set_component_label(self, component_id: ComponentID, label: GraphComponentLabel | None) -> None:
|
||||
if label is None:
|
||||
self._graph.component_labels.pop(component_id, None)
|
||||
label = GraphComponentLabel()
|
||||
else:
|
||||
self._graph.component_labels[component_id] = label
|
||||
item = self._component_label_items.pop(component_id, None)
|
||||
if item is not None:
|
||||
item.setParentItem(None)
|
||||
self.scene.removeItem(item)
|
||||
component = self._component
|
||||
if component is not None and label.visible and component_id in component.implementation.graph.components:
|
||||
self._create_component_label_item(component_id, component.implementation.graph.components[component_id].name, label)
|
||||
|
||||
def _create_component_label_item(self, component_id: ComponentID, text: str, label: GraphComponentLabel) -> None:
|
||||
item = GraphComponentLabelItem(component_id, text, label, self._component_items[component_id], self)
|
||||
self._component_label_items[component_id] = item
|
||||
|
||||
def set_component_position(self, component_id: ComponentID, position: tuple[int, int] | None) -> None:
|
||||
item = self._component_items.get(component_id)
|
||||
if item is None:
|
||||
|
||||
Reference in New Issue
Block a user