Small fixes and bigger library

This commit is contained in:
2026-08-18 14:22:01 +02:00
parent 60a30d4ee3
commit 1d535c1f5b
21 changed files with 935 additions and 41 deletions

View File

@@ -5,40 +5,46 @@ from bedit_gui.models import Icon
class AddEmptyGraphComponent(QUndoCommand):
def __init__(self, document: object, parent: Component) -> None:
def __init__(self, document: object, parent: Component | dict[ComponentID, Component]) -> None:
super().__init__("Add graph component")
if not isinstance(parent.implementation, GraphImplementation):
raise TypeError("parent component must have a graph implementation")
self.document = document
self.graph = parent.implementation.graph
if isinstance(parent, Component):
if not isinstance(parent.implementation, GraphImplementation):
raise TypeError("parent component must have a graph implementation")
self.components = parent.implementation.graph.components
else:
self.components = parent
self.component_id = ComponentID()
self.component = Component(name="New Graph Component", interface=Interface(), parameters={}, implementation=GraphImplementation(Graph()))
self.component = Component(name="new_graph_component", interface=Interface(), parameters={}, implementation=GraphImplementation(Graph()))
def redo(self) -> None:
self.graph.components[self.component_id] = self.component
self.components[self.component_id] = self.component
self.document.model_changed.emit(self.document.model)
def undo(self) -> None:
del self.graph.components[self.component_id]
del self.components[self.component_id]
self.document.model_changed.emit(self.document.model)
class AddEmptyEquationComponent(QUndoCommand):
def __init__(self, document: object, parent: Component) -> None:
def __init__(self, document: object, parent: Component | dict[ComponentID, Component]) -> None:
super().__init__("Add equation component")
if not isinstance(parent.implementation, GraphImplementation):
raise TypeError("parent component must have a graph implementation")
self.document = document
self.graph = parent.implementation.graph
if isinstance(parent, Component):
if not isinstance(parent.implementation, GraphImplementation):
raise TypeError("parent component must have a graph implementation")
self.components = parent.implementation.graph.components
else:
self.components = parent
self.component_id = ComponentID()
self.component = Component(name="New Equation Component", interface=Interface(), parameters={}, implementation=EquationImplementation())
def redo(self) -> None:
self.graph.components[self.component_id] = self.component
self.components[self.component_id] = self.component
self.document.model_changed.emit(self.document.model)
def undo(self) -> None:
del self.graph.components[self.component_id]
del self.components[self.component_id]
self.document.model_changed.emit(self.document.model)

View File

@@ -17,7 +17,7 @@ from bedit_gui.views.main_window import MainWindow
from bedit_gui.views.models.document_tree_model import DocumentTreeModel
from bedit_gui.utils.icon import render_fitted_icon
ICON_SIZE = QSize(32, 32)
ICON_SIZE = QSize(16, 16)
class InterfaceEditorLike(Protocol):
def exec(self) -> int: ...
@@ -84,11 +84,11 @@ class DocumentTreeController(QObject):
window.ui.documentTree.setHeaderHidden(True)
window.ui.documentTree.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection)
window.ui.documentTree.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
window.ui.documentTree.setIconSize(QSize(48, 48))
window.ui.documentTree.setIconSize(QSize(24, 24))
window.ui.documentTree.header().setStretchLastSection(False)
window.ui.documentTree.header().setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch)
window.ui.documentTree.header().setSectionResizeMode(1, QHeaderView.ResizeMode.Fixed)
window.ui.documentTree.setColumnWidth(1, 56)
window.ui.documentTree.setColumnWidth(1, 28)
self._tree_viewport = window.ui.documentTree.viewport()
self._tree_viewport.installEventFilter(self)
@@ -175,11 +175,22 @@ class DocumentTreeController(QObject):
def _show_context_menu(self, position: QPoint) -> None:
index = self.window.ui.documentTree.indexAt(position)
component = self.model.value(index)
if not isinstance(component, Component):
return
value = self.model.value(index)
global_position = self.window.ui.documentTree.viewport().mapToGlobal(position)
if isinstance(value, CoreDocument):
self._show_root_context_menu(global_position)
elif isinstance(value, Component):
self._show_component_context_menu(value, global_position)
self._show_component_context_menu(component, self.window.ui.documentTree.viewport().mapToGlobal(position))
def _show_root_context_menu(self, global_position: QPoint) -> None:
menu = QMenu(self.window.ui.documentTree)
add_graph_component = menu.addAction("Add Graph Component")
add_equation_component = menu.addAction("Add Equation Component")
selected = menu.exec(global_position)
if selected is add_graph_component:
self.document.add_empty_root_graph_component()
elif selected is add_equation_component:
self.document.add_empty_root_equation_component()
def _show_graph_component_context_menu(self, component_id: ComponentID, global_position: QPoint) -> None:
component = self._components.get(component_id)

View File

@@ -3,6 +3,7 @@ from __future__ import annotations
import logging
from PySide6.QtCore import QObject, Signal
from PySide6.QtWidgets import QAbstractItemView
from bedit_gui.services.application_logging import configure_logging
from bedit_gui.views.main_window import MainWindow
@@ -49,4 +50,6 @@ class LogController(QObject):
self.emitter.message.connect(self.model.append)
self.model.rowsInserted.connect(window.ui.listView.scrollToBottom)
window.ui.listView.setModel(self.model)
window.ui.listView.setWordWrap(True)
window.ui.listView.setVerticalScrollMode(QAbstractItemView.ScrollMode.ScrollPerPixel)
configure_logging(self.handler, level)

View File

@@ -65,7 +65,7 @@ class SimulationController(QObject):
self.document.infer_causality(component)
build = self.compiler(component, build_directory)
except (OSError, RuntimeError, ValueError) as exc:
logger.exception("Could not compile model %s", component_path)
logger.error("Could not compile model %s:\n%s", component_path, exc)
QMessageBox.critical(self.window, "Could not compile", str(exc))
self.window.statusBar().showMessage("Compilation failed")
return None

View File

@@ -369,12 +369,22 @@ class Document(QObject):
command.component.name = self._unique_component_name(component.implementation.graph.components, command.component.name)
self.undo_stack.push(command)
def add_empty_root_graph_component(self) -> None:
command = AddEmptyGraphComponent(self, self.model.root)
command.component.name = self._unique_component_name(self.model.root, command.component.name)
self.undo_stack.push(command)
def add_empty_equation_component(self, component: Component) -> None:
if isinstance(component.implementation, GraphImplementation):
command = AddEmptyEquationComponent(self, component)
command.component.name = self._unique_component_name(component.implementation.graph.components, command.component.name)
self.undo_stack.push(command)
def add_empty_root_equation_component(self) -> None:
command = AddEmptyEquationComponent(self, self.model.root)
command.component.name = self._unique_component_name(self.model.root, command.component.name)
self.undo_stack.push(command)
def delete_component(self, component: Component) -> None:
self.undo_stack.push(DeleteComponent(self, component))

View File

@@ -24,6 +24,8 @@ class Shape:
def from_data(cls, data: Mapping[str, Any]) -> Shape:
if cls is Shape and data.get("type") == "rectangle":
return Rectangle.from_data(data)
if cls is Shape and data.get("type") == "ellipse":
return Ellipse.from_data(data)
if cls is Shape and data.get("type") == "text":
return Text.from_data(data)
if cls is Shape and data.get("type") == "line":
@@ -77,6 +79,24 @@ class Rectangle(Shape):
def to_data(self) -> dict[str, Any]:
return {**super().to_data(), "width": self.width, "height": self.height, "line_type": self.line_type.value, "line_thickness": self.line_thickness, "corner_radius": self.corner_radius, "line_color": self.line_color, "fill_color": self.fill_color}
@dataclass
class Ellipse(Shape):
type: str = field(init=False, default="ellipse")
width: float = 32.0
height: float = 32.0
line_type: LineType = LineType.SOLID
line_thickness: float = 1.0
line_color: str = "#000000ff"
fill_color: str = "#ffffff00"
@classmethod
def from_data(cls, data: Mapping[str, Any]) -> Ellipse:
pos = data.get("pos", [0, 0])
return cls(layer=int(data.get("layer", 0)), pos=(int(pos[0]), int(pos[1])), width=float(data.get("width", 100.0)), height=float(data.get("height", 100.0)), line_type=LineType(data.get("line_type", "solid")), line_thickness=float(data.get("line_thickness", 1.0)), line_color=str(data.get("line_color", "#000000ff")), fill_color=str(data.get("fill_color", "#ffffff00")))
def to_data(self) -> dict[str, Any]:
return {**super().to_data(), "width": self.width, "height": self.height, "line_type": self.line_type.value, "line_thickness": self.line_thickness, "line_color": self.line_color, "fill_color": self.fill_color}
@dataclass
class Text(Shape):
type: str = field(init=False, default="text")

View File

@@ -30,7 +30,7 @@
<x>0</x>
<y>0</y>
<width>800</width>
<height>22</height>
<height>19</height>
</rect>
</property>
<widget class="QMenu" name="menuEdit">
@@ -80,6 +80,7 @@
<bool>false</bool>
</attribute>
<addaction name="actionAdd_Rectangle"/>
<addaction name="actionAdd_Circle"/>
<addaction name="actionAdd_Text"/>
<addaction name="actionAdd_Line"/>
</widget>
@@ -236,6 +237,21 @@
<enum>QAction::MenuRole::NoRole</enum>
</property>
</action>
<action name="actionAdd_Circle">
<property name="icon">
<iconset resource="../../resources/resources.qrc">
<normaloff>:/icons/icons/draw-circle.png</normaloff>:/icons/icons/draw-circle.png</iconset>
</property>
<property name="text">
<string>Add Ellipse</string>
</property>
<property name="toolTip">
<string>Add a circle</string>
</property>
<property name="menuRole">
<enum>QAction::MenuRole::NoRole</enum>
</property>
</action>
</widget>
<resources>
<include location="../../resources/resources.qrc"/>

View File

@@ -0,0 +1,206 @@
# -*- coding: utf-8 -*-
################################################################################
## Form generated from reading UI file 'icon_editor_window.ui'
##
## Created by: Qt User Interface Compiler version 6.11.1
##
## WARNING! All changes made in this file will be lost when recompiling UI file!
################################################################################
from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale,
QMetaObject, QObject, QPoint, QRect,
QSize, QTime, QUrl, Qt)
from PySide6.QtGui import (QAction, QBrush, QColor, QConicalGradient,
QCursor, QFont, QFontDatabase, QGradient,
QIcon, QImage, QKeySequence, QLinearGradient,
QPainter, QPalette, QPixmap, QRadialGradient,
QTransform)
from PySide6.QtWidgets import (QApplication, QGraphicsView, QMainWindow, QMenu,
QMenuBar, QSizePolicy, QStatusBar, QToolBar,
QVBoxLayout, QWidget)
import resources_rc
class Ui_iconEditor(object):
def setupUi(self, iconEditor):
if not iconEditor.objectName():
iconEditor.setObjectName(u"iconEditor")
iconEditor.resize(800, 600)
icon = QIcon()
icon.addFile(u":/icons/icons/draw-path.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
iconEditor.setWindowIcon(icon)
self.actionUndo = QAction(iconEditor)
self.actionUndo.setObjectName(u"actionUndo")
icon1 = QIcon()
icon1.addFile(u":/icons/icons/edit-undo.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionUndo.setIcon(icon1)
self.actionUndo.setMenuRole(QAction.MenuRole.NoRole)
self.actionRedo = QAction(iconEditor)
self.actionRedo.setObjectName(u"actionRedo")
icon2 = QIcon()
icon2.addFile(u":/icons/icons/edit-redo.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionRedo.setIcon(icon2)
self.actionRedo.setMenuRole(QAction.MenuRole.NoRole)
self.actionSave = QAction(iconEditor)
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)
self.actionSave.setMenuRole(QAction.MenuRole.NoRole)
self.actionCancel = QAction(iconEditor)
self.actionCancel.setObjectName(u"actionCancel")
icon4 = QIcon()
icon4.addFile(u":/icons/icons/dialog-close.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionCancel.setIcon(icon4)
self.actionCancel.setMenuRole(QAction.MenuRole.NoRole)
self.actionAdd_Rectangle = QAction(iconEditor)
self.actionAdd_Rectangle.setObjectName(u"actionAdd_Rectangle")
icon5 = QIcon()
icon5.addFile(u":/icons/icons/draw-rectangle.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionAdd_Rectangle.setIcon(icon5)
self.actionAdd_Rectangle.setMenuRole(QAction.MenuRole.NoRole)
self.actionAdd_Text = QAction(iconEditor)
self.actionAdd_Text.setObjectName(u"actionAdd_Text")
icon6 = QIcon()
icon6.addFile(u":/icons/icons/draw-text.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionAdd_Text.setIcon(icon6)
self.actionAdd_Text.setMenuRole(QAction.MenuRole.NoRole)
self.actionSave_to_File = QAction(iconEditor)
self.actionSave_to_File.setObjectName(u"actionSave_to_File")
icon7 = QIcon()
icon7.addFile(u":/icons/icons/document-save-as.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionSave_to_File.setIcon(icon7)
self.actionSave_to_File.setMenuRole(QAction.MenuRole.NoRole)
self.actionOpen_from_File = QAction(iconEditor)
self.actionOpen_from_File.setObjectName(u"actionOpen_from_File")
icon8 = QIcon()
icon8.addFile(u":/icons/icons/document-open.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionOpen_from_File.setIcon(icon8)
self.actionOpen_from_File.setMenuRole(QAction.MenuRole.NoRole)
self.actionAdd_Line = QAction(iconEditor)
self.actionAdd_Line.setObjectName(u"actionAdd_Line")
self.actionAdd_Line.setIcon(icon)
self.actionAdd_Line.setMenuRole(QAction.MenuRole.NoRole)
self.actionAdd_Circle = QAction(iconEditor)
self.actionAdd_Circle.setObjectName(u"actionAdd_Circle")
icon9 = QIcon()
icon9.addFile(u":/icons/icons/draw-circle.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionAdd_Circle.setIcon(icon9)
self.actionAdd_Circle.setMenuRole(QAction.MenuRole.NoRole)
self.centralwidget = QWidget(iconEditor)
self.centralwidget.setObjectName(u"centralwidget")
self.verticalLayout = QVBoxLayout(self.centralwidget)
self.verticalLayout.setObjectName(u"verticalLayout")
self.graphicsView = QGraphicsView(self.centralwidget)
self.graphicsView.setObjectName(u"graphicsView")
self.verticalLayout.addWidget(self.graphicsView)
iconEditor.setCentralWidget(self.centralwidget)
self.menubar = QMenuBar(iconEditor)
self.menubar.setObjectName(u"menubar")
self.menubar.setGeometry(QRect(0, 0, 800, 19))
self.menuEdit = QMenu(self.menubar)
self.menuEdit.setObjectName(u"menuEdit")
self.menuFile = QMenu(self.menubar)
self.menuFile.setObjectName(u"menuFile")
iconEditor.setMenuBar(self.menubar)
self.statusbar = QStatusBar(iconEditor)
self.statusbar.setObjectName(u"statusbar")
iconEditor.setStatusBar(self.statusbar)
self.actionToolbar = QToolBar(iconEditor)
self.actionToolbar.setObjectName(u"actionToolbar")
iconEditor.addToolBar(Qt.ToolBarArea.TopToolBarArea, self.actionToolbar)
self.iconToolbar = QToolBar(iconEditor)
self.iconToolbar.setObjectName(u"iconToolbar")
iconEditor.addToolBar(Qt.ToolBarArea.TopToolBarArea, self.iconToolbar)
self.menubar.addAction(self.menuFile.menuAction())
self.menubar.addAction(self.menuEdit.menuAction())
self.menuEdit.addAction(self.actionUndo)
self.menuEdit.addAction(self.actionRedo)
self.menuFile.addAction(self.actionOpen_from_File)
self.menuFile.addAction(self.actionSave_to_File)
self.menuFile.addSeparator()
self.menuFile.addAction(self.actionSave)
self.menuFile.addAction(self.actionCancel)
self.actionToolbar.addAction(self.actionUndo)
self.actionToolbar.addAction(self.actionRedo)
self.actionToolbar.addAction(self.actionSave)
self.actionToolbar.addAction(self.actionCancel)
self.iconToolbar.addAction(self.actionAdd_Rectangle)
self.iconToolbar.addAction(self.actionAdd_Circle)
self.iconToolbar.addAction(self.actionAdd_Text)
self.iconToolbar.addAction(self.actionAdd_Line)
self.retranslateUi(iconEditor)
QMetaObject.connectSlotsByName(iconEditor)
# setupUi
def retranslateUi(self, iconEditor):
iconEditor.setWindowTitle(QCoreApplication.translate("iconEditor", u"MainWindow", None))
self.actionUndo.setText(QCoreApplication.translate("iconEditor", u"Undo", None))
#if QT_CONFIG(tooltip)
self.actionUndo.setToolTip(QCoreApplication.translate("iconEditor", u"Undo", None))
#endif // QT_CONFIG(tooltip)
#if QT_CONFIG(shortcut)
self.actionUndo.setShortcut(QCoreApplication.translate("iconEditor", u"Ctrl+Z", None))
#endif // QT_CONFIG(shortcut)
self.actionRedo.setText(QCoreApplication.translate("iconEditor", u"Redo", None))
#if QT_CONFIG(tooltip)
self.actionRedo.setToolTip(QCoreApplication.translate("iconEditor", u"Redo", None))
#endif // QT_CONFIG(tooltip)
#if QT_CONFIG(shortcut)
self.actionRedo.setShortcut(QCoreApplication.translate("iconEditor", u"Ctrl+Y", None))
#endif // QT_CONFIG(shortcut)
self.actionSave.setText(QCoreApplication.translate("iconEditor", u"Save", None))
#if QT_CONFIG(tooltip)
self.actionSave.setToolTip(QCoreApplication.translate("iconEditor", u"Save icon", None))
#endif // QT_CONFIG(tooltip)
#if QT_CONFIG(shortcut)
self.actionSave.setShortcut(QCoreApplication.translate("iconEditor", u"Return", None))
#endif // QT_CONFIG(shortcut)
self.actionCancel.setText(QCoreApplication.translate("iconEditor", u"Cancel", None))
#if QT_CONFIG(tooltip)
self.actionCancel.setToolTip(QCoreApplication.translate("iconEditor", u"Cancel icon editing", None))
#endif // QT_CONFIG(tooltip)
#if QT_CONFIG(shortcut)
self.actionCancel.setShortcut(QCoreApplication.translate("iconEditor", u"Shift+Esc", None))
#endif // QT_CONFIG(shortcut)
self.actionAdd_Rectangle.setText(QCoreApplication.translate("iconEditor", u"Add Rectangle", None))
#if QT_CONFIG(tooltip)
self.actionAdd_Rectangle.setToolTip(QCoreApplication.translate("iconEditor", u"Add a rectangle", None))
#endif // QT_CONFIG(tooltip)
self.actionAdd_Text.setText(QCoreApplication.translate("iconEditor", u"Add Text", None))
#if QT_CONFIG(tooltip)
self.actionAdd_Text.setToolTip(QCoreApplication.translate("iconEditor", u"Add a text field", None))
#endif // QT_CONFIG(tooltip)
self.actionSave_to_File.setText(QCoreApplication.translate("iconEditor", u"Save to File", None))
#if QT_CONFIG(tooltip)
self.actionSave_to_File.setToolTip(QCoreApplication.translate("iconEditor", u"Save icon to a file", None))
#endif // QT_CONFIG(tooltip)
#if QT_CONFIG(shortcut)
self.actionSave_to_File.setShortcut(QCoreApplication.translate("iconEditor", u"Ctrl+Shift+S", None))
#endif // QT_CONFIG(shortcut)
self.actionOpen_from_File.setText(QCoreApplication.translate("iconEditor", u"Open from File", None))
#if QT_CONFIG(tooltip)
self.actionOpen_from_File.setToolTip(QCoreApplication.translate("iconEditor", u"Open icon from File", None))
#endif // QT_CONFIG(tooltip)
#if QT_CONFIG(shortcut)
self.actionOpen_from_File.setShortcut(QCoreApplication.translate("iconEditor", u"Ctrl+Shift+O", None))
#endif // QT_CONFIG(shortcut)
self.actionAdd_Line.setText(QCoreApplication.translate("iconEditor", u"Add Line", None))
#if QT_CONFIG(tooltip)
self.actionAdd_Line.setToolTip(QCoreApplication.translate("iconEditor", u"Add a line", None))
#endif // QT_CONFIG(tooltip)
self.actionAdd_Circle.setText(QCoreApplication.translate("iconEditor", u"Add Ellipse", None))
#if QT_CONFIG(tooltip)
self.actionAdd_Circle.setToolTip(QCoreApplication.translate("iconEditor", u"Add a circle", None))
#endif // QT_CONFIG(tooltip)
self.menuEdit.setTitle(QCoreApplication.translate("iconEditor", u"Edit", None))
self.menuFile.setTitle(QCoreApplication.translate("iconEditor", u"File", None))
self.actionToolbar.setWindowTitle(QCoreApplication.translate("iconEditor", u"toolBar", None))
self.iconToolbar.setWindowTitle(QCoreApplication.translate("iconEditor", u"toolBar", None))
# retranslateUi

View File

@@ -6,7 +6,7 @@ from PySide6.QtCore import QRectF, QSize, Qt
from PySide6.QtGui import QBrush, QColor, QFont, QIcon, QPainter, QPen, QPixmap, QRegion
from bedit_core.models import Port, PortID, SignalDirection
from bedit_gui.models import Icon, Line, LineType, Rectangle, Text
from bedit_gui.models import Ellipse, Icon, Line, LineType, Rectangle, Text
PORT_SIZE = 16
DEFAULT_ICON_SIZE = QSize(48, 48)
@@ -21,7 +21,7 @@ def get_bounding_box(icon: Icon) -> QRectF:
for shape in icon.shapes.values():
x, y = shape.pos
points.append((x, y))
if isinstance(shape, (Rectangle, Text)):
if isinstance(shape, (Rectangle, Ellipse, Text)):
points.append((x + shape.width, y + shape.height))
elif isinstance(shape, Line):
points.append(shape.end)
@@ -72,6 +72,10 @@ def render_icon(icon: Icon, ports: dict[PortID, Port], size: QSize = DEFAULT_ICO
painter.setPen(_line_pen(shape.line_type, shape.line_thickness, shape.line_color))
painter.setBrush(QBrush(_color(shape.fill_color)))
painter.drawRoundedRect(QRectF(shape.pos[0], shape.pos[1], shape.width, shape.height), shape.corner_radius, shape.corner_radius)
elif isinstance(shape, Ellipse):
painter.setPen(_line_pen(shape.line_type, shape.line_thickness, shape.line_color))
painter.setBrush(QBrush(_color(shape.fill_color)))
painter.drawEllipse(QRectF(shape.pos[0], shape.pos[1], shape.width, shape.height))
elif isinstance(shape, Text):
font = QFont()
font.setPixelSize(max(1, round(shape.size)))

View File

@@ -162,7 +162,6 @@ class GraphComponentItem(QGraphicsPixmapItem):
def mousePressEvent(self, event: QGraphicsSceneMouseEvent) -> None:
if event.button() == Qt.MouseButton.LeftButton and self.editor.mode is GraphEditorMode.CONNECTION:
self.editor.choose_connection_component(self.component_id, event.screenPos())
event.accept()
return
self._drag_start = QPointF(self.pos())
@@ -171,6 +170,8 @@ class GraphComponentItem(QGraphicsPixmapItem):
def mouseReleaseEvent(self, event: QGraphicsSceneMouseEvent) -> None:
if self.editor.mode is GraphEditorMode.CONNECTION:
if event.button() == Qt.MouseButton.LeftButton:
self.editor.choose_connection_component(self.component_id, event.screenPos())
event.accept()
return
super().mouseReleaseEvent(event)
@@ -723,7 +724,7 @@ class GraphEditorWidget(QWidget):
@staticmethod
def _port_available(port_id: PortID, port: Port, used_ports: set[PortID]) -> bool:
if isinstance(port, SignalPort):
return port.direction is SignalDirection.OUTPUT or port_id not in used_ports
return port.direction is SignalDirection.OUTPUT or port.multiplicity or port_id not in used_ports
if isinstance(port, BondPort):
return port.multiplicity or port_id not in used_ports
return False

View File

@@ -11,7 +11,7 @@ from bedit_gui.models import Icon, Shape, ShapeID
from bedit_gui.services import icon_files
from bedit_gui.services.application_logging import get_logger
from bedit_gui.ui.generated.ui_icon_editor_window import Ui_iconEditor
from bedit_gui.views.icon_graphics_scene import IconGraphicsScene, LineCreationTool, RectangleCreationTool, TextCreationTool
from bedit_gui.views.icon_graphics_scene import EllipseCreationTool, IconGraphicsScene, LineCreationTool, RectangleCreationTool, TextCreationTool
from bedit_gui.views.shape_options_dialog import ShapeOptionsDialog
logger = get_logger(__name__)
@@ -123,6 +123,8 @@ class IconEditorWindow(QMainWindow):
self.ui.graphicsView.viewport().installEventFilter(self)
self.ui.actionAdd_Rectangle.setCheckable(True)
self.ui.actionAdd_Rectangle.triggered.connect(self._start_rectangle_tool)
self.ui.actionAdd_Circle.setCheckable(True)
self.ui.actionAdd_Circle.triggered.connect(self._start_ellipse_tool)
self.ui.actionAdd_Text.setCheckable(True)
self.ui.actionAdd_Text.triggered.connect(self._start_text_tool)
self.ui.actionAdd_Line.setCheckable(True)
@@ -306,14 +308,24 @@ class IconEditorWindow(QMainWindow):
def _start_rectangle_tool(self) -> None:
layer = max((shape.layer for shape in self._icon.shapes.values()), default=-1) + 1
self.ui.actionAdd_Circle.setChecked(False)
self.ui.actionAdd_Text.setChecked(False)
self.ui.actionAdd_Line.setChecked(False)
self.scene.set_creation_tool(RectangleCreationTool(self.scene, layer))
self.ui.actionAdd_Rectangle.setChecked(True)
def _start_ellipse_tool(self) -> None:
layer = max((shape.layer for shape in self._icon.shapes.values()), default=-1) + 1
self.ui.actionAdd_Rectangle.setChecked(False)
self.ui.actionAdd_Text.setChecked(False)
self.ui.actionAdd_Line.setChecked(False)
self.scene.set_creation_tool(EllipseCreationTool(self.scene, layer))
self.ui.actionAdd_Circle.setChecked(True)
def _start_text_tool(self) -> None:
layer = max((shape.layer for shape in self._icon.shapes.values()), default=-1) + 1
self.ui.actionAdd_Rectangle.setChecked(False)
self.ui.actionAdd_Circle.setChecked(False)
self.ui.actionAdd_Line.setChecked(False)
self.scene.set_creation_tool(TextCreationTool(self.scene, layer))
self.ui.actionAdd_Text.setChecked(True)
@@ -321,6 +333,7 @@ class IconEditorWindow(QMainWindow):
def _start_line_tool(self) -> None:
layer = max((shape.layer for shape in self._icon.shapes.values()), default=-1) + 1
self.ui.actionAdd_Rectangle.setChecked(False)
self.ui.actionAdd_Circle.setChecked(False)
self.ui.actionAdd_Text.setChecked(False)
self.scene.set_creation_tool(LineCreationTool(self.scene, layer))
self.ui.actionAdd_Line.setChecked(True)
@@ -328,6 +341,7 @@ class IconEditorWindow(QMainWindow):
def _tool_active_changed(self, active: bool) -> None:
if not active:
self.ui.actionAdd_Rectangle.setChecked(False)
self.ui.actionAdd_Circle.setChecked(False)
self.ui.actionAdd_Text.setChecked(False)
self.ui.actionAdd_Line.setChecked(False)
cursor = Qt.CursorShape.CrossCursor if active else Qt.CursorShape.ArrowCursor

View File

@@ -6,10 +6,10 @@ from typing import Protocol
from PySide6.QtCore import QLineF, QObject, QPointF, QRectF, Qt, Signal
from PySide6.QtGui import QBrush, QColor, QFont, QPainter, QPainterPath, QPen
from PySide6.QtWidgets import QGraphicsItem, QGraphicsLineItem, QGraphicsPathItem, QGraphicsRectItem, QGraphicsScene, QGraphicsSceneContextMenuEvent, QGraphicsSceneMouseEvent, QMenu, QStyleOptionGraphicsItem, QWidget
from PySide6.QtWidgets import QGraphicsEllipseItem, QGraphicsItem, QGraphicsLineItem, QGraphicsPathItem, QGraphicsRectItem, QGraphicsScene, QGraphicsSceneContextMenuEvent, QGraphicsSceneMouseEvent, QMenu, QStyleOptionGraphicsItem, QWidget
from bedit_core.models import Port, PortID, SignalDirection
from bedit_gui.models import Icon, Line, LineType, Rectangle, Shape, ShapeID, Text
from bedit_gui.models import Ellipse, Icon, Line, LineType, Rectangle, Shape, ShapeID, Text
ICON_SCENE_SIZE = 512
@@ -26,7 +26,7 @@ class RectangleCreationTool:
self.scene = scene
self.layer = layer
self.start: QPointF | None = None
self.preview: QGraphicsRectItem | None = None
self.preview: QGraphicsRectItem | QGraphicsEllipseItem | None = None
def begin(self, position: QPointF) -> None:
position = self._bounded(position)
@@ -69,6 +69,16 @@ class TextCreationTool(RectangleCreationTool):
return Text(layer=self.layer, pos=(round(rect.x()), round(rect.y())), width=rect.width(), height=rect.height(), text="Text")
class EllipseCreationTool(RectangleCreationTool):
def begin(self, position: QPointF) -> None:
position = self._bounded(position)
self.start = position
self.preview = self.scene.addEllipse(QRectF(position, position), QPen(Qt.PenStyle.DashLine))
def create_shape(self, rect: QRectF) -> Shape:
return Ellipse(layer=self.layer, pos=(round(rect.x()), round(rect.y())), width=rect.width(), height=rect.height())
class LineCreationTool:
def __init__(self, scene: QGraphicsScene, layer: int) -> None:
self.scene = scene
@@ -109,7 +119,8 @@ class LineCreationTool:
class ShapeGraphicsItem(QGraphicsPathItem):
handle_size = 6.0
handle_size = 8.0
handle_hit_size = 16.0
def __init__(self, shape_id: ShapeID, shape: Shape, scene: IconGraphicsScene) -> None:
super().__init__()
@@ -128,13 +139,24 @@ class ShapeGraphicsItem(QGraphicsPathItem):
def resize_handles(self) -> dict[str, QRectF]:
return {"size": self.resize_handle_rect()}
def resize_handle_hit_rects(self) -> dict[str, QRectF]:
size = self.handle_hit_size
return {name: QRectF(rect.center().x() - size / 2, rect.center().y() - size / 2, size, size) for name, rect in self.resize_handles().items()}
def boundingRect(self) -> QRectF:
margin = self.handle_size / 2
margin = self.handle_hit_size / 2
return super().boundingRect().adjusted(-margin, -margin, margin, margin)
def shape(self) -> QPainterPath:
path = super().shape()
if self.isSelected():
for rect in self.resize_handle_hit_rects().values():
path.addRect(rect)
return path
def mousePressEvent(self, event: QGraphicsSceneMouseEvent) -> None:
self._original_shape = self.current_shape()
self._resize_handle = next((name for name, rect in self.resize_handles().items() if self.isSelected() and rect.contains(event.pos())), None)
self._resize_handle = next((name for name, rect in self.resize_handle_hit_rects().items() if self.isSelected() and rect.contains(event.pos())), None)
if self._resize_handle is not None:
event.accept()
return
@@ -226,6 +248,37 @@ class RectangleGraphicsItem(ShapeGraphicsItem):
self.setPath(path)
class EllipseGraphicsItem(ShapeGraphicsItem):
def __init__(self, shape_id: ShapeID, shape: Ellipse, scene: IconGraphicsScene) -> None:
super().__init__(shape_id, shape, scene)
self.ellipse = deepcopy(shape)
self.setPos(shape.pos[0], shape.pos[1])
self._set_size(shape.width, shape.height)
self.setPen(scene._pen(shape))
self.setBrush(QBrush(scene._color(shape.fill_color)))
self.setZValue(shape.layer)
def current_shape(self) -> Ellipse:
shape = deepcopy(self.ellipse)
shape.pos = (round(self.pos().x()), round(self.pos().y()))
rect = self.path().boundingRect()
shape.width = rect.width()
shape.height = rect.height()
return shape
def resize_to(self, position: QPointF, _handle: str) -> None:
bounds = self.icon_scene.sceneRect()
width = max(1.0, min(bounds.right(), round(position.x())) - self.pos().x())
height = max(1.0, min(bounds.bottom(), round(position.y())) - self.pos().y())
self._set_size(width, height)
def _set_size(self, width: float, height: float) -> None:
self.prepareGeometryChange()
path = QPainterPath()
path.addEllipse(QRectF(0, 0, width, height))
self.setPath(path)
class TextGraphicsItem(ShapeGraphicsItem):
def __init__(self, shape_id: ShapeID, shape: Text, scene: IconGraphicsScene) -> None:
super().__init__(shape_id, shape, scene)
@@ -440,13 +493,15 @@ class IconGraphicsScene(QGraphicsScene):
def _add_shape_item(self, shape_id: ShapeID, shape: Shape) -> None:
if isinstance(shape, Rectangle):
self.addItem(RectangleGraphicsItem(shape_id, shape, self))
elif isinstance(shape, Ellipse):
self.addItem(EllipseGraphicsItem(shape_id, shape, self))
elif isinstance(shape, Text):
self.addItem(TextGraphicsItem(shape_id, shape, self))
elif isinstance(shape, Line):
self.addItem(LineGraphicsItem(shape_id, shape, self))
@staticmethod
def _pen(shape: Rectangle) -> QPen:
def _pen(shape: Rectangle | Ellipse) -> QPen:
return IconGraphicsScene._line_pen(shape.line_type, shape.line_thickness, shape.line_color)
@staticmethod

View File

@@ -5,7 +5,7 @@ from copy import deepcopy
from PySide6.QtCore import QRectF
from PySide6.QtWidgets import QCheckBox, QComboBox, QDialog, QDialogButtonBox, QDoubleSpinBox, QFormLayout, QLabel, QLineEdit, QSpinBox, QVBoxLayout, QWidget
from bedit_gui.models import Line, LineType, Rectangle, Shape, Text
from bedit_gui.models import Ellipse, Line, LineType, Rectangle, Shape, Text
from bedit_gui.views.color_button import ColorButton
@@ -34,6 +34,8 @@ class ShapeOptionsDialog(QDialog):
if isinstance(shape, Rectangle):
self._add_rectangle_fields(shape)
elif isinstance(shape, Ellipse):
self._add_ellipse_fields(shape)
elif isinstance(shape, Text):
self._add_text_fields(shape)
elif isinstance(shape, Line):
@@ -51,6 +53,10 @@ class ShapeOptionsDialog(QDialog):
width = min(self.width.value(), self._scene_rect.right() - self.x.value())
height = min(self.height.value(), self._scene_rect.bottom() - self.y.value())
return Rectangle(layer=self.layer.value(), pos=(self.x.value(), self.y.value()), width=width, height=height, line_type=self.line_type.currentData(), line_thickness=self.line_thickness.value(), corner_radius=self.corner_radius.value(), line_color=self.line_color.color(), fill_color=self.fill_color.color())
if isinstance(self._shape, Ellipse):
width = min(self.width.value(), self._scene_rect.right() - self.x.value())
height = min(self.height.value(), self._scene_rect.bottom() - self.y.value())
return Ellipse(layer=self.layer.value(), pos=(self.x.value(), self.y.value()), width=width, height=height, line_type=self.line_type.currentData(), line_thickness=self.line_thickness.value(), line_color=self.line_color.color(), fill_color=self.fill_color.color())
if isinstance(self._shape, Text):
width = min(self.width.value(), self._scene_rect.right() - self.x.value())
height = min(self.height.value(), self._scene_rect.bottom() - self.y.value())
@@ -89,6 +95,29 @@ class ShapeOptionsDialog(QDialog):
self.form.addRow("Line color", self.line_color)
self.form.addRow("Fill color", self.fill_color)
def _add_ellipse_fields(self, shape: Ellipse) -> None:
self.width = QSpinBox()
self.width.setRange(1, round(self._scene_rect.width()))
self.width.setValue(round(shape.width))
self.height = QSpinBox()
self.height.setRange(1, round(self._scene_rect.height()))
self.height.setValue(round(shape.height))
self.line_type = QComboBox()
for line_type in LineType:
self.line_type.addItem(line_type.value.replace("_", " ").title(), line_type)
self.line_type.setCurrentIndex(self.line_type.findData(shape.line_type))
self.line_thickness = QDoubleSpinBox()
self.line_thickness.setRange(0, 1000)
self.line_thickness.setValue(shape.line_thickness)
self.line_color = ColorButton(shape.line_color)
self.fill_color = ColorButton(shape.fill_color)
self.form.addRow("Width", self.width)
self.form.addRow("Height", self.height)
self.form.addRow("Line type", self.line_type)
self.form.addRow("Line thickness", self.line_thickness)
self.form.addRow("Line color", self.line_color)
self.form.addRow("Fill color", self.fill_color)
def _add_text_fields(self, shape: Text) -> None:
self.width = QSpinBox()
self.width.setRange(1, round(self._scene_rect.width()))