Icon in document tree
This commit is contained in:
25
Se.icon.json
Normal file
25
Se.icon.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"shapes": {
|
||||
"2a5bf670-c821-442b-9d46-1b07faa70722": {
|
||||
"layer": 0,
|
||||
"type": "text",
|
||||
"pos": [
|
||||
-48,
|
||||
-32
|
||||
],
|
||||
"width": 96.0,
|
||||
"height": 64.0,
|
||||
"color": "#000000ff",
|
||||
"bold": true,
|
||||
"italic": false,
|
||||
"size": 64.0,
|
||||
"text": "Se"
|
||||
}
|
||||
},
|
||||
"port_positions": {
|
||||
"5b6a8a0c-0875-402c-bd74-11d086aac372": [
|
||||
-8,
|
||||
-8
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -2,18 +2,21 @@ from collections.abc import Callable
|
||||
from functools import partial
|
||||
from typing import Protocol
|
||||
|
||||
from PySide6.QtCore import QObject, QPoint, Qt
|
||||
from PySide6.QtWidgets import QDialog, QMenu
|
||||
from PySide6.QtCore import QObject, QPoint, QSize, Qt
|
||||
from PySide6.QtWidgets import QDialog, QHeaderView, QMenu
|
||||
|
||||
from bedit_core.models import Component, Port, PortID, Parameter, ParameterID
|
||||
from bedit_core.models import Component, ComponentID, GraphImplementation, Port, PortID, Parameter, ParameterID
|
||||
from bedit_core.models import Document as CoreDocument
|
||||
from bedit_gui.documents import Document
|
||||
from bedit_gui.models import 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
|
||||
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_icon
|
||||
|
||||
ICON_SIZE = QSize(32, 32)
|
||||
|
||||
class InterfaceEditorLike(Protocol):
|
||||
def exec(self) -> int: ...
|
||||
@@ -51,13 +54,20 @@ class DocumentTreeController(QObject):
|
||||
self.interface_editor_factory = interface_editor_factory
|
||||
self.param_editor_factory = param_editor_factory
|
||||
self._icon_editors: list[IconEditorWindow] = []
|
||||
self._components: dict[ComponentID, Component] = {}
|
||||
|
||||
window.ui.documentTree.setModel(self.model)
|
||||
document.model_changed.connect(self._on_document_changed)
|
||||
document.icon_changed.connect(self._on_icon_changed)
|
||||
self.model.rename_document_requested.connect(self.document.rename)
|
||||
self.model.rename_component_requested.connect(self.document.rename_component)
|
||||
|
||||
window.ui.documentTree.setHeaderHidden(True)
|
||||
window.ui.documentTree.setIconSize(QSize(48, 48))
|
||||
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.setContextMenuPolicy(
|
||||
Qt.ContextMenuPolicy.CustomContextMenu
|
||||
)
|
||||
@@ -70,11 +80,28 @@ class DocumentTreeController(QObject):
|
||||
def _on_document_changed(self, model: CoreDocument) -> None:
|
||||
"""Rebuild the tree whenever New/Open replaces the core document."""
|
||||
self.model.set_document(model)
|
||||
self._components = {}
|
||||
self._collect_components(model.root)
|
||||
for component_id, component in self._components.items():
|
||||
icon = self.document.component_icon(component_id)
|
||||
self.model.set_component_icon(component_id, render_icon(icon, component.interface.ports, ICON_SIZE))
|
||||
|
||||
# Optional presentation behavior. Later, you could instead remember
|
||||
# expanded component IDs and restore only those nodes.
|
||||
self.window.ui.documentTree.expandAll()
|
||||
|
||||
def _on_icon_changed(self, component_id: ComponentID, icon: object) -> None:
|
||||
component = self._components.get(component_id)
|
||||
if component is None:
|
||||
return
|
||||
self.model.set_component_icon(component_id, render_icon(icon if isinstance(icon, Icon) else Icon(), component.interface.ports, ICON_SIZE))
|
||||
|
||||
def _collect_components(self, components: dict[ComponentID, Component]) -> None:
|
||||
for component_id, component in components.items():
|
||||
self._components[component_id] = component
|
||||
if isinstance(component.implementation, GraphImplementation):
|
||||
self._collect_components(component.implementation.graph.components)
|
||||
|
||||
def _show_context_menu(self, position: QPoint) -> None:
|
||||
index = self.window.ui.documentTree.indexAt(position)
|
||||
component = self.model.value(index)
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from PySide6.QtCore import QRectF
|
||||
from PySide6.QtCore import QRectF, QSize, Qt
|
||||
from PySide6.QtGui import QBrush, QColor, QFont, QIcon, QPainter, QPen, QPixmap
|
||||
|
||||
from bedit_gui.models import Icon, Line, Rectangle, Text
|
||||
from bedit_core.models import Port, PortID, SignalDirection
|
||||
from bedit_gui.models import Icon, Line, LineType, Rectangle, Text
|
||||
|
||||
PORT_SIZE = 16
|
||||
DEFAULT_ICON_SIZE = QSize(48, 48)
|
||||
|
||||
|
||||
def get_bounding_box(icon: Icon) -> QRectF:
|
||||
@@ -30,3 +33,64 @@ def get_bounding_box(icon: Icon) -> QRectF:
|
||||
right = max(point[0] for point in points)
|
||||
bottom = max(point[1] for point in points)
|
||||
return QRectF(left, top, right - left, bottom - top)
|
||||
|
||||
|
||||
def render_icon(icon: Icon, ports: dict[PortID, Port], size: QSize = DEFAULT_ICON_SIZE, render_ports: bool = False) -> QIcon:
|
||||
pixmap = QPixmap(size)
|
||||
pixmap.fill(Qt.GlobalColor.transparent)
|
||||
bounds = get_bounding_box(icon)
|
||||
if not icon.shapes and not icon.port_positions:
|
||||
return QIcon(pixmap)
|
||||
|
||||
available_width = max(1, size.width() - 4)
|
||||
available_height = max(1, size.height() - 4)
|
||||
scale = min(available_width / max(1, bounds.width()), available_height / max(1, bounds.height()))
|
||||
painter = QPainter(pixmap)
|
||||
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
|
||||
painter.translate(size.width() / 2, size.height() / 2)
|
||||
painter.scale(scale, scale)
|
||||
painter.translate(-bounds.center())
|
||||
|
||||
for shape in sorted(icon.shapes.values(), key=lambda item: item.layer):
|
||||
if isinstance(shape, Rectangle):
|
||||
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, Text):
|
||||
font = QFont()
|
||||
font.setPixelSize(max(1, round(shape.size)))
|
||||
font.setBold(shape.bold)
|
||||
font.setItalic(shape.italic)
|
||||
painter.setFont(font)
|
||||
painter.setPen(_color(shape.color))
|
||||
painter.drawText(QRectF(shape.pos[0], shape.pos[1], shape.width, shape.height), Qt.AlignmentFlag.AlignCenter | Qt.TextFlag.TextWordWrap, shape.text)
|
||||
elif isinstance(shape, Line):
|
||||
painter.setPen(_line_pen(shape.line_type, shape.line_thickness, shape.line_color))
|
||||
painter.drawLine(shape.pos[0], shape.pos[1], shape.end[0], shape.end[1])
|
||||
|
||||
if render_ports:
|
||||
painter.setPen(QPen(QColor("#000000")))
|
||||
for port_id, position in icon.port_positions.items():
|
||||
port = ports.get(port_id)
|
||||
if port is None:
|
||||
continue
|
||||
color = QColor("#000000") if port.direction is SignalDirection.INPUT else QColor("#ffffff")
|
||||
painter.setBrush(QBrush(color))
|
||||
painter.drawRect(position[0], position[1], PORT_SIZE, PORT_SIZE)
|
||||
|
||||
painter.end()
|
||||
return QIcon(pixmap)
|
||||
|
||||
|
||||
def _line_pen(line_type: LineType, thickness: float, color: str) -> QPen:
|
||||
styles = {LineType.SOLID: Qt.PenStyle.SolidLine, LineType.DASHED: Qt.PenStyle.DashLine, LineType.DOTTED: Qt.PenStyle.DotLine, LineType.DASH_DOT: Qt.PenStyle.DashDotLine}
|
||||
if line_type is LineType.NONE:
|
||||
return QPen(Qt.PenStyle.NoPen)
|
||||
return QPen(_color(color), thickness, styles[line_type])
|
||||
|
||||
|
||||
def _color(value: str) -> QColor:
|
||||
color = value.removeprefix("#")
|
||||
if len(color) == 8:
|
||||
return QColor(int(color[0:2], 16), int(color[2:4], 16), int(color[4:6], 16), int(color[6:8], 16))
|
||||
return QColor(value)
|
||||
|
||||
@@ -3,6 +3,7 @@ from __future__ import annotations
|
||||
from dataclasses import dataclass
|
||||
|
||||
from PySide6.QtCore import QAbstractItemModel, QModelIndex, Qt, Signal
|
||||
from PySide6.QtGui import QIcon
|
||||
|
||||
from bedit_core.models import Component, ComponentID, GraphImplementation
|
||||
from bedit_core.models import Document as CoreDocument
|
||||
@@ -12,6 +13,7 @@ from bedit_core.models import Document as CoreDocument
|
||||
class DocumentTreeNode:
|
||||
name: str
|
||||
value: object
|
||||
component_id: ComponentID | None
|
||||
parent: DocumentTreeNode | None
|
||||
children: list[DocumentTreeNode]
|
||||
|
||||
@@ -23,23 +25,40 @@ class DocumentTreeModel(QAbstractItemModel):
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self._document: CoreDocument | None = None
|
||||
self._root = DocumentTreeNode("Document", None, None, [])
|
||||
self._root = DocumentTreeNode("Document", None, None, None, [])
|
||||
self._component_icons: dict[ComponentID, QIcon] = {}
|
||||
self._component_nodes: dict[ComponentID, DocumentTreeNode] = {}
|
||||
|
||||
def set_document(self, document: CoreDocument) -> None:
|
||||
self.beginResetModel()
|
||||
self._document = document
|
||||
self._component_icons = {}
|
||||
self._component_nodes = {}
|
||||
self._root = self._build_tree(document)
|
||||
self.endResetModel()
|
||||
|
||||
def set_component_icon(self, component_id: ComponentID, icon: QIcon) -> None:
|
||||
node = self._component_nodes.get(component_id)
|
||||
if node is None or node.parent is None:
|
||||
return
|
||||
self._component_icons[component_id] = icon
|
||||
row = node.parent.children.index(node)
|
||||
index = self.createIndex(row, 1, node)
|
||||
self.dataChanged.emit(index, index, [Qt.ItemDataRole.DecorationRole])
|
||||
|
||||
def rowCount(self, parent: QModelIndex | None = None) -> int:
|
||||
if parent is not None and parent.isValid() and parent.column() != 0:
|
||||
return 0
|
||||
return len(self._node(parent).children)
|
||||
|
||||
def columnCount(self, _parent: QModelIndex | None = None) -> int:
|
||||
return 1
|
||||
return 2
|
||||
|
||||
def index(self, row: int, column: int, parent: QModelIndex | None = None) -> QModelIndex:
|
||||
if parent is not None and parent.isValid() and parent.column() != 0:
|
||||
return QModelIndex()
|
||||
parent_node = self._node(parent)
|
||||
if column != 0 or row < 0 or row >= len(parent_node.children):
|
||||
if column not in (0,1) or row < 0 or row >= len(parent_node.children):
|
||||
return QModelIndex()
|
||||
return self.createIndex(row, column, parent_node.children[row])
|
||||
|
||||
@@ -69,13 +88,19 @@ class DocumentTreeModel(QAbstractItemModel):
|
||||
if not isinstance(node, DocumentTreeNode):
|
||||
return None
|
||||
|
||||
if index.column() == 0:
|
||||
if role in (Qt.ItemDataRole.DisplayRole, Qt.ItemDataRole.EditRole):
|
||||
return node.name
|
||||
elif index.column() == 1:
|
||||
if role == Qt.ItemDataRole.DecorationRole:
|
||||
return self._component_icons.get(node.component_id)
|
||||
if role == Qt.ItemDataRole.TextAlignmentRole:
|
||||
return Qt.AlignmentFlag.AlignCenter
|
||||
|
||||
return None
|
||||
|
||||
def setData(self, index: QModelIndex, value: object, role: int = Qt.ItemDataRole.EditRole) -> bool:
|
||||
if role != Qt.ItemDataRole.EditRole or not index.isValid():
|
||||
if role != Qt.ItemDataRole.EditRole or not index.isValid() or index.column() != 0:
|
||||
return False
|
||||
|
||||
node = index.internalPointer()
|
||||
@@ -115,7 +140,7 @@ class DocumentTreeModel(QAbstractItemModel):
|
||||
node = index.internalPointer()
|
||||
|
||||
# Make the document root node editable
|
||||
if isinstance(node, DocumentTreeNode) and isinstance(node.value, (CoreDocument, Component)):
|
||||
if index.column() == 0 and isinstance(node, DocumentTreeNode) and isinstance(node.value, (CoreDocument, Component)):
|
||||
flags |= Qt.ItemFlag.ItemIsEditable
|
||||
|
||||
return flags
|
||||
@@ -125,17 +150,19 @@ class DocumentTreeModel(QAbstractItemModel):
|
||||
root = DocumentTreeNode(
|
||||
name="",
|
||||
value=None,
|
||||
component_id=None,
|
||||
parent=None,
|
||||
children=[],
|
||||
)
|
||||
# Add itself as a child so the document root is visible in the tree
|
||||
document_root = DocumentTreeNode(name=document.name, value=document, parent=root, children=[])
|
||||
document_root = DocumentTreeNode(name=document.name, value=document, component_id=None, parent=root, children=[])
|
||||
root.children.append(document_root)
|
||||
|
||||
def _list_children(root: DocumentTreeNode, components: dict[ComponentID, Component]) -> None:
|
||||
for component in components.values():
|
||||
component_node = DocumentTreeNode(name=component.name, value=component, parent=root, children=[])
|
||||
for component_id, component in components.items():
|
||||
component_node = DocumentTreeNode(name=component.name, value=component, component_id=component_id, parent=root, children=[])
|
||||
root.children.append(component_node)
|
||||
self._component_nodes[component_id] = component_node
|
||||
|
||||
if isinstance(component.implementation, GraphImplementation):
|
||||
_list_children(component_node, component.implementation.graph.components)
|
||||
|
||||
@@ -438,8 +438,8 @@
|
||||
-32,
|
||||
-32
|
||||
],
|
||||
"width": 64,
|
||||
"height": 64,
|
||||
"width": 64.0,
|
||||
"height": 64.0,
|
||||
"color": "#000000ff",
|
||||
"bold": true,
|
||||
"italic": false,
|
||||
@@ -492,8 +492,8 @@
|
||||
-32,
|
||||
-32
|
||||
],
|
||||
"width": 64,
|
||||
"height": 64,
|
||||
"width": 64.0,
|
||||
"height": 64.0,
|
||||
"color": "#000000ff",
|
||||
"bold": true,
|
||||
"italic": false,
|
||||
@@ -521,8 +521,8 @@
|
||||
-32,
|
||||
-32
|
||||
],
|
||||
"width": 64,
|
||||
"height": 64,
|
||||
"width": 64.0,
|
||||
"height": 64.0,
|
||||
"color": "#000000ff",
|
||||
"bold": true,
|
||||
"italic": false,
|
||||
@@ -546,8 +546,8 @@
|
||||
-32,
|
||||
-32
|
||||
],
|
||||
"width": 64,
|
||||
"height": 64,
|
||||
"width": 64.0,
|
||||
"height": 64.0,
|
||||
"color": "#000000ff",
|
||||
"bold": true,
|
||||
"italic": false,
|
||||
@@ -561,6 +561,31 @@
|
||||
-8
|
||||
]
|
||||
}
|
||||
},
|
||||
"2804f2f1-6123-4a53-aff1-87a3a8202711": {
|
||||
"shapes": {
|
||||
"2a5bf670-c821-442b-9d46-1b07faa70722": {
|
||||
"layer": 0,
|
||||
"type": "text",
|
||||
"pos": [
|
||||
-48,
|
||||
-32
|
||||
],
|
||||
"width": 96.0,
|
||||
"height": 64.0,
|
||||
"color": "#000000ff",
|
||||
"bold": true,
|
||||
"italic": false,
|
||||
"size": 64.0,
|
||||
"text": "Se"
|
||||
}
|
||||
},
|
||||
"port_positions": {
|
||||
"5b6a8a0c-0875-402c-bd74-11d086aac372": [
|
||||
-8,
|
||||
-8
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user