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/__init__.py
Normal file
1
BEdit/src/bedit/gui/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Qt user interface and application adapters."""
|
||||
51
BEdit/src/bedit/gui/app.py
Normal file
51
BEdit/src/bedit/gui/app.py
Normal file
@@ -0,0 +1,51 @@
|
||||
import sys
|
||||
|
||||
from PySide6.QtCore import QCoreApplication, Qt
|
||||
from PySide6.QtGui import QColor, QPalette
|
||||
from PySide6.QtWidgets import QApplication, QStyleFactory
|
||||
|
||||
from bedit.gui.main_window import MainWindow
|
||||
|
||||
|
||||
def apply_light_theme(app: QApplication) -> None:
|
||||
"""Use a predictable light Qt theme, independent of the desktop theme."""
|
||||
app.setStyle(QStyleFactory.create("Fusion"))
|
||||
|
||||
palette = QPalette()
|
||||
palette.setColor(QPalette.ColorRole.Window, QColor(240, 240, 240))
|
||||
palette.setColor(QPalette.ColorRole.WindowText, Qt.GlobalColor.black)
|
||||
palette.setColor(QPalette.ColorRole.Base, Qt.GlobalColor.white)
|
||||
palette.setColor(QPalette.ColorRole.AlternateBase, QColor(233, 233, 233))
|
||||
palette.setColor(QPalette.ColorRole.ToolTipBase, Qt.GlobalColor.white)
|
||||
palette.setColor(QPalette.ColorRole.ToolTipText, Qt.GlobalColor.black)
|
||||
palette.setColor(QPalette.ColorRole.Text, Qt.GlobalColor.black)
|
||||
palette.setColor(QPalette.ColorRole.Button, QColor(240, 240, 240))
|
||||
palette.setColor(QPalette.ColorRole.ButtonText, Qt.GlobalColor.black)
|
||||
palette.setColor(QPalette.ColorRole.BrightText, Qt.GlobalColor.red)
|
||||
palette.setColor(QPalette.ColorRole.Link, QColor(0, 102, 204))
|
||||
palette.setColor(QPalette.ColorRole.Highlight, QColor(0, 120, 215))
|
||||
palette.setColor(QPalette.ColorRole.HighlightedText, Qt.GlobalColor.white)
|
||||
palette.setColor(
|
||||
QPalette.ColorGroup.Disabled,
|
||||
QPalette.ColorRole.Text,
|
||||
QColor(109, 109, 109),
|
||||
)
|
||||
palette.setColor(
|
||||
QPalette.ColorGroup.Disabled,
|
||||
QPalette.ColorRole.ButtonText,
|
||||
QColor(109, 109, 109),
|
||||
)
|
||||
app.setPalette(palette)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
QCoreApplication.setApplicationName("BEdit")
|
||||
QCoreApplication.setOrganizationName("BEdit")
|
||||
QCoreApplication.setApplicationVersion("0.1.0")
|
||||
app = QApplication(sys.argv)
|
||||
app.setApplicationDisplayName("BEdit")
|
||||
|
||||
apply_light_theme(app)
|
||||
window = MainWindow()
|
||||
window.show()
|
||||
return app.exec()
|
||||
5
BEdit/src/bedit/gui/controllers/__init__.py
Normal file
5
BEdit/src/bedit/gui/controllers/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
"""Qt-aware application controllers and undo commands."""
|
||||
|
||||
from bedit.gui.controllers.document import DocumentController
|
||||
|
||||
__all__ = ["DocumentController"]
|
||||
262
BEdit/src/bedit/gui/controllers/commands.py
Normal file
262
BEdit/src/bedit/gui/controllers/commands.py
Normal file
@@ -0,0 +1,262 @@
|
||||
from PySide6.QtCore import QPointF
|
||||
from PySide6.QtGui import QUndoCommand
|
||||
|
||||
from bedit.core.model import Component, Connection, Port
|
||||
|
||||
|
||||
class AddComponentCommand(QUndoCommand):
|
||||
def __init__(self, controller, owner_id: str | None, component: Component) -> None:
|
||||
super().__init__(f"Add {component.name}")
|
||||
self.controller = controller
|
||||
self.owner_id = owner_id
|
||||
self.component = component
|
||||
|
||||
def redo(self) -> None:
|
||||
self.controller._insert_component(self.owner_id, self.component)
|
||||
|
||||
def undo(self) -> None:
|
||||
self.controller._remove_component(self.owner_id, self.component.id)
|
||||
|
||||
|
||||
class MoveComponentCommand(QUndoCommand):
|
||||
def __init__(
|
||||
self,
|
||||
controller,
|
||||
owner_id: str,
|
||||
component_id: str,
|
||||
old: QPointF,
|
||||
new: QPointF,
|
||||
) -> None:
|
||||
super().__init__("Move component")
|
||||
self.controller = controller
|
||||
self.owner_id = owner_id
|
||||
self.component_id = component_id
|
||||
self.old = old
|
||||
self.new = new
|
||||
|
||||
def redo(self) -> None:
|
||||
self.controller._move_component(self.owner_id, self.component_id, self.new)
|
||||
|
||||
def undo(self) -> None:
|
||||
self.controller._move_component(self.owner_id, self.component_id, self.old)
|
||||
|
||||
|
||||
class RotateComponentsCommand(QUndoCommand):
|
||||
def __init__(
|
||||
self,
|
||||
controller,
|
||||
owner_id: str,
|
||||
rotations: dict[str, tuple[float, float]],
|
||||
) -> None:
|
||||
super().__init__("Rotate components")
|
||||
self.controller = controller
|
||||
self.owner_id = owner_id
|
||||
self.rotations = rotations
|
||||
|
||||
def redo(self) -> None:
|
||||
for component_id, (_old, new) in self.rotations.items():
|
||||
self.controller._rotate_component(self.owner_id, component_id, new)
|
||||
|
||||
def undo(self) -> None:
|
||||
for component_id, (old, _new) in self.rotations.items():
|
||||
self.controller._rotate_component(self.owner_id, component_id, old)
|
||||
|
||||
|
||||
class AddConnectionCommand(QUndoCommand):
|
||||
def __init__(self, controller, owner_id: str, connection: Connection) -> None:
|
||||
super().__init__("Connect components")
|
||||
self.controller = controller
|
||||
self.owner_id = owner_id
|
||||
self.connection = connection
|
||||
|
||||
def redo(self) -> None:
|
||||
self.controller._insert_connection(self.owner_id, self.connection)
|
||||
|
||||
def undo(self) -> None:
|
||||
self.controller._remove_connection(self.owner_id, self.connection.id)
|
||||
|
||||
|
||||
class ReplaceComponentCommand(QUndoCommand):
|
||||
def __init__(self, controller, old: Component, new: Component) -> None:
|
||||
super().__init__("Apply JSON changes")
|
||||
self.controller = controller
|
||||
self.old = old
|
||||
self.new = new
|
||||
|
||||
def redo(self) -> None:
|
||||
self.controller._replace_component(self.old.id, self.new)
|
||||
|
||||
def undo(self) -> None:
|
||||
self.controller._replace_component(self.new.id, self.old)
|
||||
|
||||
|
||||
class AddInterfacePortCommand(QUndoCommand):
|
||||
def __init__(self, controller, owner_id: str, direction: str, port: Port) -> None:
|
||||
super().__init__(f"Add {direction}")
|
||||
self.controller = controller
|
||||
self.owner_id = owner_id
|
||||
self.direction = direction
|
||||
self.port = port
|
||||
|
||||
def redo(self) -> None:
|
||||
self.controller._insert_interface_port(self.owner_id, self.direction, self.port)
|
||||
|
||||
def undo(self) -> None:
|
||||
self.controller._remove_interface_port(self.owner_id, self.direction, self.port.id)
|
||||
|
||||
|
||||
class ReplaceSourceCommand(QUndoCommand):
|
||||
def __init__(self, controller, component_id: str, old: dict, new: dict) -> None:
|
||||
super().__init__("Apply source JSON")
|
||||
self.controller = controller
|
||||
self.component_id = component_id
|
||||
self.old = old
|
||||
self.new = new
|
||||
|
||||
def redo(self) -> None:
|
||||
self.controller._replace_source(self.component_id, self.new)
|
||||
|
||||
def undo(self) -> None:
|
||||
self.controller._replace_source(self.component_id, self.old)
|
||||
|
||||
|
||||
class MoveInterfacePortCommand(QUndoCommand):
|
||||
def __init__(self, controller, owner_id: str, port_id: str, old: QPointF, new: QPointF) -> None:
|
||||
super().__init__("Move interface terminal")
|
||||
self.controller = controller
|
||||
self.owner_id = owner_id
|
||||
self.port_id = port_id
|
||||
self.old = old
|
||||
self.new = new
|
||||
|
||||
def redo(self) -> None:
|
||||
self.controller._move_interface_port(self.owner_id, self.port_id, self.new)
|
||||
|
||||
def undo(self) -> None:
|
||||
self.controller._move_interface_port(self.owner_id, self.port_id, self.old)
|
||||
|
||||
|
||||
class EditComponentAppearanceCommand(QUndoCommand):
|
||||
def __init__(self, controller, component_id: str, old: dict, new: dict) -> None:
|
||||
super().__init__("Edit component appearance")
|
||||
self.controller = controller
|
||||
self.component_id = component_id
|
||||
self.old = old
|
||||
self.new = new
|
||||
|
||||
def redo(self) -> None:
|
||||
self.controller._set_component_appearance(self.component_id, self.new)
|
||||
|
||||
def undo(self) -> None:
|
||||
self.controller._set_component_appearance(self.component_id, self.old)
|
||||
|
||||
|
||||
class RenameInterfacePortCommand(QUndoCommand):
|
||||
def __init__(self, controller, owner_id: str, port_id: str, old: str, new: str) -> None:
|
||||
super().__init__("Rename interface port")
|
||||
self.controller, self.owner_id, self.port_id = controller, owner_id, port_id
|
||||
self.old, self.new = old, new
|
||||
|
||||
def redo(self) -> None:
|
||||
self.controller._rename_interface_port(self.owner_id, self.port_id, self.new)
|
||||
|
||||
def undo(self) -> None:
|
||||
self.controller._rename_interface_port(self.owner_id, self.port_id, self.old)
|
||||
|
||||
|
||||
class RenameConnectionCommand(QUndoCommand):
|
||||
def __init__(self, controller, owner_id: str, connection_id: str, old: str, new: str) -> None:
|
||||
super().__init__("Rename connection")
|
||||
self.controller, self.owner_id, self.connection_id = controller, owner_id, connection_id
|
||||
self.old, self.new = old, new
|
||||
|
||||
def redo(self) -> None:
|
||||
self.controller._rename_connection(self.owner_id, self.connection_id, self.new)
|
||||
|
||||
def undo(self) -> None:
|
||||
self.controller._rename_connection(self.owner_id, self.connection_id, self.old)
|
||||
|
||||
|
||||
class DeleteSelectionCommand(QUndoCommand):
|
||||
def __init__(
|
||||
self,
|
||||
controller,
|
||||
owner_id: str | None,
|
||||
blocks: dict[str, Component],
|
||||
connections: dict[str, Connection],
|
||||
inputs: list[Port],
|
||||
outputs: list[Port],
|
||||
) -> None:
|
||||
super().__init__("Delete selection")
|
||||
self.controller = controller
|
||||
self.owner_id = owner_id
|
||||
self.blocks = blocks
|
||||
self.connections = connections
|
||||
self.inputs = inputs
|
||||
self.outputs = outputs
|
||||
|
||||
def redo(self) -> None:
|
||||
self.controller._delete_items(
|
||||
self.owner_id,
|
||||
set(self.blocks),
|
||||
set(self.connections),
|
||||
{port.id for port in self.inputs},
|
||||
{port.id for port in self.outputs},
|
||||
)
|
||||
|
||||
def undo(self) -> None:
|
||||
self.controller._restore_items(
|
||||
self.owner_id,
|
||||
self.blocks,
|
||||
self.connections,
|
||||
self.inputs,
|
||||
self.outputs,
|
||||
)
|
||||
|
||||
|
||||
class PasteSelectionCommand(QUndoCommand):
|
||||
def __init__(
|
||||
self,
|
||||
controller,
|
||||
owner_id: str,
|
||||
blocks: dict[str, Component],
|
||||
connections: dict[str, Connection],
|
||||
) -> None:
|
||||
super().__init__("Paste selection")
|
||||
self.controller = controller
|
||||
self.owner_id = owner_id
|
||||
self.blocks = blocks
|
||||
self.connections = connections
|
||||
|
||||
def redo(self) -> None:
|
||||
self.controller._restore_items(
|
||||
self.owner_id,
|
||||
self.blocks,
|
||||
self.connections,
|
||||
[],
|
||||
[],
|
||||
)
|
||||
|
||||
def undo(self) -> None:
|
||||
self.controller._delete_items(
|
||||
self.owner_id,
|
||||
set(self.blocks),
|
||||
set(self.connections),
|
||||
set(),
|
||||
set(),
|
||||
)
|
||||
|
||||
|
||||
class EditTextDefinitionCommand(QUndoCommand):
|
||||
def __init__(self, controller, component_id: str, old: dict, new: dict) -> None:
|
||||
super().__init__("Edit text component")
|
||||
self.controller = controller
|
||||
self.component_id = component_id
|
||||
self.old = old
|
||||
self.new = new
|
||||
|
||||
def redo(self) -> None:
|
||||
self.controller._set_text_definition(self.component_id, self.new)
|
||||
|
||||
def undo(self) -> None:
|
||||
self.controller._set_text_definition(self.component_id, self.old)
|
||||
741
BEdit/src/bedit/gui/controllers/document.py
Normal file
741
BEdit/src/bedit/gui/controllers/document.py
Normal file
@@ -0,0 +1,741 @@
|
||||
from copy import deepcopy
|
||||
from pathlib import Path
|
||||
from uuid import uuid4
|
||||
|
||||
from PySide6.QtCore import QObject, QPointF, Signal
|
||||
from PySide6.QtGui import QUndoStack
|
||||
|
||||
from bedit.gui.controllers.commands import (
|
||||
AddComponentCommand,
|
||||
AddConnectionCommand,
|
||||
AddInterfacePortCommand,
|
||||
DeleteSelectionCommand,
|
||||
EditTextDefinitionCommand,
|
||||
EditComponentAppearanceCommand,
|
||||
MoveComponentCommand,
|
||||
MoveInterfacePortCommand,
|
||||
PasteSelectionCommand,
|
||||
RenameConnectionCommand,
|
||||
RenameInterfacePortCommand,
|
||||
ReplaceSourceCommand,
|
||||
RotateComponentsCommand,
|
||||
)
|
||||
from bedit.core.model import (
|
||||
Component,
|
||||
Connection,
|
||||
Endpoint,
|
||||
GraphDocument,
|
||||
Icon,
|
||||
Port,
|
||||
clone_component,
|
||||
)
|
||||
from bedit.core.port_types import PortTypeRegistry
|
||||
from bedit.core.serializer import JsonDocumentSerializer
|
||||
|
||||
|
||||
class DocumentController(QObject):
|
||||
documentReset = Signal()
|
||||
documentOpenedChanged = Signal(bool)
|
||||
activeGraphChanged = Signal()
|
||||
componentAdded = Signal(str)
|
||||
componentRemoved = Signal(str)
|
||||
componentMoved = Signal(str, QPointF)
|
||||
componentRotated = Signal(str, float)
|
||||
connectionAdded = Signal(str)
|
||||
connectionRemoved = Signal(str)
|
||||
interfaceChanged = Signal()
|
||||
filePathChanged = Signal(object)
|
||||
modifiedChanged = Signal(bool)
|
||||
|
||||
def __init__(self, parent=None) -> None:
|
||||
super().__init__(parent)
|
||||
self.document: GraphDocument | None = None
|
||||
self.active_component_id: str | None = None
|
||||
self.file_path: Path | None = None
|
||||
self.undo_stack = QUndoStack(self)
|
||||
self.undo_stack.cleanChanged.connect(self._clean_changed)
|
||||
|
||||
def _clean_changed(self, clean: bool) -> None:
|
||||
self.modifiedChanged.emit(not clean)
|
||||
|
||||
@property
|
||||
def active_component(self) -> Component | None:
|
||||
if self.document is None or self.active_component_id is None:
|
||||
return None
|
||||
return self.document.find_component(self.active_component_id)
|
||||
|
||||
@property
|
||||
def active_graph(self):
|
||||
component = self.active_component
|
||||
if component is None or component.implementation_kind != "graph":
|
||||
raise ValueError("There is no active graph")
|
||||
return component.graph
|
||||
|
||||
def new_document(self) -> None:
|
||||
self.document = GraphDocument.empty()
|
||||
self.active_component_id = None
|
||||
self.file_path = None
|
||||
self.undo_stack.clear()
|
||||
self.documentOpenedChanged.emit(True)
|
||||
self.documentReset.emit()
|
||||
self.activeGraphChanged.emit()
|
||||
self.filePathChanged.emit(None)
|
||||
|
||||
def close_document(self) -> None:
|
||||
self.document = None
|
||||
self.active_component_id = None
|
||||
self.file_path = None
|
||||
self.undo_stack.clear()
|
||||
self.documentOpenedChanged.emit(False)
|
||||
self.documentReset.emit()
|
||||
self.activeGraphChanged.emit()
|
||||
self.filePathChanged.emit(None)
|
||||
|
||||
def load(self, path: Path) -> None:
|
||||
self.document = JsonDocumentSerializer.load(path)
|
||||
self.active_component_id = next(iter(self.document.roots), None)
|
||||
self.file_path = path
|
||||
self.undo_stack.clear()
|
||||
self.undo_stack.setClean()
|
||||
self.documentOpenedChanged.emit(True)
|
||||
self.documentReset.emit()
|
||||
self.activeGraphChanged.emit()
|
||||
self.filePathChanged.emit(path)
|
||||
|
||||
def save(self, path: Path | None = None) -> Path:
|
||||
if self.document is None:
|
||||
raise ValueError("There is no open document")
|
||||
target = path or self.file_path
|
||||
if target is None:
|
||||
raise ValueError("No file path has been selected")
|
||||
JsonDocumentSerializer.save(self.document, target)
|
||||
self.file_path = target
|
||||
self.undo_stack.setClean()
|
||||
self.filePathChanged.emit(target)
|
||||
return target
|
||||
|
||||
def activate_component(self, component_id: str) -> None:
|
||||
if self.document is None or self.document.find_component(component_id) is None:
|
||||
return
|
||||
self.active_component_id = component_id
|
||||
self.activeGraphChanged.emit()
|
||||
|
||||
def navigate_up(self) -> None:
|
||||
if self.document is None or self.active_component_id is None:
|
||||
return
|
||||
parent = self.document.find_parent(self.active_component_id)
|
||||
if parent is not None:
|
||||
self.activate_component(parent.id)
|
||||
|
||||
def breadcrumb(self) -> list[str]:
|
||||
if self.document is None or self.active_component is None:
|
||||
return []
|
||||
names = [self.active_component.name]
|
||||
current = self.active_component
|
||||
while True:
|
||||
parent = self.document.find_parent(current.id)
|
||||
if parent is None:
|
||||
break
|
||||
names.append(parent.name)
|
||||
current = parent
|
||||
return list(reversed(names))
|
||||
|
||||
def add_root(self, kind: str) -> str:
|
||||
if self.document is None:
|
||||
raise ValueError("Open or create a document first")
|
||||
number = len(self.document.roots) + 1
|
||||
component = Component(
|
||||
id=str(uuid4()),
|
||||
name=f"New {'Graph' if kind == 'graph' else 'Text'} Block {number}",
|
||||
implementation_kind=kind,
|
||||
icon=Icon(text="Graph" if kind == "graph" else "Text"),
|
||||
source={"equations": [], "parameters": {}} if kind == "text" else {},
|
||||
)
|
||||
self.undo_stack.push(AddComponentCommand(self, None, component))
|
||||
self.activate_component(component.id)
|
||||
return component.id
|
||||
|
||||
def add_child(self, owner_id: str, kind: str) -> str:
|
||||
if self.document is None:
|
||||
raise ValueError("Open or create a document first")
|
||||
owner = self.document.find_component(owner_id)
|
||||
if owner is None or owner.implementation_kind != "graph":
|
||||
raise ValueError("Children can only be added to graph components")
|
||||
number = len(owner.graph.blocks) + 1
|
||||
component = Component(
|
||||
id=str(uuid4()),
|
||||
name=f"New {'Graph' if kind == 'graph' else 'Text'} Block {number}",
|
||||
implementation_kind=kind,
|
||||
icon=Icon(text="Graph" if kind == "graph" else "Text"),
|
||||
source={"equations": [], "parameters": {}} if kind == "text" else {},
|
||||
)
|
||||
self.undo_stack.push(AddComponentCommand(self, owner_id, component))
|
||||
return component.id
|
||||
|
||||
def delete_component(self, component_id: str) -> None:
|
||||
if self.document is None:
|
||||
return
|
||||
component = self.document.find_component(component_id)
|
||||
if component is None:
|
||||
return
|
||||
parent = self.document.find_parent(component_id)
|
||||
owner_id = parent.id if parent else None
|
||||
connections = {}
|
||||
if parent is not None:
|
||||
connections = {
|
||||
connection.id: connection
|
||||
for connection in parent.graph.connections.values()
|
||||
if component_id in (connection.source.block, connection.target.block)
|
||||
}
|
||||
self.undo_stack.push(
|
||||
DeleteSelectionCommand(
|
||||
self,
|
||||
owner_id,
|
||||
{component_id: component},
|
||||
connections,
|
||||
[],
|
||||
[],
|
||||
)
|
||||
)
|
||||
|
||||
def add_component_copy(self, source: Component, position: QPointF) -> str:
|
||||
if self.active_component is None or self.active_component.implementation_kind != "graph":
|
||||
raise ValueError("Open a graph component before placing components")
|
||||
component = clone_component(source)
|
||||
component.x, component.y = position.x(), position.y()
|
||||
self.undo_stack.push(AddComponentCommand(self, self.active_component_id, component))
|
||||
return component.id
|
||||
|
||||
def move_component(self, component_id: str, old: QPointF, new: QPointF) -> None:
|
||||
if old != new and self.active_component_id is not None:
|
||||
self.undo_stack.push(
|
||||
MoveComponentCommand(self, self.active_component_id, component_id, old, new)
|
||||
)
|
||||
|
||||
def rotate_components(self, component_ids: set[str]) -> None:
|
||||
if self.active_component is None or self.active_component_id is None:
|
||||
return
|
||||
rotations = {
|
||||
component_id: (component.rotation, (component.rotation + 90.0) % 360.0)
|
||||
for component_id in component_ids
|
||||
if (component := self.active_component.graph.blocks.get(component_id)) is not None
|
||||
}
|
||||
if rotations:
|
||||
self.undo_stack.push(
|
||||
RotateComponentsCommand(self, self.active_component_id, rotations)
|
||||
)
|
||||
|
||||
def connect(self, source: Endpoint, target: Endpoint) -> str:
|
||||
if self.active_component_id is None:
|
||||
raise ValueError("There is no active graph")
|
||||
source_port = self._port_for_endpoint(source, "source")
|
||||
target_port = self._port_for_endpoint(target, "target")
|
||||
if source_port is None or target_port is None:
|
||||
raise ValueError("A connection endpoint no longer exists")
|
||||
if not PortTypeRegistry.compatible(source_port.type, target_port.type):
|
||||
raise ValueError(
|
||||
f"Cannot connect {source_port.type!r} to {target_port.type!r}"
|
||||
)
|
||||
connection = Connection(str(uuid4()), source, target)
|
||||
self.undo_stack.push(
|
||||
AddConnectionCommand(self, self.active_component_id, connection)
|
||||
)
|
||||
return connection.id
|
||||
|
||||
def _port_for_endpoint(self, endpoint: Endpoint, role: str) -> Port | None:
|
||||
owner = self.active_component
|
||||
if owner is None:
|
||||
return None
|
||||
if endpoint.interface is not None:
|
||||
ports = owner.inputs if role == "source" else owner.outputs
|
||||
else:
|
||||
component = owner.graph.blocks.get(endpoint.block or "")
|
||||
if component is None:
|
||||
return None
|
||||
ports = component.outputs if role == "source" else component.inputs
|
||||
return next((port for port in ports if port.id == (endpoint.interface or endpoint.port)), None)
|
||||
|
||||
def add_interface_port(self, direction: str, position: QPointF) -> str:
|
||||
component = self.active_component
|
||||
if component is None or component.implementation_kind != "graph":
|
||||
raise ValueError("Open a graph component before adding an interface")
|
||||
ports = component.inputs if direction == "input" else component.outputs
|
||||
port = Port(
|
||||
id=f"{direction}-{uuid4().hex[:8]}",
|
||||
name=f"{direction.title()} {len(ports) + 1}",
|
||||
x=position.x(),
|
||||
y=position.y(),
|
||||
)
|
||||
self.undo_stack.push(
|
||||
AddInterfacePortCommand(self, component.id, direction, port)
|
||||
)
|
||||
return port.id
|
||||
|
||||
def move_interface_port(self, port_id: str, old: QPointF, new: QPointF) -> None:
|
||||
if old != new and self.active_component_id is not None:
|
||||
self.undo_stack.push(
|
||||
MoveInterfacePortCommand(self, self.active_component_id, port_id, old, new)
|
||||
)
|
||||
|
||||
def rename_interface_port(self, port_id: str, name: str) -> None:
|
||||
owner = self.active_component
|
||||
if owner is None:
|
||||
return
|
||||
port = next((p for p in (*owner.inputs, *owner.outputs) if p.id == port_id), None)
|
||||
if port is not None and port.name != name:
|
||||
self.undo_stack.push(
|
||||
RenameInterfacePortCommand(self, owner.id, port_id, port.name, name)
|
||||
)
|
||||
|
||||
def rename_connection(self, connection_id: str, name: str) -> None:
|
||||
owner = self.active_component
|
||||
if owner is None or owner.implementation_kind != "graph":
|
||||
return
|
||||
connection = owner.graph.connections.get(connection_id)
|
||||
if connection is not None and connection.name != name:
|
||||
self.undo_stack.push(
|
||||
RenameConnectionCommand(self, owner.id, connection_id, connection.name, name)
|
||||
)
|
||||
|
||||
def replace_active_source(self, source: dict) -> None:
|
||||
component = self.active_component
|
||||
if component is None or component.implementation_kind != "text":
|
||||
raise ValueError("Only text-defined components have source JSON")
|
||||
self.undo_stack.push(
|
||||
ReplaceSourceCommand(
|
||||
self,
|
||||
component.id,
|
||||
deepcopy(component.source),
|
||||
deepcopy(source),
|
||||
)
|
||||
)
|
||||
|
||||
def replace_active_text_definition(
|
||||
self,
|
||||
inputs: list[Port],
|
||||
outputs: list[Port],
|
||||
source: dict,
|
||||
) -> None:
|
||||
component = self.active_component
|
||||
if component is None or component.implementation_kind != "text":
|
||||
raise ValueError("Only text-defined components can be edited here")
|
||||
input_ids = [port.id for port in inputs]
|
||||
output_ids = [port.id for port in outputs]
|
||||
if len(set(input_ids)) != len(input_ids) or len(set(output_ids)) != len(output_ids):
|
||||
raise ValueError("Input and output IDs must be unique")
|
||||
if self.document is not None:
|
||||
parent = self.document.find_parent(component.id)
|
||||
if parent is not None:
|
||||
for connection in parent.graph.connections.values():
|
||||
if connection.target.block == component.id and connection.target.port not in input_ids:
|
||||
raise ValueError(
|
||||
f"Input {connection.target.port!r} is still connected in the containing graph"
|
||||
)
|
||||
if connection.source.block == component.id and connection.source.port not in output_ids:
|
||||
raise ValueError(
|
||||
f"Output {connection.source.port!r} is still connected in the containing graph"
|
||||
)
|
||||
old = {
|
||||
"inputs": [port.to_dict() for port in component.inputs],
|
||||
"outputs": [port.to_dict() for port in component.outputs],
|
||||
"source": deepcopy(component.source),
|
||||
}
|
||||
new = {
|
||||
"inputs": [port.to_dict() for port in inputs],
|
||||
"outputs": [port.to_dict() for port in outputs],
|
||||
"source": deepcopy(source),
|
||||
}
|
||||
if old != new:
|
||||
self.undo_stack.push(EditTextDefinitionCommand(self, component.id, old, new))
|
||||
|
||||
def edit_component_appearance(
|
||||
self,
|
||||
component_id: str,
|
||||
name: str,
|
||||
icon: Icon,
|
||||
inputs: list[Port],
|
||||
outputs: list[Port],
|
||||
show_subtree: bool,
|
||||
) -> None:
|
||||
if self.document is None:
|
||||
return
|
||||
component = self.document.find_component(component_id)
|
||||
if component is None:
|
||||
return
|
||||
old = {
|
||||
"name": component.name,
|
||||
"icon": component.icon.to_dict(),
|
||||
"inputs": [port.to_dict() for port in component.inputs],
|
||||
"outputs": [port.to_dict() for port in component.outputs],
|
||||
"show_subtree": component.show_subtree_in_library,
|
||||
}
|
||||
new = {
|
||||
"name": name,
|
||||
"icon": icon.to_dict(),
|
||||
"inputs": [port.to_dict() for port in inputs],
|
||||
"outputs": [port.to_dict() for port in outputs],
|
||||
"show_subtree": show_subtree,
|
||||
}
|
||||
if old != new:
|
||||
candidate = deepcopy(self.document)
|
||||
candidate_component = candidate.find_component(component_id)
|
||||
candidate_component.name = name
|
||||
candidate_component.icon = Icon.from_dict(icon.to_dict())
|
||||
candidate_component.inputs = deepcopy(inputs)
|
||||
candidate_component.outputs = deepcopy(outputs)
|
||||
candidate.validate()
|
||||
self.undo_stack.push(EditComponentAppearanceCommand(self, component_id, old, new))
|
||||
|
||||
def edit_component_ports(
|
||||
self, component_id: str, inputs: list[Port], outputs: list[Port]
|
||||
) -> None:
|
||||
if self.document is None:
|
||||
return
|
||||
component = self.document.find_component(component_id)
|
||||
if component is None:
|
||||
return
|
||||
input_ids = {port.id for port in inputs}
|
||||
output_ids = {port.id for port in outputs}
|
||||
parent = self.document.find_parent(component_id)
|
||||
if parent is not None:
|
||||
for connection in parent.graph.connections.values():
|
||||
if connection.target.block == component_id and connection.target.port not in input_ids:
|
||||
raise ValueError("An input cannot be removed or reoriented while connected")
|
||||
if connection.source.block == component_id and connection.source.port not in output_ids:
|
||||
raise ValueError("An output cannot be removed or reoriented while connected")
|
||||
for connection in component.graph.connections.values():
|
||||
if connection.source.interface and connection.source.interface not in input_ids:
|
||||
raise ValueError("An interface input cannot be removed while connected")
|
||||
if connection.target.interface and connection.target.interface not in output_ids:
|
||||
raise ValueError("An interface output cannot be removed while connected")
|
||||
candidate = deepcopy(self.document)
|
||||
candidate_component = candidate.find_component(component_id)
|
||||
candidate_component.inputs = deepcopy(inputs)
|
||||
candidate_component.outputs = deepcopy(outputs)
|
||||
candidate.validate()
|
||||
old = {
|
||||
"name": component.name,
|
||||
"icon": component.icon.to_dict(),
|
||||
"inputs": [port.to_dict() for port in component.inputs],
|
||||
"outputs": [port.to_dict() for port in component.outputs],
|
||||
"show_subtree": component.show_subtree_in_library,
|
||||
}
|
||||
new = {
|
||||
**old,
|
||||
"inputs": [port.to_dict() for port in inputs],
|
||||
"outputs": [port.to_dict() for port in outputs],
|
||||
}
|
||||
if old != new:
|
||||
self.undo_stack.push(EditComponentAppearanceCommand(self, component_id, old, new))
|
||||
|
||||
def delete_selection(
|
||||
self,
|
||||
block_ids: set[str],
|
||||
connection_ids: set[str],
|
||||
input_ids: set[str],
|
||||
output_ids: set[str],
|
||||
) -> None:
|
||||
component = self.active_component
|
||||
if component is None or component.implementation_kind != "graph":
|
||||
return
|
||||
graph = component.graph
|
||||
all_connection_ids = set(connection_ids)
|
||||
for connection in graph.connections.values():
|
||||
if (
|
||||
connection.source.block in block_ids
|
||||
or connection.target.block in block_ids
|
||||
or connection.source.interface in input_ids
|
||||
or connection.target.interface in output_ids
|
||||
):
|
||||
all_connection_ids.add(connection.id)
|
||||
blocks = {block_id: graph.blocks[block_id] for block_id in block_ids if block_id in graph.blocks}
|
||||
connections = {
|
||||
connection_id: graph.connections[connection_id]
|
||||
for connection_id in all_connection_ids
|
||||
if connection_id in graph.connections
|
||||
}
|
||||
inputs = [port for port in component.inputs if port.id in input_ids]
|
||||
outputs = [port for port in component.outputs if port.id in output_ids]
|
||||
if not (blocks or connections or inputs or outputs):
|
||||
return
|
||||
self.undo_stack.push(
|
||||
DeleteSelectionCommand(
|
||||
self,
|
||||
component.id,
|
||||
blocks,
|
||||
connections,
|
||||
inputs,
|
||||
outputs,
|
||||
)
|
||||
)
|
||||
|
||||
def paste_selection(
|
||||
self,
|
||||
source_components: list[Component],
|
||||
source_connections: list[Connection],
|
||||
offset: QPointF,
|
||||
) -> list[str]:
|
||||
owner = self.active_component
|
||||
if owner is None or owner.implementation_kind != "graph":
|
||||
return []
|
||||
pairs = [(source, clone_component(source)) for source in source_components]
|
||||
id_map = {source.id: clone.id for source, clone in pairs}
|
||||
blocks = {}
|
||||
for _source, clone in pairs:
|
||||
clone.x += offset.x()
|
||||
clone.y += offset.y()
|
||||
blocks[clone.id] = clone
|
||||
connections = {}
|
||||
for source in source_connections:
|
||||
if source.source.block not in id_map or source.target.block not in id_map:
|
||||
continue
|
||||
connection = Connection(
|
||||
id=str(uuid4()),
|
||||
source=Endpoint(block=id_map[source.source.block], port=source.source.port),
|
||||
target=Endpoint(block=id_map[source.target.block], port=source.target.port),
|
||||
)
|
||||
connections[connection.id] = connection
|
||||
if blocks:
|
||||
self.undo_stack.push(
|
||||
PasteSelectionCommand(self, owner.id, blocks, connections)
|
||||
)
|
||||
return list(blocks)
|
||||
|
||||
def _graph_for(self, owner_id: str):
|
||||
if self.document is None:
|
||||
raise ValueError("There is no open document")
|
||||
owner = self.document.find_component(owner_id)
|
||||
if owner is None:
|
||||
raise ValueError("The containing component is no longer in the document")
|
||||
return owner.graph
|
||||
|
||||
def _insert_component(self, owner_id: str | None, component: Component) -> None:
|
||||
if self.document is None:
|
||||
raise ValueError("There is no open document")
|
||||
if owner_id is None:
|
||||
self.document.roots[component.id] = component
|
||||
else:
|
||||
self._graph_for(owner_id).blocks[component.id] = component
|
||||
if owner_id == self.active_component_id:
|
||||
self.componentAdded.emit(component.id)
|
||||
self.documentReset.emit()
|
||||
|
||||
def _remove_component(self, owner_id: str | None, component_id: str) -> None:
|
||||
if self.document is None:
|
||||
return
|
||||
if owner_id is None:
|
||||
self.document.roots.pop(component_id, None)
|
||||
if self.active_component_id == component_id:
|
||||
self.active_component_id = next(iter(self.document.roots), None)
|
||||
self.activeGraphChanged.emit()
|
||||
else:
|
||||
self._graph_for(owner_id).blocks.pop(component_id, None)
|
||||
if owner_id == self.active_component_id:
|
||||
self.componentRemoved.emit(component_id)
|
||||
self.documentReset.emit()
|
||||
|
||||
def _move_component(self, owner_id: str, component_id: str, position: QPointF) -> None:
|
||||
component = self._graph_for(owner_id).blocks[component_id]
|
||||
component.x, component.y = position.x(), position.y()
|
||||
if owner_id == self.active_component_id:
|
||||
self.componentMoved.emit(component_id, position)
|
||||
self.documentReset.emit()
|
||||
|
||||
def _rotate_component(
|
||||
self, owner_id: str, component_id: str, rotation: float
|
||||
) -> None:
|
||||
component = self._graph_for(owner_id).blocks.get(component_id)
|
||||
if component is None:
|
||||
return
|
||||
component.rotation = rotation
|
||||
if owner_id == self.active_component_id:
|
||||
self.componentRotated.emit(component_id, rotation)
|
||||
|
||||
def _insert_connection(self, owner_id: str, connection: Connection) -> None:
|
||||
self._graph_for(owner_id).connections[connection.id] = connection
|
||||
if owner_id == self.active_component_id:
|
||||
self.connectionAdded.emit(connection.id)
|
||||
self.documentReset.emit()
|
||||
|
||||
def _remove_connection(self, owner_id: str, connection_id: str) -> None:
|
||||
self._graph_for(owner_id).connections.pop(connection_id, None)
|
||||
if owner_id == self.active_component_id:
|
||||
self.connectionRemoved.emit(connection_id)
|
||||
self.documentReset.emit()
|
||||
|
||||
def _replace_component(self, old_id: str, replacement: Component) -> None:
|
||||
if self.document is None:
|
||||
return
|
||||
was_active = self.active_component_id == old_id
|
||||
if old_id in self.document.roots:
|
||||
self.document.roots.pop(old_id)
|
||||
self.document.roots[replacement.id] = replacement
|
||||
else:
|
||||
parent = self.document.find_parent(old_id)
|
||||
if parent is None:
|
||||
raise ValueError("The component is no longer in this document")
|
||||
parent.graph.blocks.pop(old_id)
|
||||
parent.graph.blocks[replacement.id] = replacement
|
||||
if was_active:
|
||||
self.active_component_id = replacement.id
|
||||
self.document.validate()
|
||||
self.documentReset.emit()
|
||||
if was_active:
|
||||
self.activeGraphChanged.emit()
|
||||
|
||||
def _insert_interface_port(self, owner_id: str, direction: str, port: Port) -> None:
|
||||
if self.document is None:
|
||||
return
|
||||
owner = self.document.find_component(owner_id)
|
||||
if owner is None:
|
||||
return
|
||||
ports = owner.inputs if direction == "input" else owner.outputs
|
||||
if all(existing.id != port.id for existing in ports):
|
||||
ports.append(port)
|
||||
self.interfaceChanged.emit()
|
||||
self.documentReset.emit()
|
||||
|
||||
def _remove_interface_port(self, owner_id: str, direction: str, port_id: str) -> None:
|
||||
if self.document is None:
|
||||
return
|
||||
owner = self.document.find_component(owner_id)
|
||||
if owner is None:
|
||||
return
|
||||
ports = owner.inputs if direction == "input" else owner.outputs
|
||||
ports[:] = [port for port in ports if port.id != port_id]
|
||||
self.interfaceChanged.emit()
|
||||
self.documentReset.emit()
|
||||
|
||||
def _move_interface_port(self, owner_id: str, port_id: str, position: QPointF) -> None:
|
||||
if self.document is None:
|
||||
return
|
||||
owner = self.document.find_component(owner_id)
|
||||
if owner is None:
|
||||
return
|
||||
port = next((p for p in (*owner.inputs, *owner.outputs) if p.id == port_id), None)
|
||||
if port is not None:
|
||||
port.x, port.y = position.x(), position.y()
|
||||
self.interfaceChanged.emit()
|
||||
self.documentReset.emit()
|
||||
|
||||
def _rename_interface_port(self, owner_id: str, port_id: str, name: str) -> None:
|
||||
owner = self.document.find_component(owner_id) if self.document else None
|
||||
if owner is None:
|
||||
return
|
||||
port = next((p for p in (*owner.inputs, *owner.outputs) if p.id == port_id), None)
|
||||
if port is not None:
|
||||
port.name = name
|
||||
self.interfaceChanged.emit()
|
||||
self.documentReset.emit()
|
||||
|
||||
def _rename_connection(self, owner_id: str, connection_id: str, name: str) -> None:
|
||||
connection = self._graph_for(owner_id).connections.get(connection_id)
|
||||
if connection is not None:
|
||||
connection.name = name
|
||||
self.documentReset.emit()
|
||||
|
||||
def _replace_source(self, component_id: str, source: dict) -> None:
|
||||
if self.document is None:
|
||||
return
|
||||
component = self.document.find_component(component_id)
|
||||
if component is None:
|
||||
return
|
||||
component.source = deepcopy(source)
|
||||
self.documentReset.emit()
|
||||
if component_id == self.active_component_id:
|
||||
self.activeGraphChanged.emit()
|
||||
|
||||
def _set_component_appearance(self, component_id: str, values: dict) -> None:
|
||||
if self.document is None:
|
||||
return
|
||||
component = self.document.find_component(component_id)
|
||||
if component is None:
|
||||
return
|
||||
component.name = values["name"]
|
||||
component.icon = Icon.from_dict(values["icon"])
|
||||
component.inputs = [Port.from_dict(port) for port in values["inputs"]]
|
||||
component.outputs = [Port.from_dict(port) for port in values["outputs"]]
|
||||
component.show_subtree_in_library = values["show_subtree"]
|
||||
self.documentReset.emit()
|
||||
if component_id == self.active_component_id:
|
||||
self.activeGraphChanged.emit()
|
||||
|
||||
def _delete_items(
|
||||
self,
|
||||
owner_id: str | None,
|
||||
block_ids: set[str],
|
||||
connection_ids: set[str],
|
||||
input_ids: set[str],
|
||||
output_ids: set[str],
|
||||
) -> None:
|
||||
if self.document is None:
|
||||
return
|
||||
deleted_component_ids: set[str] = set()
|
||||
for block_id in block_ids:
|
||||
component = self.document.find_component(block_id)
|
||||
if component is not None:
|
||||
deleted_component_ids.update(
|
||||
child.id for child in self._component_subtree(component)
|
||||
)
|
||||
active_was_deleted = self.active_component_id in deleted_component_ids
|
||||
if owner_id is None:
|
||||
for block_id in block_ids:
|
||||
self.document.roots.pop(block_id, None)
|
||||
else:
|
||||
owner = self.document.find_component(owner_id)
|
||||
if owner is None:
|
||||
return
|
||||
for block_id in block_ids:
|
||||
owner.graph.blocks.pop(block_id, None)
|
||||
for connection_id in connection_ids:
|
||||
owner.graph.connections.pop(connection_id, None)
|
||||
owner.inputs[:] = [port for port in owner.inputs if port.id not in input_ids]
|
||||
owner.outputs[:] = [port for port in owner.outputs if port.id not in output_ids]
|
||||
if active_was_deleted:
|
||||
self.active_component_id = owner_id or next(iter(self.document.roots), None)
|
||||
self.activeGraphChanged.emit()
|
||||
self.documentReset.emit()
|
||||
|
||||
def _restore_items(
|
||||
self,
|
||||
owner_id: str | None,
|
||||
blocks: dict[str, Component],
|
||||
connections: dict[str, Connection],
|
||||
inputs: list[Port],
|
||||
outputs: list[Port],
|
||||
) -> None:
|
||||
if self.document is None:
|
||||
return
|
||||
if owner_id is None:
|
||||
self.document.roots.update(blocks)
|
||||
else:
|
||||
owner = self.document.find_component(owner_id)
|
||||
if owner is None:
|
||||
return
|
||||
owner.graph.blocks.update(blocks)
|
||||
owner.graph.connections.update(connections)
|
||||
existing_inputs = {port.id for port in owner.inputs}
|
||||
existing_outputs = {port.id for port in owner.outputs}
|
||||
owner.inputs.extend(port for port in inputs if port.id not in existing_inputs)
|
||||
owner.outputs.extend(port for port in outputs if port.id not in existing_outputs)
|
||||
self.documentReset.emit()
|
||||
|
||||
@staticmethod
|
||||
def _component_subtree(component: Component):
|
||||
yield component
|
||||
for child in component.graph.blocks.values():
|
||||
yield from DocumentController._component_subtree(child)
|
||||
|
||||
def _set_text_definition(self, component_id: str, values: dict) -> None:
|
||||
if self.document is None:
|
||||
return
|
||||
component = self.document.find_component(component_id)
|
||||
if component is None:
|
||||
return
|
||||
component.inputs = [Port.from_dict(item) for item in values["inputs"]]
|
||||
component.outputs = [Port.from_dict(item) for item in values["outputs"]]
|
||||
component.source = deepcopy(values["source"])
|
||||
self.interfaceChanged.emit()
|
||||
self.documentReset.emit()
|
||||
if component_id == self.active_component_id:
|
||||
self.activeGraphChanged.emit()
|
||||
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()
|
||||
1
BEdit/src/bedit/gui/generated/__init__.py
Normal file
1
BEdit/src/bedit/gui/generated/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Generated Qt code. Do not edit these modules by hand."""
|
||||
1089
BEdit/src/bedit/gui/generated/resources_rc.py
Normal file
1089
BEdit/src/bedit/gui/generated/resources_rc.py
Normal file
File diff suppressed because it is too large
Load Diff
125
BEdit/src/bedit/gui/generated/ui_component_options_dialog.py
Normal file
125
BEdit/src/bedit/gui/generated/ui_component_options_dialog.py
Normal file
@@ -0,0 +1,125 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
################################################################################
|
||||
## Form generated from reading UI file 'component_options_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, QCheckBox, QComboBox,
|
||||
QDialog, QDialogButtonBox, QFormLayout, QLabel,
|
||||
QLineEdit, QSizePolicy, QSpacerItem, QVBoxLayout,
|
||||
QWidget)
|
||||
|
||||
class Ui_ComponentOptionsDialog(object):
|
||||
def setupUi(self, ComponentOptionsDialog):
|
||||
if not ComponentOptionsDialog.objectName():
|
||||
ComponentOptionsDialog.setObjectName(u"ComponentOptionsDialog")
|
||||
ComponentOptionsDialog.resize(420, 260)
|
||||
self.dialogLayout = QVBoxLayout(ComponentOptionsDialog)
|
||||
self.dialogLayout.setObjectName(u"dialogLayout")
|
||||
self.optionsForm = QFormLayout()
|
||||
self.optionsForm.setObjectName(u"optionsForm")
|
||||
self.nameLabel = QLabel(ComponentOptionsDialog)
|
||||
self.nameLabel.setObjectName(u"nameLabel")
|
||||
|
||||
self.optionsForm.setWidget(0, QFormLayout.ItemRole.LabelRole, self.nameLabel)
|
||||
|
||||
self.nameEdit = QLineEdit(ComponentOptionsDialog)
|
||||
self.nameEdit.setObjectName(u"nameEdit")
|
||||
|
||||
self.optionsForm.setWidget(0, QFormLayout.ItemRole.FieldRole, self.nameEdit)
|
||||
|
||||
self.shapeLabel = QLabel(ComponentOptionsDialog)
|
||||
self.shapeLabel.setObjectName(u"shapeLabel")
|
||||
|
||||
self.optionsForm.setWidget(1, QFormLayout.ItemRole.LabelRole, self.shapeLabel)
|
||||
|
||||
self.shapeCombo = QComboBox(ComponentOptionsDialog)
|
||||
self.shapeCombo.addItem("")
|
||||
self.shapeCombo.addItem("")
|
||||
self.shapeCombo.setObjectName(u"shapeCombo")
|
||||
|
||||
self.optionsForm.setWidget(1, QFormLayout.ItemRole.FieldRole, self.shapeCombo)
|
||||
|
||||
self.iconTextLabel = QLabel(ComponentOptionsDialog)
|
||||
self.iconTextLabel.setObjectName(u"iconTextLabel")
|
||||
|
||||
self.optionsForm.setWidget(2, QFormLayout.ItemRole.LabelRole, self.iconTextLabel)
|
||||
|
||||
self.iconTextEdit = QLineEdit(ComponentOptionsDialog)
|
||||
self.iconTextEdit.setObjectName(u"iconTextEdit")
|
||||
|
||||
self.optionsForm.setWidget(2, QFormLayout.ItemRole.FieldRole, self.iconTextEdit)
|
||||
|
||||
self.fillLabel = QLabel(ComponentOptionsDialog)
|
||||
self.fillLabel.setObjectName(u"fillLabel")
|
||||
|
||||
self.optionsForm.setWidget(3, QFormLayout.ItemRole.LabelRole, self.fillLabel)
|
||||
|
||||
self.fillEdit = QLineEdit(ComponentOptionsDialog)
|
||||
self.fillEdit.setObjectName(u"fillEdit")
|
||||
|
||||
self.optionsForm.setWidget(3, QFormLayout.ItemRole.FieldRole, self.fillEdit)
|
||||
|
||||
self.borderLabel = QLabel(ComponentOptionsDialog)
|
||||
self.borderLabel.setObjectName(u"borderLabel")
|
||||
|
||||
self.optionsForm.setWidget(4, QFormLayout.ItemRole.LabelRole, self.borderLabel)
|
||||
|
||||
self.borderEdit = QLineEdit(ComponentOptionsDialog)
|
||||
self.borderEdit.setObjectName(u"borderEdit")
|
||||
|
||||
self.optionsForm.setWidget(4, QFormLayout.ItemRole.FieldRole, self.borderEdit)
|
||||
|
||||
self.showSubtreeCheckBox = QCheckBox(ComponentOptionsDialog)
|
||||
self.showSubtreeCheckBox.setObjectName(u"showSubtreeCheckBox")
|
||||
self.showSubtreeCheckBox.setChecked(True)
|
||||
|
||||
self.optionsForm.setWidget(5, QFormLayout.ItemRole.SpanningRole, self.showSubtreeCheckBox)
|
||||
|
||||
|
||||
self.dialogLayout.addLayout(self.optionsForm)
|
||||
|
||||
self.optionsSpacer = QSpacerItem(20, 40, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Expanding)
|
||||
|
||||
self.dialogLayout.addItem(self.optionsSpacer)
|
||||
|
||||
self.buttonBox = QDialogButtonBox(ComponentOptionsDialog)
|
||||
self.buttonBox.setObjectName(u"buttonBox")
|
||||
self.buttonBox.setStandardButtons(QDialogButtonBox.StandardButton.Cancel|QDialogButtonBox.StandardButton.Ok)
|
||||
|
||||
self.dialogLayout.addWidget(self.buttonBox)
|
||||
|
||||
|
||||
self.retranslateUi(ComponentOptionsDialog)
|
||||
self.buttonBox.accepted.connect(ComponentOptionsDialog.accept)
|
||||
self.buttonBox.rejected.connect(ComponentOptionsDialog.reject)
|
||||
|
||||
QMetaObject.connectSlotsByName(ComponentOptionsDialog)
|
||||
# setupUi
|
||||
|
||||
def retranslateUi(self, ComponentOptionsDialog):
|
||||
ComponentOptionsDialog.setWindowTitle(QCoreApplication.translate("ComponentOptionsDialog", u"Component Options", None))
|
||||
self.nameLabel.setText(QCoreApplication.translate("ComponentOptionsDialog", u"Name:", None))
|
||||
self.shapeLabel.setText(QCoreApplication.translate("ComponentOptionsDialog", u"Icon shape:", None))
|
||||
self.shapeCombo.setItemText(0, QCoreApplication.translate("ComponentOptionsDialog", u"rectangle", None))
|
||||
self.shapeCombo.setItemText(1, QCoreApplication.translate("ComponentOptionsDialog", u"ellipse", None))
|
||||
|
||||
self.iconTextLabel.setText(QCoreApplication.translate("ComponentOptionsDialog", u"Icon text:", None))
|
||||
self.fillLabel.setText(QCoreApplication.translate("ComponentOptionsDialog", u"Fill color:", None))
|
||||
self.fillEdit.setPlaceholderText(QCoreApplication.translate("ComponentOptionsDialog", u"#dbeafe", None))
|
||||
self.borderLabel.setText(QCoreApplication.translate("ComponentOptionsDialog", u"Border color:", None))
|
||||
self.borderEdit.setPlaceholderText(QCoreApplication.translate("ComponentOptionsDialog", u"#303030", None))
|
||||
self.showSubtreeCheckBox.setText(QCoreApplication.translate("ComponentOptionsDialog", u"Show contained components in the Libraries tree", None))
|
||||
# retranslateUi
|
||||
|
||||
450
BEdit/src/bedit/gui/generated/ui_main_window.py
Normal file
450
BEdit/src/bedit/gui/generated/ui_main_window.py
Normal file
@@ -0,0 +1,450 @@
|
||||
# -*- 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, QFrame, QHBoxLayout,
|
||||
QHeaderView, QLabel, QMainWindow, QMenu,
|
||||
QMenuBar, QPlainTextEdit, QPushButton, QSizePolicy,
|
||||
QSpacerItem, QSplitter, QStackedWidget, QToolBar,
|
||||
QToolButton, QTreeView, QVBoxLayout, QWidget)
|
||||
|
||||
from bedit.gui.graphics.workspace import GraphWorkspaceView
|
||||
from . import resources_rc
|
||||
|
||||
class Ui_MainWindow(object):
|
||||
def setupUi(self, MainWindow):
|
||||
if not MainWindow.objectName():
|
||||
MainWindow.setObjectName(u"MainWindow")
|
||||
MainWindow.resize(1000, 700)
|
||||
self.actionNew = QAction(MainWindow)
|
||||
self.actionNew.setObjectName(u"actionNew")
|
||||
icon = QIcon()
|
||||
icon.addFile(u":/icons/icons/document-new.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
||||
self.actionNew.setIcon(icon)
|
||||
self.actionRotateClockwise = QAction(MainWindow)
|
||||
self.actionRotateClockwise.setObjectName(u"actionRotateClockwise")
|
||||
icon1 = QIcon()
|
||||
icon1.addFile(u":/icons/icons/transform-rotate.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
||||
self.actionRotateClockwise.setIcon(icon1)
|
||||
self.actionOpen = QAction(MainWindow)
|
||||
self.actionOpen.setObjectName(u"actionOpen")
|
||||
icon2 = QIcon()
|
||||
icon2.addFile(u":/icons/icons/document-open.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
||||
self.actionOpen.setIcon(icon2)
|
||||
self.actionSave = QAction(MainWindow)
|
||||
self.actionSave.setObjectName(u"actionSave")
|
||||
icon3 = QIcon()
|
||||
icon3.addFile(u":/icons/icons/document-save.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
||||
self.actionSave.setIcon(icon3)
|
||||
self.actionSaveAs = QAction(MainWindow)
|
||||
self.actionSaveAs.setObjectName(u"actionSaveAs")
|
||||
icon4 = QIcon()
|
||||
icon4.addFile(u":/icons/icons/document-save-as.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
||||
self.actionSaveAs.setIcon(icon4)
|
||||
self.actionExit = QAction(MainWindow)
|
||||
self.actionExit.setObjectName(u"actionExit")
|
||||
self.actionClose = QAction(MainWindow)
|
||||
self.actionClose.setObjectName(u"actionClose")
|
||||
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.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.actionCut = QAction(MainWindow)
|
||||
self.actionCut.setObjectName(u"actionCut")
|
||||
icon7 = QIcon()
|
||||
icon7.addFile(u":/icons/icons/edit-cut.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
||||
self.actionCut.setIcon(icon7)
|
||||
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.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.actionSelectAll = QAction(MainWindow)
|
||||
self.actionSelectAll.setObjectName(u"actionSelectAll")
|
||||
self.actionDelete = QAction(MainWindow)
|
||||
self.actionDelete.setObjectName(u"actionDelete")
|
||||
self.actionAbout = QAction(MainWindow)
|
||||
self.actionAbout.setObjectName(u"actionAbout")
|
||||
self.actionSettings = QAction(MainWindow)
|
||||
self.actionSettings.setObjectName(u"actionSettings")
|
||||
self.actionAboutQt = QAction(MainWindow)
|
||||
self.actionAboutQt.setObjectName(u"actionAboutQt")
|
||||
self.centralwidget = QWidget(MainWindow)
|
||||
self.centralwidget.setObjectName(u"centralwidget")
|
||||
self.workspaceLayout = QHBoxLayout(self.centralwidget)
|
||||
self.workspaceLayout.setSpacing(0)
|
||||
self.workspaceLayout.setObjectName(u"workspaceLayout")
|
||||
self.workspaceLayout.setContentsMargins(0, 0, 0, 0)
|
||||
self.workspaceSplitter = QSplitter(self.centralwidget)
|
||||
self.workspaceSplitter.setObjectName(u"workspaceSplitter")
|
||||
sizePolicy = QSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred)
|
||||
sizePolicy.setHorizontalStretch(0)
|
||||
sizePolicy.setVerticalStretch(0)
|
||||
sizePolicy.setHeightForWidth(self.workspaceSplitter.sizePolicy().hasHeightForWidth())
|
||||
self.workspaceSplitter.setSizePolicy(sizePolicy)
|
||||
self.workspaceSplitter.setOrientation(Qt.Orientation.Horizontal)
|
||||
self.workspaceSplitter.setChildrenCollapsible(False)
|
||||
self.leftDockHost = QMainWindow(self.workspaceSplitter)
|
||||
self.leftDockHost.setObjectName(u"leftDockHost")
|
||||
self.leftDockHost.setMinimumSize(QSize(220, 0))
|
||||
self.panel_libraries = QDockWidget(self.leftDockHost)
|
||||
self.panel_libraries.setObjectName(u"panel_libraries")
|
||||
self.panel_libraries.setMinimumSize(QSize(220, 91))
|
||||
self.dockWidgetContents = QWidget()
|
||||
self.dockWidgetContents.setObjectName(u"dockWidgetContents")
|
||||
self.librariesLayout = QVBoxLayout(self.dockWidgetContents)
|
||||
self.librariesLayout.setSpacing(0)
|
||||
self.librariesLayout.setObjectName(u"librariesLayout")
|
||||
self.librariesLayout.setContentsMargins(0, 0, 0, 0)
|
||||
self.treeView = QTreeView(self.dockWidgetContents)
|
||||
self.treeView.setObjectName(u"treeView")
|
||||
self.treeView.setAlternatingRowColors(True)
|
||||
self.treeView.setUniformRowHeights(True)
|
||||
|
||||
self.librariesLayout.addWidget(self.treeView)
|
||||
|
||||
self.panel_libraries.setWidget(self.dockWidgetContents)
|
||||
self.leftDockHost.addDockWidget(Qt.DockWidgetArea.LeftDockWidgetArea, self.panel_libraries)
|
||||
self.panel_document = QDockWidget(self.leftDockHost)
|
||||
self.panel_document.setObjectName(u"panel_document")
|
||||
self.panel_document.setMinimumSize(QSize(220, 91))
|
||||
self.documentDockContents = QWidget()
|
||||
self.documentDockContents.setObjectName(u"documentDockContents")
|
||||
self.documentPanelLayout = QVBoxLayout(self.documentDockContents)
|
||||
self.documentPanelLayout.setSpacing(0)
|
||||
self.documentPanelLayout.setObjectName(u"documentPanelLayout")
|
||||
self.documentPanelLayout.setContentsMargins(0, 0, 0, 0)
|
||||
self.documentTreeView = QTreeView(self.documentDockContents)
|
||||
self.documentTreeView.setObjectName(u"documentTreeView")
|
||||
self.documentTreeView.setAlternatingRowColors(True)
|
||||
self.documentTreeView.setUniformRowHeights(True)
|
||||
|
||||
self.documentPanelLayout.addWidget(self.documentTreeView)
|
||||
|
||||
self.panel_document.setWidget(self.documentDockContents)
|
||||
self.leftDockHost.addDockWidget(Qt.DockWidgetArea.LeftDockWidgetArea, self.panel_document)
|
||||
self.workspaceSplitter.addWidget(self.leftDockHost)
|
||||
self.workspace = QWidget(self.workspaceSplitter)
|
||||
self.workspace.setObjectName(u"workspace")
|
||||
sizePolicy1 = QSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred)
|
||||
sizePolicy1.setHorizontalStretch(1)
|
||||
sizePolicy1.setVerticalStretch(0)
|
||||
sizePolicy1.setHeightForWidth(self.workspace.sizePolicy().hasHeightForWidth())
|
||||
self.workspace.setSizePolicy(sizePolicy1)
|
||||
self.workspaceEditorLayout = QVBoxLayout(self.workspace)
|
||||
self.workspaceEditorLayout.setSpacing(0)
|
||||
self.workspaceEditorLayout.setObjectName(u"workspaceEditorLayout")
|
||||
self.workspaceEditorLayout.setContentsMargins(0, 0, 0, 0)
|
||||
self.workspaceHeader = QFrame(self.workspace)
|
||||
self.workspaceHeader.setObjectName(u"workspaceHeader")
|
||||
self.workspaceHeader.setMinimumSize(QSize(0, 34))
|
||||
self.workspaceHeader.setMaximumSize(QSize(16777215, 34))
|
||||
self.workspaceHeader.setFrameShape(QFrame.Shape.StyledPanel)
|
||||
self.workspaceHeaderLayout = QHBoxLayout(self.workspaceHeader)
|
||||
self.workspaceHeaderLayout.setObjectName(u"workspaceHeaderLayout")
|
||||
self.workspaceHeaderLayout.setContentsMargins(6, 2, 6, 2)
|
||||
self.navigateUpButton = QToolButton(self.workspaceHeader)
|
||||
self.navigateUpButton.setObjectName(u"navigateUpButton")
|
||||
|
||||
self.workspaceHeaderLayout.addWidget(self.navigateUpButton)
|
||||
|
||||
self.graphBreadcrumbLabel = QLabel(self.workspaceHeader)
|
||||
self.graphBreadcrumbLabel.setObjectName(u"graphBreadcrumbLabel")
|
||||
|
||||
self.workspaceHeaderLayout.addWidget(self.graphBreadcrumbLabel)
|
||||
|
||||
self.workspaceModeLabel = QLabel(self.workspaceHeader)
|
||||
self.workspaceModeLabel.setObjectName(u"workspaceModeLabel")
|
||||
|
||||
self.workspaceHeaderLayout.addWidget(self.workspaceModeLabel)
|
||||
|
||||
self.workspaceHeaderSpacer = QSpacerItem(40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum)
|
||||
|
||||
self.workspaceHeaderLayout.addItem(self.workspaceHeaderSpacer)
|
||||
|
||||
self.applyJsonButton = QPushButton(self.workspaceHeader)
|
||||
self.applyJsonButton.setObjectName(u"applyJsonButton")
|
||||
self.applyJsonButton.setVisible(False)
|
||||
|
||||
self.workspaceHeaderLayout.addWidget(self.applyJsonButton)
|
||||
|
||||
self.pointerToolButton = QToolButton(self.workspaceHeader)
|
||||
self.pointerToolButton.setObjectName(u"pointerToolButton")
|
||||
self.pointerToolButton.setCheckable(True)
|
||||
self.pointerToolButton.setChecked(True)
|
||||
self.pointerToolButton.setAutoExclusive(True)
|
||||
|
||||
self.workspaceHeaderLayout.addWidget(self.pointerToolButton)
|
||||
|
||||
self.inputToolButton = QToolButton(self.workspaceHeader)
|
||||
self.inputToolButton.setObjectName(u"inputToolButton")
|
||||
self.inputToolButton.setCheckable(True)
|
||||
self.inputToolButton.setAutoExclusive(True)
|
||||
|
||||
self.workspaceHeaderLayout.addWidget(self.inputToolButton)
|
||||
|
||||
self.outputToolButton = QToolButton(self.workspaceHeader)
|
||||
self.outputToolButton.setObjectName(u"outputToolButton")
|
||||
self.outputToolButton.setCheckable(True)
|
||||
self.outputToolButton.setAutoExclusive(True)
|
||||
|
||||
self.workspaceHeaderLayout.addWidget(self.outputToolButton)
|
||||
|
||||
|
||||
self.workspaceEditorLayout.addWidget(self.workspaceHeader)
|
||||
|
||||
self.workspaceStack = QStackedWidget(self.workspace)
|
||||
self.workspaceStack.setObjectName(u"workspaceStack")
|
||||
self.graphPage = QWidget()
|
||||
self.graphPage.setObjectName(u"graphPage")
|
||||
self.graphPageLayout = QVBoxLayout(self.graphPage)
|
||||
self.graphPageLayout.setObjectName(u"graphPageLayout")
|
||||
self.graphPageLayout.setContentsMargins(0, 0, 0, 0)
|
||||
self.graphView = GraphWorkspaceView(self.graphPage)
|
||||
self.graphView.setObjectName(u"graphView")
|
||||
|
||||
self.graphPageLayout.addWidget(self.graphView)
|
||||
|
||||
self.workspaceStack.addWidget(self.graphPage)
|
||||
self.jsonPage = QWidget()
|
||||
self.jsonPage.setObjectName(u"jsonPage")
|
||||
self.jsonPageLayout = QVBoxLayout(self.jsonPage)
|
||||
self.jsonPageLayout.setObjectName(u"jsonPageLayout")
|
||||
self.jsonPageLayout.setContentsMargins(0, 0, 0, 0)
|
||||
self.jsonEditor = QPlainTextEdit(self.jsonPage)
|
||||
self.jsonEditor.setObjectName(u"jsonEditor")
|
||||
self.jsonEditor.setLineWrapMode(QPlainTextEdit.LineWrapMode.NoWrap)
|
||||
|
||||
self.jsonPageLayout.addWidget(self.jsonEditor)
|
||||
|
||||
self.workspaceStack.addWidget(self.jsonPage)
|
||||
self.emptyPage = QWidget()
|
||||
self.emptyPage.setObjectName(u"emptyPage")
|
||||
self.emptyPageLayout = QVBoxLayout(self.emptyPage)
|
||||
self.emptyPageLayout.setObjectName(u"emptyPageLayout")
|
||||
self.emptyWorkspaceLabel = QLabel(self.emptyPage)
|
||||
self.emptyWorkspaceLabel.setObjectName(u"emptyWorkspaceLabel")
|
||||
self.emptyWorkspaceLabel.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
|
||||
self.emptyPageLayout.addWidget(self.emptyWorkspaceLabel)
|
||||
|
||||
self.workspaceStack.addWidget(self.emptyPage)
|
||||
|
||||
self.workspaceEditorLayout.addWidget(self.workspaceStack)
|
||||
|
||||
self.workspaceSplitter.addWidget(self.workspace)
|
||||
|
||||
self.workspaceLayout.addWidget(self.workspaceSplitter)
|
||||
|
||||
MainWindow.setCentralWidget(self.centralwidget)
|
||||
self.menubar = QMenuBar(MainWindow)
|
||||
self.menubar.setObjectName(u"menubar")
|
||||
self.menubar.setGeometry(QRect(0, 0, 1000, 24))
|
||||
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.menuPanels = QMenu(self.menuView)
|
||||
self.menuPanels.setObjectName(u"menuPanels")
|
||||
self.menuToolbars = QMenu(self.menuView)
|
||||
self.menuToolbars.setObjectName(u"menuToolbars")
|
||||
self.menuHelp = QMenu(self.menubar)
|
||||
self.menuHelp.setObjectName(u"menuHelp")
|
||||
MainWindow.setMenuBar(self.menubar)
|
||||
self.fileToolbar = QToolBar(MainWindow)
|
||||
self.fileToolbar.setObjectName(u"fileToolbar")
|
||||
sizePolicy2 = QSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
|
||||
sizePolicy2.setHorizontalStretch(0)
|
||||
sizePolicy2.setVerticalStretch(0)
|
||||
sizePolicy2.setHeightForWidth(self.fileToolbar.sizePolicy().hasHeightForWidth())
|
||||
self.fileToolbar.setSizePolicy(sizePolicy2)
|
||||
self.fileToolbar.setMinimumSize(QSize(0, 40))
|
||||
self.fileToolbar.setMaximumSize(QSize(16777215, 40))
|
||||
self.fileToolbar.setIconSize(QSize(24, 24))
|
||||
MainWindow.addToolBar(Qt.ToolBarArea.TopToolBarArea, self.fileToolbar)
|
||||
self.editToolbar = QToolBar(MainWindow)
|
||||
self.editToolbar.setObjectName(u"editToolbar")
|
||||
MainWindow.addToolBar(Qt.ToolBarArea.TopToolBarArea, self.editToolbar)
|
||||
self.transformToolbar = QToolBar(MainWindow)
|
||||
self.transformToolbar.setObjectName(u"transformToolbar")
|
||||
MainWindow.addToolBar(Qt.ToolBarArea.TopToolBarArea, self.transformToolbar)
|
||||
|
||||
self.menubar.addAction(self.menuFile.menuAction())
|
||||
self.menubar.addAction(self.menuEdit.menuAction())
|
||||
self.menubar.addAction(self.menuView.menuAction())
|
||||
self.menubar.addAction(self.menuHelp.menuAction())
|
||||
self.menuFile.addAction(self.actionNew)
|
||||
self.menuFile.addAction(self.actionOpen)
|
||||
self.menuFile.addSeparator()
|
||||
self.menuFile.addAction(self.actionSave)
|
||||
self.menuFile.addAction(self.actionSaveAs)
|
||||
self.menuFile.addSeparator()
|
||||
self.menuFile.addAction(self.actionClose)
|
||||
self.menuFile.addAction(self.actionExit)
|
||||
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.addAction(self.actionDelete)
|
||||
self.menuEdit.addAction(self.actionSelectAll)
|
||||
self.menuEdit.addSeparator()
|
||||
self.menuEdit.addAction(self.actionSettings)
|
||||
self.menuView.addAction(self.menuPanels.menuAction())
|
||||
self.menuView.addAction(self.menuToolbars.menuAction())
|
||||
self.menuHelp.addAction(self.actionAbout)
|
||||
self.menuHelp.addAction(self.actionAboutQt)
|
||||
self.fileToolbar.addAction(self.actionNew)
|
||||
self.fileToolbar.addAction(self.actionOpen)
|
||||
self.fileToolbar.addAction(self.actionSave)
|
||||
self.fileToolbar.addAction(self.actionSaveAs)
|
||||
self.editToolbar.addAction(self.actionUndo)
|
||||
self.editToolbar.addAction(self.actionRedo)
|
||||
self.editToolbar.addAction(self.actionCopy)
|
||||
self.editToolbar.addAction(self.actionCut)
|
||||
self.editToolbar.addAction(self.actionPaste)
|
||||
self.transformToolbar.addAction(self.actionRotateClockwise)
|
||||
|
||||
self.retranslateUi(MainWindow)
|
||||
|
||||
self.workspaceStack.setCurrentIndex(0)
|
||||
|
||||
|
||||
QMetaObject.connectSlotsByName(MainWindow)
|
||||
# setupUi
|
||||
|
||||
def retranslateUi(self, MainWindow):
|
||||
MainWindow.setWindowTitle(QCoreApplication.translate("MainWindow", u"BEdit", None))
|
||||
self.actionNew.setText(QCoreApplication.translate("MainWindow", u"&New", None))
|
||||
#if QT_CONFIG(statustip)
|
||||
self.actionNew.setStatusTip(QCoreApplication.translate("MainWindow", u"Create a new document", None))
|
||||
#endif // QT_CONFIG(statustip)
|
||||
#if QT_CONFIG(shortcut)
|
||||
self.actionNew.setShortcut(QCoreApplication.translate("MainWindow", u"Ctrl+N", None))
|
||||
#endif // QT_CONFIG(shortcut)
|
||||
self.actionRotateClockwise.setText(QCoreApplication.translate("MainWindow", u"Rotate Clockwise", None))
|
||||
#if QT_CONFIG(tooltip)
|
||||
self.actionRotateClockwise.setToolTip(QCoreApplication.translate("MainWindow", u"Rotate selected blocks clockwise by 90 degrees", None))
|
||||
#endif // QT_CONFIG(tooltip)
|
||||
#if QT_CONFIG(shortcut)
|
||||
self.actionRotateClockwise.setShortcut(QCoreApplication.translate("MainWindow", u"Ctrl+R", None))
|
||||
#endif // QT_CONFIG(shortcut)
|
||||
self.actionOpen.setText(QCoreApplication.translate("MainWindow", u"&Open\u2026", None))
|
||||
#if QT_CONFIG(statustip)
|
||||
self.actionOpen.setStatusTip(QCoreApplication.translate("MainWindow", u"Open a document", None))
|
||||
#endif // QT_CONFIG(statustip)
|
||||
#if QT_CONFIG(shortcut)
|
||||
self.actionOpen.setShortcut(QCoreApplication.translate("MainWindow", u"Ctrl+O", None))
|
||||
#endif // QT_CONFIG(shortcut)
|
||||
self.actionSave.setText(QCoreApplication.translate("MainWindow", u"&Save", None))
|
||||
#if QT_CONFIG(statustip)
|
||||
self.actionSave.setStatusTip(QCoreApplication.translate("MainWindow", u"Save the current document", None))
|
||||
#endif // QT_CONFIG(statustip)
|
||||
#if QT_CONFIG(shortcut)
|
||||
self.actionSave.setShortcut(QCoreApplication.translate("MainWindow", u"Ctrl+S", None))
|
||||
#endif // QT_CONFIG(shortcut)
|
||||
self.actionSaveAs.setText(QCoreApplication.translate("MainWindow", u"Save &As\u2026", None))
|
||||
#if QT_CONFIG(shortcut)
|
||||
self.actionSaveAs.setShortcut(QCoreApplication.translate("MainWindow", u"Ctrl+Shift+S", None))
|
||||
#endif // QT_CONFIG(shortcut)
|
||||
self.actionExit.setText(QCoreApplication.translate("MainWindow", u"E&xit", None))
|
||||
#if QT_CONFIG(shortcut)
|
||||
self.actionExit.setShortcut(QCoreApplication.translate("MainWindow", u"Ctrl+Q", None))
|
||||
#endif // QT_CONFIG(shortcut)
|
||||
self.actionClose.setText(QCoreApplication.translate("MainWindow", u"&Close Document", None))
|
||||
#if QT_CONFIG(shortcut)
|
||||
self.actionClose.setShortcut(QCoreApplication.translate("MainWindow", u"Ctrl+W", None))
|
||||
#endif // QT_CONFIG(shortcut)
|
||||
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.actionCut.setText(QCoreApplication.translate("MainWindow", u"Cu&t", None))
|
||||
#if QT_CONFIG(shortcut)
|
||||
self.actionCut.setShortcut(QCoreApplication.translate("MainWindow", u"Ctrl+X", None))
|
||||
#endif // QT_CONFIG(shortcut)
|
||||
self.actionCopy.setText(QCoreApplication.translate("MainWindow", u"&Copy", None))
|
||||
#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(shortcut)
|
||||
self.actionPaste.setShortcut(QCoreApplication.translate("MainWindow", u"Ctrl+V", None))
|
||||
#endif // QT_CONFIG(shortcut)
|
||||
self.actionSelectAll.setText(QCoreApplication.translate("MainWindow", u"Select &All", None))
|
||||
#if QT_CONFIG(shortcut)
|
||||
self.actionSelectAll.setShortcut(QCoreApplication.translate("MainWindow", u"Ctrl+A", None))
|
||||
#endif // QT_CONFIG(shortcut)
|
||||
self.actionDelete.setText(QCoreApplication.translate("MainWindow", u"&Delete", None))
|
||||
#if QT_CONFIG(shortcut)
|
||||
self.actionDelete.setShortcut(QCoreApplication.translate("MainWindow", u"Del", None))
|
||||
#endif // QT_CONFIG(shortcut)
|
||||
self.actionAbout.setText(QCoreApplication.translate("MainWindow", u"&About BEdit", None))
|
||||
self.actionSettings.setText(QCoreApplication.translate("MainWindow", u"&Settings\u2026", None))
|
||||
#if QT_CONFIG(statustip)
|
||||
self.actionSettings.setStatusTip(QCoreApplication.translate("MainWindow", u"Configure BEdit", None))
|
||||
#endif // QT_CONFIG(statustip)
|
||||
self.actionAboutQt.setText(QCoreApplication.translate("MainWindow", u"About &Qt", None))
|
||||
self.panel_libraries.setWindowTitle(QCoreApplication.translate("MainWindow", u"Libraries", None))
|
||||
self.panel_document.setWindowTitle(QCoreApplication.translate("MainWindow", u"Document", None))
|
||||
self.navigateUpButton.setText(QCoreApplication.translate("MainWindow", u"Up", None))
|
||||
#if QT_CONFIG(tooltip)
|
||||
self.navigateUpButton.setToolTip(QCoreApplication.translate("MainWindow", u"Open the containing graph", None))
|
||||
#endif // QT_CONFIG(tooltip)
|
||||
self.graphBreadcrumbLabel.setText(QCoreApplication.translate("MainWindow", u"Untitled", None))
|
||||
self.workspaceModeLabel.setText(QCoreApplication.translate("MainWindow", u"Graph", None))
|
||||
self.applyJsonButton.setText(QCoreApplication.translate("MainWindow", u"Apply JSON", None))
|
||||
self.pointerToolButton.setText(QCoreApplication.translate("MainWindow", u"Pointer", None))
|
||||
self.inputToolButton.setText(QCoreApplication.translate("MainWindow", u"Input", None))
|
||||
#if QT_CONFIG(tooltip)
|
||||
self.inputToolButton.setToolTip(QCoreApplication.translate("MainWindow", u"Add an interface input", None))
|
||||
#endif // QT_CONFIG(tooltip)
|
||||
self.outputToolButton.setText(QCoreApplication.translate("MainWindow", u"Output", None))
|
||||
#if QT_CONFIG(tooltip)
|
||||
self.outputToolButton.setToolTip(QCoreApplication.translate("MainWindow", u"Add an interface output", None))
|
||||
#endif // QT_CONFIG(tooltip)
|
||||
self.jsonEditor.setPlaceholderText(QCoreApplication.translate("MainWindow", u"Component JSON", None))
|
||||
self.emptyWorkspaceLabel.setText(QCoreApplication.translate("MainWindow", u"No document open", 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.menuPanels.setTitle(QCoreApplication.translate("MainWindow", u"&Panels", None))
|
||||
self.menuToolbars.setTitle(QCoreApplication.translate("MainWindow", u"&Toolbars", None))
|
||||
self.menuHelp.setTitle(QCoreApplication.translate("MainWindow", u"&Help", None))
|
||||
self.fileToolbar.setWindowTitle(QCoreApplication.translate("MainWindow", u"File", None))
|
||||
self.editToolbar.setWindowTitle(QCoreApplication.translate("MainWindow", u"Edit", None))
|
||||
self.transformToolbar.setWindowTitle(QCoreApplication.translate("MainWindow", u"Transform", None))
|
||||
# retranslateUi
|
||||
|
||||
132
BEdit/src/bedit/gui/generated/ui_settings_dialog.py
Normal file
132
BEdit/src/bedit/gui/generated/ui_settings_dialog.py
Normal file
@@ -0,0 +1,132 @@
|
||||
# -*- 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, QDialog, QDialogButtonBox,
|
||||
QFormLayout, QGroupBox, QHBoxLayout, QLabel,
|
||||
QListWidget, QListWidgetItem, QPushButton, QSizePolicy,
|
||||
QSpacerItem, QSpinBox, QTabWidget, QVBoxLayout,
|
||||
QWidget)
|
||||
|
||||
class Ui_SettingsDialog(object):
|
||||
def setupUi(self, SettingsDialog):
|
||||
if not SettingsDialog.objectName():
|
||||
SettingsDialog.setObjectName(u"SettingsDialog")
|
||||
SettingsDialog.resize(480, 300)
|
||||
SettingsDialog.setModal(True)
|
||||
self.dialogLayout = QVBoxLayout(SettingsDialog)
|
||||
self.dialogLayout.setObjectName(u"dialogLayout")
|
||||
self.settingsTabs = QTabWidget(SettingsDialog)
|
||||
self.settingsTabs.setObjectName(u"settingsTabs")
|
||||
self.generalTab = QWidget()
|
||||
self.generalTab.setObjectName(u"generalTab")
|
||||
self.generalLayout = QVBoxLayout(self.generalTab)
|
||||
self.generalLayout.setObjectName(u"generalLayout")
|
||||
self.autosaveGroupBox = QGroupBox(self.generalTab)
|
||||
self.autosaveGroupBox.setObjectName(u"autosaveGroupBox")
|
||||
self.autosaveGroupBox.setCheckable(True)
|
||||
self.autosaveGroupBox.setChecked(False)
|
||||
self.autosaveLayout = QFormLayout(self.autosaveGroupBox)
|
||||
self.autosaveLayout.setObjectName(u"autosaveLayout")
|
||||
self.autosaveIntervalSpinBox = QSpinBox(self.autosaveGroupBox)
|
||||
self.autosaveIntervalSpinBox.setObjectName(u"autosaveIntervalSpinBox")
|
||||
self.autosaveIntervalSpinBox.setMinimum(1)
|
||||
self.autosaveIntervalSpinBox.setMaximum(120)
|
||||
self.autosaveIntervalSpinBox.setValue(5)
|
||||
|
||||
self.autosaveLayout.setWidget(0, QFormLayout.ItemRole.FieldRole, self.autosaveIntervalSpinBox)
|
||||
|
||||
|
||||
self.generalLayout.addWidget(self.autosaveGroupBox)
|
||||
|
||||
self.generalSpacer = QSpacerItem(20, 40, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Expanding)
|
||||
|
||||
self.generalLayout.addItem(self.generalSpacer)
|
||||
|
||||
self.settingsTabs.addTab(self.generalTab, "")
|
||||
self.librariesTab = QWidget()
|
||||
self.librariesTab.setObjectName(u"librariesTab")
|
||||
self.librariesTabLayout = QVBoxLayout(self.librariesTab)
|
||||
self.librariesTabLayout.setObjectName(u"librariesTabLayout")
|
||||
self.libraryPathsLabel = QLabel(self.librariesTab)
|
||||
self.libraryPathsLabel.setObjectName(u"libraryPathsLabel")
|
||||
self.libraryPathsLabel.setWordWrap(True)
|
||||
|
||||
self.librariesTabLayout.addWidget(self.libraryPathsLabel)
|
||||
|
||||
self.libraryPathsList = QListWidget(self.librariesTab)
|
||||
self.libraryPathsList.setObjectName(u"libraryPathsList")
|
||||
|
||||
self.librariesTabLayout.addWidget(self.libraryPathsList)
|
||||
|
||||
self.libraryPathButtonsLayout = QHBoxLayout()
|
||||
self.libraryPathButtonsLayout.setObjectName(u"libraryPathButtonsLayout")
|
||||
self.addLibraryFileButton = QPushButton(self.librariesTab)
|
||||
self.addLibraryFileButton.setObjectName(u"addLibraryFileButton")
|
||||
|
||||
self.libraryPathButtonsLayout.addWidget(self.addLibraryFileButton)
|
||||
|
||||
self.addLibraryFolderButton = QPushButton(self.librariesTab)
|
||||
self.addLibraryFolderButton.setObjectName(u"addLibraryFolderButton")
|
||||
|
||||
self.libraryPathButtonsLayout.addWidget(self.addLibraryFolderButton)
|
||||
|
||||
self.removeLibraryPathButton = QPushButton(self.librariesTab)
|
||||
self.removeLibraryPathButton.setObjectName(u"removeLibraryPathButton")
|
||||
|
||||
self.libraryPathButtonsLayout.addWidget(self.removeLibraryPathButton)
|
||||
|
||||
self.libraryButtonsSpacer = QSpacerItem(40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum)
|
||||
|
||||
self.libraryPathButtonsLayout.addItem(self.libraryButtonsSpacer)
|
||||
|
||||
|
||||
self.librariesTabLayout.addLayout(self.libraryPathButtonsLayout)
|
||||
|
||||
self.settingsTabs.addTab(self.librariesTab, "")
|
||||
|
||||
self.dialogLayout.addWidget(self.settingsTabs)
|
||||
|
||||
self.buttonBox = QDialogButtonBox(SettingsDialog)
|
||||
self.buttonBox.setObjectName(u"buttonBox")
|
||||
self.buttonBox.setOrientation(Qt.Orientation.Horizontal)
|
||||
self.buttonBox.setStandardButtons(QDialogButtonBox.StandardButton.Cancel|QDialogButtonBox.StandardButton.Ok)
|
||||
|
||||
self.dialogLayout.addWidget(self.buttonBox)
|
||||
|
||||
|
||||
self.retranslateUi(SettingsDialog)
|
||||
self.buttonBox.accepted.connect(SettingsDialog.accept)
|
||||
self.buttonBox.rejected.connect(SettingsDialog.reject)
|
||||
|
||||
self.settingsTabs.setCurrentIndex(0)
|
||||
|
||||
|
||||
QMetaObject.connectSlotsByName(SettingsDialog)
|
||||
# setupUi
|
||||
|
||||
def retranslateUi(self, SettingsDialog):
|
||||
SettingsDialog.setWindowTitle(QCoreApplication.translate("SettingsDialog", u"Settings", None))
|
||||
self.autosaveGroupBox.setTitle(QCoreApplication.translate("SettingsDialog", u"Automatic saving", None))
|
||||
self.autosaveIntervalSpinBox.setSuffix(QCoreApplication.translate("SettingsDialog", u" minutes", None))
|
||||
self.settingsTabs.setTabText(self.settingsTabs.indexOf(self.generalTab), QCoreApplication.translate("SettingsDialog", u"General", None))
|
||||
self.libraryPathsLabel.setText(QCoreApplication.translate("SettingsDialog", u"Load library JSON files from these files or folders at startup:", None))
|
||||
self.addLibraryFileButton.setText(QCoreApplication.translate("SettingsDialog", u"Add File\u2026", None))
|
||||
self.addLibraryFolderButton.setText(QCoreApplication.translate("SettingsDialog", u"Add Folder\u2026", None))
|
||||
self.removeLibraryPathButton.setText(QCoreApplication.translate("SettingsDialog", u"Remove", None))
|
||||
self.settingsTabs.setTabText(self.settingsTabs.indexOf(self.librariesTab), QCoreApplication.translate("SettingsDialog", u"Libraries", None))
|
||||
# retranslateUi
|
||||
|
||||
5
BEdit/src/bedit/gui/graphics/__init__.py
Normal file
5
BEdit/src/bedit/gui/graphics/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
"""Graphics scenes, views, editors, and renderers."""
|
||||
|
||||
from bedit.gui.graphics.workspace import GraphWorkspaceView
|
||||
|
||||
__all__ = ["GraphWorkspaceView"]
|
||||
381
BEdit/src/bedit/gui/graphics/icon_editor.py
Normal file
381
BEdit/src/bedit/gui/graphics/icon_editor.py
Normal file
@@ -0,0 +1,381 @@
|
||||
from copy import deepcopy
|
||||
|
||||
from PySide6.QtCore import QPointF, QRectF, Qt
|
||||
from PySide6.QtGui import QColor, QPainter, QPen, QPolygonF
|
||||
from PySide6.QtWidgets import (
|
||||
QColorDialog,
|
||||
QComboBox,
|
||||
QDialog,
|
||||
QDialogButtonBox,
|
||||
QDoubleSpinBox,
|
||||
QFormLayout,
|
||||
QGraphicsEllipseItem,
|
||||
QGraphicsItem,
|
||||
QGraphicsObject,
|
||||
QGraphicsScene,
|
||||
QGraphicsSceneContextMenuEvent,
|
||||
QGraphicsSceneMouseEvent,
|
||||
QGraphicsView,
|
||||
QHBoxLayout,
|
||||
QInputDialog,
|
||||
QLabel,
|
||||
QLineEdit,
|
||||
QMenu,
|
||||
QPushButton,
|
||||
QToolButton,
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
)
|
||||
|
||||
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:
|
||||
return application_settings().value("grid/iconSize", 8, type=int)
|
||||
|
||||
|
||||
def _snap(value: float) -> float:
|
||||
grid = _icon_grid_size()
|
||||
return round(value / grid) * grid
|
||||
|
||||
|
||||
class IconEditorView(QGraphicsView):
|
||||
def drawBackground(self, painter: QPainter, rect: QRectF) -> None: # noqa: N802
|
||||
painter.fillRect(rect, QColor("#ffffff"))
|
||||
grid = _icon_grid_size()
|
||||
painter.setPen(QPen(QColor("#dbeafe"), 0))
|
||||
left = int(rect.left()) - int(rect.left()) % grid
|
||||
top = int(rect.top()) - int(rect.top()) % grid
|
||||
for x in range(left, int(rect.right()) + grid, grid):
|
||||
painter.drawLine(x, rect.top(), x, rect.bottom())
|
||||
for y in range(top, int(rect.bottom()) + grid, grid):
|
||||
painter.drawLine(rect.left(), y, rect.right(), y)
|
||||
|
||||
|
||||
class ResizeHandle(QGraphicsEllipseItem):
|
||||
def __init__(self, owner: "ShapeItem") -> None:
|
||||
super().__init__(-4, -4, 8, 8, owner)
|
||||
self.owner = owner
|
||||
self.setBrush(QColor("#ffffff"))
|
||||
self.setPen(QPen(QColor("#2563eb"), 1.5))
|
||||
self.setCursor(Qt.CursorShape.SizeFDiagCursor)
|
||||
self.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsMovable)
|
||||
self.setFlag(QGraphicsItem.GraphicsItemFlag.ItemSendsGeometryChanges)
|
||||
self.setZValue(20)
|
||||
|
||||
def itemChange(self, change, value): # noqa: N802
|
||||
if change == QGraphicsItem.GraphicsItemChange.ItemPositionChange:
|
||||
maximum = (
|
||||
self.owner.scene().sceneRect().bottomRight() - self.owner.pos()
|
||||
if self.owner.scene()
|
||||
else QPointF(128, 128)
|
||||
)
|
||||
value = QPointF(
|
||||
min(maximum.x(), max(_icon_grid_size(), _snap(value.x()))),
|
||||
min(maximum.y(), max(_icon_grid_size(), _snap(value.y()))),
|
||||
)
|
||||
if self.owner.element.get("type") == "circle":
|
||||
side = min(maximum.x(), maximum.y(), max(value.x(), value.y()))
|
||||
value = QPointF(side, side)
|
||||
self.owner.prepareGeometryChange()
|
||||
self.owner.element["width"] = value.x()
|
||||
self.owner.element["height"] = value.y()
|
||||
self.owner.update()
|
||||
return super().itemChange(change, value)
|
||||
|
||||
|
||||
class ColorButton(QPushButton):
|
||||
def __init__(self, color: str, allow_none: bool = False, parent=None) -> None:
|
||||
super().__init__(parent)
|
||||
self.color = color
|
||||
self.allow_none = allow_none
|
||||
self.clicked.connect(self.choose)
|
||||
self._refresh()
|
||||
|
||||
def _refresh(self) -> None:
|
||||
self.setText("No fill" if self.color == "none" else self.color)
|
||||
swatch = "transparent" if self.color == "none" else self.color
|
||||
self.setStyleSheet(f"QPushButton {{ background: {swatch}; }}")
|
||||
|
||||
def choose(self) -> None:
|
||||
initial = QColor("#ffffff" if self.color == "none" else self.color)
|
||||
color = QColorDialog.getColor(initial, self, "Choose colour", QColorDialog.ColorDialogOption.ShowAlphaChannel)
|
||||
if color.isValid():
|
||||
self.color = color.name(QColor.NameFormat.HexArgb) if color.alpha() < 255 else color.name()
|
||||
self._refresh()
|
||||
|
||||
|
||||
class ShapeOptionsDialog(QDialog):
|
||||
def __init__(self, element: dict, parent=None) -> None:
|
||||
super().__init__(parent)
|
||||
self.element = deepcopy(element)
|
||||
self.setWindowTitle("Shape Options")
|
||||
layout = QVBoxLayout(self)
|
||||
form = QFormLayout()
|
||||
self.line_style = QComboBox()
|
||||
self.line_style.addItems(["solid", "dash", "dot", "dash-dot", "none"])
|
||||
self.line_style.setCurrentText(self.element.get("lineStyle", "solid"))
|
||||
self.line_width = QDoubleSpinBox()
|
||||
self.line_width.setRange(0.1, 20.0)
|
||||
self.line_width.setValue(float(self.element.get("lineWidth", 1.5)))
|
||||
self.stroke = ColorButton(self.element.get("stroke", "#303030"))
|
||||
self.fill_type = QComboBox()
|
||||
self.fill_type.addItems(["solid", "none"])
|
||||
fill = self.element.get("fill", "#ffffff")
|
||||
self.fill_type.setCurrentText("none" if fill in {"none", "transparent", ""} else "solid")
|
||||
self.fill = ColorButton("#ffffff" if fill in {"none", "transparent", ""} else fill)
|
||||
self.width = QDoubleSpinBox()
|
||||
self.width.setRange(1, 500)
|
||||
self.width.setValue(float(self.element.get("width", 20)))
|
||||
self.height = QDoubleSpinBox()
|
||||
self.height.setRange(1, 500)
|
||||
self.height.setValue(float(self.element.get("height", 20)))
|
||||
form.addRow("Width:", self.width)
|
||||
form.addRow("Height:", self.height)
|
||||
form.addRow("Line style:", self.line_style)
|
||||
form.addRow("Line width:", self.line_width)
|
||||
form.addRow("Line colour:", self.stroke)
|
||||
if self.element.get("type") != "line":
|
||||
form.addRow("Fill type:", self.fill_type)
|
||||
form.addRow("Fill colour:", self.fill)
|
||||
self.radius = None
|
||||
if self.element.get("type") == "rectangle":
|
||||
self.radius = QDoubleSpinBox()
|
||||
self.radius.setRange(0, 50)
|
||||
self.radius.setValue(float(self.element.get("cornerRadius", 0)))
|
||||
form.addRow("Corner radius:", self.radius)
|
||||
self.text_edit = None
|
||||
self.font_size = None
|
||||
if self.element.get("type") == "text":
|
||||
self.text_edit = QLineEdit(str(self.element.get("text", "Text")))
|
||||
self.font_size = QDoubleSpinBox()
|
||||
self.font_size.setRange(4, 96)
|
||||
self.font_size.setValue(float(self.element.get("fontSize", 12)))
|
||||
form.addRow("Text:", self.text_edit)
|
||||
form.addRow("Font size:", self.font_size)
|
||||
layout.addLayout(form)
|
||||
buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
|
||||
buttons.accepted.connect(self.accept)
|
||||
buttons.rejected.connect(self.reject)
|
||||
layout.addWidget(buttons)
|
||||
|
||||
def accept(self) -> None:
|
||||
self.element["lineStyle"] = self.line_style.currentText()
|
||||
self.element["lineWidth"] = self.line_width.value()
|
||||
self.element["stroke"] = self.stroke.color
|
||||
self.element["width"] = self.width.value()
|
||||
self.element["height"] = self.height.value()
|
||||
if self.element.get("type") != "line":
|
||||
self.element["fill"] = self.fill.color if self.fill_type.currentText() == "solid" else "none"
|
||||
if self.radius is not None:
|
||||
self.element["cornerRadius"] = self.radius.value()
|
||||
if self.text_edit is not None:
|
||||
self.element["text"] = self.text_edit.text()
|
||||
self.element["fontSize"] = self.font_size.value()
|
||||
self.element["color"] = self.stroke.color
|
||||
super().accept()
|
||||
|
||||
|
||||
class ShapeItem(QGraphicsObject):
|
||||
def __init__(self, element: dict) -> None:
|
||||
super().__init__()
|
||||
self.element = element
|
||||
self.setPos(float(element.get("x", 0)), float(element.get("y", 0)))
|
||||
self.setFlags(QGraphicsItem.GraphicsItemFlag.ItemIsMovable | QGraphicsItem.GraphicsItemFlag.ItemIsSelectable | QGraphicsItem.GraphicsItemFlag.ItemSendsGeometryChanges)
|
||||
self.resize_handle = ResizeHandle(self)
|
||||
self.resize_handle.setPos(float(element.get("width", 20)), float(element.get("height", 20)))
|
||||
self.resize_handle.hide()
|
||||
|
||||
def boundingRect(self) -> QRectF: # noqa: N802
|
||||
margin = max(3.0, float(self.element.get("lineWidth", 1.5)))
|
||||
return QRectF(0, 0, float(self.element.get("width", 20)), float(self.element.get("height", 20))).adjusted(-margin, -margin, margin, margin)
|
||||
|
||||
def paint(self, painter: QPainter, option, widget=None) -> None:
|
||||
del option, widget
|
||||
rect = QRectF(0, 0, float(self.element.get("width", 20)), float(self.element.get("height", 20)))
|
||||
painter.setPen(_pen(self.element))
|
||||
fill = self.element.get("fill", "none")
|
||||
painter.setBrush(Qt.BrushStyle.NoBrush if fill in {"none", "transparent", ""} else QColor(fill))
|
||||
kind = self.element.get("type")
|
||||
if kind == "rectangle":
|
||||
radius = float(self.element.get("cornerRadius", 0))
|
||||
painter.drawRoundedRect(rect, radius, radius)
|
||||
elif kind in {"circle", "ellipse"}:
|
||||
painter.drawEllipse(rect)
|
||||
elif kind == "line":
|
||||
painter.drawLine(rect.topLeft(), rect.bottomRight())
|
||||
elif kind == "triangle":
|
||||
painter.drawPolygon(QPolygonF([QPointF(rect.center().x(), 0), rect.bottomRight(), rect.bottomLeft()]))
|
||||
elif kind == "text":
|
||||
painter.setPen(QColor(self.element.get("color", "#202020")))
|
||||
font = painter.font()
|
||||
font.setPointSizeF(float(self.element.get("fontSize", 12)))
|
||||
painter.setFont(font)
|
||||
painter.drawText(rect, Qt.AlignmentFlag.AlignCenter, str(self.element.get("text", "Text")))
|
||||
if self.isSelected():
|
||||
painter.setBrush(Qt.BrushStyle.NoBrush)
|
||||
painter.setPen(QPen(QColor("#2563eb"), 1, Qt.PenStyle.DashLine))
|
||||
painter.drawRect(rect)
|
||||
|
||||
def itemChange(self, change, value): # noqa: N802
|
||||
if change == QGraphicsItem.GraphicsItemChange.ItemPositionChange:
|
||||
bounds = self.scene().sceneRect() if self.scene() else QRectF(0, 0, 128, 128)
|
||||
value = QPointF(
|
||||
max(
|
||||
bounds.left(),
|
||||
min(
|
||||
bounds.right() - float(self.element.get("width", 20)),
|
||||
_snap(value.x()),
|
||||
),
|
||||
),
|
||||
max(
|
||||
bounds.top(),
|
||||
min(
|
||||
bounds.bottom() - float(self.element.get("height", 20)),
|
||||
_snap(value.y()),
|
||||
),
|
||||
),
|
||||
)
|
||||
elif change == QGraphicsItem.GraphicsItemChange.ItemPositionHasChanged:
|
||||
self.element["x"], self.element["y"] = value.x(), value.y()
|
||||
elif change == QGraphicsItem.GraphicsItemChange.ItemSelectedHasChanged:
|
||||
self.resize_handle.setVisible(bool(value))
|
||||
return super().itemChange(change, value)
|
||||
|
||||
def mouseReleaseEvent(self, event: QGraphicsSceneMouseEvent) -> None: # noqa: N802
|
||||
super().mouseReleaseEvent(event)
|
||||
bounds = self.scene().sceneRect() if self.scene() else QRectF(0, 0, 128, 128)
|
||||
self.setPos(
|
||||
max(bounds.left(), min(bounds.right() - float(self.element.get("width", 20)), _snap(self.pos().x()))),
|
||||
max(bounds.top(), min(bounds.bottom() - float(self.element.get("height", 20)), _snap(self.pos().y()))),
|
||||
)
|
||||
|
||||
def contextMenuEvent(self, event: QGraphicsSceneContextMenuEvent) -> None: # noqa: N802
|
||||
menu = QMenu()
|
||||
options = menu.addAction("Shape Options…")
|
||||
delete = menu.addAction("Delete Shape")
|
||||
chosen = menu.exec(event.screenPos())
|
||||
if chosen is options:
|
||||
dialog = ShapeOptionsDialog(self.element)
|
||||
if dialog.exec() == dialog.DialogCode.Accepted:
|
||||
self.prepareGeometryChange()
|
||||
self.element.clear()
|
||||
self.element.update(dialog.element)
|
||||
self.resize_handle.setPos(
|
||||
float(self.element.get("width", 20)),
|
||||
float(self.element.get("height", 20)),
|
||||
)
|
||||
self.update()
|
||||
elif chosen is delete and self.scene() is not None:
|
||||
self.scene().removeItem(self)
|
||||
self.element["_deleted"] = True
|
||||
event.accept()
|
||||
|
||||
|
||||
class PortHandle(QGraphicsEllipseItem):
|
||||
def __init__(self, port: Port, direction: str, position: QPointF) -> None:
|
||||
super().__init__(-5, -5, 10, 10)
|
||||
self.port, self.direction = port, direction
|
||||
self.setPos(position)
|
||||
self.setBrush(QColor("#16a34a" if direction == "input" else "#dc2626"))
|
||||
self.setPen(QPen(QColor("#ffffff"), 1.5))
|
||||
self.setToolTip(f"{direction.title()}: {port.name} (drag to position)")
|
||||
self.setZValue(10)
|
||||
self.setFlags(QGraphicsItem.GraphicsItemFlag.ItemIsMovable | QGraphicsItem.GraphicsItemFlag.ItemIsSelectable | QGraphicsItem.GraphicsItemFlag.ItemSendsGeometryChanges)
|
||||
|
||||
def itemChange(self, change, value): # noqa: N802
|
||||
if change == QGraphicsItem.GraphicsItemChange.ItemPositionChange:
|
||||
bounds = self.scene().sceneRect() if self.scene() else QRectF(0, 0, 128, 128)
|
||||
value = QPointF(
|
||||
min(bounds.right(), max(bounds.left(), _snap(value.x()))),
|
||||
min(bounds.bottom(), max(bounds.top(), _snap(value.y()))),
|
||||
)
|
||||
elif change == QGraphicsItem.GraphicsItemChange.ItemPositionHasChanged:
|
||||
self.port.properties["iconPosition"] = {"x": value.x(), "y": value.y()}
|
||||
return super().itemChange(change, value)
|
||||
|
||||
def mouseReleaseEvent(self, event: QGraphicsSceneMouseEvent) -> None: # noqa: N802
|
||||
super().mouseReleaseEvent(event)
|
||||
bounds = self.scene().sceneRect() if self.scene() else QRectF(0, 0, 128, 128)
|
||||
self.setPos(
|
||||
min(bounds.right(), max(bounds.left(), _snap(self.pos().x()))),
|
||||
min(bounds.bottom(), max(bounds.top(), _snap(self.pos().y()))),
|
||||
)
|
||||
|
||||
|
||||
class IconEditorDialog(QDialog):
|
||||
def __init__(self, component: Component, parent=None) -> None:
|
||||
super().__init__(parent)
|
||||
self.setWindowTitle(f"Icon Editor — {component.name}")
|
||||
self.resize(850, 600)
|
||||
self.icon = Icon.from_dict(component.icon.to_dict())
|
||||
self.inputs = deepcopy(component.inputs)
|
||||
self.outputs = deepcopy(component.outputs)
|
||||
layout = QVBoxLayout(self)
|
||||
toolbar = QHBoxLayout()
|
||||
toolbar.addWidget(QLabel("Add:"))
|
||||
for kind in ("rectangle", "circle", "ellipse", "line", "triangle", "text"):
|
||||
button = QToolButton()
|
||||
button.setText(kind.title())
|
||||
button.clicked.connect(lambda _checked=False, value=kind: self.add_shape(value))
|
||||
toolbar.addWidget(button)
|
||||
toolbar.addStretch()
|
||||
delete = QPushButton("Delete selected")
|
||||
delete.clicked.connect(self.delete_selected)
|
||||
toolbar.addWidget(delete)
|
||||
layout.addLayout(toolbar)
|
||||
self.scene = QGraphicsScene(0, 0, self.icon.width, self.icon.height, self)
|
||||
self.view = IconEditorView(self.scene)
|
||||
self.view.setRenderHint(QPainter.RenderHint.Antialiasing)
|
||||
self.view.setDragMode(QGraphicsView.DragMode.RubberBandDrag)
|
||||
self.scene.addRect(self.scene.sceneRect(), QPen(QColor("#64748b"), 0)).setZValue(-100)
|
||||
layout.addWidget(self.view, 1)
|
||||
layout.addWidget(QLabel("Green points are inputs; red points are outputs. Drag them to place connection anchors."))
|
||||
for element in self.icon.elements:
|
||||
self.scene.addItem(ShapeItem(element))
|
||||
self._add_ports(self.inputs, "input", 0.0)
|
||||
self._add_ports(self.outputs, "output", self.icon.width)
|
||||
buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
|
||||
buttons.accepted.connect(self.accept)
|
||||
buttons.rejected.connect(self.reject)
|
||||
layout.addWidget(buttons)
|
||||
self.view.fitInView(self.scene.sceneRect().adjusted(-10, -10, 10, 10), Qt.AspectRatioMode.KeepAspectRatio)
|
||||
|
||||
def _add_ports(self, ports: list[Port], direction: str, default_x: float) -> None:
|
||||
spacing = self.icon.height / (len(ports) + 1)
|
||||
for index, port in enumerate(ports, 1):
|
||||
saved = port.properties.get("iconPosition", {})
|
||||
position = QPointF(float(saved.get("x", default_x)), float(saved.get("y", spacing * index)))
|
||||
self.scene.addItem(PortHandle(port, direction, position))
|
||||
|
||||
def add_shape(self, kind: str) -> None:
|
||||
count = len([item for item in self.scene.items() if isinstance(item, ShapeItem)])
|
||||
x, y = 15 + (count * 5) % 40, 15 + (count * 4) % 25
|
||||
element = {"type": kind, "x": x, "y": y, "width": 55, "height": 35, "fill": "#dbeafe", "stroke": "#303030", "lineWidth": 1.5, "lineStyle": "solid"}
|
||||
if kind == "circle":
|
||||
element["width"] = element["height"] = 35
|
||||
if kind == "line":
|
||||
element["fill"] = "none"
|
||||
if kind == "rectangle":
|
||||
element["cornerRadius"] = 0
|
||||
if kind == "text":
|
||||
text, accepted = QInputDialog.getText(self, "Add Text", "Text:", text="Text")
|
||||
if not accepted:
|
||||
return
|
||||
element.update({"text": text, "fontSize": 12, "color": "#202020", "fill": "none", "lineStyle": "none"})
|
||||
self.icon.elements.append(element)
|
||||
item = ShapeItem(element)
|
||||
self.scene.addItem(item)
|
||||
item.setSelected(True)
|
||||
|
||||
def delete_selected(self) -> None:
|
||||
for item in self.scene.selectedItems():
|
||||
if isinstance(item, ShapeItem):
|
||||
self.scene.removeItem(item)
|
||||
item.element["_deleted"] = True
|
||||
|
||||
def accept(self) -> None:
|
||||
self.icon.elements = [element for element in self.icon.elements if not element.pop("_deleted", False)]
|
||||
super().accept()
|
||||
98
BEdit/src/bedit/gui/graphics/icon_renderer.py
Normal file
98
BEdit/src/bedit/gui/graphics/icon_renderer.py
Normal file
@@ -0,0 +1,98 @@
|
||||
from PySide6.QtCore import QPointF, QRectF, Qt
|
||||
from PySide6.QtGui import QColor, QFont, QIcon, QPainter, QPen, QPixmap, QPolygonF
|
||||
|
||||
from bedit.core.model import Icon
|
||||
|
||||
|
||||
def icon_bounds(icon: Icon) -> QRectF:
|
||||
"""Return the automatic hitbox of all visible vector elements."""
|
||||
bounds = QRectF()
|
||||
for element in icon.elements:
|
||||
if element.get("_deleted"):
|
||||
continue
|
||||
rect = QRectF(
|
||||
float(element.get("x", 0)),
|
||||
float(element.get("y", 0)),
|
||||
max(0.0, float(element.get("width", 0))),
|
||||
max(0.0, float(element.get("height", 0))),
|
||||
)
|
||||
bounds = rect if bounds.isNull() else bounds.united(rect)
|
||||
return bounds if not bounds.isNull() else QRectF(32, 32, 64, 64)
|
||||
|
||||
|
||||
def _pen(element: dict) -> QPen:
|
||||
styles = {
|
||||
"solid": Qt.PenStyle.SolidLine,
|
||||
"dash": Qt.PenStyle.DashLine,
|
||||
"dot": Qt.PenStyle.DotLine,
|
||||
"dash-dot": Qt.PenStyle.DashDotLine,
|
||||
"none": Qt.PenStyle.NoPen,
|
||||
}
|
||||
return QPen(
|
||||
QColor(element.get("stroke", "#303030")),
|
||||
float(element.get("lineWidth", 1.5)),
|
||||
styles.get(element.get("lineStyle", "solid"), Qt.PenStyle.SolidLine),
|
||||
)
|
||||
|
||||
|
||||
def paint_icon(
|
||||
painter: QPainter,
|
||||
icon: Icon,
|
||||
target: QRectF,
|
||||
source: QRectF | None = None,
|
||||
) -> None:
|
||||
painter.save()
|
||||
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
|
||||
if source is None:
|
||||
source = QRectF(0, 0, icon.width, icon.height)
|
||||
painter.translate(target.topLeft())
|
||||
painter.scale(target.width() / source.width(), target.height() / source.height())
|
||||
painter.translate(-source.left(), -source.top())
|
||||
for element in icon.elements:
|
||||
kind = element.get("type", "rectangle")
|
||||
rect = QRectF(
|
||||
float(element.get("x", 0)), float(element.get("y", 0)),
|
||||
float(element.get("width", 20)), float(element.get("height", 20)),
|
||||
)
|
||||
painter.setPen(_pen(element))
|
||||
fill = element.get("fill", "#ffffff")
|
||||
painter.setBrush(Qt.BrushStyle.NoBrush if fill in {"none", "transparent", ""} else QColor(fill))
|
||||
if kind == "rectangle":
|
||||
radius = float(element.get("cornerRadius", 0))
|
||||
painter.drawRoundedRect(rect, radius, radius)
|
||||
elif kind in {"circle", "ellipse"}:
|
||||
painter.drawEllipse(rect)
|
||||
elif kind == "line":
|
||||
painter.drawLine(rect.topLeft(), rect.bottomRight())
|
||||
elif kind == "triangle":
|
||||
painter.drawPolygon(QPolygonF([QPointF(rect.center().x(), rect.top()), rect.bottomRight(), rect.bottomLeft()]))
|
||||
elif kind == "text":
|
||||
painter.setPen(QColor(element.get("color", element.get("stroke", "#202020"))))
|
||||
font = QFont()
|
||||
font.setPointSizeF(float(element.get("fontSize", 12)))
|
||||
painter.setFont(font)
|
||||
painter.drawText(rect, Qt.AlignmentFlag.AlignCenter, str(element.get("text", "Text")))
|
||||
painter.restore()
|
||||
|
||||
|
||||
def icon_pixmap(icon: Icon, size: int = 16) -> QPixmap:
|
||||
pixmap = QPixmap(size, size)
|
||||
pixmap.fill(Qt.GlobalColor.transparent)
|
||||
painter = QPainter(pixmap)
|
||||
bounds = icon_bounds(icon)
|
||||
ratio = min(size / bounds.width(), size / bounds.height())
|
||||
width, height = bounds.width() * ratio, bounds.height() * ratio
|
||||
paint_icon(
|
||||
painter,
|
||||
icon,
|
||||
QRectF((size - width) / 2, (size - height) / 2, width, height),
|
||||
bounds,
|
||||
)
|
||||
painter.end()
|
||||
return pixmap
|
||||
|
||||
|
||||
def library_icon(icon: Icon) -> QIcon:
|
||||
# Render large enough for the tall Libraries rows. icon_pixmap crops to the
|
||||
# vector hitbox first, so a 32x32 drawing is shown as large as a 128x128 one.
|
||||
return QIcon(icon_pixmap(icon, 28))
|
||||
616
BEdit/src/bedit/gui/graphics/workspace.py
Normal file
616
BEdit/src/bedit/gui/graphics/workspace.py
Normal file
@@ -0,0 +1,616 @@
|
||||
import json
|
||||
|
||||
from PySide6.QtCore import QMimeData, QPointF, QRectF, Qt, Signal
|
||||
from PySide6.QtGui import (
|
||||
QColor,
|
||||
QDragEnterEvent,
|
||||
QDropEvent,
|
||||
QMouseEvent,
|
||||
QWheelEvent,
|
||||
QPainter,
|
||||
QPainterPath,
|
||||
QPen,
|
||||
QTransform,
|
||||
)
|
||||
from PySide6.QtWidgets import (
|
||||
QGraphicsEllipseItem,
|
||||
QGraphicsItem,
|
||||
QGraphicsObject,
|
||||
QGraphicsPathItem,
|
||||
QGraphicsScene,
|
||||
QGraphicsSceneContextMenuEvent,
|
||||
QGraphicsSceneMouseEvent,
|
||||
QGraphicsView,
|
||||
QApplication,
|
||||
QMenu,
|
||||
QStyleOptionGraphicsItem,
|
||||
QToolTip,
|
||||
QWidget,
|
||||
)
|
||||
|
||||
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"
|
||||
|
||||
|
||||
def _graph_snap_size() -> int:
|
||||
return application_settings().value("grid/graphSnapSize", 8, type=int)
|
||||
|
||||
|
||||
def _graph_grid_size() -> int:
|
||||
return application_settings().value("grid/graphSize", 64, type=int)
|
||||
|
||||
|
||||
def _snapped(position: QPointF) -> QPointF:
|
||||
grid = _graph_snap_size()
|
||||
return QPointF(round(position.x() / grid) * grid, round(position.y() / grid) * grid)
|
||||
|
||||
|
||||
class ConnectionPortItem(QGraphicsEllipseItem):
|
||||
def __init__(self, endpoint: Endpoint, role: str, label: str, parent: QGraphicsItem) -> None:
|
||||
super().__init__(-6, -6, 12, 12, parent)
|
||||
self.endpoint = endpoint
|
||||
self.role = role
|
||||
self.setBrush(QColor("#ffffff"))
|
||||
self.setPen(QPen(QColor("#303030"), 1.5))
|
||||
self.setZValue(2)
|
||||
self.setToolTip(label)
|
||||
|
||||
|
||||
class ComponentGraphicsItem(QGraphicsObject):
|
||||
WIDTH = 128.0
|
||||
HEIGHT = 128.0
|
||||
|
||||
def __init__(self, component: Component, controller: DocumentController) -> None:
|
||||
super().__init__()
|
||||
self.component_id = component.id
|
||||
self.component = component
|
||||
self.controller = controller
|
||||
self.drag_start = QPointF()
|
||||
self.hitbox = icon_bounds(component.icon)
|
||||
self.setFlags(
|
||||
QGraphicsItem.GraphicsItemFlag.ItemIsMovable
|
||||
| QGraphicsItem.GraphicsItemFlag.ItemIsSelectable
|
||||
| QGraphicsItem.GraphicsItemFlag.ItemSendsGeometryChanges
|
||||
)
|
||||
self.input_ports = self._create_ports(component.inputs, "target", 0.0)
|
||||
self.output_ports = self._create_ports(component.outputs, "source", self.WIDTH)
|
||||
self.setTransformOriginPoint(self.hitbox.center())
|
||||
self.setRotation(component.rotation)
|
||||
|
||||
def _create_ports(self, ports, role: str, x: float) -> dict[str, ConnectionPortItem]:
|
||||
result = {}
|
||||
spacing = self.HEIGHT / (len(ports) + 1)
|
||||
for index, port in enumerate(ports, start=1):
|
||||
endpoint = Endpoint(block=self.component_id, port=port.id)
|
||||
item = ConnectionPortItem(endpoint, role, port.name, self)
|
||||
position = port.properties.get("iconPosition", {})
|
||||
item.setPos(
|
||||
float(position.get("x", x)) * self.WIDTH / self.component.icon.width,
|
||||
float(position.get("y", spacing * index)) * self.HEIGHT / self.component.icon.height,
|
||||
)
|
||||
result[port.id] = item
|
||||
return result
|
||||
|
||||
def boundingRect(self) -> QRectF: # noqa: N802
|
||||
return self.hitbox
|
||||
|
||||
def paint(
|
||||
self,
|
||||
painter: QPainter,
|
||||
option: QStyleOptionGraphicsItem,
|
||||
widget: QWidget | None = None,
|
||||
) -> None:
|
||||
del option, widget
|
||||
paint_icon(
|
||||
painter,
|
||||
self.component.icon,
|
||||
QRectF(0, 0, self.WIDTH, self.HEIGHT),
|
||||
)
|
||||
if self.isSelected():
|
||||
painter.setBrush(Qt.BrushStyle.NoBrush)
|
||||
painter.setPen(QPen(QColor("#2563eb"), 2, Qt.PenStyle.DashLine))
|
||||
painter.drawRect(self.boundingRect())
|
||||
|
||||
def mouseDoubleClickEvent(self, event: QGraphicsSceneMouseEvent) -> None: # noqa: N802
|
||||
self.controller.activate_component(self.component_id)
|
||||
event.accept()
|
||||
|
||||
def contextMenuEvent(self, event: QGraphicsSceneContextMenuEvent) -> None: # noqa: N802
|
||||
if not self.isSelected():
|
||||
scene = self.scene()
|
||||
if scene is not None:
|
||||
scene.clearSelection()
|
||||
self.setSelected(True)
|
||||
menu = QMenu()
|
||||
options_action = menu.addAction("Component Options…")
|
||||
ports_action = menu.addAction("Port Options…")
|
||||
selected = menu.exec(event.screenPos())
|
||||
if selected is options_action:
|
||||
scene = self.scene()
|
||||
if isinstance(scene, GraphScene):
|
||||
scene.componentOptionsRequested.emit(self.component_id)
|
||||
elif selected is ports_action:
|
||||
scene = self.scene()
|
||||
if isinstance(scene, GraphScene):
|
||||
scene.componentPortOptionsRequested.emit(self.component_id)
|
||||
event.accept()
|
||||
|
||||
def mousePressEvent(self, event: QGraphicsSceneMouseEvent) -> None: # noqa: N802
|
||||
self.drag_start = self.pos()
|
||||
super().mousePressEvent(event)
|
||||
|
||||
def mouseReleaseEvent(self, event: QGraphicsSceneMouseEvent) -> None: # noqa: N802
|
||||
super().mouseReleaseEvent(event)
|
||||
snapped = _snapped(self.pos())
|
||||
self.setPos(snapped)
|
||||
self.controller.move_component(self.component_id, self.drag_start, snapped)
|
||||
|
||||
def itemChange(self, change, value): # noqa: N802
|
||||
if change == QGraphicsItem.GraphicsItemChange.ItemPositionChange:
|
||||
value = _snapped(value)
|
||||
elif change == QGraphicsItem.GraphicsItemChange.ItemPositionHasChanged:
|
||||
scene = self.scene()
|
||||
if isinstance(scene, GraphScene):
|
||||
scene.update_connections_for_block(self.component_id)
|
||||
return super().itemChange(change, value)
|
||||
|
||||
|
||||
class InterfaceTerminalItem(QGraphicsObject):
|
||||
WIDTH = 110.0
|
||||
HEIGHT = 36.0
|
||||
|
||||
def __init__(self, port: Port, direction: str, controller: DocumentController) -> None:
|
||||
super().__init__()
|
||||
self.port = port
|
||||
self.direction = direction
|
||||
self.controller = controller
|
||||
self.drag_start = QPointF()
|
||||
role = "source" if direction == "input" else "target"
|
||||
self.connection_port = ConnectionPortItem(
|
||||
Endpoint(interface=port.id), role, port.name, self
|
||||
)
|
||||
connection_x = self.WIDTH if direction == "input" else 0.0
|
||||
self.connection_port.setPos(connection_x, self.HEIGHT / 2)
|
||||
self.setFlags(
|
||||
QGraphicsItem.GraphicsItemFlag.ItemIsMovable
|
||||
| QGraphicsItem.GraphicsItemFlag.ItemIsSelectable
|
||||
| QGraphicsItem.GraphicsItemFlag.ItemSendsGeometryChanges
|
||||
)
|
||||
self.setToolTip(f"Component {direction}: {port.name}")
|
||||
|
||||
def boundingRect(self) -> QRectF: # noqa: N802
|
||||
return QRectF(0, 0, self.WIDTH, self.HEIGHT)
|
||||
|
||||
def paint(
|
||||
self,
|
||||
painter: QPainter,
|
||||
option: QStyleOptionGraphicsItem,
|
||||
widget: QWidget | None = None,
|
||||
) -> None:
|
||||
del option, widget
|
||||
painter.setBrush(QColor("#e5e7eb"))
|
||||
painter.setPen(QPen(QColor("#4b5563"), 1.5))
|
||||
painter.drawRoundedRect(self.boundingRect(), 4, 4)
|
||||
painter.setPen(QColor("#202020"))
|
||||
marker = "IN" if self.direction == "input" else "OUT"
|
||||
painter.drawText(
|
||||
self.boundingRect().adjusted(8, 0, -8, 0),
|
||||
Qt.AlignmentFlag.AlignVCenter | Qt.AlignmentFlag.AlignLeft,
|
||||
f"{marker} {self.port.name}",
|
||||
)
|
||||
|
||||
def mousePressEvent(self, event: QGraphicsSceneMouseEvent) -> None: # noqa: N802
|
||||
self.drag_start = self.pos()
|
||||
super().mousePressEvent(event)
|
||||
|
||||
def contextMenuEvent(self, event: QGraphicsSceneContextMenuEvent) -> None: # noqa: N802
|
||||
menu = QMenu()
|
||||
options_action = menu.addAction(f"{self.direction.title()} Options…")
|
||||
if menu.exec(event.screenPos()) is options_action:
|
||||
scene = self.scene()
|
||||
if isinstance(scene, GraphScene):
|
||||
scene.portOptionsRequested.emit(self.port.id, self.direction)
|
||||
event.accept()
|
||||
|
||||
def mouseReleaseEvent(self, event: QGraphicsSceneMouseEvent) -> None: # noqa: N802
|
||||
super().mouseReleaseEvent(event)
|
||||
snapped = _snapped(self.pos())
|
||||
self.setPos(snapped)
|
||||
self.controller.move_interface_port(self.port.id, self.drag_start, snapped)
|
||||
|
||||
def itemChange(self, change, value): # noqa: N802
|
||||
if change == QGraphicsItem.GraphicsItemChange.ItemPositionChange:
|
||||
value = _snapped(value)
|
||||
elif change == QGraphicsItem.GraphicsItemChange.ItemPositionHasChanged:
|
||||
scene = self.scene()
|
||||
if isinstance(scene, GraphScene):
|
||||
scene.update_connections_for_interface(self.port.id)
|
||||
return super().itemChange(change, value)
|
||||
|
||||
|
||||
class ConnectionGraphicsItem(QGraphicsPathItem):
|
||||
def __init__(self, connection_id: str, name: str = "") -> None:
|
||||
super().__init__()
|
||||
self.connection_id = connection_id
|
||||
self.name = name
|
||||
self.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsSelectable)
|
||||
self._update_pen()
|
||||
self.setZValue(-1)
|
||||
self.setToolTip(name or "Connection")
|
||||
|
||||
def contextMenuEvent(self, event: QGraphicsSceneContextMenuEvent) -> None: # noqa: N802
|
||||
menu = QMenu()
|
||||
options_action = menu.addAction("Connection Options…")
|
||||
if menu.exec(event.screenPos()) is options_action:
|
||||
scene = self.scene()
|
||||
if isinstance(scene, GraphScene):
|
||||
scene.connectionOptionsRequested.emit(self.connection_id)
|
||||
event.accept()
|
||||
|
||||
def itemChange(self, change, value): # noqa: N802
|
||||
result = super().itemChange(change, value)
|
||||
if change == QGraphicsItem.GraphicsItemChange.ItemSelectedHasChanged:
|
||||
self._update_pen()
|
||||
return result
|
||||
|
||||
def _update_pen(self) -> None:
|
||||
self.setPen(
|
||||
QPen(
|
||||
QColor("#f59e0b") if self.isSelected() else QColor("#285f9e"),
|
||||
4.0 if self.isSelected() else 2.5,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class GraphScene(QGraphicsScene):
|
||||
componentOptionsRequested = Signal(str)
|
||||
componentPortOptionsRequested = Signal(str)
|
||||
portOptionsRequested = Signal(str, str)
|
||||
connectionOptionsRequested = Signal(str)
|
||||
|
||||
def __init__(self, controller: DocumentController, parent=None) -> None:
|
||||
super().__init__(parent)
|
||||
self.controller = controller
|
||||
self.component_items: dict[str, ComponentGraphicsItem] = {}
|
||||
self.input_items: dict[str, InterfaceTerminalItem] = {}
|
||||
self.output_items: dict[str, InterfaceTerminalItem] = {}
|
||||
self.connection_items: dict[str, ConnectionGraphicsItem] = {}
|
||||
self.pending_source: ConnectionPortItem | None = None
|
||||
self.setSceneRect(-2000, -2000, 4000, 4000)
|
||||
|
||||
controller.documentReset.connect(self.rebuild)
|
||||
controller.activeGraphChanged.connect(self.rebuild)
|
||||
controller.componentMoved.connect(self.set_component_position)
|
||||
controller.componentRotated.connect(self.set_component_rotation)
|
||||
self.rebuild()
|
||||
|
||||
def rebuild(self) -> None:
|
||||
self.clear()
|
||||
self.component_items.clear()
|
||||
self.input_items.clear()
|
||||
self.output_items.clear()
|
||||
self.connection_items.clear()
|
||||
self.pending_source = None
|
||||
owner = self.controller.active_component
|
||||
if owner is None or owner.implementation_kind != "graph":
|
||||
return
|
||||
for port in owner.inputs:
|
||||
item = InterfaceTerminalItem(port, "input", self.controller)
|
||||
self.addItem(item)
|
||||
item.setPos(port.x, port.y)
|
||||
self.input_items[port.id] = item
|
||||
for port in owner.outputs:
|
||||
item = InterfaceTerminalItem(port, "output", self.controller)
|
||||
self.addItem(item)
|
||||
item.setPos(port.x, port.y)
|
||||
self.output_items[port.id] = item
|
||||
for component in owner.graph.blocks.values():
|
||||
item = ComponentGraphicsItem(component, self.controller)
|
||||
self.addItem(item)
|
||||
item.setPos(component.x, component.y)
|
||||
self.component_items[component.id] = item
|
||||
for connection in owner.graph.connections.values():
|
||||
item = ConnectionGraphicsItem(connection.id, connection.name)
|
||||
self.addItem(item)
|
||||
self.connection_items[connection.id] = item
|
||||
self.update_connection(connection.id)
|
||||
|
||||
def set_component_position(self, component_id: str, position: QPointF) -> None:
|
||||
item = self.component_items.get(component_id)
|
||||
if item is not None and item.pos() != position:
|
||||
item.setPos(position)
|
||||
|
||||
def set_component_rotation(self, component_id: str, rotation: float) -> None:
|
||||
item = self.component_items.get(component_id)
|
||||
if item is not None:
|
||||
item.setRotation(rotation)
|
||||
self.update_connections_for_block(component_id)
|
||||
|
||||
def update_connections_for_block(self, component_id: str) -> None:
|
||||
for connection in self.controller.active_graph.connections.values():
|
||||
if component_id in (connection.source.block, connection.target.block):
|
||||
self.update_connection(connection.id)
|
||||
|
||||
def update_connections_for_interface(self, port_id: str) -> None:
|
||||
for connection in self.controller.active_graph.connections.values():
|
||||
if port_id in (connection.source.interface, connection.target.interface):
|
||||
self.update_connection(connection.id)
|
||||
|
||||
def drawBackground(self, painter: QPainter, rect: QRectF) -> None: # noqa: N802
|
||||
# The view paints the grid so it always covers the complete viewport.
|
||||
del painter, rect
|
||||
|
||||
def _endpoint_item(self, endpoint: Endpoint, role: str) -> ConnectionPortItem | None:
|
||||
if endpoint.interface is not None:
|
||||
terminals = self.input_items if role == "source" else self.output_items
|
||||
terminal = terminals.get(endpoint.interface)
|
||||
return terminal.connection_port if terminal else None
|
||||
component = self.component_items.get(endpoint.block or "")
|
||||
if component is None:
|
||||
return None
|
||||
ports = component.output_ports if role == "source" else component.input_ports
|
||||
return ports.get(endpoint.port or "")
|
||||
|
||||
def update_connection(self, connection_id: str) -> None:
|
||||
connection = self.controller.active_graph.connections.get(connection_id)
|
||||
graphics = self.connection_items.get(connection_id)
|
||||
if connection is None or graphics is None:
|
||||
return
|
||||
source = self._endpoint_item(connection.source, "source")
|
||||
target = self._endpoint_item(connection.target, "target")
|
||||
if source is None or target is None:
|
||||
return
|
||||
start, end = source.scenePos(), target.scenePos()
|
||||
distance = max(50.0, abs(end.x() - start.x()) * 0.5)
|
||||
path = QPainterPath(start)
|
||||
path.cubicTo(start + QPointF(distance, 0), end - QPointF(distance, 0), end)
|
||||
graphics.setPath(path)
|
||||
|
||||
def mousePressEvent(self, event: QGraphicsSceneMouseEvent) -> None: # noqa: N802
|
||||
item = self.itemAt(event.scenePos(), QTransform())
|
||||
if isinstance(item, ConnectionPortItem):
|
||||
if item.role == "source":
|
||||
self._clear_pending_source()
|
||||
self.pending_source = item
|
||||
item.setBrush(QColor("#f5b642"))
|
||||
elif self.pending_source is not None:
|
||||
if self.pending_source.endpoint != item.endpoint:
|
||||
try:
|
||||
self.controller.connect(self.pending_source.endpoint, item.endpoint)
|
||||
except ValueError as error:
|
||||
QToolTip.showText(event.screenPos(), str(error))
|
||||
self._clear_pending_source()
|
||||
event.accept()
|
||||
return
|
||||
self._clear_pending_source()
|
||||
super().mousePressEvent(event)
|
||||
|
||||
def _clear_pending_source(self) -> None:
|
||||
if self.pending_source is not None:
|
||||
self.pending_source.setBrush(QColor("#ffffff"))
|
||||
self.pending_source = None
|
||||
|
||||
|
||||
class GraphWorkspaceView(QGraphicsView):
|
||||
toolUsed = Signal()
|
||||
componentOptionsRequested = Signal(str)
|
||||
componentPortOptionsRequested = Signal(str)
|
||||
portOptionsRequested = Signal(str, str)
|
||||
connectionOptionsRequested = Signal(str)
|
||||
selectionAvailabilityChanged = Signal(bool)
|
||||
|
||||
def __init__(self, parent=None) -> None:
|
||||
super().__init__(parent)
|
||||
self.controller: DocumentController | None = None
|
||||
self.tool_mode = "pointer"
|
||||
self.paste_count = 0
|
||||
self.setAcceptDrops(True)
|
||||
self.setRenderHint(QPainter.RenderHint.Antialiasing)
|
||||
self.setDragMode(QGraphicsView.DragMode.RubberBandDrag)
|
||||
self.setBackgroundBrush(QColor("#f7f7f7"))
|
||||
self.setTransformationAnchor(QGraphicsView.ViewportAnchor.AnchorUnderMouse)
|
||||
self.setResizeAnchor(QGraphicsView.ViewportAnchor.AnchorViewCenter)
|
||||
|
||||
def drawBackground(self, painter: QPainter, rect: QRectF) -> None: # noqa: N802
|
||||
"""Paint the visible graph viewport in scene coordinates."""
|
||||
painter.fillRect(rect, QColor("#f7f7f7"))
|
||||
if (
|
||||
self.controller is None
|
||||
or self.controller.document is None
|
||||
or self.controller.active_component is None
|
||||
):
|
||||
return
|
||||
spacing = _graph_grid_size()
|
||||
left = int(rect.left()) - (int(rect.left()) % spacing)
|
||||
top = int(rect.top()) - (int(rect.top()) % spacing)
|
||||
painter.setPen(QPen(QColor("#c5cbd1"), 0))
|
||||
for x in range(left, int(rect.right()) + spacing, spacing):
|
||||
painter.drawLine(QPointF(x, rect.top()), QPointF(x, rect.bottom()))
|
||||
for y in range(top, int(rect.bottom()) + spacing, spacing):
|
||||
painter.drawLine(QPointF(rect.left(), y), QPointF(rect.right(), y))
|
||||
|
||||
def _zoom(self, factor: float) -> None:
|
||||
current = self.transform().m11()
|
||||
target = current * factor
|
||||
if 0.1 <= target <= 8.0:
|
||||
self.scale(factor, factor)
|
||||
|
||||
def zoom_in(self) -> None:
|
||||
self._zoom(1.2)
|
||||
|
||||
def zoom_out(self) -> None:
|
||||
self._zoom(1 / 1.2)
|
||||
|
||||
def center_workspace(self) -> None:
|
||||
scene = self.scene()
|
||||
if scene is None:
|
||||
return
|
||||
bounds = scene.itemsBoundingRect()
|
||||
if bounds.isEmpty():
|
||||
self.resetTransform()
|
||||
self.centerOn(0, 0)
|
||||
else:
|
||||
self.fitInView(bounds.adjusted(-80, -80, 80, 80), Qt.AspectRatioMode.KeepAspectRatio)
|
||||
|
||||
def wheelEvent(self, event: QWheelEvent) -> None: # noqa: N802
|
||||
self._zoom(1.2 if event.angleDelta().y() > 0 else 1 / 1.2)
|
||||
event.accept()
|
||||
|
||||
def set_model(self, controller: DocumentController) -> None:
|
||||
self.controller = controller
|
||||
scene = GraphScene(controller, self)
|
||||
scene.componentOptionsRequested.connect(self.componentOptionsRequested)
|
||||
scene.componentPortOptionsRequested.connect(self.componentPortOptionsRequested)
|
||||
scene.portOptionsRequested.connect(self.portOptionsRequested)
|
||||
scene.connectionOptionsRequested.connect(self.connectionOptionsRequested)
|
||||
scene.selectionChanged.connect(
|
||||
lambda: self.selectionAvailabilityChanged.emit(bool(scene.selectedItems()))
|
||||
)
|
||||
self.setScene(scene)
|
||||
|
||||
def select_all(self) -> None:
|
||||
scene = self.scene()
|
||||
if scene is None:
|
||||
return
|
||||
for item in scene.items():
|
||||
if item.flags() & QGraphicsItem.GraphicsItemFlag.ItemIsSelectable:
|
||||
item.setSelected(True)
|
||||
|
||||
def delete_selected(self) -> None:
|
||||
if self.controller is None or not isinstance(self.scene(), GraphScene):
|
||||
return
|
||||
blocks: set[str] = set()
|
||||
connections: set[str] = set()
|
||||
inputs: set[str] = set()
|
||||
outputs: set[str] = set()
|
||||
for item in self.scene().selectedItems():
|
||||
if isinstance(item, ComponentGraphicsItem):
|
||||
blocks.add(item.component_id)
|
||||
elif isinstance(item, ConnectionGraphicsItem):
|
||||
connections.add(item.connection_id)
|
||||
elif isinstance(item, InterfaceTerminalItem):
|
||||
(inputs if item.direction == "input" else outputs).add(item.port.id)
|
||||
self.controller.delete_selection(blocks, connections, inputs, outputs)
|
||||
|
||||
def has_selected_components(self) -> bool:
|
||||
scene = self.scene()
|
||||
return bool(
|
||||
scene
|
||||
and any(isinstance(item, ComponentGraphicsItem) for item in scene.selectedItems())
|
||||
)
|
||||
|
||||
def rotate_selected(self) -> None:
|
||||
if self.controller is None or self.scene() is None:
|
||||
return
|
||||
component_ids = {
|
||||
item.component_id
|
||||
for item in self.scene().selectedItems()
|
||||
if isinstance(item, ComponentGraphicsItem)
|
||||
}
|
||||
self.controller.rotate_components(component_ids)
|
||||
|
||||
def copy_selection(self) -> bool:
|
||||
if self.controller is None or not isinstance(self.scene(), GraphScene):
|
||||
return False
|
||||
selected_ids = {
|
||||
item.component_id
|
||||
for item in self.scene().selectedItems()
|
||||
if isinstance(item, ComponentGraphicsItem)
|
||||
}
|
||||
if not selected_ids:
|
||||
return False
|
||||
graph = self.controller.active_graph
|
||||
components = [graph.blocks[component_id].to_dict() for component_id in selected_ids]
|
||||
connections = [
|
||||
connection.to_dict()
|
||||
for connection in graph.connections.values()
|
||||
if connection.source.block in selected_ids and connection.target.block in selected_ids
|
||||
]
|
||||
mime_data = QMimeData()
|
||||
mime_data.setData(
|
||||
SELECTION_MIME_TYPE,
|
||||
json.dumps({"components": components, "connections": connections}).encode("utf-8"),
|
||||
)
|
||||
QApplication.clipboard().setMimeData(mime_data)
|
||||
self.paste_count = 0
|
||||
return True
|
||||
|
||||
def cut_selection(self) -> None:
|
||||
if self.copy_selection():
|
||||
self.delete_selected()
|
||||
|
||||
def paste_selection(self) -> None:
|
||||
if self.controller is None:
|
||||
return
|
||||
mime_data = QApplication.clipboard().mimeData()
|
||||
if not mime_data.hasFormat(SELECTION_MIME_TYPE):
|
||||
return
|
||||
try:
|
||||
payload = json.loads(bytes(mime_data.data(SELECTION_MIME_TYPE)).decode("utf-8"))
|
||||
components = [Component.from_dict(item) for item in payload.get("components", [])]
|
||||
connections = [Connection.from_dict(item) for item in payload.get("connections", [])]
|
||||
except (KeyError, TypeError, ValueError, json.JSONDecodeError):
|
||||
return
|
||||
self.paste_count += 1
|
||||
new_ids = self.controller.paste_selection(
|
||||
components,
|
||||
connections,
|
||||
QPointF(32 * self.paste_count, 32 * self.paste_count),
|
||||
)
|
||||
scene = self.scene()
|
||||
if isinstance(scene, GraphScene):
|
||||
scene.clearSelection()
|
||||
for component_id in new_ids:
|
||||
item = scene.component_items.get(component_id)
|
||||
if item is not None:
|
||||
item.setSelected(True)
|
||||
|
||||
def set_tool_mode(self, mode: str) -> None:
|
||||
self.tool_mode = mode
|
||||
self.setDragMode(
|
||||
QGraphicsView.DragMode.RubberBandDrag
|
||||
if mode == "pointer"
|
||||
else QGraphicsView.DragMode.NoDrag
|
||||
)
|
||||
|
||||
def mousePressEvent(self, event: QMouseEvent) -> None: # noqa: N802
|
||||
super().mousePressEvent(event)
|
||||
|
||||
def dragEnterEvent(self, event: QDragEnterEvent) -> None: # noqa: N802
|
||||
if (
|
||||
event.mimeData().hasFormat(COMPONENT_MIME_TYPE)
|
||||
and self.controller is not None
|
||||
and self.controller.active_component is not None
|
||||
and self.controller.active_component.implementation_kind == "graph"
|
||||
):
|
||||
event.acceptProposedAction()
|
||||
return
|
||||
super().dragEnterEvent(event)
|
||||
|
||||
def dragMoveEvent(self, event) -> None: # noqa: N802
|
||||
if (
|
||||
event.mimeData().hasFormat(COMPONENT_MIME_TYPE)
|
||||
and self.controller is not None
|
||||
and self.controller.active_component is not None
|
||||
and self.controller.active_component.implementation_kind == "graph"
|
||||
):
|
||||
event.acceptProposedAction()
|
||||
return
|
||||
super().dragMoveEvent(event)
|
||||
|
||||
def dropEvent(self, event: QDropEvent) -> None: # noqa: N802
|
||||
if self.controller is None or not event.mimeData().hasFormat(COMPONENT_MIME_TYPE):
|
||||
super().dropEvent(event)
|
||||
return
|
||||
data = json.loads(bytes(event.mimeData().data(COMPONENT_MIME_TYPE)).decode("utf-8"))
|
||||
source = Component.from_dict(data)
|
||||
self.controller.add_component_copy(
|
||||
source, _snapped(self.mapToScene(event.position().toPoint()))
|
||||
)
|
||||
event.acceptProposedAction()
|
||||
561
BEdit/src/bedit/gui/main_window.py
Normal file
561
BEdit/src/bedit/gui/main_window.py
Normal file
@@ -0,0 +1,561 @@
|
||||
import json
|
||||
from copy import deepcopy
|
||||
from pathlib import Path
|
||||
|
||||
from PySide6.QtCore import QSize, Qt, Slot
|
||||
from PySide6.QtGui import QAction, QCloseEvent
|
||||
from PySide6.QtWidgets import QFileDialog, QMainWindow, QMenu, QMessageBox, QToolBar
|
||||
|
||||
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.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):
|
||||
"""Application shell and owner of the single active document."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.ui = Ui_MainWindow()
|
||||
self.ui.setupUi(self)
|
||||
self.ui.emptyPage.setStyleSheet("background-color: #9a9a9a;")
|
||||
self.ui.emptyWorkspaceLabel.setStyleSheet(
|
||||
"background: transparent; color: #202020;"
|
||||
)
|
||||
self._create_camera_toolbar()
|
||||
self.settings = application_settings()
|
||||
|
||||
self.libraries = LibraryRepository(self)
|
||||
self.document_controller = DocumentController(self)
|
||||
self.library_tree_model = LibraryTreeModel(
|
||||
self.libraries,
|
||||
self.document_controller,
|
||||
self,
|
||||
)
|
||||
self.document_tree_model = DocumentTreeModel(self.document_controller, self)
|
||||
self._configure_models()
|
||||
self._connect_actions()
|
||||
self._populate_view_menu()
|
||||
self._restore_window_geometry()
|
||||
|
||||
self.ui.leftDockHost.setWindowFlags(Qt.WindowType.Widget)
|
||||
self.ui.leftDockHost.show()
|
||||
self.ui.panel_libraries.show()
|
||||
self.ui.panel_document.show()
|
||||
self.ui.leftDockHost.splitDockWidget(
|
||||
self.ui.panel_document,
|
||||
self.ui.panel_libraries,
|
||||
Qt.Orientation.Vertical,
|
||||
)
|
||||
self.ui.workspaceSplitter.setSizes([280, 720])
|
||||
self.reload_libraries()
|
||||
self._active_graph_changed()
|
||||
self._update_title()
|
||||
|
||||
def _configure_models(self) -> None:
|
||||
self.ui.treeView.setModel(self.library_tree_model)
|
||||
self.ui.treeView.setIconSize(QSize(28, 28))
|
||||
self.ui.treeView.setStyleSheet("QTreeView::item { height: 32px; }")
|
||||
self.ui.treeView.setHeaderHidden(True)
|
||||
self.ui.treeView.setDragEnabled(True)
|
||||
self.ui.treeView.setDragDropMode(self.ui.treeView.DragDropMode.DragOnly)
|
||||
self.ui.treeView.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
|
||||
self.ui.treeView.customContextMenuRequested.connect(
|
||||
self.show_external_library_context_menu
|
||||
)
|
||||
self.library_tree_model.rebuilt.connect(self.ui.treeView.expandAll)
|
||||
self.ui.documentTreeView.setModel(self.document_tree_model)
|
||||
self.ui.documentTreeView.setIconSize(QSize(16, 16))
|
||||
self.ui.documentTreeView.setHeaderHidden(True)
|
||||
self.ui.documentTreeView.setDragEnabled(True)
|
||||
self.ui.documentTreeView.setDragDropMode(
|
||||
self.ui.documentTreeView.DragDropMode.DragOnly
|
||||
)
|
||||
self.ui.documentTreeView.setContextMenuPolicy(
|
||||
Qt.ContextMenuPolicy.CustomContextMenu
|
||||
)
|
||||
self.ui.documentTreeView.customContextMenuRequested.connect(
|
||||
self.show_library_context_menu
|
||||
)
|
||||
self.ui.documentTreeView.doubleClicked.connect(self.activate_tree_component)
|
||||
self.document_tree_model.rebuilt.connect(self.ui.documentTreeView.expandAll)
|
||||
self.ui.graphView.set_model(self.document_controller)
|
||||
self.ui.graphView.componentOptionsRequested.connect(self.show_component_options)
|
||||
self.ui.graphView.componentPortOptionsRequested.connect(self.show_component_port_options)
|
||||
self.ui.graphView.portOptionsRequested.connect(self.show_port_options)
|
||||
self.ui.graphView.connectionOptionsRequested.connect(self.show_connection_options)
|
||||
self.ui.graphView.selectionAvailabilityChanged.connect(
|
||||
lambda _available: self._update_edit_actions()
|
||||
)
|
||||
self.ui.navigateUpButton.clicked.connect(self.navigate_up)
|
||||
self.ui.pointerToolButton.clicked.connect(lambda: self.set_graph_tool("pointer"))
|
||||
self.ui.inputToolButton.hide()
|
||||
self.ui.outputToolButton.hide()
|
||||
self.ui.applyJsonButton.clicked.connect(self.apply_json)
|
||||
self.document_controller.activeGraphChanged.connect(self._active_graph_changed)
|
||||
|
||||
def _create_camera_toolbar(self) -> None:
|
||||
self.cameraToolbar = QToolBar("Camera", self)
|
||||
self.cameraToolbar.setObjectName("cameraToolbar")
|
||||
self.actionZoomIn = QAction("Zoom In", self)
|
||||
self.actionZoomIn.setShortcut("Ctrl++")
|
||||
self.actionZoomOut = QAction("Zoom Out", self)
|
||||
self.actionZoomOut.setShortcut("Ctrl+-")
|
||||
self.actionCenterView = QAction("Center", self)
|
||||
self.actionCenterView.setShortcut("Ctrl+0")
|
||||
self.cameraToolbar.addActions(
|
||||
(self.actionZoomIn, self.actionZoomOut, self.actionCenterView)
|
||||
)
|
||||
self.addToolBar(Qt.ToolBarArea.TopToolBarArea, self.cameraToolbar)
|
||||
|
||||
def _connect_actions(self) -> None:
|
||||
self.ui.actionNew.triggered.connect(self.new_document)
|
||||
self.ui.actionOpen.triggered.connect(self.open_document)
|
||||
self.ui.actionSave.triggered.connect(self.save_document)
|
||||
self.ui.actionSaveAs.triggered.connect(self.save_document_as)
|
||||
self.ui.actionClose.triggered.connect(self.close_document)
|
||||
self.ui.actionExit.triggered.connect(self.close)
|
||||
self.ui.actionSettings.triggered.connect(self.show_settings)
|
||||
self.ui.actionAbout.triggered.connect(self.show_about)
|
||||
self.ui.actionAboutQt.triggered.connect(
|
||||
lambda: QMessageBox.aboutQt(self, "About Qt Framework")
|
||||
)
|
||||
self.ui.actionUndo.triggered.connect(self.document_controller.undo_stack.undo)
|
||||
self.ui.actionRedo.triggered.connect(self.document_controller.undo_stack.redo)
|
||||
self.ui.actionDelete.triggered.connect(self.ui.graphView.delete_selected)
|
||||
self.ui.actionCopy.triggered.connect(self.ui.graphView.copy_selection)
|
||||
self.ui.actionCut.triggered.connect(self.ui.graphView.cut_selection)
|
||||
self.ui.actionPaste.triggered.connect(self.ui.graphView.paste_selection)
|
||||
self.ui.actionSelectAll.triggered.connect(self.ui.graphView.select_all)
|
||||
self.ui.actionRotateClockwise.triggered.connect(self.ui.graphView.rotate_selected)
|
||||
self.actionZoomIn.triggered.connect(self.ui.graphView.zoom_in)
|
||||
self.actionZoomOut.triggered.connect(self.ui.graphView.zoom_out)
|
||||
self.actionCenterView.triggered.connect(self.ui.graphView.center_workspace)
|
||||
self.document_controller.undo_stack.canUndoChanged.connect(self.ui.actionUndo.setEnabled)
|
||||
self.document_controller.undo_stack.canRedoChanged.connect(self.ui.actionRedo.setEnabled)
|
||||
self.document_controller.modifiedChanged.connect(lambda _modified: self._update_title())
|
||||
self.document_controller.filePathChanged.connect(lambda _path: self._update_title())
|
||||
self.document_controller.documentOpenedChanged.connect(self._document_opened_changed)
|
||||
self.ui.actionUndo.setEnabled(False)
|
||||
self.ui.actionRedo.setEnabled(False)
|
||||
self._update_edit_actions()
|
||||
self._document_opened_changed(False)
|
||||
|
||||
def _populate_view_menu(self) -> None:
|
||||
for panel in (self.ui.panel_document, self.ui.panel_libraries):
|
||||
self.ui.menuPanels.addAction(panel.toggleViewAction())
|
||||
for toolbar in (
|
||||
self.ui.fileToolbar,
|
||||
self.ui.editToolbar,
|
||||
self.ui.transformToolbar,
|
||||
self.cameraToolbar,
|
||||
):
|
||||
self.ui.menuToolbars.addAction(toolbar.toggleViewAction())
|
||||
|
||||
def reload_libraries(self) -> None:
|
||||
self.libraries.load_paths(SettingsDialog.library_paths(self.settings))
|
||||
self.ui.treeView.expandAll()
|
||||
if self.libraries.load_warnings:
|
||||
QMessageBox.warning(
|
||||
self,
|
||||
"Some libraries could not be loaded",
|
||||
"\n".join(self.libraries.load_warnings),
|
||||
)
|
||||
|
||||
def _restore_window_geometry(self) -> None:
|
||||
geometry = self.settings.value("window/geometry")
|
||||
if geometry is not None:
|
||||
self.restoreGeometry(geometry)
|
||||
|
||||
def _update_title(self) -> None:
|
||||
if self.document_controller.document is None:
|
||||
self.setWindowTitle("BEdit")
|
||||
return
|
||||
name = self.document_controller.file_path.name if self.document_controller.file_path else "Untitled"
|
||||
modified = "*" if not self.document_controller.undo_stack.isClean() else ""
|
||||
self.setWindowTitle(f"{modified}{name} — BEdit")
|
||||
|
||||
def _active_graph_changed(self) -> None:
|
||||
component = self.document_controller.active_component
|
||||
if component is None:
|
||||
self.ui.graphBreadcrumbLabel.setText("No component selected")
|
||||
self.ui.navigateUpButton.setEnabled(False)
|
||||
self.ui.workspaceModeLabel.setText("")
|
||||
self.ui.workspaceStack.setCurrentWidget(self.ui.emptyPage)
|
||||
self.ui.applyJsonButton.setVisible(False)
|
||||
for button in (
|
||||
self.ui.pointerToolButton,
|
||||
self.ui.inputToolButton,
|
||||
self.ui.outputToolButton,
|
||||
):
|
||||
button.setVisible(False)
|
||||
self._update_edit_actions()
|
||||
return
|
||||
self.ui.graphBreadcrumbLabel.setText(" › ".join(self.document_controller.breadcrumb()))
|
||||
self.ui.navigateUpButton.setEnabled(
|
||||
self.document_controller.document.find_parent(component.id) is not None
|
||||
)
|
||||
is_graph = component.implementation_kind == "graph"
|
||||
self.ui.workspaceModeLabel.setText("Graph" if is_graph else "Text")
|
||||
self.ui.workspaceStack.setCurrentWidget(self.ui.graphPage if is_graph else self.ui.jsonPage)
|
||||
self.ui.applyJsonButton.setVisible(not is_graph)
|
||||
self.ui.pointerToolButton.setVisible(is_graph)
|
||||
self.ui.inputToolButton.hide()
|
||||
self.ui.outputToolButton.hide()
|
||||
if is_graph:
|
||||
self.set_graph_tool("pointer")
|
||||
else:
|
||||
self._load_source_json()
|
||||
self._update_edit_actions()
|
||||
|
||||
def _update_edit_actions(self) -> None:
|
||||
component = self.document_controller.active_component
|
||||
is_graph = component is not None and component.implementation_kind == "graph"
|
||||
has_selection = bool(self.ui.graphView.scene() and self.ui.graphView.scene().selectedItems())
|
||||
for action in (self.ui.actionCut, self.ui.actionCopy, self.ui.actionDelete):
|
||||
action.setEnabled(is_graph and has_selection)
|
||||
self.ui.actionRotateClockwise.setEnabled(
|
||||
is_graph and self.ui.graphView.has_selected_components()
|
||||
)
|
||||
self.ui.actionSelectAll.setEnabled(is_graph)
|
||||
self.ui.actionPaste.setEnabled(is_graph)
|
||||
|
||||
@Slot()
|
||||
def navigate_up(self) -> None:
|
||||
if self._resolve_source_edits():
|
||||
self.document_controller.navigate_up()
|
||||
|
||||
def set_graph_tool(self, mode: str) -> None:
|
||||
self.ui.graphView.set_tool_mode("pointer")
|
||||
self.ui.pointerToolButton.setChecked(True)
|
||||
|
||||
def _load_source_json(self) -> None:
|
||||
component = self.document_controller.active_component
|
||||
if component is None:
|
||||
return
|
||||
text = json.dumps(
|
||||
{
|
||||
"inputs": [port.to_dict() for port in component.inputs],
|
||||
"outputs": [port.to_dict() for port in component.outputs],
|
||||
"source": component.source,
|
||||
},
|
||||
indent=2,
|
||||
)
|
||||
self.ui.jsonEditor.setPlainText(text)
|
||||
self.ui.jsonEditor.document().setModified(False)
|
||||
|
||||
def _resolve_source_edits(self) -> bool:
|
||||
component = self.document_controller.active_component
|
||||
if (
|
||||
component is None
|
||||
or component.implementation_kind != "text"
|
||||
or not self.ui.jsonEditor.document().isModified()
|
||||
):
|
||||
return True
|
||||
answer = QMessageBox.question(
|
||||
self,
|
||||
"Apply text component changes?",
|
||||
"The text component has unapplied input, output, or source changes.",
|
||||
QMessageBox.StandardButton.Apply
|
||||
| QMessageBox.StandardButton.Discard
|
||||
| QMessageBox.StandardButton.Cancel,
|
||||
)
|
||||
if answer == QMessageBox.StandardButton.Apply:
|
||||
return self.apply_json()
|
||||
return answer == QMessageBox.StandardButton.Discard
|
||||
|
||||
@Slot()
|
||||
def apply_json(self) -> bool:
|
||||
try:
|
||||
data = json.loads(self.ui.jsonEditor.toPlainText())
|
||||
if not isinstance(data, dict):
|
||||
raise ValueError("The text component JSON must be an object")
|
||||
if not isinstance(data.get("inputs"), list):
|
||||
raise ValueError("'inputs' must be a list")
|
||||
if not isinstance(data.get("outputs"), list):
|
||||
raise ValueError("'outputs' must be a list")
|
||||
if not isinstance(data.get("source"), dict):
|
||||
raise ValueError("'source' must be an object")
|
||||
inputs = [Port.from_dict(item) for item in data["inputs"]]
|
||||
outputs = [Port.from_dict(item) for item in data["outputs"]]
|
||||
self.document_controller.replace_active_text_definition(
|
||||
inputs, outputs, data["source"]
|
||||
)
|
||||
except (TypeError, ValueError, json.JSONDecodeError) as error:
|
||||
QMessageBox.critical(self, "Invalid text component JSON", str(error))
|
||||
return False
|
||||
self._load_source_json()
|
||||
return True
|
||||
|
||||
def _maybe_save(self) -> bool:
|
||||
if self.document_controller.document is None:
|
||||
return True
|
||||
if self.document_controller.undo_stack.isClean():
|
||||
return True
|
||||
answer = QMessageBox.warning(
|
||||
self,
|
||||
"Unsaved changes",
|
||||
"The current graph contains unsaved changes.",
|
||||
QMessageBox.StandardButton.Save
|
||||
| QMessageBox.StandardButton.Discard
|
||||
| QMessageBox.StandardButton.Cancel,
|
||||
)
|
||||
if answer == QMessageBox.StandardButton.Save:
|
||||
return self.save_document()
|
||||
return answer == QMessageBox.StandardButton.Discard
|
||||
|
||||
@Slot()
|
||||
def new_document(self) -> None:
|
||||
if self._resolve_source_edits() and self._maybe_save():
|
||||
self.document_controller.new_document()
|
||||
|
||||
@Slot()
|
||||
def close_document(self) -> None:
|
||||
if self._resolve_source_edits() and self._maybe_save():
|
||||
self.document_controller.close_document()
|
||||
|
||||
@Slot()
|
||||
def open_document(self) -> None:
|
||||
if not self._resolve_source_edits() or not self._maybe_save():
|
||||
return
|
||||
filename, _ = QFileDialog.getOpenFileName(
|
||||
self, "Open graph", "", "BEdit graphs (*.bedit.json *.json);;All files (*)"
|
||||
)
|
||||
if not filename:
|
||||
return
|
||||
try:
|
||||
self.document_controller.load(Path(filename))
|
||||
except (OSError, ValueError) as error:
|
||||
QMessageBox.critical(self, "Could not open graph", str(error))
|
||||
|
||||
@Slot()
|
||||
def save_document(self) -> bool:
|
||||
if self.document_controller.document is None:
|
||||
return False
|
||||
if self.document_controller.file_path is None:
|
||||
return self.save_document_as()
|
||||
try:
|
||||
self.document_controller.save()
|
||||
except OSError as error:
|
||||
QMessageBox.critical(self, "Could not save graph", str(error))
|
||||
return False
|
||||
return True
|
||||
|
||||
@Slot()
|
||||
def save_document_as(self) -> bool:
|
||||
if self.document_controller.document is None:
|
||||
return False
|
||||
filename, _ = QFileDialog.getSaveFileName(
|
||||
self,
|
||||
"Save graph",
|
||||
"untitled.bedit.json",
|
||||
"BEdit graphs (*.bedit.json);;JSON files (*.json);;All files (*)",
|
||||
)
|
||||
if not filename:
|
||||
return False
|
||||
try:
|
||||
self.document_controller.save(Path(filename))
|
||||
except OSError as error:
|
||||
QMessageBox.critical(self, "Could not save graph", str(error))
|
||||
return False
|
||||
return True
|
||||
|
||||
@Slot()
|
||||
def show_settings(self) -> None:
|
||||
dialog = SettingsDialog(self)
|
||||
dialog.settingsChanged.connect(self.reload_libraries)
|
||||
dialog.settingsChanged.connect(self.refresh_editor_settings)
|
||||
dialog.exec()
|
||||
|
||||
def refresh_editor_settings(self) -> None:
|
||||
scene = self.ui.graphView.scene()
|
||||
if scene is not None:
|
||||
scene.update()
|
||||
self.ui.graphView.viewport().update()
|
||||
|
||||
def _document_opened_changed(self, opened: bool) -> None:
|
||||
for action in (self.ui.actionClose, self.ui.actionSave, self.ui.actionSaveAs):
|
||||
action.setEnabled(opened)
|
||||
self._active_graph_changed()
|
||||
|
||||
@Slot(object)
|
||||
def activate_tree_component(self, index) -> None:
|
||||
if index.data(ITEM_KIND_ROLE) != "current-component":
|
||||
return
|
||||
component_id = index.data(COMPONENT_ID_ROLE)
|
||||
if component_id:
|
||||
self.document_controller.activate_component(component_id)
|
||||
|
||||
@Slot(object)
|
||||
def show_library_context_menu(self, position) -> None:
|
||||
tree_view = self.ui.documentTreeView
|
||||
index = tree_view.indexAt(position)
|
||||
kind = index.data(ITEM_KIND_ROLE)
|
||||
if kind == "current-component":
|
||||
component_id = index.data(COMPONENT_ID_ROLE)
|
||||
document = self.document_controller.document
|
||||
component = document.find_component(component_id) if document else None
|
||||
if component is None:
|
||||
return
|
||||
menu = QMenu(self)
|
||||
graph_action = None
|
||||
text_action = None
|
||||
if component.implementation_kind == "graph":
|
||||
graph_action = menu.addAction("New Graph Block")
|
||||
text_action = menu.addAction("New Text Block")
|
||||
menu.addSeparator()
|
||||
options_action = menu.addAction("Component Options…")
|
||||
ports_action = menu.addAction("Port Options…")
|
||||
delete_action = menu.addAction("Delete")
|
||||
selected = menu.exec(tree_view.viewport().mapToGlobal(position))
|
||||
if selected is graph_action:
|
||||
self.document_controller.add_child(component_id, "graph")
|
||||
elif selected is text_action:
|
||||
self.document_controller.add_child(component_id, "text")
|
||||
elif selected is options_action:
|
||||
self.show_component_options(component_id)
|
||||
elif selected is ports_action:
|
||||
self.show_component_port_options(component_id)
|
||||
elif selected is delete_action:
|
||||
answer = QMessageBox.question(
|
||||
self,
|
||||
"Delete component?",
|
||||
f"Delete {component.name!r} and all of its contents?",
|
||||
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
||||
)
|
||||
if answer == QMessageBox.StandardButton.Yes:
|
||||
self.document_controller.delete_component(component_id)
|
||||
return
|
||||
if kind != "current-document":
|
||||
return
|
||||
menu = QMenu(self)
|
||||
graph_action = menu.addAction("New Graph Block")
|
||||
text_action = menu.addAction("New Text Block")
|
||||
selected = menu.exec(tree_view.viewport().mapToGlobal(position))
|
||||
if selected is graph_action:
|
||||
self.document_controller.add_root("graph")
|
||||
elif selected is text_action:
|
||||
self.document_controller.add_root("text")
|
||||
|
||||
@Slot(object)
|
||||
def show_external_library_context_menu(self, position) -> None:
|
||||
tree = self.ui.treeView
|
||||
index = tree.indexAt(position)
|
||||
component = index.data(COMPONENT_INSTANCE_ROLE)
|
||||
if not isinstance(component, Component):
|
||||
return
|
||||
menu = QMenu(self)
|
||||
ports_action = menu.addAction("Port Options…")
|
||||
if menu.exec(tree.viewport().mapToGlobal(position)) is ports_action:
|
||||
dialog = PortOptionsDialog(component, self)
|
||||
if dialog.exec() == dialog.DialogCode.Accepted:
|
||||
old_inputs, old_outputs = deepcopy(component.inputs), deepcopy(component.outputs)
|
||||
component.inputs = dialog.inputs
|
||||
component.outputs = dialog.outputs
|
||||
library = next(
|
||||
(
|
||||
library
|
||||
for library in self.libraries.libraries
|
||||
if any(item is component for item in library.document.all_components())
|
||||
),
|
||||
None,
|
||||
)
|
||||
try:
|
||||
if library is not None:
|
||||
library.document.validate()
|
||||
JsonDocumentSerializer.save(
|
||||
library.document, Path(library.source_path)
|
||||
)
|
||||
except (OSError, ValueError) as error:
|
||||
component.inputs, component.outputs = old_inputs, old_outputs
|
||||
QMessageBox.warning(self, "Cannot change library ports", str(error))
|
||||
self.library_tree_model.rebuild()
|
||||
|
||||
@Slot(str)
|
||||
def show_component_port_options(self, component_id: str) -> None:
|
||||
document = self.document_controller.document
|
||||
component = document.find_component(component_id) if document else None
|
||||
if component is None:
|
||||
return
|
||||
dialog = PortOptionsDialog(component, self)
|
||||
if dialog.exec() != dialog.DialogCode.Accepted:
|
||||
return
|
||||
try:
|
||||
self.document_controller.edit_component_ports(
|
||||
component_id, dialog.inputs, dialog.outputs
|
||||
)
|
||||
except ValueError as error:
|
||||
QMessageBox.warning(self, "Cannot change ports", str(error))
|
||||
|
||||
@Slot(str)
|
||||
def show_component_options(self, component_id: str) -> None:
|
||||
document = self.document_controller.document
|
||||
component = document.find_component(component_id) if document else None
|
||||
if component is None:
|
||||
return
|
||||
dialog = ComponentOptionsDialog(component, self)
|
||||
if dialog.exec() == dialog.DialogCode.Accepted:
|
||||
self.document_controller.edit_component_appearance(
|
||||
component_id,
|
||||
dialog.ui.nameEdit.text().strip(),
|
||||
dialog.edited_icon,
|
||||
dialog.edited_inputs,
|
||||
dialog.edited_outputs,
|
||||
dialog.ui.showSubtreeCheckBox.isChecked(),
|
||||
)
|
||||
|
||||
@Slot(str, str)
|
||||
def show_port_options(self, port_id: str, direction: str) -> None:
|
||||
owner = self.document_controller.active_component
|
||||
if owner is None:
|
||||
return
|
||||
ports = owner.inputs if direction == "input" else owner.outputs
|
||||
port = next((item for item in ports if item.id == port_id), None)
|
||||
if port is None:
|
||||
return
|
||||
dialog = ItemOptionsDialog(f"{direction.title()} Options", port.name, self)
|
||||
if dialog.exec() == dialog.DialogCode.Accepted:
|
||||
self.document_controller.rename_interface_port(port_id, dialog.name)
|
||||
|
||||
@Slot(str)
|
||||
def show_connection_options(self, connection_id: str) -> None:
|
||||
owner = self.document_controller.active_component
|
||||
if owner is None or owner.implementation_kind != "graph":
|
||||
return
|
||||
connection = owner.graph.connections.get(connection_id)
|
||||
if connection is None:
|
||||
return
|
||||
dialog = ItemOptionsDialog(
|
||||
"Connection Options", connection.name, self, name_required=False
|
||||
)
|
||||
if dialog.exec() == dialog.DialogCode.Accepted:
|
||||
self.document_controller.rename_connection(connection_id, dialog.name)
|
||||
|
||||
@Slot()
|
||||
def show_about(self) -> None:
|
||||
QMessageBox.about(
|
||||
self,
|
||||
"About BEdit",
|
||||
"<h3>BEdit</h3><p>A graphical editor built with Python and Qt.</p>",
|
||||
)
|
||||
|
||||
def closeEvent(self, event: QCloseEvent) -> None: # noqa: N802 (Qt API name)
|
||||
if not self._resolve_source_edits() or not self._maybe_save():
|
||||
event.ignore()
|
||||
return
|
||||
self.settings.setValue("window/geometry", self.saveGeometry())
|
||||
event.accept()
|
||||
1
BEdit/src/bedit/gui/models/__init__.py
Normal file
1
BEdit/src/bedit/gui/models/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Qt model/view adapters and repositories."""
|
||||
31
BEdit/src/bedit/gui/models/library_repository.py
Normal file
31
BEdit/src/bedit/gui/models/library_repository.py
Normal 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)
|
||||
95
BEdit/src/bedit/gui/models/library_tree.py
Normal file
95
BEdit/src/bedit/gui/models/library_tree.py
Normal file
@@ -0,0 +1,95 @@
|
||||
import json
|
||||
|
||||
from PySide6.QtCore import QByteArray, QMimeData, QModelIndex, Qt, Signal
|
||||
from PySide6.QtGui import QStandardItem, QStandardItemModel
|
||||
|
||||
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
|
||||
COMPONENT_ID_ROLE = Qt.ItemDataRole.UserRole + 2
|
||||
ITEM_KIND_ROLE = Qt.ItemDataRole.UserRole + 3
|
||||
COMPONENT_INSTANCE_ROLE = Qt.ItemDataRole.UserRole + 4
|
||||
COMPONENT_MIME_TYPE = "application/x-bedit-component"
|
||||
|
||||
|
||||
class LibraryTreeModel(QStandardItemModel):
|
||||
rebuilt = Signal()
|
||||
def __init__(
|
||||
self,
|
||||
repository: LibraryRepository,
|
||||
controller: DocumentController,
|
||||
parent=None,
|
||||
) -> None:
|
||||
super().__init__(parent)
|
||||
self.repository = repository
|
||||
self.controller = controller
|
||||
repository.librariesChanged.connect(self.rebuild)
|
||||
self.rebuild()
|
||||
|
||||
def rebuild(self) -> None:
|
||||
self.clear()
|
||||
self.setHorizontalHeaderLabels(["Libraries"])
|
||||
for library in self.repository.libraries:
|
||||
root = QStandardItem(library.name)
|
||||
root.setDragEnabled(False)
|
||||
root.setToolTip(library.source_path)
|
||||
for component in library.document.roots.values():
|
||||
root.appendRow(self._component_item(component))
|
||||
self.appendRow(root)
|
||||
self.rebuilt.emit()
|
||||
|
||||
def _component_item(self, component: Component, current: bool = False) -> QStandardItem:
|
||||
item = QStandardItem(component.name)
|
||||
item.setEditable(False)
|
||||
item.setIcon(library_icon(component.icon))
|
||||
item.setData(component.to_dict(), COMPONENT_ROLE)
|
||||
item.setData(component.id, COMPONENT_ID_ROLE)
|
||||
item.setData("current-component" if current else "library-component", ITEM_KIND_ROLE)
|
||||
item.setData(component, COMPONENT_INSTANCE_ROLE)
|
||||
if component.show_subtree_in_library:
|
||||
for child in component.graph.blocks.values():
|
||||
item.appendRow(self._component_item(child, current=current))
|
||||
return item
|
||||
|
||||
def mimeTypes(self) -> list[str]: # noqa: N802
|
||||
return [COMPONENT_MIME_TYPE]
|
||||
|
||||
def mimeData(self, indexes: list[QModelIndex]) -> QMimeData: # noqa: N802
|
||||
mime_data = QMimeData()
|
||||
for index in indexes:
|
||||
component = index.data(COMPONENT_ROLE)
|
||||
if component:
|
||||
encoded = json.dumps(component).encode("utf-8")
|
||||
mime_data.setData(COMPONENT_MIME_TYPE, QByteArray(encoded))
|
||||
break
|
||||
return mime_data
|
||||
|
||||
def supportedDragActions(self): # noqa: N802
|
||||
return Qt.DropAction.CopyAction
|
||||
|
||||
|
||||
class DocumentTreeModel(LibraryTreeModel):
|
||||
def __init__(self, controller: DocumentController, parent=None) -> None:
|
||||
QStandardItemModel.__init__(self, parent)
|
||||
self.controller = controller
|
||||
controller.documentReset.connect(self.rebuild)
|
||||
controller.componentMoved.connect(lambda _component_id, _position: self.rebuild())
|
||||
controller.connectionAdded.connect(lambda _connection_id: self.rebuild())
|
||||
controller.connectionRemoved.connect(lambda _connection_id: self.rebuild())
|
||||
self.rebuild()
|
||||
|
||||
def rebuild(self) -> None:
|
||||
self.clear()
|
||||
self.setHorizontalHeaderLabels(["Document"])
|
||||
if self.controller.document is not None:
|
||||
current_root = QStandardItem("Current Document")
|
||||
current_root.setDragEnabled(False)
|
||||
current_root.setData("current-document", ITEM_KIND_ROLE)
|
||||
for component in self.controller.document.roots.values():
|
||||
current_root.appendRow(self._component_item(component, current=True))
|
||||
self.appendRow(current_root)
|
||||
self.rebuilt.emit()
|
||||
6
BEdit/src/bedit/gui/preferences.py
Normal file
6
BEdit/src/bedit/gui/preferences.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from PySide6.QtCore import QSettings
|
||||
|
||||
|
||||
def application_settings() -> QSettings:
|
||||
"""Return BEdit's explicit, disk-backed settings store."""
|
||||
return QSettings("BEdit", "BEdit")
|
||||
Reference in New Issue
Block a user