drawsvg-ui 0.4.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,415 @@
1
+ """Interactive folder tree widget item."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Mapping
6
+
7
+ from PySide6 import QtCore, QtGui, QtWidgets
8
+
9
+ from constants import PEN_SELECTED, DEFAULT_FONT_FAMILY
10
+ from ..base import HandleAwareItemMixin, _should_draw_selection
11
+
12
+
13
+ class FolderTreeNode:
14
+ __slots__ = ("name", "is_folder", "children", "parent")
15
+
16
+ def __init__(
17
+ self,
18
+ name: str,
19
+ is_folder: bool = True,
20
+ parent: "FolderTreeNode | None" = None,
21
+ ) -> None:
22
+ self.name = name
23
+ self.is_folder = is_folder
24
+ self.parent = parent
25
+ self.children: list["FolderTreeNode"] = []
26
+
27
+ def add_child(self, child: "FolderTreeNode") -> "FolderTreeNode":
28
+ child.parent = self
29
+ self.children.append(child)
30
+ return child
31
+
32
+ def remove_child(self, child: "FolderTreeNode") -> None:
33
+ try:
34
+ self.children.remove(child)
35
+ child.parent = None
36
+ except ValueError:
37
+ pass
38
+
39
+ def to_dict(self) -> dict[str, Any]:
40
+ return {
41
+ "name": self.name,
42
+ "folder": self.is_folder,
43
+ "children": [child.to_dict() for child in self.children],
44
+ }
45
+
46
+ @staticmethod
47
+ def from_dict(
48
+ data: Mapping[str, Any],
49
+ parent: "FolderTreeNode | None" = None,
50
+ ) -> "FolderTreeNode":
51
+ name = str(data.get("name", "node"))
52
+ is_folder = bool(data.get("folder", True))
53
+ node = FolderTreeNode(name, is_folder, parent)
54
+ for child_data in data.get("children", []):
55
+ if isinstance(child_data, Mapping):
56
+ node.add_child(FolderTreeNode.from_dict(child_data, node))
57
+ return node
58
+
59
+
60
+ class FolderTreeBranchDot(QtWidgets.QGraphicsEllipseItem):
61
+ def __init__(self, tree: "FolderTreeItem", node: FolderTreeNode, radius: float):
62
+ super().__init__(-radius, -radius, radius * 2.0, radius * 2.0, tree)
63
+ self._tree = tree
64
+ self._node = node
65
+ self.setBrush(QtGui.QColor("#f28c28"))
66
+ self.setPen(QtGui.QPen(QtCore.Qt.PenStyle.NoPen))
67
+ self.setAcceptedMouseButtons(
68
+ QtCore.Qt.MouseButton.LeftButton | QtCore.Qt.MouseButton.RightButton
69
+ )
70
+ self.setCursor(QtCore.Qt.CursorShape.PointingHandCursor)
71
+ self.setFlag(
72
+ QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIgnoresTransformations,
73
+ True,
74
+ )
75
+ self.setZValue(1.0)
76
+
77
+ def node(self) -> FolderTreeNode:
78
+ return self._node
79
+
80
+ def mousePressEvent(self, event: QtWidgets.QGraphicsSceneMouseEvent) -> None:
81
+ if event.button() in (
82
+ QtCore.Qt.MouseButton.LeftButton,
83
+ QtCore.Qt.MouseButton.RightButton,
84
+ ):
85
+ self._tree.open_branch_menu(self._node, event.screenPos())
86
+ event.accept()
87
+ return
88
+ super().mousePressEvent(event)
89
+
90
+ def contextMenuEvent(self, event: QtWidgets.QGraphicsSceneContextMenuEvent) -> None:
91
+ self._tree.open_branch_menu(self._node, event.screenPos())
92
+ event.accept()
93
+
94
+
95
+ class FolderTreeItem(HandleAwareItemMixin, QtWidgets.QGraphicsItem):
96
+ LINE_COLOR = QtGui.QColor("#7a7a7a")
97
+ FOLDER_COLOR = QtGui.QColor("#000000")
98
+ FILE_COLOR = QtGui.QColor("#000000")
99
+ TEXT_COLOR = QtGui.QColor("#000000")
100
+
101
+ def __init__(
102
+ self,
103
+ x: float,
104
+ y: float,
105
+ w: float,
106
+ h: float,
107
+ structure: Mapping[str, Any] | None = None,
108
+ ) -> None:
109
+ QtWidgets.QGraphicsItem.__init__(self)
110
+ self.setPos(x, y)
111
+ self.setFlags(
112
+ QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsMovable
113
+ | QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsSelectable
114
+ | QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemSendsGeometryChanges
115
+ | QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsFocusable
116
+ )
117
+ self.setAcceptedMouseButtons(QtCore.Qt.MouseButton.LeftButton)
118
+
119
+ self._padding = 18.0
120
+ self._indent = 54.0
121
+ self._line_height = 28.0
122
+ self._dot_radius = 6.0
123
+ self._text_gap = 10.0
124
+ self._font = QtGui.QFont(DEFAULT_FONT_FAMILY, 11)
125
+
126
+ self._line_pen = QtGui.QPen(self.LINE_COLOR, 1.6)
127
+ self._line_pen.setCapStyle(QtCore.Qt.PenCapStyle.RoundCap)
128
+ self._folder_pen = QtGui.QPen(self.FOLDER_COLOR)
129
+ self._file_pen = QtGui.QPen(self.FILE_COLOR)
130
+
131
+ if structure:
132
+ self._root = FolderTreeNode.from_dict(structure)
133
+ else:
134
+ self._root = self._build_default_structure()
135
+
136
+ self._node_info: dict[FolderTreeNode, dict[str, Any]] = {}
137
+ self._order: list[FolderTreeNode] = []
138
+ self._bounding_rect = QtCore.QRectF()
139
+ self._dot_items: dict[FolderTreeNode, FolderTreeBranchDot] = {}
140
+
141
+ self._rebuild_layout()
142
+ self.setTransformOriginPoint(self.boundingRect().center())
143
+
144
+ def update_handles(self) -> None:
145
+ """Folder trees do not expose resize handles."""
146
+
147
+ return
148
+
149
+ def show_handles(self):
150
+ return
151
+
152
+ def hide_handles(self):
153
+ return
154
+
155
+ def boundingRect(self) -> QtCore.QRectF: # type: ignore[override]
156
+ return QtCore.QRectF(self._bounding_rect)
157
+
158
+ def paint(
159
+ self,
160
+ painter: QtGui.QPainter,
161
+ option: QtWidgets.QStyleOptionGraphicsItem,
162
+ widget: QtWidgets.QWidget | None = None,
163
+ ) -> None:
164
+ del option, widget
165
+ painter.setRenderHint(QtGui.QPainter.RenderHint.Antialiasing, True)
166
+
167
+ if _should_draw_selection(self):
168
+ painter.save()
169
+ painter.setPen(PEN_SELECTED)
170
+ painter.setBrush(QtCore.Qt.BrushStyle.NoBrush)
171
+ painter.drawRect(self.boundingRect())
172
+ painter.restore()
173
+
174
+ painter.setPen(self._line_pen)
175
+ painter.setBrush(QtCore.Qt.BrushStyle.NoBrush)
176
+ for node in self._order:
177
+ parent = node.parent
178
+ if parent is None:
179
+ continue
180
+ info = self._node_info[node]
181
+ parent_info = self._node_info[parent]
182
+ parent_center: QtCore.QPointF = parent_info["dot_center"]
183
+ child_center: QtCore.QPointF = info["dot_center"]
184
+ offset = self._dot_radius - 1.0
185
+ start = QtCore.QPointF(parent_center.x(), parent_center.y() + offset)
186
+ end = QtCore.QPointF(parent_center.x(), child_center.y())
187
+ painter.drawLine(start, end)
188
+ start_horizontal = QtCore.QPointF(parent_center.x(), child_center.y())
189
+ end_horizontal = QtCore.QPointF(
190
+ child_center.x() - (self._dot_radius - 1.0), child_center.y()
191
+ )
192
+ painter.drawLine(start_horizontal, end_horizontal)
193
+
194
+ painter.setFont(self._font)
195
+ for node in self._order:
196
+ info = self._node_info[node]
197
+ text_rect: QtCore.QRectF = info["text_rect"]
198
+ label = self._node_label(node)
199
+ painter.setPen(self._folder_pen if node.is_folder else self._file_pen)
200
+ painter.drawText(
201
+ text_rect,
202
+ QtCore.Qt.AlignmentFlag.AlignVCenter | QtCore.Qt.AlignmentFlag.AlignLeft,
203
+ label,
204
+ )
205
+
206
+ def _node_label(self, node: FolderTreeNode) -> str:
207
+ return f"{node.name}/" if node.is_folder else node.name
208
+
209
+ def _build_default_structure(self) -> FolderTreeNode:
210
+ root = FolderTreeNode("projekt", True)
211
+ docs = root.add_child(FolderTreeNode("docs", True))
212
+ api = docs.add_child(FolderTreeNode("api", True))
213
+ api.add_child(FolderTreeNode("index.md", False))
214
+ src = root.add_child(FolderTreeNode("src", True))
215
+ src.add_child(FolderTreeNode("app", True))
216
+ src.add_child(FolderTreeNode("main.py", False))
217
+ tests = root.add_child(FolderTreeNode("tests", True))
218
+ tests.add_child(FolderTreeNode("test_main.py", False))
219
+ return root
220
+
221
+ def _rebuild_layout(self) -> None:
222
+ fm = QtGui.QFontMetricsF(self._font)
223
+ self._line_height = max(self._line_height, fm.height() + 8.0)
224
+
225
+ self._node_info.clear()
226
+ self._order.clear()
227
+
228
+ def traverse(node: FolderTreeNode, depth: int) -> None:
229
+ info = {"depth": depth, "row": len(self._order)}
230
+ self._node_info[node] = info
231
+ self._order.append(node)
232
+ for child in node.children:
233
+ traverse(child, depth + 1)
234
+
235
+ traverse(self._root, 0)
236
+
237
+ max_text_right = self._padding
238
+ for node in self._order:
239
+ info = self._node_info[node]
240
+ depth = info["depth"]
241
+ row = info["row"]
242
+ y = self._padding + row * self._line_height
243
+ x = self._padding + depth * self._indent
244
+ text_x = x + self._dot_radius + self._text_gap
245
+ label = self._node_label(node)
246
+ text_width = fm.horizontalAdvance(label)
247
+ max_text_right = max(max_text_right, text_x + text_width)
248
+ info["dot_center"] = QtCore.QPointF(x, y + self._line_height / 2.0)
249
+ info["text_x"] = text_x
250
+ info["y"] = y
251
+
252
+ width = max_text_right + self._padding
253
+ min_width = self._padding * 2.0 + self._indent
254
+ width = max(width, min_width)
255
+ height = self._padding * 2.0 + max(1, len(self._order)) * self._line_height
256
+
257
+ self.prepareGeometryChange()
258
+ self._bounding_rect = QtCore.QRectF(0.0, 0.0, width, height)
259
+
260
+ for node in self._order:
261
+ info = self._node_info[node]
262
+ text_rect = QtCore.QRectF(
263
+ info["text_x"],
264
+ info["y"],
265
+ width - info["text_x"] - self._padding,
266
+ self._line_height,
267
+ )
268
+ info["text_rect"] = text_rect
269
+
270
+ self._update_branch_dots()
271
+ self.setTransformOriginPoint(self._bounding_rect.center())
272
+ self.update()
273
+
274
+ def _update_branch_dots(self) -> None:
275
+ current_nodes = set(self._order)
276
+ for node in list(self._dot_items.keys()):
277
+ if node not in current_nodes:
278
+ dot = self._dot_items.pop(node)
279
+ dot.setParentItem(None)
280
+ if dot.scene() is not None:
281
+ dot.scene().removeItem(dot)
282
+ for node in self._order:
283
+ info = self._node_info[node]
284
+ dot = self._dot_items.get(node)
285
+ if dot is None:
286
+ dot = FolderTreeBranchDot(self, node, self._dot_radius)
287
+ self._dot_items[node] = dot
288
+ dot.setPos(info["dot_center"])
289
+
290
+ def open_branch_menu(
291
+ self,
292
+ node: FolderTreeNode,
293
+ global_pos: QtCore.QPointF | QtCore.QPoint,
294
+ ) -> None:
295
+ menu = QtWidgets.QMenu()
296
+ add_folder_action = None
297
+ add_file_action = None
298
+ delete_action = None
299
+
300
+ if node.is_folder:
301
+ add_folder_action = menu.addAction("Add Folder")
302
+ add_file_action = menu.addAction("Add File")
303
+ if node.parent is not None:
304
+ if menu.actions():
305
+ menu.addSeparator()
306
+ delete_action = menu.addAction("Remove Entry")
307
+
308
+ if isinstance(global_pos, QtCore.QPointF):
309
+ global_point = QtCore.QPoint(round(global_pos.x()), round(global_pos.y()))
310
+ else:
311
+ global_point = global_pos
312
+
313
+ selected = menu.exec(global_point)
314
+ if not selected:
315
+ return
316
+ if selected is add_folder_action:
317
+ self._create_child(node, True)
318
+ elif selected is add_file_action:
319
+ self._create_child(node, False)
320
+ elif selected is delete_action:
321
+ self._delete_node(node)
322
+
323
+ def _view_widget(self) -> QtWidgets.QWidget | None:
324
+ scene = self.scene()
325
+ if scene and scene.views():
326
+ return scene.views()[0]
327
+ return None
328
+
329
+ def _prompt_name(
330
+ self,
331
+ title: str,
332
+ label: str,
333
+ default: str = "",
334
+ ) -> str | None:
335
+ parent = self._view_widget()
336
+ text, ok = QtWidgets.QInputDialog.getText(
337
+ parent,
338
+ title,
339
+ label,
340
+ QtWidgets.QLineEdit.EchoMode.Normal,
341
+ default,
342
+ )
343
+ if not ok:
344
+ return None
345
+ name = text.strip()
346
+ return name or None
347
+
348
+ def _create_child(self, parent: FolderTreeNode, is_folder: bool) -> None:
349
+ title = "Add Folder" if is_folder else "Add File"
350
+ prompt = "Folder name:" if is_folder else "File name:"
351
+ name = self._prompt_name(title, prompt)
352
+ if not name:
353
+ return
354
+ parent.add_child(FolderTreeNode(name, is_folder))
355
+ self._rebuild_layout()
356
+
357
+ def _delete_node(self, node: FolderTreeNode) -> None:
358
+ if node.parent is None:
359
+ return
360
+ parent_widget = self._view_widget()
361
+ reply = QtWidgets.QMessageBox.question(
362
+ parent_widget,
363
+ "Remove Entry",
364
+ f"Remove '{self._node_label(node)}'?",
365
+ )
366
+ if reply != QtWidgets.QMessageBox.StandardButton.Yes:
367
+ return
368
+ node.parent.remove_child(node)
369
+ self._rebuild_layout()
370
+
371
+ def _rename_node(self, node: FolderTreeNode) -> None:
372
+ current = node.name
373
+ title = "Rename Entry"
374
+ prompt = "New name:"
375
+ name = self._prompt_name(title, prompt, current)
376
+ if not name or name == current:
377
+ return
378
+ node.name = name
379
+ self._rebuild_layout()
380
+
381
+ def structure(self) -> dict[str, Any]:
382
+ return self._root.to_dict()
383
+
384
+ def set_structure(self, structure: Mapping[str, Any]) -> None:
385
+ self._root = FolderTreeNode.from_dict(structure)
386
+ self._rebuild_layout()
387
+
388
+ def mousePressEvent(self, event: QtWidgets.QGraphicsSceneMouseEvent) -> None:
389
+ super().mousePressEvent(event)
390
+ if event.button() != QtCore.Qt.MouseButton.LeftButton:
391
+ return
392
+
393
+ local_pos = event.pos()
394
+ for node in self._order:
395
+ if self._node_info[node]["text_rect"].contains(local_pos):
396
+ event.accept()
397
+ return
398
+
399
+ def mouseDoubleClickEvent(self, event: QtWidgets.QGraphicsSceneMouseEvent) -> None:
400
+ if event.button() != QtCore.Qt.MouseButton.LeftButton:
401
+ super().mouseDoubleClickEvent(event)
402
+ return
403
+
404
+ local_pos = event.pos()
405
+ for node in self._order:
406
+ text_rect: QtCore.QRectF = self._node_info[node]["text_rect"]
407
+ if text_rect.contains(local_pos):
408
+ self._rename_node(node)
409
+ event.accept()
410
+ return
411
+
412
+ super().mouseDoubleClickEvent(event)
413
+
414
+
415
+ __all__ = ["FolderTreeBranchDot", "FolderTreeItem", "FolderTreeNode"]
main.py ADDED
@@ -0,0 +1,23 @@
1
+ import sys
2
+ import warnings
3
+
4
+ from PySide6 import QtCore, QtGui, QtWidgets
5
+
6
+ from main_window import MainWindow
7
+
8
+
9
+ def main():
10
+ app = QtWidgets.QApplication(sys.argv)
11
+ win = MainWindow()
12
+ win.show()
13
+ sys.exit(app.exec())
14
+
15
+
16
+ if __name__ == "__main__":
17
+ warnings.filterwarnings(
18
+ "ignore",
19
+ message="Enum value 'Qt::ApplicationAttribute.AA_UseHighDpiPixmaps' is marked as deprecated",
20
+ category=DeprecationWarning,
21
+ )
22
+ QtCore.QCoreApplication.setAttribute(QtCore.Qt.AA_UseHighDpiPixmaps, True)
23
+ main()
main_window.py ADDED
@@ -0,0 +1,254 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+
5
+ from PySide6 import QtCore, QtWidgets, QtGui
6
+ from PySide6.QtUiTools import QUiLoader
7
+
8
+ from app_info import GITHUB_URL, get_version
9
+ from canvas_view import CanvasView
10
+ from export_drawsvg import export_drawsvg_py
11
+ from import_drawsvg import import_drawsvg_py
12
+ from palette import PaletteList
13
+ from properties_panel import PropertiesPanel
14
+
15
+ _UI_PATH = Path(__file__).resolve().parent / "ui" / "main_window.ui"
16
+
17
+
18
+ class MainWindow(QtWidgets.QMainWindow):
19
+ def __init__(self):
20
+ super().__init__()
21
+
22
+ self._load_ui()
23
+
24
+ self._install_custom_widgets()
25
+ self._configure_actions()
26
+
27
+ self.statusBar().showMessage(
28
+ "Tip: Ctrl+drag duplicates selected objects, Alt+mouse wheel zooms"
29
+ )
30
+
31
+ self.properties_panel.clear()
32
+ self.canvas.selectionSnapshotChanged.connect(self._handle_selection_snapshot)
33
+
34
+ history = self.canvas.history()
35
+ history.historyChanged.connect(self._update_history_actions)
36
+ self._update_history_actions(history.can_undo(), history.can_redo())
37
+
38
+ def _load_ui(self) -> None:
39
+ loader = QUiLoader()
40
+ ui_file = QtCore.QFile(str(_UI_PATH))
41
+ if not ui_file.open(QtCore.QIODevice.OpenModeFlag.ReadOnly):
42
+ raise RuntimeError(f"Could not open UI file: {_UI_PATH}")
43
+ form = loader.load(ui_file, None)
44
+ ui_file.close()
45
+ if form is None:
46
+ raise RuntimeError(f"Failed to load UI from {_UI_PATH}")
47
+
48
+ central = self._take_widget(form, QtWidgets.QWidget, "centralwidget")
49
+ if central is None:
50
+ raise RuntimeError("Missing central widget in UI file")
51
+ self.setCentralWidget(central)
52
+ menu_bar = self._take_widget(form, QtWidgets.QMenuBar, "menubar")
53
+ if menu_bar is not None:
54
+ self.setMenuBar(menu_bar)
55
+ status_bar = self._take_widget(form, QtWidgets.QStatusBar, "statusbar")
56
+ if status_bar is not None:
57
+ self.setStatusBar(status_bar)
58
+ self.resize(form.size())
59
+ self.setWindowTitle(form.windowTitle())
60
+
61
+ self.mainSplitter = self._require_widget(central, QtWidgets.QSplitter, "mainSplitter")
62
+ self.paletteContainer = self._require_widget(central, QtWidgets.QWidget, "paletteContainer")
63
+ self.palettePlaceholder = self._require_widget(central, QtWidgets.QWidget, "palettePlaceholder")
64
+ self.canvasContainer = self._require_widget(central, QtWidgets.QWidget, "canvasContainer")
65
+ self.canvasPlaceholder = self._require_widget(central, QtWidgets.QGraphicsView, "canvasPlaceholder")
66
+ self.propertiesContainer = self._require_widget(central, QtWidgets.QWidget, "propertiesContainer")
67
+ self.propertiesPlaceholder = self._require_widget(central, QtWidgets.QWidget, "propertiesPlaceholder")
68
+
69
+ layouts = [
70
+ central.layout(),
71
+ self.paletteContainer.layout(),
72
+ self.canvasContainer.layout(),
73
+ self.propertiesContainer.layout(),
74
+ ]
75
+ for layout in layouts:
76
+ if layout is not None:
77
+ layout.setContentsMargins(0, 0, 0, 0)
78
+ layout.setSpacing(0)
79
+
80
+ action_names = [
81
+ "actionLoad_drawsvg_py",
82
+ "actionSave_drawsvg_py",
83
+ "actionQuit",
84
+ "actionUndo",
85
+ "actionRedo",
86
+ "actionClear_canvas",
87
+ "actionShow_grid",
88
+ "actionInfo",
89
+ ]
90
+ for name in action_names:
91
+ action = form.findChild(QtGui.QAction, name)
92
+ if action is None:
93
+ raise RuntimeError(f"Missing QAction '{name}' in UI file")
94
+ action.setParent(self)
95
+ setattr(self, name, action)
96
+
97
+ form.deleteLater()
98
+
99
+ def _take_widget(
100
+ self,
101
+ parent: QtWidgets.QWidget,
102
+ widget_type: type[QtWidgets.QWidget],
103
+ object_name: str,
104
+ ) -> QtWidgets.QWidget | None:
105
+ widget = parent.findChild(widget_type, object_name)
106
+ if widget is None:
107
+ return None
108
+ widget.setParent(None)
109
+ return widget
110
+
111
+ @staticmethod
112
+ def _require_widget(
113
+ parent: QtWidgets.QWidget,
114
+ widget_type: type[QtWidgets.QWidget],
115
+ object_name: str,
116
+ ) -> QtWidgets.QWidget:
117
+ widget = parent.findChild(widget_type, object_name)
118
+ if widget is None:
119
+ raise RuntimeError(f"Missing widget '{object_name}' in UI file")
120
+ return widget
121
+
122
+ def _install_custom_widgets(self) -> None:
123
+ self.splitter = self.mainSplitter
124
+
125
+ palette_container = self.paletteContainer
126
+ canvas_container = self.canvasContainer
127
+ properties_container = self.propertiesContainer
128
+ properties_container.setSizePolicy(
129
+ QtWidgets.QSizePolicy.Policy.Preferred,
130
+ QtWidgets.QSizePolicy.Policy.Expanding,
131
+ )
132
+
133
+ self.palette = PaletteList(palette_container)
134
+ self.palette.setMinimumWidth(220)
135
+ self.palette.shapeClicked.connect(self._add_shape_at_center)
136
+
137
+ self.canvas = CanvasView(canvas_container)
138
+
139
+ self.properties_panel = PropertiesPanel(self.canvas)
140
+ properties_min_width = max(
141
+ 260,
142
+ self.properties_panel.minimumWidth(),
143
+ self.properties_panel.minimumSizeHint().width(),
144
+ )
145
+ properties_container.setMinimumWidth(properties_min_width)
146
+
147
+ self._replace_placeholder(palette_container, self.palettePlaceholder, self.palette)
148
+ self._replace_placeholder(canvas_container, self.canvasPlaceholder, self.canvas)
149
+ self._replace_placeholder(
150
+ properties_container, self.propertiesPlaceholder, self.properties_panel
151
+ )
152
+
153
+ self.splitter.setStretchFactor(0, 0)
154
+ self.splitter.setStretchFactor(1, 1)
155
+ self.splitter.setStretchFactor(2, 0)
156
+ self.splitter.setCollapsible(2, True)
157
+ self.splitter.setSizes([self.palette.minimumWidth(), 900, properties_min_width + 40])
158
+
159
+ @staticmethod
160
+ def _replace_placeholder(
161
+ container: QtWidgets.QWidget,
162
+ placeholder: QtWidgets.QWidget,
163
+ replacement: QtWidgets.QWidget,
164
+ ) -> None:
165
+ layout = container.layout()
166
+ if layout is None:
167
+ layout = QtWidgets.QVBoxLayout(container)
168
+ layout.setContentsMargins(0, 0, 0, 0)
169
+ layout.setSpacing(0)
170
+ index = layout.indexOf(placeholder)
171
+ if index >= 0:
172
+ item = layout.takeAt(index)
173
+ if item is not None:
174
+ orphan = item.widget()
175
+ if orphan is not None:
176
+ orphan.setParent(None)
177
+ placeholder.setParent(None)
178
+ placeholder.deleteLater()
179
+ if index >= 0:
180
+ layout.insertWidget(index, replacement)
181
+ else:
182
+ layout.addWidget(replacement)
183
+
184
+ def _configure_actions(self) -> None:
185
+ self.actionLoad_drawsvg_py.triggered.connect(self.load_drawsvg_py)
186
+ self.actionSave_drawsvg_py.triggered.connect(self.export_drawsvg_py)
187
+ self.actionQuit.triggered.connect(self.close)
188
+
189
+ self.actionUndo.setShortcutContext(
190
+ QtCore.Qt.ShortcutContext.WidgetWithChildrenShortcut
191
+ )
192
+ self.actionUndo.triggered.connect(self._handle_undo)
193
+
194
+ self.actionRedo.setShortcutContext(
195
+ QtCore.Qt.ShortcutContext.WidgetWithChildrenShortcut
196
+ )
197
+ self.actionRedo.triggered.connect(self._handle_redo)
198
+
199
+ self.actionClear_canvas.triggered.connect(self._handle_clear_canvas)
200
+
201
+ self.actionShow_grid.setCheckable(True)
202
+ self.actionShow_grid.setChecked(True)
203
+ self.actionShow_grid.toggled.connect(self._handle_toggle_grid)
204
+ self.canvas.gridVisibilityChanged.connect(self.actionShow_grid.setChecked)
205
+
206
+ self.actionInfo.triggered.connect(self._show_about_dialog)
207
+
208
+ def _handle_undo(self) -> None:
209
+ self.canvas.undo()
210
+
211
+ def _handle_redo(self) -> None:
212
+ self.canvas.redo()
213
+
214
+ def _handle_clear_canvas(self) -> None:
215
+ self.canvas.clear_canvas()
216
+
217
+ def _handle_toggle_grid(self, visible: bool) -> None:
218
+ self.canvas.set_grid_visible(visible)
219
+
220
+ def _show_about_dialog(self) -> None:
221
+ version = get_version()
222
+ body = (
223
+ "<b>DrawSVG UI</b><br>"
224
+ f"Version: {version}<br>"
225
+ f'<a href="{GITHUB_URL}">{GITHUB_URL}</a>'
226
+ )
227
+
228
+ dialog = QtWidgets.QMessageBox(self)
229
+ dialog.setIcon(QtWidgets.QMessageBox.Icon.Information)
230
+ dialog.setWindowTitle("About")
231
+ dialog.setTextFormat(QtCore.Qt.TextFormat.RichText)
232
+ dialog.setText(body)
233
+ dialog.setTextInteractionFlags(
234
+ QtCore.Qt.TextInteractionFlag.TextBrowserInteraction
235
+ | QtCore.Qt.TextInteractionFlag.LinksAccessibleByMouse
236
+ )
237
+ dialog.setStandardButtons(QtWidgets.QMessageBox.StandardButton.Ok)
238
+ dialog.exec()
239
+
240
+ def export_drawsvg_py(self) -> None:
241
+ export_drawsvg_py(self.canvas.scene(), self)
242
+
243
+ def load_drawsvg_py(self) -> None:
244
+ import_drawsvg_py(self.canvas.scene(), self)
245
+
246
+ def _add_shape_at_center(self, shape: str) -> None:
247
+ self.canvas.add_shape_at_view_center(shape)
248
+
249
+ def _update_history_actions(self, can_undo: bool, can_redo: bool) -> None:
250
+ self.actionUndo.setEnabled(can_undo)
251
+ self.actionRedo.setEnabled(can_redo)
252
+
253
+ def _handle_selection_snapshot(self, payload: dict) -> None:
254
+ self.properties_panel.update_snapshot(payload)