diff --git a/AGENTS.md b/AGENTS.md index 22cfb34..ad4356a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -60,7 +60,7 @@ Responsibilities: - `views/`: handwritten widget/window/graphics behavior. - `views/models/`: Qt item models used by views. - `services/`: non-visual functionality such as files, clipboard, logging, and settings. -- `models.py`: GUI metadata persisted inside the core document, currently including icons and shapes. +- `models.py`: GUI metadata persisted inside the core document, including icon, graph, and simulation databases. - `bedit_core`: domain model and serialization. It must never import from `bedit_gui`. Preferred direction: @@ -136,10 +136,12 @@ Clipboard support is intentionally extensible: - `services/component_clipboard.py`: component payload serialization and ID remapping. - `controllers/clipboard_controller.py`: focus-based action router and handlers. -`ClipboardHandler` is the base implementation for future editors. Add a graph-editor handler later by subclassing it and registering that handler in `application.py`. +`ClipboardHandler` is the base implementation for editor-specific routing. The document tree and graph editor have component handlers registered in `application.py`. Text widgets use their native `copy()`, `cut()`, and `paste()` methods. Component clipboard data uses the custom BEdit MIME type and JSON; never use pickle or live object references. +The graph handler copies/cuts/deletes selected component items and deletes selected connection items. Paste targets the displayed graph and places new components at the mouse position, or at the viewport center when the mouse is outside the canvas. + ## Icon editor conventions - Icons are GUI metadata stored in `document.metadata["icon_database"]`. @@ -153,6 +155,23 @@ Text widgets use their native `copy()`, `cut()`, and `paste()` methods. Componen - Keep reusable widgets such as the RGBA color button independent of the icon editor. - Static icon previews belong in rendering utilities, not in the interactive editor scene. +## Graph editor conventions + +- Graph GUI metadata is stored in `document.metadata["graph_database"]`. `GraphDatabase`, `Graph`, `GraphConnection`, and `GraphComponentLabel` live in `bedit_gui.models` and serialize through `to_data()`/`from_data()`. +- Core graph topology and connections remain in `bedit_core.models`; positions, routed points, labels, and other presentation metadata belong in the GUI graph database. +- Component positions are absolute scene positions. Connection metadata stores the full point list, including endpoints; only interior points are draggable corner items. +- Component labels are visible by default, italic, and centered below the rendered icon. Their persisted position is relative to the icon’s bottom-center. Label visibility and completed label moves are undoable. +- The graph editor has normal and connection modes. The toolbar actions are exclusive and Space toggles modes while focus is inside the editor. +- Normal mode supports component and label dragging. Connection mode shows icon ports, prevents component/label movement, and uses two component clicks to choose a compatible port pair. +- Signal connections require output-to-input. Signal outputs may fan out; signal inputs accept only one connection. A bond port accepts another connection only when its `multiplicity` is true. Bond domains must be compatible. +- While choosing a connection, the first component is highlighted and a temporary dashed line follows the mouse. When several port pairs are possible, output-to-input choices are listed first. +- Signal connections render with full arrows. Bond connections render with half arrows and a perpendicular causality tick. Connection endpoints are clipped to rendered icon bounds plus `CONNECTION_BOUNDING_BOX_SPACING`. +- Components, connections, and connection points are separate graphics items. Connections are selectable/deletable; connection points are draggable and have their own delete context action. +- Persistent canvas edits go through `Document` methods and graph-specific `QUndoCommand` classes. Incremental document signals must also update the editor’s cached `Graph`; otherwise rebuilding items during a mode switch can restore stale metadata. +- `render_icon(..., render_ports=True)` is the single port-rendering path. Do not duplicate icon or port geometry in the graph editor. +- Graph sizing and rendering constants, including `COMPONENT_LABEL_FONT_SIZE`, live near the top of `views/graph_editor_widget.py`. +- Double-clicking a canvas component selects its document-tree row and opens its graph or equation editor. Canvas component context menus share the document-tree editing actions and add graph-only presentation actions such as Show Label. + ## Actions and shortcut routing - Put visible actions in Designer menus/toolbars. @@ -161,6 +180,12 @@ Text widgets use their native `copy()`, `cut()`, and `paste()` methods. Componen - Scope destructive shortcuts to the relevant widget where appropriate. - Always guard the operation itself even when an action is disabled for presentation. +## Application settings + +- `ApplicationSettings` is the typed `QSettings` facade for BEdit preferences. The current settings include log level, graph snap-to-grid size, and ordered library paths under `libraries/paths`. +- The Settings dialog edits library paths locally until OK is accepted. It supports BEdit `.bedit.json`/`.json` and `.beb` files, directories, duplicate suppression, extended selection, and list-focused Delete-key removal. +- Add settings behavior in the handwritten dialog/controller/service modules, never in generated UI Python. + ## Simulator application - `bedit_gui/simulation_application.py` is the composition root for the separate `bedit-sim` Qt application. @@ -173,6 +198,14 @@ Text widgets use their native `copy()`, `cut()`, and `paste()` methods. Componen - BEdit launches the simulator as a separate process. Compile/Open Simulation integration may transfer a `.bes` file to that process. - Keep simulator file workflows in their own services/controllers rather than adding them to `MainWindow` or the editor document controller. - Keep compiled-executable launching, time-window progression, result loading, and cancellation inside `bedit_simulation`. GUI controllers may schedule backend calls and present state, but must not execute or manage simulation binaries themselves. +- The BEdit Compile action performs bond-graph causality inference before Modelica compilation. Inference runs on a deep copy first, then inferred `causality`/`undesired` state is applied to the open document with an undoable command. +- OpenModelica compilation runs `checkModel(...)` before `buildModel(...)`; the equation/variable summary flows through `ModelBuildResult.output` and is shown in the application log. + +### Causality inference caveats + +- `_CausalityEngine.inference()` clears every flattened bond’s causality to `NONE` before inference; it does not continue from saved causalities. It currently does not clear old `undesired` flags. +- Preferred causalities are assigned before all junction constraints are resolved, and the engine has no backtracking to relax a preferred assignment. Some soft-preference conflicts therefore raise a junction error instead of marking a bond undesired. +- `propagate_to_neighbor()` currently selects `connection.target` in both branches. When propagation starts at a target component, it should traverse to the source; account for this known bug when diagnosing inference behavior. ## Verification diff --git a/src/bedit_gui/controllers/settings_controller.py b/src/bedit_gui/controllers/settings_controller.py index 928290e..90efc4b 100644 --- a/src/bedit_gui/controllers/settings_controller.py +++ b/src/bedit_gui/controllers/settings_controller.py @@ -21,10 +21,13 @@ class SettingsDialogLike(Protocol): @property def snap_to_grid_size(self) -> int: ... + @property + def library_paths(self) -> list[str]: ... + def exec(self) -> int: ... -SettingsDialogFactory = Callable[[int, int, MainWindow], SettingsDialogLike] +SettingsDialogFactory = Callable[[int, int, list[str], MainWindow], SettingsDialogLike] class SettingsController(QObject): @@ -44,12 +47,13 @@ class SettingsController(QObject): window.ui.actionSettings.triggered.connect(self.open_settings) def open_settings(self) -> None: - dialog = self.dialog_factory(self.settings.log_level, self.settings.snap_to_grid_size, self.window) + dialog = self.dialog_factory(self.settings.log_level, self.settings.snap_to_grid_size, self.settings.library_paths, self.window) if dialog.exec() != QDialog.DialogCode.Accepted: return 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) logger.info("Application settings updated") diff --git a/src/bedit_gui/services/application_settings.py b/src/bedit_gui/services/application_settings.py index 651d642..30df34c 100644 --- a/src/bedit_gui/services/application_settings.py +++ b/src/bedit_gui/services/application_settings.py @@ -12,6 +12,7 @@ class ApplicationSettings: DEFAULT_LOG_LEVEL = logging.INFO SNAP_TO_GRID_SIZE_KEY = "graph/snap_to_grid_size" DEFAULT_SNAP_TO_GRID_SIZE = 4 + LIBRARY_PATHS_KEY = "libraries/paths" def __init__(self, settings: QSettings | None = None) -> None: self._settings = settings if settings is not None else QSettings() @@ -38,6 +39,17 @@ class ApplicationSettings: raise ValueError("snap-to-grid size must be positive") self._settings.setValue(self.SNAP_TO_GRID_SIZE_KEY, size) + @property + def library_paths(self) -> list[str]: + value = self._settings.value(self.LIBRARY_PATHS_KEY, []) + if isinstance(value, str): + return [value] + return [str(path) for path in value] if isinstance(value, (list, tuple)) else [] + + @library_paths.setter + def library_paths(self, paths: list[str]) -> None: + self._settings.setValue(self.LIBRARY_PATHS_KEY, list(dict.fromkeys(paths))) + class SimulationApplicationSettings: """Typed access to persistent BEsim application settings.""" diff --git a/src/bedit_gui/ui/forms/main_window.ui b/src/bedit_gui/ui/forms/main_window.ui index 57b43c4..fedf032 100644 --- a/src/bedit_gui/ui/forms/main_window.ui +++ b/src/bedit_gui/ui/forms/main_window.ui @@ -30,7 +30,7 @@ 0 0 940 - 22 + 19 @@ -127,7 +127,7 @@ 150 - 533 + 200 @@ -180,6 +180,27 @@ + + + + 150 + 200 + + + + Libraries + + + 1 + + + + + + + + + diff --git a/src/bedit_gui/ui/forms/main_window_ui.py b/src/bedit_gui/ui/forms/main_window_ui.py new file mode 100644 index 0000000..0df19d5 --- /dev/null +++ b/src/bedit_gui/ui/forms/main_window_ui.py @@ -0,0 +1,362 @@ +# -*- coding: utf-8 -*- + +################################################################################ +## Form generated from reading UI file 'main_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, QDockWidget, QHeaderView, QListView, + QMainWindow, QMenu, QMenuBar, QSizePolicy, + QStatusBar, QTabWidget, QToolBar, QTreeView, + QVBoxLayout, QWidget) +import resources_rc + +class Ui_MainWindow(object): + def setupUi(self, MainWindow): + if not MainWindow.objectName(): + MainWindow.setObjectName(u"MainWindow") + MainWindow.resize(940, 729) + icon = QIcon() + icon.addFile(u":/icons/icons/office-chart-line.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) + MainWindow.setWindowIcon(icon) + MainWindow.setDocumentMode(False) + MainWindow.setTabShape(QTabWidget.TabShape.Triangular) + self.actionOpen_File = QAction(MainWindow) + self.actionOpen_File.setObjectName(u"actionOpen_File") + icon1 = QIcon() + icon1.addFile(u":/icons/icons/document-open.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) + self.actionOpen_File.setIcon(icon1) + self.actionOpen_File.setMenuRole(QAction.MenuRole.NoRole) + self.actionNew_File = QAction(MainWindow) + self.actionNew_File.setObjectName(u"actionNew_File") + icon2 = QIcon() + icon2.addFile(u":/icons/icons/document-new.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) + self.actionNew_File.setIcon(icon2) + self.actionNew_File.setMenuRole(QAction.MenuRole.NoRole) + self.actionSave_File = QAction(MainWindow) + self.actionSave_File.setObjectName(u"actionSave_File") + icon3 = QIcon() + icon3.addFile(u":/icons/icons/document-save.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) + self.actionSave_File.setIcon(icon3) + self.actionSave_File.setMenuRole(QAction.MenuRole.NoRole) + self.actionSave_File_As = QAction(MainWindow) + self.actionSave_File_As.setObjectName(u"actionSave_File_As") + icon4 = QIcon() + icon4.addFile(u":/icons/icons/document-save-as.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) + self.actionSave_File_As.setIcon(icon4) + self.actionSave_File_As.setMenuRole(QAction.MenuRole.NoRole) + self.actionClose = QAction(MainWindow) + self.actionClose.setObjectName(u"actionClose") + self.actionClose.setMenuRole(QAction.MenuRole.NoRole) + self.actionAbout_QT = QAction(MainWindow) + self.actionAbout_QT.setObjectName(u"actionAbout_QT") + self.actionAbout_QT.setMenuRole(QAction.MenuRole.AboutQtRole) + self.actionUndo = QAction(MainWindow) + self.actionUndo.setObjectName(u"actionUndo") + icon5 = QIcon() + icon5.addFile(u":/icons/icons/edit-undo.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) + self.actionUndo.setIcon(icon5) + self.actionUndo.setMenuRole(QAction.MenuRole.NoRole) + self.actionRedo = QAction(MainWindow) + self.actionRedo.setObjectName(u"actionRedo") + icon6 = QIcon() + icon6.addFile(u":/icons/icons/edit-redo.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) + self.actionRedo.setIcon(icon6) + self.actionRedo.setMenuRole(QAction.MenuRole.NoRole) + self.actionReset_Layout = QAction(MainWindow) + self.actionReset_Layout.setObjectName(u"actionReset_Layout") + self.actionReset_Layout.setMenuRole(QAction.MenuRole.NoRole) + self.actionPanels = QAction(MainWindow) + self.actionPanels.setObjectName(u"actionPanels") + self.actionToolbars = QAction(MainWindow) + self.actionToolbars.setObjectName(u"actionToolbars") + self.actionSettings = QAction(MainWindow) + self.actionSettings.setObjectName(u"actionSettings") + self.actionDelete = QAction(MainWindow) + self.actionDelete.setObjectName(u"actionDelete") + icon7 = QIcon() + icon7.addFile(u":/icons/icons/edit-delete.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) + self.actionDelete.setIcon(icon7) + self.actionDelete.setMenuRole(QAction.MenuRole.NoRole) + self.actionEscape = QAction(MainWindow) + self.actionEscape.setObjectName(u"actionEscape") + self.actionEscape.setMenuRole(QAction.MenuRole.NoRole) + self.actionCopy = QAction(MainWindow) + self.actionCopy.setObjectName(u"actionCopy") + icon8 = QIcon() + icon8.addFile(u":/icons/icons/edit-copy.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) + self.actionCopy.setIcon(icon8) + self.actionCopy.setMenuRole(QAction.MenuRole.NoRole) + self.actionPaste = QAction(MainWindow) + self.actionPaste.setObjectName(u"actionPaste") + icon9 = QIcon() + icon9.addFile(u":/icons/icons/edit-paste.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) + self.actionPaste.setIcon(icon9) + self.actionPaste.setMenuRole(QAction.MenuRole.NoRole) + self.actionCut = QAction(MainWindow) + self.actionCut.setObjectName(u"actionCut") + icon10 = QIcon() + icon10.addFile(u":/icons/icons/edit-cut.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) + self.actionCut.setIcon(icon10) + self.actionCut.setMenuRole(QAction.MenuRole.NoRole) + self.actionSimulation_Settings = QAction(MainWindow) + self.actionSimulation_Settings.setObjectName(u"actionSimulation_Settings") + icon11 = QIcon() + icon11.addFile(u":/icons/icons/configure.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) + self.actionSimulation_Settings.setIcon(icon11) + self.actionSimulation_Settings.setMenuRole(QAction.MenuRole.NoRole) + self.actionEdit_Parameters = QAction(MainWindow) + self.actionEdit_Parameters.setObjectName(u"actionEdit_Parameters") + icon12 = QIcon() + icon12.addFile(u":/icons/icons/view-form-table.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) + self.actionEdit_Parameters.setIcon(icon12) + self.actionEdit_Parameters.setMenuRole(QAction.MenuRole.NoRole) + self.actionCompile_Model = QAction(MainWindow) + self.actionCompile_Model.setObjectName(u"actionCompile_Model") + icon13 = QIcon() + icon13.addFile(u":/icons/icons/run-build.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) + self.actionCompile_Model.setIcon(icon13) + self.actionCompile_Model.setMenuRole(QAction.MenuRole.NoRole) + self.actionOpen_Simulation_Window = QAction(MainWindow) + self.actionOpen_Simulation_Window.setObjectName(u"actionOpen_Simulation_Window") + self.actionOpen_Simulation_Window.setIcon(icon) + self.actionOpen_Simulation_Window.setMenuRole(QAction.MenuRole.NoRole) + self.actionAbout = QAction(MainWindow) + self.actionAbout.setObjectName(u"actionAbout") + self.centralwidget = QWidget(MainWindow) + self.centralwidget.setObjectName(u"centralwidget") + MainWindow.setCentralWidget(self.centralwidget) + self.menubar = QMenuBar(MainWindow) + self.menubar.setObjectName(u"menubar") + self.menubar.setGeometry(QRect(0, 0, 940, 19)) + self.menuFile = QMenu(self.menubar) + self.menuFile.setObjectName(u"menuFile") + self.menuEdit = QMenu(self.menubar) + self.menuEdit.setObjectName(u"menuEdit") + self.menuView = QMenu(self.menubar) + self.menuView.setObjectName(u"menuView") + self.menuHelp = QMenu(self.menubar) + self.menuHelp.setObjectName(u"menuHelp") + self.menuSimulation = QMenu(self.menubar) + self.menuSimulation.setObjectName(u"menuSimulation") + MainWindow.setMenuBar(self.menubar) + self.statusbar = QStatusBar(MainWindow) + self.statusbar.setObjectName(u"statusbar") + MainWindow.setStatusBar(self.statusbar) + self.fileToolBar = QToolBar(MainWindow) + self.fileToolBar.setObjectName(u"fileToolBar") + MainWindow.addToolBar(Qt.ToolBarArea.TopToolBarArea, self.fileToolBar) + self.undoToolBar = QToolBar(MainWindow) + self.undoToolBar.setObjectName(u"undoToolBar") + MainWindow.addToolBar(Qt.ToolBarArea.TopToolBarArea, self.undoToolBar) + self.documentTreeWidget = QDockWidget(MainWindow) + self.documentTreeWidget.setObjectName(u"documentTreeWidget") + self.documentTreeWidget.setMinimumSize(QSize(150, 200)) + self.dockWidgetContents = QWidget() + self.dockWidgetContents.setObjectName(u"dockWidgetContents") + self.verticalLayout = QVBoxLayout(self.dockWidgetContents) + self.verticalLayout.setObjectName(u"verticalLayout") + self.documentTree = QTreeView(self.dockWidgetContents) + self.documentTree.setObjectName(u"documentTree") + + self.verticalLayout.addWidget(self.documentTree) + + self.documentTreeWidget.setWidget(self.dockWidgetContents) + MainWindow.addDockWidget(Qt.DockWidgetArea.LeftDockWidgetArea, self.documentTreeWidget) + self.logWidget = QDockWidget(MainWindow) + self.logWidget.setObjectName(u"logWidget") + self.logWidget.setMinimumSize(QSize(150, 107)) + self.dockWidgetContents_5 = QWidget() + self.dockWidgetContents_5.setObjectName(u"dockWidgetContents_5") + self.verticalLayout_2 = QVBoxLayout(self.dockWidgetContents_5) + self.verticalLayout_2.setObjectName(u"verticalLayout_2") + self.listView = QListView(self.dockWidgetContents_5) + self.listView.setObjectName(u"listView") + + self.verticalLayout_2.addWidget(self.listView) + + self.logWidget.setWidget(self.dockWidgetContents_5) + MainWindow.addDockWidget(Qt.DockWidgetArea.BottomDockWidgetArea, self.logWidget) + self.simToolBar = QToolBar(MainWindow) + self.simToolBar.setObjectName(u"simToolBar") + MainWindow.addToolBar(Qt.ToolBarArea.TopToolBarArea, self.simToolBar) + self.libraryWidget = QDockWidget(MainWindow) + self.libraryWidget.setObjectName(u"libraryWidget") + self.libraryWidget.setMinimumSize(QSize(150, 200)) + self.dockWidgetContents_4 = QWidget() + self.dockWidgetContents_4.setObjectName(u"dockWidgetContents_4") + self.verticalLayout_4 = QVBoxLayout(self.dockWidgetContents_4) + self.verticalLayout_4.setObjectName(u"verticalLayout_4") + self.libraryTree = QTreeView(self.dockWidgetContents_4) + self.libraryTree.setObjectName(u"libraryTree") + + self.verticalLayout_4.addWidget(self.libraryTree) + + self.libraryWidget.setWidget(self.dockWidgetContents_4) + MainWindow.addDockWidget(Qt.DockWidgetArea.LeftDockWidgetArea, self.libraryWidget) + + self.menubar.addAction(self.menuFile.menuAction()) + self.menubar.addAction(self.menuEdit.menuAction()) + self.menubar.addAction(self.menuView.menuAction()) + self.menubar.addAction(self.menuSimulation.menuAction()) + self.menubar.addAction(self.menuHelp.menuAction()) + self.menuFile.addAction(self.actionNew_File) + self.menuFile.addAction(self.actionOpen_File) + self.menuFile.addSeparator() + self.menuFile.addAction(self.actionSave_File) + self.menuFile.addAction(self.actionSave_File_As) + self.menuFile.addSeparator() + self.menuFile.addAction(self.actionClose) + self.menuEdit.addAction(self.actionUndo) + self.menuEdit.addAction(self.actionRedo) + self.menuEdit.addSeparator() + self.menuEdit.addAction(self.actionCopy) + self.menuEdit.addAction(self.actionCut) + self.menuEdit.addAction(self.actionPaste) + self.menuEdit.addSeparator() + self.menuEdit.addAction(self.actionDelete) + self.menuEdit.addSeparator() + self.menuEdit.addAction(self.actionSettings) + self.menuView.addAction(self.actionReset_Layout) + self.menuView.addAction(self.actionPanels) + self.menuView.addAction(self.actionToolbars) + self.menuHelp.addAction(self.actionAbout) + self.menuHelp.addAction(self.actionAbout_QT) + self.menuSimulation.addAction(self.actionSimulation_Settings) + self.menuSimulation.addAction(self.actionEdit_Parameters) + self.menuSimulation.addSeparator() + self.menuSimulation.addAction(self.actionCompile_Model) + self.menuSimulation.addAction(self.actionOpen_Simulation_Window) + self.fileToolBar.addAction(self.actionNew_File) + self.fileToolBar.addAction(self.actionOpen_File) + self.fileToolBar.addAction(self.actionSave_File) + self.fileToolBar.addAction(self.actionSave_File_As) + self.undoToolBar.addAction(self.actionUndo) + self.undoToolBar.addAction(self.actionRedo) + self.undoToolBar.addAction(self.actionCopy) + self.undoToolBar.addAction(self.actionCut) + self.undoToolBar.addAction(self.actionPaste) + self.simToolBar.addAction(self.actionSimulation_Settings) + self.simToolBar.addAction(self.actionEdit_Parameters) + self.simToolBar.addAction(self.actionCompile_Model) + self.simToolBar.addAction(self.actionOpen_Simulation_Window) + + self.retranslateUi(MainWindow) + + QMetaObject.connectSlotsByName(MainWindow) + # setupUi + + def retranslateUi(self, MainWindow): + MainWindow.setWindowTitle(QCoreApplication.translate("MainWindow", u"MainWindow", None)) + self.actionOpen_File.setText(QCoreApplication.translate("MainWindow", u"Open File", None)) +#if QT_CONFIG(tooltip) + self.actionOpen_File.setToolTip(QCoreApplication.translate("MainWindow", u"Open a file", None)) +#endif // QT_CONFIG(tooltip) +#if QT_CONFIG(shortcut) + self.actionOpen_File.setShortcut(QCoreApplication.translate("MainWindow", u"Ctrl+O", None)) +#endif // QT_CONFIG(shortcut) + self.actionNew_File.setText(QCoreApplication.translate("MainWindow", u"New File", None)) +#if QT_CONFIG(tooltip) + self.actionNew_File.setToolTip(QCoreApplication.translate("MainWindow", u"Create a new file", None)) +#endif // QT_CONFIG(tooltip) +#if QT_CONFIG(shortcut) + self.actionNew_File.setShortcut(QCoreApplication.translate("MainWindow", u"Ctrl+N", None)) +#endif // QT_CONFIG(shortcut) + self.actionSave_File.setText(QCoreApplication.translate("MainWindow", u"Save File", None)) +#if QT_CONFIG(tooltip) + self.actionSave_File.setToolTip(QCoreApplication.translate("MainWindow", u"Save a file to disk", None)) +#endif // QT_CONFIG(tooltip) +#if QT_CONFIG(shortcut) + self.actionSave_File.setShortcut(QCoreApplication.translate("MainWindow", u"Ctrl+S", None)) +#endif // QT_CONFIG(shortcut) + self.actionSave_File_As.setText(QCoreApplication.translate("MainWindow", u"Save File As...", None)) +#if QT_CONFIG(tooltip) + self.actionSave_File_As.setToolTip(QCoreApplication.translate("MainWindow", u"Save file to disk as another file", None)) +#endif // QT_CONFIG(tooltip) +#if QT_CONFIG(shortcut) + self.actionSave_File_As.setShortcut(QCoreApplication.translate("MainWindow", u"Ctrl+Shift+S", None)) +#endif // QT_CONFIG(shortcut) + self.actionClose.setText(QCoreApplication.translate("MainWindow", u"Close", None)) +#if QT_CONFIG(tooltip) + self.actionClose.setToolTip(QCoreApplication.translate("MainWindow", u"Close application", None)) +#endif // QT_CONFIG(tooltip) +#if QT_CONFIG(shortcut) + self.actionClose.setShortcut(QCoreApplication.translate("MainWindow", u"Ctrl+Q", None)) +#endif // QT_CONFIG(shortcut) + self.actionAbout_QT.setText(QCoreApplication.translate("MainWindow", u"About QT", None)) + self.actionUndo.setText(QCoreApplication.translate("MainWindow", u"Undo", None)) +#if QT_CONFIG(shortcut) + self.actionUndo.setShortcut(QCoreApplication.translate("MainWindow", u"Ctrl+Z", None)) +#endif // QT_CONFIG(shortcut) + self.actionRedo.setText(QCoreApplication.translate("MainWindow", u"Redo", None)) +#if QT_CONFIG(shortcut) + self.actionRedo.setShortcut(QCoreApplication.translate("MainWindow", u"Ctrl+Y", None)) +#endif // QT_CONFIG(shortcut) + self.actionReset_Layout.setText(QCoreApplication.translate("MainWindow", u"Reset Layout", None)) +#if QT_CONFIG(tooltip) + self.actionReset_Layout.setToolTip(QCoreApplication.translate("MainWindow", u"Reset window layout to default", None)) +#endif // QT_CONFIG(tooltip) + self.actionPanels.setText(QCoreApplication.translate("MainWindow", u"Panels", None)) + self.actionToolbars.setText(QCoreApplication.translate("MainWindow", u"Toolbars", None)) + self.actionSettings.setText(QCoreApplication.translate("MainWindow", u"Settings", None)) + self.actionDelete.setText(QCoreApplication.translate("MainWindow", u"Delete", None)) +#if QT_CONFIG(tooltip) + self.actionDelete.setToolTip(QCoreApplication.translate("MainWindow", u"Delete selected", None)) +#endif // QT_CONFIG(tooltip) +#if QT_CONFIG(shortcut) + self.actionDelete.setShortcut(QCoreApplication.translate("MainWindow", u"Del", None)) +#endif // QT_CONFIG(shortcut) + self.actionEscape.setText(QCoreApplication.translate("MainWindow", u"Escape", None)) +#if QT_CONFIG(shortcut) + self.actionEscape.setShortcut(QCoreApplication.translate("MainWindow", u"Esc", None)) +#endif // QT_CONFIG(shortcut) + self.actionCopy.setText(QCoreApplication.translate("MainWindow", u"Copy", None)) +#if QT_CONFIG(tooltip) + self.actionCopy.setToolTip(QCoreApplication.translate("MainWindow", u"Copy selected", None)) +#endif // QT_CONFIG(tooltip) +#if QT_CONFIG(shortcut) + self.actionCopy.setShortcut(QCoreApplication.translate("MainWindow", u"Ctrl+C", None)) +#endif // QT_CONFIG(shortcut) + self.actionPaste.setText(QCoreApplication.translate("MainWindow", u"Paste", None)) +#if QT_CONFIG(tooltip) + self.actionPaste.setToolTip(QCoreApplication.translate("MainWindow", u"Paste selected", None)) +#endif // QT_CONFIG(tooltip) +#if QT_CONFIG(shortcut) + self.actionPaste.setShortcut(QCoreApplication.translate("MainWindow", u"Ctrl+V", None)) +#endif // QT_CONFIG(shortcut) + self.actionCut.setText(QCoreApplication.translate("MainWindow", u"Cut", None)) +#if QT_CONFIG(tooltip) + self.actionCut.setToolTip(QCoreApplication.translate("MainWindow", u"Cut selected", None)) +#endif // QT_CONFIG(tooltip) +#if QT_CONFIG(shortcut) + self.actionCut.setShortcut(QCoreApplication.translate("MainWindow", u"Ctrl+X", None)) +#endif // QT_CONFIG(shortcut) + self.actionSimulation_Settings.setText(QCoreApplication.translate("MainWindow", u"Simulation Settings", None)) + self.actionEdit_Parameters.setText(QCoreApplication.translate("MainWindow", u"Edit Parameters", None)) + self.actionCompile_Model.setText(QCoreApplication.translate("MainWindow", u"Compile Model", None)) + self.actionOpen_Simulation_Window.setText(QCoreApplication.translate("MainWindow", u"Open Simulation Window", None)) + self.actionAbout.setText(QCoreApplication.translate("MainWindow", u"About", None)) + self.menuFile.setTitle(QCoreApplication.translate("MainWindow", u"File", None)) + self.menuEdit.setTitle(QCoreApplication.translate("MainWindow", u"Edit", None)) + self.menuView.setTitle(QCoreApplication.translate("MainWindow", u"View", None)) + self.menuHelp.setTitle(QCoreApplication.translate("MainWindow", u"Help", None)) + self.menuSimulation.setTitle(QCoreApplication.translate("MainWindow", u"Simulation", None)) + self.fileToolBar.setWindowTitle(QCoreApplication.translate("MainWindow", u"toolBar", None)) + self.undoToolBar.setWindowTitle(QCoreApplication.translate("MainWindow", u"toolBar", None)) + self.documentTreeWidget.setWindowTitle(QCoreApplication.translate("MainWindow", u"Document Tree", None)) + self.logWidget.setWindowTitle(QCoreApplication.translate("MainWindow", u"Log", None)) + self.simToolBar.setWindowTitle(QCoreApplication.translate("MainWindow", u"toolBar", None)) + self.libraryWidget.setWindowTitle(QCoreApplication.translate("MainWindow", u"Libraries", None)) + # retranslateUi + diff --git a/src/bedit_gui/ui/forms/settings_dialog.ui b/src/bedit_gui/ui/forms/settings_dialog.ui index 315d4ef..b8a0175 100644 --- a/src/bedit_gui/ui/forms/settings_dialog.ui +++ b/src/bedit_gui/ui/forms/settings_dialog.ui @@ -17,7 +17,7 @@ - 0 + 1 @@ -90,6 +90,34 @@ + + + Libraries + + + + + + + + + + + Add library + + + + + + + Add directory + + + + + + + diff --git a/src/bedit_gui/ui/forms/settings_dialog_ui.py b/src/bedit_gui/ui/forms/settings_dialog_ui.py new file mode 100644 index 0000000..1975eba --- /dev/null +++ b/src/bedit_gui/ui/forms/settings_dialog_ui.py @@ -0,0 +1,128 @@ +# -*- coding: utf-8 -*- + +################################################################################ +## Form generated from reading UI file 'settings_dialog.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 (QBrush, QColor, QConicalGradient, QCursor, + QFont, QFontDatabase, QGradient, QIcon, + QImage, QKeySequence, QLinearGradient, QPainter, + QPalette, QPixmap, QRadialGradient, QTransform) +from PySide6.QtWidgets import (QAbstractButton, QApplication, QComboBox, QDialog, + QDialogButtonBox, QFormLayout, QHBoxLayout, QLabel, + QListView, QPushButton, QSizePolicy, QSpacerItem, + QSpinBox, QTabWidget, QVBoxLayout, QWidget) + +class Ui_Settings(object): + def setupUi(self, Settings): + if not Settings.objectName(): + Settings.setObjectName(u"Settings") + Settings.resize(400, 230) + self.verticalLayout = QVBoxLayout(Settings) + self.verticalLayout.setObjectName(u"verticalLayout") + self.tabWidget = QTabWidget(Settings) + self.tabWidget.setObjectName(u"tabWidget") + self.General = QWidget() + self.General.setObjectName(u"General") + self.formLayout = QFormLayout(self.General) + self.formLayout.setObjectName(u"formLayout") + self.logLevel = QComboBox(self.General) + self.logLevel.addItem("") + self.logLevel.addItem("") + self.logLevel.addItem("") + self.logLevel.addItem("") + self.logLevel.setObjectName(u"logLevel") + + self.formLayout.setWidget(0, QFormLayout.ItemRole.FieldRole, self.logLevel) + + self.lableLogLevel = QLabel(self.General) + self.lableLogLevel.setObjectName(u"lableLogLevel") + + self.formLayout.setWidget(0, QFormLayout.ItemRole.LabelRole, self.lableLogLevel) + + self.labelSnapToGridSize = QLabel(self.General) + self.labelSnapToGridSize.setObjectName(u"labelSnapToGridSize") + + self.formLayout.setWidget(1, QFormLayout.ItemRole.LabelRole, self.labelSnapToGridSize) + + self.snapToGridSize = QSpinBox(self.General) + self.snapToGridSize.setObjectName(u"snapToGridSize") + self.snapToGridSize.setMinimum(1) + self.snapToGridSize.setMaximum(256) + self.snapToGridSize.setValue(4) + + self.formLayout.setWidget(1, QFormLayout.ItemRole.FieldRole, self.snapToGridSize) + + self.verticalSpacer = QSpacerItem(20, 40, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Expanding) + + self.formLayout.setItem(2, QFormLayout.ItemRole.FieldRole, self.verticalSpacer) + + self.tabWidget.addTab(self.General, "") + self.Libraries = QWidget() + self.Libraries.setObjectName(u"Libraries") + self.verticalLayout_2 = QVBoxLayout(self.Libraries) + self.verticalLayout_2.setObjectName(u"verticalLayout_2") + self.listView = QListView(self.Libraries) + self.listView.setObjectName(u"listView") + + self.verticalLayout_2.addWidget(self.listView) + + self.horizontalLayout = QHBoxLayout() + self.horizontalLayout.setObjectName(u"horizontalLayout") + self.addLibButton = QPushButton(self.Libraries) + self.addLibButton.setObjectName(u"addLibButton") + + self.horizontalLayout.addWidget(self.addLibButton) + + self.addDirButton = QPushButton(self.Libraries) + self.addDirButton.setObjectName(u"addDirButton") + + self.horizontalLayout.addWidget(self.addDirButton) + + + self.verticalLayout_2.addLayout(self.horizontalLayout) + + self.tabWidget.addTab(self.Libraries, "") + + self.verticalLayout.addWidget(self.tabWidget) + + self.buttonBox = QDialogButtonBox(Settings) + self.buttonBox.setObjectName(u"buttonBox") + self.buttonBox.setOrientation(Qt.Orientation.Horizontal) + self.buttonBox.setStandardButtons(QDialogButtonBox.StandardButton.Cancel|QDialogButtonBox.StandardButton.Ok) + + self.verticalLayout.addWidget(self.buttonBox) + + + self.retranslateUi(Settings) + self.buttonBox.accepted.connect(Settings.accept) + self.buttonBox.rejected.connect(Settings.reject) + + self.tabWidget.setCurrentIndex(1) + + + QMetaObject.connectSlotsByName(Settings) + # setupUi + + def retranslateUi(self, Settings): + Settings.setWindowTitle(QCoreApplication.translate("Settings", u"Settings", None)) + self.logLevel.setItemText(0, QCoreApplication.translate("Settings", u"Debug", None)) + self.logLevel.setItemText(1, QCoreApplication.translate("Settings", u"Info", None)) + self.logLevel.setItemText(2, QCoreApplication.translate("Settings", u"Warning", None)) + self.logLevel.setItemText(3, QCoreApplication.translate("Settings", u"Error", None)) + + self.lableLogLevel.setText(QCoreApplication.translate("Settings", u"Log level:", None)) + self.labelSnapToGridSize.setText(QCoreApplication.translate("Settings", u"Snap-to-grid size:", None)) + self.tabWidget.setTabText(self.tabWidget.indexOf(self.General), QCoreApplication.translate("Settings", u"General", None)) + self.addLibButton.setText(QCoreApplication.translate("Settings", u"Add library", None)) + self.addDirButton.setText(QCoreApplication.translate("Settings", u"Add directory", None)) + self.tabWidget.setTabText(self.tabWidget.indexOf(self.Libraries), QCoreApplication.translate("Settings", u"Libraries", None)) + # retranslateUi + diff --git a/src/bedit_gui/views/dialogs/settings_dialog.py b/src/bedit_gui/views/dialogs/settings_dialog.py index a8bc8f5..6c975a2 100644 --- a/src/bedit_gui/views/dialogs/settings_dialog.py +++ b/src/bedit_gui/views/dialogs/settings_dialog.py @@ -1,8 +1,11 @@ from __future__ import annotations import logging +from pathlib import Path -from PySide6.QtWidgets import QDialog, QWidget +from PySide6.QtCore import QStringListModel, Qt +from PySide6.QtGui import QKeySequence, QShortcut +from PySide6.QtWidgets import QAbstractItemView, QDialog, QFileDialog, QWidget from bedit_gui.ui.generated.ui_settings_dialog import Ui_Settings @@ -21,6 +24,7 @@ class SettingsDialog(QDialog): self, log_level: int, snap_to_grid_size: int, + library_paths: list[str], parent: QWidget | None = None, ) -> None: super().__init__(parent) @@ -37,6 +41,14 @@ class SettingsDialog(QDialog): selected if selected >= 0 else self.LOG_LEVELS.index(logging.INFO) ) self.ui.snapToGridSize.setValue(snap_to_grid_size) + self._library_paths = QStringListModel(list(library_paths), self) + self.ui.listView.setModel(self._library_paths) + self.ui.listView.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection) + self.ui.addLibButton.clicked.connect(self._add_library_file) + self.ui.addDirButton.clicked.connect(self._add_library_directory) + self._delete_shortcut = QShortcut(QKeySequence.StandardKey.Delete, self.ui.listView) + self._delete_shortcut.setContext(Qt.ShortcutContext.WidgetShortcut) + self._delete_shortcut.activated.connect(self._delete_selected_paths) @property def log_level(self) -> int: @@ -45,3 +57,29 @@ class SettingsDialog(QDialog): @property def snap_to_grid_size(self) -> int: return self.ui.snapToGridSize.value() + + @property + def library_paths(self) -> list[str]: + return self._library_paths.stringList() + + def _add_library_file(self) -> None: + path, _selected_filter = QFileDialog.getOpenFileName(self, "Add BEdit Library", "", "BEdit documents (*.bedit.json *.beb *.json)") + if path: + self._add_library_path(path) + + def _add_library_directory(self) -> None: + path = QFileDialog.getExistingDirectory(self, "Add Library Directory") + if path: + self._add_library_path(path) + + def _add_library_path(self, path: str) -> None: + normalized = str(Path(path).resolve()) + paths = self._library_paths.stringList() + if normalized not in paths: + paths.append(normalized) + self._library_paths.setStringList(paths) + + def _delete_selected_paths(self) -> None: + rows = sorted((index.row() for index in self.ui.listView.selectionModel().selectedRows()), reverse=True) + for row in rows: + self._library_paths.removeRow(row)