Library tree filled with items

This commit is contained in:
2026-08-17 17:47:26 +02:00
parent 8d37b2441a
commit b1f453a6df
5 changed files with 115 additions and 16 deletions

View File

@@ -8,6 +8,7 @@ from PySide6.QtWidgets import QApplication, QMessageBox
from bedit_gui.controllers.clipboard_controller import ClipboardController, DocumentTreeClipboardHandler, GraphEditorClipboardHandler, TextClipboardHandler from bedit_gui.controllers.clipboard_controller import ClipboardController, DocumentTreeClipboardHandler, GraphEditorClipboardHandler, TextClipboardHandler
from bedit_gui.controllers.document_controller import DocumentController from bedit_gui.controllers.document_controller import DocumentController
from bedit_gui.controllers.log_controller import LogController from bedit_gui.controllers.log_controller import LogController
from bedit_gui.controllers.library_controller import LibraryController
from bedit_gui.controllers.settings_controller import SettingsController from bedit_gui.controllers.settings_controller import SettingsController
from bedit_gui.controllers.simulation_settings_controller import SimulationSettingsController from bedit_gui.controllers.simulation_settings_controller import SimulationSettingsController
from bedit_gui.controllers.simulation_controller import SimulationController from bedit_gui.controllers.simulation_controller import SimulationController
@@ -52,12 +53,14 @@ def main() -> int:
LogController(window, settings.log_level) LogController(window, settings.log_level)
DocumentController(document, window) DocumentController(document, window)
SettingsController(window, settings) settings_controller = SettingsController(window, settings)
SimulationSettingsController(document, window) SimulationSettingsController(document, window)
SimulationController(document, window) SimulationController(document, window)
UndoController(document, window) UndoController(document, window)
ViewMenuController(window) ViewMenuController(window)
document_tree_controller = DocumentTreeController(document, window) document_tree_controller = DocumentTreeController(document, window)
library_controller = LibraryController(window, settings)
settings_controller.library_paths_changed.connect(library_controller.reload)
clipboard = ClipboardService(app) clipboard = ClipboardService(app)
ClipboardController(window, clipboard, [TextClipboardHandler(clipboard), DocumentTreeClipboardHandler(document, window.ui.documentTree, document_tree_controller.model, clipboard), GraphEditorClipboardHandler(document, window.graph_editor, clipboard)]) ClipboardController(window, clipboard, [TextClipboardHandler(clipboard), DocumentTreeClipboardHandler(document, window.ui.documentTree, document_tree_controller.model, clipboard), GraphEditorClipboardHandler(document, window.graph_editor, clipboard)])

View File

@@ -0,0 +1,48 @@
from PySide6.QtCore import QObject, QSize
from PySide6.QtWidgets import QAbstractItemView, QHeaderView
from bedit_core.models import Component, ComponentID, GraphImplementation
from bedit_gui.models import Icon, IconDatabase
from bedit_gui.services.application_settings import ApplicationSettings
from bedit_gui.services.libraries import load_library_documents
from bedit_gui.utils.icon import render_fitted_icon
from bedit_gui.views.main_window import MainWindow
from bedit_gui.views.models.document_tree_model import DocumentTreeModel
ICON_SIZE = QSize(32, 32)
class LibraryController(QObject):
def __init__(self, window: MainWindow, settings: ApplicationSettings) -> None:
super().__init__(window)
self.window = window
self.settings = settings
self.model = DocumentTreeModel(editable=False)
tree = window.ui.libraryTree
tree.setModel(self.model)
tree.setHeaderHidden(True)
tree.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection)
tree.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
tree.setIconSize(QSize(48, 48))
tree.header().setStretchLastSection(False)
tree.header().setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch)
tree.header().setSectionResizeMode(1, QHeaderView.ResizeMode.Fixed)
tree.setColumnWidth(1, 56)
self.reload()
def reload(self) -> None:
libraries = load_library_documents(self.settings.library_paths)
self.model.set_documents([library.document for library in libraries])
for library in libraries:
database = library.document.metadata.get("icon_database") if library.document.metadata is not None else None
icons = database.icons if isinstance(database, IconDatabase) else {}
self._set_component_icons(library.document.root, icons)
self.window.ui.libraryTree.expandAll()
def _set_component_icons(self, components: dict[ComponentID, Component], icons: dict[ComponentID, Icon]) -> None:
for component_id, component in components.items():
self.model.set_component_icon(component_id, render_fitted_icon(icons.get(component_id, Icon()), component.interface.ports, ICON_SIZE))
if isinstance(component.implementation, GraphImplementation):
self._set_component_icons(component.implementation.graph.components, icons)

View File

@@ -3,7 +3,7 @@ from __future__ import annotations
from collections.abc import Callable from collections.abc import Callable
from typing import Protocol from typing import Protocol
from PySide6.QtCore import QObject from PySide6.QtCore import QObject, Signal
from PySide6.QtWidgets import QDialog from PySide6.QtWidgets import QDialog
from bedit_gui.services.application_logging import get_logger, set_log_level from bedit_gui.services.application_logging import get_logger, set_log_level
@@ -33,6 +33,8 @@ SettingsDialogFactory = Callable[[int, int, list[str], MainWindow], SettingsDial
class SettingsController(QObject): class SettingsController(QObject):
"""Opens the settings dialog and applies accepted preferences.""" """Opens the settings dialog and applies accepted preferences."""
library_paths_changed = Signal()
def __init__( def __init__(
self, self,
window: MainWindow, window: MainWindow,
@@ -51,9 +53,12 @@ class SettingsController(QObject):
if dialog.exec() != QDialog.DialogCode.Accepted: if dialog.exec() != QDialog.DialogCode.Accepted:
return return
old_library_paths = self.settings.library_paths
self.settings.log_level = dialog.log_level self.settings.log_level = dialog.log_level
self.settings.snap_to_grid_size = dialog.snap_to_grid_size self.settings.snap_to_grid_size = dialog.snap_to_grid_size
self.settings.library_paths = dialog.library_paths self.settings.library_paths = dialog.library_paths
self.window.graph_editor.set_snap_to_grid_size(dialog.snap_to_grid_size) self.window.graph_editor.set_snap_to_grid_size(dialog.snap_to_grid_size)
set_log_level(dialog.log_level) set_log_level(dialog.log_level)
if self.settings.library_paths != old_library_paths:
self.library_paths_changed.emit()
logger.info("Application settings updated") logger.info("Application settings updated")

View File

@@ -0,0 +1,42 @@
from __future__ import annotations
from dataclasses import dataclass
from pathlib import Path
from bedit_core.models import Document
from bedit_gui.services import document_files
from bedit_gui.services.application_logging import get_logger
logger = get_logger(__name__)
@dataclass(frozen=True)
class LoadedLibrary:
path: Path
document: Document
def list_library_files(library_paths: list[str]) -> list[Path]:
files = []
seen = set()
for configured_path in library_paths:
path = Path(configured_path).expanduser()
candidates = [path] if path.is_file() else sorted(path.rglob("*"), key=lambda candidate: str(candidate).casefold()) if path.is_dir() else []
for candidate in candidates:
if not candidate.is_file() or candidate.suffix.lower() not in (".beb", ".json"):
continue
resolved = candidate.resolve()
if resolved not in seen:
seen.add(resolved)
files.append(resolved)
return files
def load_library_documents(library_paths: list[str]) -> list[LoadedLibrary]:
libraries = []
for path in list_library_files(library_paths):
try:
libraries.append(LoadedLibrary(path, document_files.load(path)))
except (KeyError, OSError, TypeError, ValueError) as exc:
logger.warning("Could not load library %s: %s", path, exc)
return libraries

View File

@@ -22,19 +22,21 @@ class DocumentTreeModel(QAbstractItemModel):
rename_document_requested = Signal(str) rename_document_requested = Signal(str)
rename_component_requested = Signal(Component, str) rename_component_requested = Signal(Component, str)
def __init__(self) -> None: def __init__(self, editable: bool = True) -> None:
super().__init__() super().__init__()
self._document: CoreDocument | None = None self._editable = editable
self._root = DocumentTreeNode("Document", None, None, None, []) self._root = DocumentTreeNode("Document", None, None, None, [])
self._component_icons: dict[ComponentID, QIcon] = {} self._component_icons: dict[ComponentID, QIcon] = {}
self._component_nodes: dict[ComponentID, DocumentTreeNode] = {} self._component_nodes: dict[ComponentID, DocumentTreeNode] = {}
def set_document(self, document: CoreDocument) -> None: def set_document(self, document: CoreDocument) -> None:
self.set_documents([document])
def set_documents(self, documents: list[CoreDocument]) -> None:
self.beginResetModel() self.beginResetModel()
self._document = document
self._component_icons = {} self._component_icons = {}
self._component_nodes = {} self._component_nodes = {}
self._root = self._build_tree(document) self._root = self._build_tree(documents)
self.endResetModel() self.endResetModel()
def set_component_icon(self, component_id: ComponentID, icon: QIcon) -> None: def set_component_icon(self, component_id: ComponentID, icon: QIcon) -> None:
@@ -146,12 +148,12 @@ class DocumentTreeModel(QAbstractItemModel):
node = index.internalPointer() node = index.internalPointer()
# Make the document root node editable # Make the document root node editable
if index.column() == 0 and isinstance(node, DocumentTreeNode) and isinstance(node.value, (CoreDocument, Component)): if self._editable and 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
def _build_tree(self, document: CoreDocument) -> DocumentTreeNode: def _build_tree(self, documents: list[CoreDocument]) -> DocumentTreeNode:
# QT's invisible root # QT's invisible root
root = DocumentTreeNode( root = DocumentTreeNode(
name="", name="",
@@ -160,19 +162,18 @@ class DocumentTreeModel(QAbstractItemModel):
parent=None, parent=None,
children=[], children=[],
) )
# Add itself as a child so the document root is visible in the tree def _list_children(parent: DocumentTreeNode, components: dict[ComponentID, Component]) -> None:
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_id, component in sorted(components.items(), key=lambda item: (item[1].name.casefold(), item[1].name, str(item[0]))): for component_id, component in sorted(components.items(), key=lambda item: (item[1].name.casefold(), item[1].name, str(item[0]))):
component_node = DocumentTreeNode(name=component.name, value=component, component_id=component_id, parent=root, children=[]) component_node = DocumentTreeNode(name=component.name, value=component, component_id=component_id, parent=parent, children=[])
root.children.append(component_node) parent.children.append(component_node)
self._component_nodes[component_id] = 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)
for document in documents:
document_root = DocumentTreeNode(name=document.name, value=document, component_id=None, parent=root, children=[])
root.children.append(document_root)
_list_children(document_root, document.root) _list_children(document_root, document.root)
return root return root