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 functools import partial
|
||||||
from typing import Protocol
|
from typing import Protocol
|
||||||
|
|
||||||
from PySide6.QtCore import QObject, QPoint, Qt
|
from PySide6.QtCore import QObject, QPoint, QSize, Qt
|
||||||
from PySide6.QtWidgets import QDialog, QMenu
|
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_core.models import Document as CoreDocument
|
||||||
from bedit_gui.documents import Document
|
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.interface_editor_dialog import InterfaceEditorDialog
|
||||||
from bedit_gui.views.dialogs.param_editor_dialog import ParamEditorDialog
|
from bedit_gui.views.dialogs.param_editor_dialog import ParamEditorDialog
|
||||||
from bedit_gui.views.icon_editor_window import IconEditorWindow
|
from bedit_gui.views.icon_editor_window import IconEditorWindow
|
||||||
from bedit_gui.views.main_window import MainWindow
|
from bedit_gui.views.main_window import MainWindow
|
||||||
from bedit_gui.views.models.document_tree_model import DocumentTreeModel
|
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):
|
class InterfaceEditorLike(Protocol):
|
||||||
def exec(self) -> int: ...
|
def exec(self) -> int: ...
|
||||||
@@ -51,13 +54,20 @@ class DocumentTreeController(QObject):
|
|||||||
self.interface_editor_factory = interface_editor_factory
|
self.interface_editor_factory = interface_editor_factory
|
||||||
self.param_editor_factory = param_editor_factory
|
self.param_editor_factory = param_editor_factory
|
||||||
self._icon_editors: list[IconEditorWindow] = []
|
self._icon_editors: list[IconEditorWindow] = []
|
||||||
|
self._components: dict[ComponentID, Component] = {}
|
||||||
|
|
||||||
window.ui.documentTree.setModel(self.model)
|
window.ui.documentTree.setModel(self.model)
|
||||||
document.model_changed.connect(self._on_document_changed)
|
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_document_requested.connect(self.document.rename)
|
||||||
self.model.rename_component_requested.connect(self.document.rename_component)
|
self.model.rename_component_requested.connect(self.document.rename_component)
|
||||||
|
|
||||||
window.ui.documentTree.setHeaderHidden(True)
|
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(
|
window.ui.documentTree.setContextMenuPolicy(
|
||||||
Qt.ContextMenuPolicy.CustomContextMenu
|
Qt.ContextMenuPolicy.CustomContextMenu
|
||||||
)
|
)
|
||||||
@@ -70,11 +80,28 @@ class DocumentTreeController(QObject):
|
|||||||
def _on_document_changed(self, model: CoreDocument) -> None:
|
def _on_document_changed(self, model: CoreDocument) -> None:
|
||||||
"""Rebuild the tree whenever New/Open replaces the core document."""
|
"""Rebuild the tree whenever New/Open replaces the core document."""
|
||||||
self.model.set_document(model)
|
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
|
# Optional presentation behavior. Later, you could instead remember
|
||||||
# expanded component IDs and restore only those nodes.
|
# expanded component IDs and restore only those nodes.
|
||||||
self.window.ui.documentTree.expandAll()
|
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:
|
def _show_context_menu(self, position: QPoint) -> None:
|
||||||
index = self.window.ui.documentTree.indexAt(position)
|
index = self.window.ui.documentTree.indexAt(position)
|
||||||
component = self.model.value(index)
|
component = self.model.value(index)
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
from __future__ import annotations
|
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
|
PORT_SIZE = 16
|
||||||
|
DEFAULT_ICON_SIZE = QSize(48, 48)
|
||||||
|
|
||||||
|
|
||||||
def get_bounding_box(icon: Icon) -> QRectF:
|
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)
|
right = max(point[0] for point in points)
|
||||||
bottom = max(point[1] for point in points)
|
bottom = max(point[1] for point in points)
|
||||||
return QRectF(left, top, right - left, bottom - top)
|
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 dataclasses import dataclass
|
||||||
|
|
||||||
from PySide6.QtCore import QAbstractItemModel, QModelIndex, Qt, Signal
|
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 Component, ComponentID, GraphImplementation
|
||||||
from bedit_core.models import Document as CoreDocument
|
from bedit_core.models import Document as CoreDocument
|
||||||
@@ -12,6 +13,7 @@ from bedit_core.models import Document as CoreDocument
|
|||||||
class DocumentTreeNode:
|
class DocumentTreeNode:
|
||||||
name: str
|
name: str
|
||||||
value: object
|
value: object
|
||||||
|
component_id: ComponentID | None
|
||||||
parent: DocumentTreeNode | None
|
parent: DocumentTreeNode | None
|
||||||
children: list[DocumentTreeNode]
|
children: list[DocumentTreeNode]
|
||||||
|
|
||||||
@@ -23,23 +25,40 @@ class DocumentTreeModel(QAbstractItemModel):
|
|||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self._document: CoreDocument | None = None
|
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:
|
def set_document(self, document: CoreDocument) -> None:
|
||||||
self.beginResetModel()
|
self.beginResetModel()
|
||||||
self._document = document
|
self._document = document
|
||||||
|
self._component_icons = {}
|
||||||
|
self._component_nodes = {}
|
||||||
self._root = self._build_tree(document)
|
self._root = self._build_tree(document)
|
||||||
self.endResetModel()
|
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:
|
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)
|
return len(self._node(parent).children)
|
||||||
|
|
||||||
def columnCount(self, _parent: QModelIndex | None = None) -> int:
|
def columnCount(self, _parent: QModelIndex | None = None) -> int:
|
||||||
return 1
|
return 2
|
||||||
|
|
||||||
def index(self, row: int, column: int, parent: QModelIndex | None = None) -> QModelIndex:
|
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)
|
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 QModelIndex()
|
||||||
return self.createIndex(row, column, parent_node.children[row])
|
return self.createIndex(row, column, parent_node.children[row])
|
||||||
|
|
||||||
@@ -69,13 +88,19 @@ class DocumentTreeModel(QAbstractItemModel):
|
|||||||
if not isinstance(node, DocumentTreeNode):
|
if not isinstance(node, DocumentTreeNode):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
if index.column() == 0:
|
||||||
if role in (Qt.ItemDataRole.DisplayRole, Qt.ItemDataRole.EditRole):
|
if role in (Qt.ItemDataRole.DisplayRole, Qt.ItemDataRole.EditRole):
|
||||||
return node.name
|
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
|
return None
|
||||||
|
|
||||||
def setData(self, index: QModelIndex, value: object, role: int = Qt.ItemDataRole.EditRole) -> bool:
|
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
|
return False
|
||||||
|
|
||||||
node = index.internalPointer()
|
node = index.internalPointer()
|
||||||
@@ -115,7 +140,7 @@ class DocumentTreeModel(QAbstractItemModel):
|
|||||||
node = index.internalPointer()
|
node = index.internalPointer()
|
||||||
|
|
||||||
# Make the document root node editable
|
# 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
|
flags |= Qt.ItemFlag.ItemIsEditable
|
||||||
|
|
||||||
return flags
|
return flags
|
||||||
@@ -125,17 +150,19 @@ class DocumentTreeModel(QAbstractItemModel):
|
|||||||
root = DocumentTreeNode(
|
root = DocumentTreeNode(
|
||||||
name="",
|
name="",
|
||||||
value=None,
|
value=None,
|
||||||
|
component_id=None,
|
||||||
parent=None,
|
parent=None,
|
||||||
children=[],
|
children=[],
|
||||||
)
|
)
|
||||||
# Add itself as a child so the document root is visible in the tree
|
# 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)
|
root.children.append(document_root)
|
||||||
|
|
||||||
def _list_children(root: DocumentTreeNode, components: dict[ComponentID, Component]) -> None:
|
def _list_children(root: DocumentTreeNode, components: dict[ComponentID, Component]) -> None:
|
||||||
for component in components.values():
|
for component_id, component in components.items():
|
||||||
component_node = DocumentTreeNode(name=component.name, value=component, parent=root, children=[])
|
component_node = DocumentTreeNode(name=component.name, value=component, component_id=component_id, parent=root, children=[])
|
||||||
root.children.append(component_node)
|
root.children.append(component_node)
|
||||||
|
self._component_nodes[component_id] = component_node
|
||||||
|
|
||||||
if isinstance(component.implementation, GraphImplementation):
|
if isinstance(component.implementation, GraphImplementation):
|
||||||
_list_children(component_node, component.implementation.graph.components)
|
_list_children(component_node, component.implementation.graph.components)
|
||||||
|
|||||||
@@ -438,8 +438,8 @@
|
|||||||
-32,
|
-32,
|
||||||
-32
|
-32
|
||||||
],
|
],
|
||||||
"width": 64,
|
"width": 64.0,
|
||||||
"height": 64,
|
"height": 64.0,
|
||||||
"color": "#000000ff",
|
"color": "#000000ff",
|
||||||
"bold": true,
|
"bold": true,
|
||||||
"italic": false,
|
"italic": false,
|
||||||
@@ -492,8 +492,8 @@
|
|||||||
-32,
|
-32,
|
||||||
-32
|
-32
|
||||||
],
|
],
|
||||||
"width": 64,
|
"width": 64.0,
|
||||||
"height": 64,
|
"height": 64.0,
|
||||||
"color": "#000000ff",
|
"color": "#000000ff",
|
||||||
"bold": true,
|
"bold": true,
|
||||||
"italic": false,
|
"italic": false,
|
||||||
@@ -521,8 +521,8 @@
|
|||||||
-32,
|
-32,
|
||||||
-32
|
-32
|
||||||
],
|
],
|
||||||
"width": 64,
|
"width": 64.0,
|
||||||
"height": 64,
|
"height": 64.0,
|
||||||
"color": "#000000ff",
|
"color": "#000000ff",
|
||||||
"bold": true,
|
"bold": true,
|
||||||
"italic": false,
|
"italic": false,
|
||||||
@@ -546,8 +546,8 @@
|
|||||||
-32,
|
-32,
|
||||||
-32
|
-32
|
||||||
],
|
],
|
||||||
"width": 64,
|
"width": 64.0,
|
||||||
"height": 64,
|
"height": 64.0,
|
||||||
"color": "#000000ff",
|
"color": "#000000ff",
|
||||||
"bold": true,
|
"bold": true,
|
||||||
"italic": false,
|
"italic": false,
|
||||||
@@ -561,6 +561,31 @@
|
|||||||
-8
|
-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