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:
2026-07-20 12:09:00 +02:00
parent 1a47952358
commit 48a2b4c8d0
43 changed files with 1011 additions and 310 deletions

View File

@@ -1,6 +1,5 @@
from bedit.app import main
from bedit.gui.app import main
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -0,0 +1,5 @@
"""Pure BEdit domain model and persistence; this package has no Qt dependency."""
from bedit.core.model import Component, Connection, Endpoint, Graph, GraphDocument, Icon, Port
__all__ = ["Component", "Connection", "Endpoint", "Graph", "GraphDocument", "Icon", "Port"]

View File

@@ -0,0 +1,32 @@
import json
from dataclasses import dataclass
from pathlib import Path
from bedit.core.model import GraphDocument
@dataclass(frozen=True)
class LibraryDocument:
name: str
document: GraphDocument
source_path: str
def bundled_library_path() -> Path:
return Path(__file__).resolve().parents[1] / "data" / "libraries" / "example.json"
def default_library_paths() -> list[str]:
return [str(bundled_library_path())]
def load_library_file(path: Path) -> LibraryDocument:
with path.open(encoding="utf-8") as file:
data = json.load(file)
document = GraphDocument.from_dict(data)
name = str(document.metadata.get("name") or path.stem)
return LibraryDocument(name, document, str(path))
def library_candidates(path: Path) -> list[Path]:
return sorted(path.glob("*.json")) if path.is_dir() else [path]

View File

@@ -5,7 +5,7 @@ from dataclasses import dataclass, field
from typing import Any
from uuid import uuid4
from bedit.document.port_types import PortTypeRegistry
from bedit.core.port_types import PortTypeRegistry
@dataclass

View File

@@ -1,7 +1,7 @@
import json
from pathlib import Path
from bedit.document.model import GraphDocument
from bedit.core.model import GraphDocument
class JsonDocumentSerializer:
@@ -20,4 +20,3 @@ class JsonDocumentSerializer:
json.dump(document.to_dict(), file, indent=2)
file.write("\n")
temporary_path.replace(path)

View File

@@ -0,0 +1,212 @@
{
"format": "bedit-document",
"version": 1,
"metadata": {
"name": "Example"
},
"roots": [
{
"id": "bf714517-e7b2-4f4b-8b53-55642e3beb28",
"name": "A",
"position": {
"x": 0.0,
"y": 0.0
},
"rotation": 0.0,
"interface": {
"inputs": [
{
"id": "port-5c5d4695",
"name": "Port 1",
"position": {
"x": 0.0,
"y": 0.0
},
"properties": {
"iconPosition": {
"x": 32.0,
"y": 64.0
}
},
"type": "signal"
}
],
"outputs": [
{
"id": "port-de109124",
"name": "Port 2",
"position": {
"x": 0.0,
"y": 0.0
},
"properties": {
"iconPosition": {
"x": 96.0,
"y": 64.0
}
},
"type": "signal"
}
]
},
"icon": {
"shape": "rectangle",
"fill": "#f4f4f4",
"border": "#303030",
"text": "Text",
"size": {
"width": 128.0,
"height": 128.0
},
"elements": [
{
"type": "rectangle",
"x": 32.0,
"y": 32.0,
"width": 64.0,
"height": 64.0,
"fill": "#f4f4f4",
"stroke": "#303030",
"lineWidth": 1.5,
"lineStyle": "solid",
"cornerRadius": 5.0
},
{
"type": "text",
"x": 40.0,
"y": 40.0,
"width": 48.0,
"height": 48.0,
"text": "A",
"color": "#303030",
"fontSize": 12.0,
"lineStyle": "solid",
"lineWidth": 1.5,
"stroke": "#303030",
"fill": "#ffffff"
}
]
},
"properties": {},
"library": {
"showSubtree": true
},
"implementation": {
"kind": "text",
"source": {
"equations": [],
"parameters": {}
}
}
},
{
"id": "73c9d0c0-293d-4a55-8b89-a1f095dfa75f",
"name": "B",
"position": {
"x": 0.0,
"y": 0.0
},
"rotation": 0.0,
"interface": {
"inputs": [
{
"id": "port-4f732b3e",
"name": "Port 1",
"position": {
"x": -176.0,
"y": -144.0
},
"properties": {
"iconPosition": {
"x": 32.0,
"y": 48.0
}
},
"type": "signal"
},
{
"id": "port-ef7c4218",
"name": "Port 2",
"position": {
"x": -176.0,
"y": -16.0
},
"properties": {
"iconPosition": {
"x": 32.0,
"y": 80.0
}
},
"type": "signal"
}
],
"outputs": [
{
"id": "port-b68679b2",
"name": "Port 3",
"position": {
"x": 128.0,
"y": -80.0
},
"properties": {
"iconPosition": {
"x": 96.0,
"y": 64.0
}
},
"type": "signal"
}
]
},
"icon": {
"shape": "rectangle",
"fill": "#f4f4f4",
"border": "#303030",
"text": "Graph",
"size": {
"width": 128.0,
"height": 128.0
},
"elements": [
{
"type": "rectangle",
"x": 32.0,
"y": 32.0,
"width": 64.0,
"height": 64.0,
"fill": "#f4f4f4",
"stroke": "#303030",
"lineWidth": 1.5,
"lineStyle": "solid",
"cornerRadius": 5.0
},
{
"type": "text",
"x": 40.0,
"y": 40.0,
"width": 48.0,
"height": 48.0,
"text": "B",
"color": "#303030",
"fontSize": 12.0,
"lineStyle": "solid",
"lineWidth": 1.5,
"stroke": "#303030",
"fill": "#ffffff"
}
]
},
"properties": {},
"library": {
"showSubtree": true
},
"implementation": {
"kind": "graph",
"graph": {
"blocks": [],
"connections": []
}
}
}
]
}

View File

@@ -1,13 +0,0 @@
from bedit.document.controller import DocumentController
from bedit.document.model import Component, Connection, Endpoint, Graph, GraphDocument, Icon, Port
__all__ = [
"Component",
"Connection",
"DocumentController",
"Endpoint",
"Graph",
"GraphDocument",
"Icon",
"Port",
]

View File

@@ -0,0 +1 @@
"""Qt user interface and application adapters."""

View File

@@ -4,7 +4,7 @@ from PySide6.QtCore import QCoreApplication, Qt
from PySide6.QtGui import QColor, QPalette
from PySide6.QtWidgets import QApplication, QStyleFactory
from bedit.main_window import MainWindow
from bedit.gui.main_window import MainWindow
def apply_light_theme(app: QApplication) -> None:

View File

@@ -0,0 +1,5 @@
"""Qt-aware application controllers and undo commands."""
from bedit.gui.controllers.document import DocumentController
__all__ = ["DocumentController"]

View File

@@ -1,7 +1,7 @@
from PySide6.QtCore import QPointF
from PySide6.QtGui import QUndoCommand
from bedit.document.model import Component, Connection, Port
from bedit.core.model import Component, Connection, Port
class AddComponentCommand(QUndoCommand):

View File

@@ -5,7 +5,7 @@ from uuid import uuid4
from PySide6.QtCore import QObject, QPointF, Signal
from PySide6.QtGui import QUndoStack
from bedit.document.commands import (
from bedit.gui.controllers.commands import (
AddComponentCommand,
AddConnectionCommand,
AddInterfacePortCommand,
@@ -20,7 +20,7 @@ from bedit.document.commands import (
ReplaceSourceCommand,
RotateComponentsCommand,
)
from bedit.document.model import (
from bedit.core.model import (
Component,
Connection,
Endpoint,
@@ -29,8 +29,8 @@ from bedit.document.model import (
Port,
clone_component,
)
from bedit.document.port_types import PortTypeRegistry
from bedit.document.serializer import JsonDocumentSerializer
from bedit.core.port_types import PortTypeRegistry
from bedit.core.serializer import JsonDocumentSerializer
class DocumentController(QObject):

View File

@@ -0,0 +1 @@
"""Application dialogs."""

View File

@@ -1,8 +1,8 @@
from PySide6.QtWidgets import QDialog, QMessageBox, QPushButton
from bedit.document.model import Component
from bedit.icon_editor import IconEditorDialog
from bedit.ui_component_options_dialog import Ui_ComponentOptionsDialog
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):

View File

@@ -19,8 +19,8 @@ from PySide6.QtWidgets import (
QWidget,
)
from bedit.document.model import Component, Port
from bedit.document.port_types import PortTypeRegistry
from bedit.core.model import Component, Port
from bedit.core.port_types import PortTypeRegistry
PORT_ROLE = Qt.ItemDataRole.UserRole

View File

@@ -3,9 +3,9 @@ from pathlib import Path
from PySide6.QtCore import QSettings, Signal
from PySide6.QtWidgets import QDialog, QFileDialog, QFormLayout, QGroupBox, QSpinBox
from bedit.library.repository import default_library_paths
from bedit.preferences import application_settings
from bedit.ui_settings_dialog import Ui_SettingsDialog
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):

View File

@@ -0,0 +1 @@
"""Generated Qt code. Do not edit these modules by hand."""

View File

@@ -22,7 +22,7 @@ from PySide6.QtWidgets import (QApplication, QDockWidget, QFrame, QHBoxLayout,
QSpacerItem, QSplitter, QStackedWidget, QToolBar,
QToolButton, QTreeView, QVBoxLayout, QWidget)
from bedit.workspace.view import GraphWorkspaceView
from bedit.gui.graphics.workspace import GraphWorkspaceView
from . import resources_rc
class Ui_MainWindow(object):

View File

@@ -0,0 +1,5 @@
"""Graphics scenes, views, editors, and renderers."""
from bedit.gui.graphics.workspace import GraphWorkspaceView
__all__ = ["GraphWorkspaceView"]

View File

@@ -27,9 +27,9 @@ from PySide6.QtWidgets import (
QWidget,
)
from bedit.document.model import Component, Icon, Port
from bedit.icon_renderer import _pen
from bedit.preferences import application_settings
from bedit.core.model import Component, Icon, Port
from bedit.gui.graphics.icon_renderer import _pen
from bedit.gui.preferences import application_settings
def _icon_grid_size() -> int:

View File

@@ -1,7 +1,7 @@
from PySide6.QtCore import QPointF, QRectF, Qt
from PySide6.QtGui import QColor, QFont, QIcon, QPainter, QPen, QPixmap, QPolygonF
from bedit.document.model import Icon
from bedit.core.model import Icon
def icon_bounds(icon: Icon) -> QRectF:

View File

@@ -28,11 +28,11 @@ from PySide6.QtWidgets import (
QWidget,
)
from bedit.document.controller import DocumentController
from bedit.document.model import Component, Connection, Endpoint, Port
from bedit.library.tree_model import COMPONENT_MIME_TYPE
from bedit.icon_renderer import icon_bounds, paint_icon
from bedit.preferences import application_settings
from bedit.core.model import Component, Connection, Endpoint, Port
from bedit.gui.controllers.document import DocumentController
from bedit.gui.models.library_tree import COMPONENT_MIME_TYPE
from bedit.gui.graphics.icon_renderer import icon_bounds, paint_icon
from bedit.gui.preferences import application_settings
SELECTION_MIME_TYPE = "application/x-bedit-selection"

View File

@@ -6,23 +6,23 @@ from PySide6.QtCore import QSize, Qt, Slot
from PySide6.QtGui import QAction, QCloseEvent
from PySide6.QtWidgets import QFileDialog, QMainWindow, QMenu, QMessageBox, QToolBar
from bedit.component_options_dialog import ComponentOptionsDialog
from bedit.document.controller import DocumentController
from bedit.document.model import Component, Port
from bedit.document.serializer import JsonDocumentSerializer
from bedit.item_options_dialog import ItemOptionsDialog
from bedit.library.repository import LibraryRepository
from bedit.library.tree_model import (
from bedit.core.model import Component, Port
from bedit.core.serializer import JsonDocumentSerializer
from bedit.gui.controllers.document import DocumentController
from bedit.gui.dialogs.component_options import ComponentOptionsDialog
from bedit.gui.dialogs.item_options import ItemOptionsDialog
from bedit.gui.models.library_repository import LibraryRepository
from bedit.gui.models.library_tree import (
COMPONENT_ID_ROLE,
COMPONENT_INSTANCE_ROLE,
ITEM_KIND_ROLE,
DocumentTreeModel,
LibraryTreeModel,
)
from bedit.settings_dialog import SettingsDialog
from bedit.port_options_dialog import PortOptionsDialog
from bedit.preferences import application_settings
from bedit.ui_main_window import Ui_MainWindow
from bedit.gui.dialogs.settings import SettingsDialog
from bedit.gui.dialogs.port_options import PortOptionsDialog
from bedit.gui.preferences import application_settings
from bedit.gui.generated.ui_main_window import Ui_MainWindow
class MainWindow(QMainWindow):

View File

@@ -0,0 +1 @@
"""Qt model/view adapters and repositories."""

View File

@@ -0,0 +1,31 @@
import json
from pathlib import Path
from PySide6.QtCore import QObject, Signal
from bedit.core.libraries import LibraryDocument, library_candidates, load_library_file
class LibraryRepository(QObject):
librariesChanged = Signal()
loadWarningsChanged = Signal(list)
def __init__(self, parent=None) -> None:
super().__init__(parent)
self.libraries: list[LibraryDocument] = []
self.load_warnings: list[str] = []
def load_paths(self, paths: list[str]) -> None:
libraries: list[LibraryDocument] = []
warnings: list[str] = []
for raw_path in paths:
path = Path(raw_path).expanduser()
for candidate in library_candidates(path):
try:
libraries.append(load_library_file(candidate))
except (OSError, ValueError, KeyError, TypeError, json.JSONDecodeError) as error:
warnings.append(f"{candidate}: {error}")
self.libraries = libraries
self.load_warnings = warnings
self.librariesChanged.emit()
self.loadWarningsChanged.emit(warnings)

View File

@@ -3,10 +3,10 @@ import json
from PySide6.QtCore import QByteArray, QMimeData, QModelIndex, Qt, Signal
from PySide6.QtGui import QStandardItem, QStandardItemModel
from bedit.document.controller import DocumentController
from bedit.document.model import Component
from bedit.icon_renderer import library_icon
from bedit.library.repository import LibraryRepository
from bedit.core.model import Component
from bedit.gui.controllers.document import DocumentController
from bedit.gui.graphics.icon_renderer import library_icon
from bedit.gui.models.library_repository import LibraryRepository
COMPONENT_ROLE = Qt.ItemDataRole.UserRole + 1

View File

@@ -1,3 +0,0 @@
from bedit.library.repository import LibraryRepository, default_library_paths
__all__ = ["LibraryRepository", "default_library_paths"]

View File

@@ -1,56 +0,0 @@
import json
from dataclasses import dataclass
from pathlib import Path
from PySide6.QtCore import QObject, Signal
from bedit.document.model import GraphDocument
@dataclass(frozen=True)
class LibraryDocument:
name: str
document: GraphDocument
source_path: str
def bundled_library_path() -> Path:
return Path(__file__).resolve().parents[1] / "data" / "libraries" / "example.json"
def default_library_paths() -> list[str]:
return [str(bundled_library_path())]
class LibraryRepository(QObject):
librariesChanged = Signal()
loadWarningsChanged = Signal(list)
def __init__(self, parent=None) -> None:
super().__init__(parent)
self.libraries: list[LibraryDocument] = []
self.load_warnings: list[str] = []
def load_paths(self, paths: list[str]) -> None:
libraries: list[LibraryDocument] = []
warnings: list[str] = []
for raw_path in paths:
path = Path(raw_path).expanduser()
candidates = sorted(path.glob("*.json")) if path.is_dir() else [path]
for candidate in candidates:
try:
libraries.append(self._load_file(candidate))
except (OSError, ValueError, KeyError, TypeError, json.JSONDecodeError) as error:
warnings.append(f"{candidate}: {error}")
self.libraries = libraries
self.load_warnings = warnings
self.librariesChanged.emit()
self.loadWarningsChanged.emit(warnings)
@staticmethod
def _load_file(path: Path) -> LibraryDocument:
with path.open(encoding="utf-8") as file:
data = json.load(file)
document = GraphDocument.from_dict(data)
name = str(document.metadata.get("name") or path.stem)
return LibraryDocument(name, document, str(path))

View File

@@ -1,4 +0,0 @@
from bedit.workspace.view import GraphWorkspaceView
__all__ = ["GraphWorkspaceView"]