Reorganized the application around a clear frontend/backend boundary.
src/bedit/
├── __main__.py
├── core/ # Pure Python, no PySide
│ ├── model.py
│ ├── port_types.py
│ ├── serializer.py
│ └── libraries.py
└── gui/ # All Qt-dependent code
├── app.py
├── main_window.py
├── preferences.py
├── controllers/
├── dialogs/
├── graphics/
├── models/
└── generated/ # Designer/resource output only
Notable improvements:
Domain models, serialization, port types, and library parsing are now Qt-free.
Qt signals and undo infrastructure are explicitly isolated under gui/controllers.
Library parsing is separated from the Qt repository and tree models.
All generated Python is contained in gui/generated.
Designer build tasks now write to the generated directory.
The application entry point and package metadata use the new paths.
README now documents the structure and dependency rules.
Removed the old mixed document, library, and workspace packages.
This commit is contained in:
1
BEdit/src/bedit/gui/dialogs/__init__.py
Normal file
1
BEdit/src/bedit/gui/dialogs/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Application dialogs."""
|
||||
45
BEdit/src/bedit/gui/dialogs/component_options.py
Normal file
45
BEdit/src/bedit/gui/dialogs/component_options.py
Normal file
@@ -0,0 +1,45 @@
|
||||
from PySide6.QtWidgets import QDialog, QMessageBox, QPushButton
|
||||
|
||||
from bedit.core.model import Component
|
||||
from bedit.gui.graphics.icon_editor import IconEditorDialog
|
||||
from bedit.gui.generated.ui_component_options_dialog import Ui_ComponentOptionsDialog
|
||||
|
||||
|
||||
class ComponentOptionsDialog(QDialog):
|
||||
def __init__(self, component: Component, parent=None) -> None:
|
||||
super().__init__(parent)
|
||||
self.ui = Ui_ComponentOptionsDialog()
|
||||
self.ui.setupUi(self)
|
||||
self.component = component
|
||||
self.edited_icon = component.icon
|
||||
self.edited_inputs = component.inputs
|
||||
self.edited_outputs = component.outputs
|
||||
self.ui.nameEdit.setText(component.name)
|
||||
for widget in (
|
||||
self.ui.shapeLabel, self.ui.shapeCombo, self.ui.iconTextLabel,
|
||||
self.ui.iconTextEdit, self.ui.fillLabel, self.ui.fillEdit,
|
||||
self.ui.borderLabel, self.ui.borderEdit,
|
||||
):
|
||||
widget.hide()
|
||||
self.icon_editor_button = QPushButton("Edit Icon…", self)
|
||||
self.icon_editor_button.setToolTip("Open the vector icon and port-position editor")
|
||||
self.icon_editor_button.clicked.connect(self.edit_icon)
|
||||
self.ui.optionsForm.insertRow(1, "Icon:", self.icon_editor_button)
|
||||
self.ui.showSubtreeCheckBox.setChecked(component.show_subtree_in_library)
|
||||
|
||||
def edit_icon(self) -> None:
|
||||
working = Component.from_dict(self.component.to_dict())
|
||||
working.icon = self.edited_icon
|
||||
working.inputs = self.edited_inputs
|
||||
working.outputs = self.edited_outputs
|
||||
dialog = IconEditorDialog(working, self)
|
||||
if dialog.exec() == dialog.DialogCode.Accepted:
|
||||
self.edited_icon = dialog.icon
|
||||
self.edited_inputs = dialog.inputs
|
||||
self.edited_outputs = dialog.outputs
|
||||
|
||||
def accept(self) -> None:
|
||||
if not self.ui.nameEdit.text().strip():
|
||||
QMessageBox.warning(self, "Invalid name", "The component name cannot be empty.")
|
||||
return
|
||||
super().accept()
|
||||
42
BEdit/src/bedit/gui/dialogs/item_options.py
Normal file
42
BEdit/src/bedit/gui/dialogs/item_options.py
Normal file
@@ -0,0 +1,42 @@
|
||||
from PySide6.QtWidgets import (
|
||||
QDialog,
|
||||
QDialogButtonBox,
|
||||
QFormLayout,
|
||||
QLineEdit,
|
||||
QMessageBox,
|
||||
QVBoxLayout,
|
||||
)
|
||||
|
||||
|
||||
class ItemOptionsDialog(QDialog):
|
||||
"""Small, extensible options dialog shared by ports and connections."""
|
||||
|
||||
def __init__(self, title: str, name: str, parent=None, *, name_required: bool = True) -> None:
|
||||
super().__init__(parent)
|
||||
self.name_required = name_required
|
||||
self.setWindowTitle(title)
|
||||
self.resize(380, 120)
|
||||
|
||||
layout = QVBoxLayout(self)
|
||||
self.form = QFormLayout()
|
||||
self.name_edit = QLineEdit(name, self)
|
||||
self.form.addRow("Name:", self.name_edit)
|
||||
layout.addLayout(self.form)
|
||||
|
||||
buttons = QDialogButtonBox(
|
||||
QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel,
|
||||
parent=self,
|
||||
)
|
||||
buttons.accepted.connect(self.accept)
|
||||
buttons.rejected.connect(self.reject)
|
||||
layout.addWidget(buttons)
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return self.name_edit.text().strip()
|
||||
|
||||
def accept(self) -> None:
|
||||
if self.name_required and not self.name:
|
||||
QMessageBox.warning(self, "Invalid name", "The name cannot be empty.")
|
||||
return
|
||||
super().accept()
|
||||
162
BEdit/src/bedit/gui/dialogs/port_options.py
Normal file
162
BEdit/src/bedit/gui/dialogs/port_options.py
Normal file
@@ -0,0 +1,162 @@
|
||||
from copy import deepcopy
|
||||
from uuid import uuid4
|
||||
|
||||
from PySide6.QtCore import Qt
|
||||
from PySide6.QtWidgets import (
|
||||
QComboBox,
|
||||
QDialog,
|
||||
QDialogButtonBox,
|
||||
QFormLayout,
|
||||
QHBoxLayout,
|
||||
QLabel,
|
||||
QLineEdit,
|
||||
QListWidget,
|
||||
QListWidgetItem,
|
||||
QMessageBox,
|
||||
QPushButton,
|
||||
QSplitter,
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
)
|
||||
|
||||
from bedit.core.model import Component, Port
|
||||
from bedit.core.port_types import PortTypeRegistry
|
||||
|
||||
|
||||
PORT_ROLE = Qt.ItemDataRole.UserRole
|
||||
|
||||
|
||||
class PortOptionsDialog(QDialog):
|
||||
"""Unified editor for a component's typed, oriented ports."""
|
||||
|
||||
def __init__(self, component: Component, parent=None, *, read_only: bool = False) -> None:
|
||||
super().__init__(parent)
|
||||
self.setWindowTitle(f"Port Options — {component.name}")
|
||||
self.resize(620, 380)
|
||||
self.ports: list[tuple[Port, str]] = [
|
||||
*((deepcopy(port), "input") for port in component.inputs),
|
||||
*((deepcopy(port), "output") for port in component.outputs),
|
||||
]
|
||||
self._loading = False
|
||||
layout = QVBoxLayout(self)
|
||||
splitter = QSplitter()
|
||||
left = QWidget()
|
||||
left_layout = QVBoxLayout(left)
|
||||
self.list = QListWidget()
|
||||
self.list.currentRowChanged.connect(self._load_current)
|
||||
left_layout.addWidget(self.list)
|
||||
port_buttons = QHBoxLayout()
|
||||
self.add_button = QPushButton("Add Port")
|
||||
self.remove_button = QPushButton("Remove Port")
|
||||
self.add_button.clicked.connect(self.add_port)
|
||||
self.remove_button.clicked.connect(self.remove_port)
|
||||
port_buttons.addWidget(self.add_button)
|
||||
port_buttons.addWidget(self.remove_button)
|
||||
left_layout.addLayout(port_buttons)
|
||||
right = QWidget()
|
||||
form = QFormLayout(right)
|
||||
self.name_edit = QLineEdit()
|
||||
self.type_combo = QComboBox()
|
||||
for port_type in PortTypeRegistry.all():
|
||||
self.type_combo.addItem(port_type.display_name, port_type.id)
|
||||
self.orientation_combo = QComboBox()
|
||||
self.orientation_combo.addItem("Input", "input")
|
||||
self.orientation_combo.addItem("Output", "output")
|
||||
form.addRow("Name:", self.name_edit)
|
||||
form.addRow("Type:", self.type_combo)
|
||||
form.addRow("Orientation:", self.orientation_combo)
|
||||
form.addRow("", QLabel("New ports start at (0, 0) in the icon editor."))
|
||||
self.name_edit.textEdited.connect(self._store_current)
|
||||
self.type_combo.currentIndexChanged.connect(self._store_current)
|
||||
self.orientation_combo.currentIndexChanged.connect(self._store_current)
|
||||
splitter.addWidget(left)
|
||||
splitter.addWidget(right)
|
||||
splitter.setSizes([250, 370])
|
||||
layout.addWidget(splitter)
|
||||
buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
|
||||
buttons.accepted.connect(self.accept)
|
||||
buttons.rejected.connect(self.reject)
|
||||
layout.addWidget(buttons)
|
||||
if read_only:
|
||||
self.add_button.setEnabled(False)
|
||||
self.remove_button.setEnabled(False)
|
||||
self.name_edit.setReadOnly(True)
|
||||
self.type_combo.setEnabled(False)
|
||||
self.orientation_combo.setEnabled(False)
|
||||
buttons.button(QDialogButtonBox.StandardButton.Ok).setText("Close")
|
||||
buttons.button(QDialogButtonBox.StandardButton.Cancel).hide()
|
||||
self._rebuild_list(0 if self.ports else -1)
|
||||
|
||||
@property
|
||||
def inputs(self) -> list[Port]:
|
||||
return [port for port, orientation in self.ports if orientation == "input"]
|
||||
|
||||
@property
|
||||
def outputs(self) -> list[Port]:
|
||||
return [port for port, orientation in self.ports if orientation == "output"]
|
||||
|
||||
def _rebuild_list(self, row: int = -1) -> None:
|
||||
self.list.clear()
|
||||
for port, orientation in self.ports:
|
||||
item = QListWidgetItem(f"{port.name} [{orientation}, {port.type}]")
|
||||
item.setData(PORT_ROLE, port.id)
|
||||
self.list.addItem(item)
|
||||
self.list.setCurrentRow(min(row, len(self.ports) - 1))
|
||||
self._update_enabled()
|
||||
|
||||
def _load_current(self, row: int) -> None:
|
||||
self._loading = True
|
||||
enabled = 0 <= row < len(self.ports)
|
||||
if enabled:
|
||||
port, orientation = self.ports[row]
|
||||
self.name_edit.setText(port.name)
|
||||
self.type_combo.setCurrentIndex(self.type_combo.findData(port.type))
|
||||
self.orientation_combo.setCurrentIndex(self.orientation_combo.findData(orientation))
|
||||
else:
|
||||
self.name_edit.clear()
|
||||
self._loading = False
|
||||
self._update_enabled()
|
||||
|
||||
def _update_enabled(self) -> None:
|
||||
enabled = self.list.currentRow() >= 0
|
||||
self.remove_button.setEnabled(enabled)
|
||||
self.name_edit.setEnabled(enabled)
|
||||
self.type_combo.setEnabled(enabled)
|
||||
self.orientation_combo.setEnabled(enabled)
|
||||
|
||||
def _store_current(self) -> None:
|
||||
row = self.list.currentRow()
|
||||
if self._loading or not (0 <= row < len(self.ports)):
|
||||
return
|
||||
port, _orientation = self.ports[row]
|
||||
port.name = self.name_edit.text()
|
||||
port.type = self.type_combo.currentData()
|
||||
self.ports[row] = (port, self.orientation_combo.currentData())
|
||||
self.list.item(row).setText(
|
||||
f"{port.name} [{self.ports[row][1]}, {port.type}]"
|
||||
)
|
||||
|
||||
def add_port(self) -> None:
|
||||
port = Port(
|
||||
id=f"port-{uuid4().hex[:8]}",
|
||||
name=f"Port {len(self.ports) + 1}",
|
||||
properties={"iconPosition": {"x": 0.0, "y": 0.0}},
|
||||
type="signal",
|
||||
)
|
||||
self.ports.append((port, "input"))
|
||||
self._rebuild_list(len(self.ports) - 1)
|
||||
self.name_edit.selectAll()
|
||||
self.name_edit.setFocus()
|
||||
|
||||
def remove_port(self) -> None:
|
||||
row = self.list.currentRow()
|
||||
if row >= 0:
|
||||
self.ports.pop(row)
|
||||
self._rebuild_list(min(row, len(self.ports) - 1))
|
||||
|
||||
def accept(self) -> None:
|
||||
self._store_current()
|
||||
if any(not port.name.strip() for port, _orientation in self.ports):
|
||||
QMessageBox.warning(self, "Invalid port", "Every port must have a name.")
|
||||
return
|
||||
super().accept()
|
||||
138
BEdit/src/bedit/gui/dialogs/settings.py
Normal file
138
BEdit/src/bedit/gui/dialogs/settings.py
Normal file
@@ -0,0 +1,138 @@
|
||||
from pathlib import Path
|
||||
|
||||
from PySide6.QtCore import QSettings, Signal
|
||||
from PySide6.QtWidgets import QDialog, QFileDialog, QFormLayout, QGroupBox, QSpinBox
|
||||
|
||||
from bedit.core.libraries import default_library_paths
|
||||
from bedit.gui.preferences import application_settings
|
||||
from bedit.gui.generated.ui_settings_dialog import Ui_SettingsDialog
|
||||
|
||||
|
||||
class SettingsDialog(QDialog):
|
||||
"""Edit application preferences defined in the Designer form."""
|
||||
|
||||
settingsChanged = Signal()
|
||||
|
||||
def __init__(self, parent=None) -> None:
|
||||
super().__init__(parent)
|
||||
self.ui = Ui_SettingsDialog()
|
||||
self.ui.setupUi(self)
|
||||
self.grid_group = QGroupBox("Editor grids", self.ui.generalTab)
|
||||
grid_form = QFormLayout(self.grid_group)
|
||||
self.graph_grid_spin = QSpinBox()
|
||||
self.graph_grid_spin.setRange(8, 512)
|
||||
self.graph_grid_spin.setSuffix(" units")
|
||||
self.graph_snap_spin = QSpinBox()
|
||||
self.graph_snap_spin.setRange(1, 128)
|
||||
self.graph_snap_spin.setSuffix(" units")
|
||||
self.graph_grid_spin.valueChanged.connect(self.graph_snap_spin.setMaximum)
|
||||
self.icon_grid_spin = QSpinBox()
|
||||
self.icon_grid_spin.setRange(1, 64)
|
||||
self.icon_grid_spin.setSuffix(" units")
|
||||
grid_form.addRow("Workspace grid size:", self.graph_grid_spin)
|
||||
grid_form.addRow("Workspace snapping size:", self.graph_snap_spin)
|
||||
grid_form.addRow("Icon grid size:", self.icon_grid_spin)
|
||||
self.ui.generalLayout.insertWidget(1, self.grid_group)
|
||||
self.settings = application_settings()
|
||||
self.ui.addLibraryFileButton.clicked.connect(self._add_library_file)
|
||||
self.ui.addLibraryFolderButton.clicked.connect(self._add_library_folder)
|
||||
self.ui.removeLibraryPathButton.clicked.connect(self._remove_library_path)
|
||||
self.ui.libraryPathsList.itemSelectionChanged.connect(self._update_remove_button)
|
||||
self._load_settings()
|
||||
|
||||
def _load_settings(self) -> None:
|
||||
enabled = self.settings.value("autosave/enabled", None)
|
||||
if enabled is None:
|
||||
enabled = self.settings.value("General/autosaveEnabled", False)
|
||||
interval = self.settings.value("autosave/intervalMinutes", None)
|
||||
if interval is None:
|
||||
interval = self.settings.value("General/autosaveInterval", 5)
|
||||
self.ui.autosaveGroupBox.setChecked(
|
||||
self._as_bool(enabled)
|
||||
)
|
||||
self.ui.autosaveIntervalSpinBox.setValue(int(interval))
|
||||
self.ui.libraryPathsList.clear()
|
||||
self.ui.libraryPathsList.addItems(self.library_paths(self.settings))
|
||||
self.graph_grid_spin.setValue(self.graph_grid_size(self.settings))
|
||||
self.graph_snap_spin.setValue(self.graph_snap_size(self.settings))
|
||||
self.icon_grid_spin.setValue(self.icon_grid_size(self.settings))
|
||||
self._update_remove_button()
|
||||
|
||||
@staticmethod
|
||||
def _as_bool(value) -> bool:
|
||||
if isinstance(value, str):
|
||||
return value.strip().lower() in {"1", "true", "yes", "on"}
|
||||
return bool(value)
|
||||
|
||||
@staticmethod
|
||||
def library_paths(settings: QSettings | None = None) -> list[str]:
|
||||
settings = settings if settings is not None else application_settings()
|
||||
value = settings.value("libraries/paths", default_library_paths())
|
||||
if isinstance(value, str):
|
||||
return [value]
|
||||
return [str(path) for path in value]
|
||||
|
||||
@staticmethod
|
||||
def graph_grid_size(settings: QSettings | None = None) -> int:
|
||||
settings = settings if settings is not None else application_settings()
|
||||
return settings.value("grid/graphSize", 64, type=int)
|
||||
|
||||
@staticmethod
|
||||
def graph_snap_size(settings: QSettings | None = None) -> int:
|
||||
settings = settings if settings is not None else application_settings()
|
||||
return settings.value("grid/graphSnapSize", 8, type=int)
|
||||
|
||||
@staticmethod
|
||||
def icon_grid_size(settings: QSettings | None = None) -> int:
|
||||
settings = settings if settings is not None else application_settings()
|
||||
return settings.value("grid/iconSize", 8, type=int)
|
||||
|
||||
def _add_library_file(self) -> None:
|
||||
path, _ = QFileDialog.getOpenFileName(
|
||||
self,
|
||||
"Add library",
|
||||
"",
|
||||
"BEdit libraries (*.json);;All files (*)",
|
||||
)
|
||||
if path:
|
||||
self._append_unique_path(path)
|
||||
|
||||
def _add_library_folder(self) -> None:
|
||||
path = QFileDialog.getExistingDirectory(self, "Add library folder")
|
||||
if path:
|
||||
self._append_unique_path(path)
|
||||
|
||||
def _append_unique_path(self, path: str) -> None:
|
||||
normalized = str(Path(path).expanduser().resolve())
|
||||
existing = {
|
||||
self.ui.libraryPathsList.item(row).text()
|
||||
for row in range(self.ui.libraryPathsList.count())
|
||||
}
|
||||
if normalized not in existing:
|
||||
self.ui.libraryPathsList.addItem(normalized)
|
||||
|
||||
def _remove_library_path(self) -> None:
|
||||
for item in self.ui.libraryPathsList.selectedItems():
|
||||
self.ui.libraryPathsList.takeItem(self.ui.libraryPathsList.row(item))
|
||||
|
||||
def _update_remove_button(self) -> None:
|
||||
self.ui.removeLibraryPathButton.setEnabled(bool(self.ui.libraryPathsList.selectedItems()))
|
||||
|
||||
def accept(self) -> None:
|
||||
self.settings.setValue("autosave/enabled", self.ui.autosaveGroupBox.isChecked())
|
||||
self.settings.setValue(
|
||||
"autosave/intervalMinutes", self.ui.autosaveIntervalSpinBox.value()
|
||||
)
|
||||
self.settings.remove("General/autosaveEnabled")
|
||||
self.settings.remove("General/autosaveInterval")
|
||||
paths = [
|
||||
self.ui.libraryPathsList.item(row).text()
|
||||
for row in range(self.ui.libraryPathsList.count())
|
||||
]
|
||||
self.settings.setValue("libraries/paths", paths)
|
||||
self.settings.setValue("grid/graphSize", self.graph_grid_spin.value())
|
||||
self.settings.setValue("grid/graphSnapSize", self.graph_snap_spin.value())
|
||||
self.settings.setValue("grid/iconSize", self.icon_grid_spin.value())
|
||||
self.settings.sync()
|
||||
self.settingsChanged.emit()
|
||||
super().accept()
|
||||
Reference in New Issue
Block a user