From b1f453a6dff918f7fab609ed48984552f11e6627 Mon Sep 17 00:00:00 2001 From: Joppe Blondel Date: Mon, 17 Aug 2026 17:47:26 +0200 Subject: [PATCH] Library tree filled with items --- src/bedit_gui/application.py | 5 +- .../controllers/library_controller.py | 48 +++++++++++++++++++ .../controllers/settings_controller.py | 7 ++- src/bedit_gui/services/libraries.py | 42 ++++++++++++++++ .../views/models/document_tree_model.py | 29 +++++------ 5 files changed, 115 insertions(+), 16 deletions(-) create mode 100644 src/bedit_gui/controllers/library_controller.py create mode 100644 src/bedit_gui/services/libraries.py diff --git a/src/bedit_gui/application.py b/src/bedit_gui/application.py index f51f090..f42cedc 100644 --- a/src/bedit_gui/application.py +++ b/src/bedit_gui/application.py @@ -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.document_controller import DocumentController 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.simulation_settings_controller import SimulationSettingsController from bedit_gui.controllers.simulation_controller import SimulationController @@ -52,12 +53,14 @@ def main() -> int: LogController(window, settings.log_level) DocumentController(document, window) - SettingsController(window, settings) + settings_controller = SettingsController(window, settings) SimulationSettingsController(document, window) SimulationController(document, window) UndoController(document, window) ViewMenuController(window) document_tree_controller = DocumentTreeController(document, window) + library_controller = LibraryController(window, settings) + settings_controller.library_paths_changed.connect(library_controller.reload) clipboard = ClipboardService(app) ClipboardController(window, clipboard, [TextClipboardHandler(clipboard), DocumentTreeClipboardHandler(document, window.ui.documentTree, document_tree_controller.model, clipboard), GraphEditorClipboardHandler(document, window.graph_editor, clipboard)]) diff --git a/src/bedit_gui/controllers/library_controller.py b/src/bedit_gui/controllers/library_controller.py new file mode 100644 index 0000000..f5cdb1c --- /dev/null +++ b/src/bedit_gui/controllers/library_controller.py @@ -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) diff --git a/src/bedit_gui/controllers/settings_controller.py b/src/bedit_gui/controllers/settings_controller.py index 90efc4b..2323913 100644 --- a/src/bedit_gui/controllers/settings_controller.py +++ b/src/bedit_gui/controllers/settings_controller.py @@ -3,7 +3,7 @@ from __future__ import annotations from collections.abc import Callable from typing import Protocol -from PySide6.QtCore import QObject +from PySide6.QtCore import QObject, Signal from PySide6.QtWidgets import QDialog 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): """Opens the settings dialog and applies accepted preferences.""" + library_paths_changed = Signal() + def __init__( self, window: MainWindow, @@ -51,9 +53,12 @@ class SettingsController(QObject): if dialog.exec() != QDialog.DialogCode.Accepted: return + old_library_paths = self.settings.library_paths self.settings.log_level = dialog.log_level self.settings.snap_to_grid_size = dialog.snap_to_grid_size self.settings.library_paths = dialog.library_paths self.window.graph_editor.set_snap_to_grid_size(dialog.snap_to_grid_size) set_log_level(dialog.log_level) + if self.settings.library_paths != old_library_paths: + self.library_paths_changed.emit() logger.info("Application settings updated") diff --git a/src/bedit_gui/services/libraries.py b/src/bedit_gui/services/libraries.py new file mode 100644 index 0000000..aa72c01 --- /dev/null +++ b/src/bedit_gui/services/libraries.py @@ -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 diff --git a/src/bedit_gui/views/models/document_tree_model.py b/src/bedit_gui/views/models/document_tree_model.py index 37740c0..8cf16e6 100644 --- a/src/bedit_gui/views/models/document_tree_model.py +++ b/src/bedit_gui/views/models/document_tree_model.py @@ -22,19 +22,21 @@ class DocumentTreeModel(QAbstractItemModel): rename_document_requested = Signal(str) rename_component_requested = Signal(Component, str) - def __init__(self) -> None: + def __init__(self, editable: bool = True) -> None: super().__init__() - self._document: CoreDocument | None = None + self._editable = editable 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.set_documents([document]) + + def set_documents(self, documents: list[CoreDocument]) -> None: self.beginResetModel() - self._document = document self._component_icons = {} self._component_nodes = {} - self._root = self._build_tree(document) + self._root = self._build_tree(documents) self.endResetModel() def set_component_icon(self, component_id: ComponentID, icon: QIcon) -> None: @@ -146,12 +148,12 @@ class DocumentTreeModel(QAbstractItemModel): node = index.internalPointer() # 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 return flags - def _build_tree(self, document: CoreDocument) -> DocumentTreeNode: + def _build_tree(self, documents: list[CoreDocument]) -> DocumentTreeNode: # QT's invisible root root = DocumentTreeNode( name="", @@ -160,19 +162,18 @@ class DocumentTreeModel(QAbstractItemModel): 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, component_id=None, parent=root, children=[]) - root.children.append(document_root) - - def _list_children(root: DocumentTreeNode, components: dict[ComponentID, Component]) -> None: + def _list_children(parent: 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]))): - component_node = DocumentTreeNode(name=component.name, value=component, component_id=component_id, parent=root, children=[]) - root.children.append(component_node) + component_node = DocumentTreeNode(name=component.name, value=component, component_id=component_id, parent=parent, children=[]) + parent.children.append(component_node) self._component_nodes[component_id] = component_node if isinstance(component.implementation, GraphImplementation): _list_children(component_node, component.implementation.graph.components) - _list_children(document_root, document.root) + 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) return root