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.
- app_info.py +61 -0
- canvas_view.py +2506 -0
- constants.py +49 -0
- drawsvg_ui-0.4.0.dist-info/METADATA +86 -0
- drawsvg_ui-0.4.0.dist-info/RECORD +28 -0
- drawsvg_ui-0.4.0.dist-info/WHEEL +5 -0
- drawsvg_ui-0.4.0.dist-info/entry_points.txt +2 -0
- drawsvg_ui-0.4.0.dist-info/top_level.txt +11 -0
- export_drawsvg.py +1700 -0
- import_drawsvg.py +807 -0
- items/__init__.py +66 -0
- items/base.py +606 -0
- items/labels.py +247 -0
- items/shapes/__init__.py +20 -0
- items/shapes/curves.py +139 -0
- items/shapes/lines.py +439 -0
- items/shapes/polygons.py +359 -0
- items/shapes/rects.py +310 -0
- items/text.py +331 -0
- items/widgets/__init__.py +5 -0
- items/widgets/folder_tree.py +415 -0
- main.py +23 -0
- main_window.py +254 -0
- palette.py +556 -0
- properties_panel.py +1406 -0
- ui/__init__.py +1 -0
- ui/main_window.ui +157 -0
- ui/properties_panel.ui +996 -0
items/shapes/polygons.py
ADDED
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
"""Polygon-based shape items."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from PySide6 import QtCore, QtGui, QtWidgets
|
|
6
|
+
|
|
7
|
+
from constants import DEFAULT_FILL, PEN_NORMAL, PEN_SELECTED
|
|
8
|
+
from ..base import (
|
|
9
|
+
DIVIDER_HANDLE_COLOR,
|
|
10
|
+
DIVIDER_HANDLE_DIAMETER,
|
|
11
|
+
ResizableItem,
|
|
12
|
+
_should_draw_selection,
|
|
13
|
+
snap_to_grid,
|
|
14
|
+
)
|
|
15
|
+
from ..labels import ShapeLabelMixin
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class TriangleItem(ResizableItem, QtWidgets.QGraphicsPolygonItem):
|
|
19
|
+
def __init__(self, x, y, w, h):
|
|
20
|
+
QtWidgets.QGraphicsPolygonItem.__init__(self)
|
|
21
|
+
ResizableItem.__init__(self)
|
|
22
|
+
self._w = w
|
|
23
|
+
self._h = h
|
|
24
|
+
self._update_polygon()
|
|
25
|
+
self.setPos(x, y)
|
|
26
|
+
self.setTransformOriginPoint(w / 2.0, h / 2.0)
|
|
27
|
+
self.setFlags(
|
|
28
|
+
QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsMovable
|
|
29
|
+
| QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsSelectable
|
|
30
|
+
| QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemSendsGeometryChanges
|
|
31
|
+
| QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsFocusable
|
|
32
|
+
)
|
|
33
|
+
self.setPen(PEN_NORMAL)
|
|
34
|
+
self.setBrush(DEFAULT_FILL)
|
|
35
|
+
|
|
36
|
+
def _update_polygon(self):
|
|
37
|
+
poly = QtGui.QPolygonF(
|
|
38
|
+
[
|
|
39
|
+
QtCore.QPointF(self._w / 2.0, 0.0),
|
|
40
|
+
QtCore.QPointF(0.0, self._h),
|
|
41
|
+
QtCore.QPointF(self._w, self._h),
|
|
42
|
+
]
|
|
43
|
+
)
|
|
44
|
+
self.setPolygon(poly)
|
|
45
|
+
|
|
46
|
+
def set_size(self, w, h, adjust_origin: bool = True):
|
|
47
|
+
self._w = w
|
|
48
|
+
self._h = h
|
|
49
|
+
self._update_polygon()
|
|
50
|
+
if adjust_origin:
|
|
51
|
+
self.setTransformOriginPoint(w / 2.0, h / 2.0)
|
|
52
|
+
|
|
53
|
+
def paint(self, painter, option, widget=None):
|
|
54
|
+
opt = QtWidgets.QStyleOptionGraphicsItem(option)
|
|
55
|
+
opt.state &= ~QtWidgets.QStyle.StateFlag.State_Selected
|
|
56
|
+
super().paint(painter, opt, widget)
|
|
57
|
+
if _should_draw_selection(self):
|
|
58
|
+
painter.save()
|
|
59
|
+
painter.setPen(PEN_SELECTED)
|
|
60
|
+
painter.setBrush(QtCore.Qt.BrushStyle.NoBrush)
|
|
61
|
+
painter.drawRect(self.boundingRect())
|
|
62
|
+
painter.restore()
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class DiamondItem(ShapeLabelMixin, ResizableItem, QtWidgets.QGraphicsPolygonItem):
|
|
66
|
+
def __init__(self, x: float, y: float, w: float, h: float):
|
|
67
|
+
QtWidgets.QGraphicsPolygonItem.__init__(self)
|
|
68
|
+
ResizableItem.__init__(self)
|
|
69
|
+
self._w = w
|
|
70
|
+
self._h = h
|
|
71
|
+
self._update_polygon()
|
|
72
|
+
self.setPos(x, y)
|
|
73
|
+
self.setTransformOriginPoint(w / 2.0, h / 2.0)
|
|
74
|
+
self.setFlags(
|
|
75
|
+
QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsMovable
|
|
76
|
+
| QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsSelectable
|
|
77
|
+
| QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemSendsGeometryChanges
|
|
78
|
+
| QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsFocusable
|
|
79
|
+
)
|
|
80
|
+
self._init_shape_label()
|
|
81
|
+
self.setPen(PEN_NORMAL)
|
|
82
|
+
self.setBrush(DEFAULT_FILL)
|
|
83
|
+
|
|
84
|
+
def _label_base_rect(self) -> QtCore.QRectF:
|
|
85
|
+
return QtCore.QRectF(self.boundingRect())
|
|
86
|
+
|
|
87
|
+
def _update_polygon(self) -> None:
|
|
88
|
+
half_w = self._w / 2.0
|
|
89
|
+
half_h = self._h / 2.0
|
|
90
|
+
poly = QtGui.QPolygonF(
|
|
91
|
+
[
|
|
92
|
+
QtCore.QPointF(half_w, 0.0),
|
|
93
|
+
QtCore.QPointF(self._w, half_h),
|
|
94
|
+
QtCore.QPointF(half_w, self._h),
|
|
95
|
+
QtCore.QPointF(0.0, half_h),
|
|
96
|
+
]
|
|
97
|
+
)
|
|
98
|
+
self.setPolygon(poly)
|
|
99
|
+
if hasattr(self, "_label"):
|
|
100
|
+
self._update_label_geometry()
|
|
101
|
+
|
|
102
|
+
def set_size(self, w: float, h: float, adjust_origin: bool = True) -> None:
|
|
103
|
+
self._w = w
|
|
104
|
+
self._h = h
|
|
105
|
+
self._update_polygon()
|
|
106
|
+
if adjust_origin:
|
|
107
|
+
self.setTransformOriginPoint(w / 2.0, h / 2.0)
|
|
108
|
+
self._update_label_geometry()
|
|
109
|
+
|
|
110
|
+
def setPen(self, pen: QtGui.QPen | QtGui.QColor) -> None: # type: ignore[override]
|
|
111
|
+
super().setPen(pen)
|
|
112
|
+
self._update_label_color()
|
|
113
|
+
|
|
114
|
+
def paint(self, painter, option, widget=None):
|
|
115
|
+
opt = QtWidgets.QStyleOptionGraphicsItem(option)
|
|
116
|
+
opt.state &= ~QtWidgets.QStyle.StateFlag.State_Selected
|
|
117
|
+
super().paint(painter, opt, widget)
|
|
118
|
+
if _should_draw_selection(self):
|
|
119
|
+
painter.save()
|
|
120
|
+
painter.setPen(PEN_SELECTED)
|
|
121
|
+
painter.setBrush(QtCore.Qt.BrushStyle.NoBrush)
|
|
122
|
+
painter.drawRect(self.boundingRect())
|
|
123
|
+
painter.restore()
|
|
124
|
+
|
|
125
|
+
def mouseDoubleClickEvent(
|
|
126
|
+
self, event: QtWidgets.QGraphicsSceneMouseEvent
|
|
127
|
+
) -> None: # type: ignore[override]
|
|
128
|
+
if event.button() == QtCore.Qt.MouseButton.LeftButton:
|
|
129
|
+
self._begin_label_edit()
|
|
130
|
+
event.accept()
|
|
131
|
+
return
|
|
132
|
+
super().mouseDoubleClickEvent(event)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class BlockArrowHandle(QtWidgets.QGraphicsEllipseItem):
|
|
136
|
+
"""Special orange handles for :class:`BlockArrowItem`."""
|
|
137
|
+
|
|
138
|
+
def __init__(self, parent: "BlockArrowItem", role: str):
|
|
139
|
+
radius = DIVIDER_HANDLE_DIAMETER / 2.0
|
|
140
|
+
super().__init__(
|
|
141
|
+
-radius,
|
|
142
|
+
-radius,
|
|
143
|
+
DIVIDER_HANDLE_DIAMETER,
|
|
144
|
+
DIVIDER_HANDLE_DIAMETER,
|
|
145
|
+
parent,
|
|
146
|
+
)
|
|
147
|
+
self._role = role
|
|
148
|
+
self.setBrush(DIVIDER_HANDLE_COLOR)
|
|
149
|
+
self.setPen(QtGui.QPen(QtCore.Qt.PenStyle.NoPen))
|
|
150
|
+
self.setAcceptedMouseButtons(QtCore.Qt.MouseButton.LeftButton)
|
|
151
|
+
self.setFlag(QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIgnoresTransformations)
|
|
152
|
+
if role == "head":
|
|
153
|
+
self.setCursor(QtCore.Qt.CursorShape.SizeHorCursor)
|
|
154
|
+
else:
|
|
155
|
+
self.setCursor(QtCore.Qt.CursorShape.SizeVerCursor)
|
|
156
|
+
self._parent_was_movable = False
|
|
157
|
+
|
|
158
|
+
def mousePressEvent(self, event: QtWidgets.QGraphicsSceneMouseEvent):
|
|
159
|
+
parent = self.parentItem()
|
|
160
|
+
if parent is not None:
|
|
161
|
+
flags = parent.flags()
|
|
162
|
+
self._parent_was_movable = bool(
|
|
163
|
+
flags & QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsMovable
|
|
164
|
+
)
|
|
165
|
+
if self._parent_was_movable:
|
|
166
|
+
parent.setFlag(
|
|
167
|
+
QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsMovable,
|
|
168
|
+
False,
|
|
169
|
+
)
|
|
170
|
+
event.accept()
|
|
171
|
+
|
|
172
|
+
def mouseMoveEvent(self, event: QtWidgets.QGraphicsSceneMouseEvent):
|
|
173
|
+
parent: "BlockArrowItem" = self.parentItem() # type: ignore[assignment]
|
|
174
|
+
if parent is not None:
|
|
175
|
+
parent._handle_special_drag(self._role, event)
|
|
176
|
+
event.accept()
|
|
177
|
+
|
|
178
|
+
def mouseReleaseEvent(self, event: QtWidgets.QGraphicsSceneMouseEvent):
|
|
179
|
+
parent = self.parentItem()
|
|
180
|
+
if parent is not None and self._parent_was_movable:
|
|
181
|
+
parent.setFlag(
|
|
182
|
+
QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsMovable,
|
|
183
|
+
True,
|
|
184
|
+
)
|
|
185
|
+
self._parent_was_movable = False
|
|
186
|
+
event.accept()
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
class BlockArrowItem(ResizableItem, QtWidgets.QGraphicsPolygonItem):
|
|
190
|
+
def __init__(self, x: float, y: float, w: float, h: float):
|
|
191
|
+
QtWidgets.QGraphicsPolygonItem.__init__(self)
|
|
192
|
+
ResizableItem.__init__(self)
|
|
193
|
+
self._w = w
|
|
194
|
+
self._h = h
|
|
195
|
+
self._head_ratio = 0.35
|
|
196
|
+
self._shaft_ratio = 0.35
|
|
197
|
+
self._head_handle: BlockArrowHandle | None = None
|
|
198
|
+
self._body_handle: BlockArrowHandle | None = None
|
|
199
|
+
self._update_polygon()
|
|
200
|
+
self.setPos(x, y)
|
|
201
|
+
self.setTransformOriginPoint(w / 2.0, h / 2.0)
|
|
202
|
+
self.setFlags(
|
|
203
|
+
QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsMovable
|
|
204
|
+
| QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsSelectable
|
|
205
|
+
| QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemSendsGeometryChanges
|
|
206
|
+
| QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsFocusable
|
|
207
|
+
)
|
|
208
|
+
self.setPen(PEN_NORMAL)
|
|
209
|
+
self.setBrush(DEFAULT_FILL)
|
|
210
|
+
|
|
211
|
+
def _clamp_head_ratio(self, ratio: float) -> float:
|
|
212
|
+
return max(0.05, min(0.8, ratio))
|
|
213
|
+
|
|
214
|
+
def _clamp_shaft_ratio(self, ratio: float) -> float:
|
|
215
|
+
return max(0.1, min(0.9, ratio))
|
|
216
|
+
|
|
217
|
+
def head_ratio(self) -> float:
|
|
218
|
+
return self._head_ratio
|
|
219
|
+
|
|
220
|
+
def set_head_ratio(self, ratio: float) -> None:
|
|
221
|
+
self._head_ratio = self._clamp_head_ratio(ratio)
|
|
222
|
+
self._update_polygon()
|
|
223
|
+
|
|
224
|
+
def shaft_ratio(self) -> float:
|
|
225
|
+
return self._shaft_ratio
|
|
226
|
+
|
|
227
|
+
def set_shaft_ratio(self, ratio: float) -> None:
|
|
228
|
+
self._shaft_ratio = self._clamp_shaft_ratio(ratio)
|
|
229
|
+
self._update_polygon()
|
|
230
|
+
|
|
231
|
+
def _head_width(self) -> float:
|
|
232
|
+
return self._w * self._clamp_head_ratio(self._head_ratio)
|
|
233
|
+
|
|
234
|
+
def _shaft_bounds(self) -> tuple[float, float]:
|
|
235
|
+
shaft_h = self._h * self._clamp_shaft_ratio(self._shaft_ratio)
|
|
236
|
+
top = (self._h - shaft_h) / 2.0
|
|
237
|
+
return top, top + shaft_h
|
|
238
|
+
|
|
239
|
+
def _update_polygon(self) -> None:
|
|
240
|
+
self.prepareGeometryChange()
|
|
241
|
+
w = self._w
|
|
242
|
+
h = self._h
|
|
243
|
+
head_w = self._head_width()
|
|
244
|
+
shaft_top, shaft_bottom = self._shaft_bounds()
|
|
245
|
+
poly = QtGui.QPolygonF(
|
|
246
|
+
[
|
|
247
|
+
QtCore.QPointF(0.0, shaft_top),
|
|
248
|
+
QtCore.QPointF(w - head_w, shaft_top),
|
|
249
|
+
QtCore.QPointF(w - head_w, 0.0),
|
|
250
|
+
QtCore.QPointF(w, h / 2.0),
|
|
251
|
+
QtCore.QPointF(w - head_w, h),
|
|
252
|
+
QtCore.QPointF(w - head_w, shaft_bottom),
|
|
253
|
+
QtCore.QPointF(0.0, shaft_bottom),
|
|
254
|
+
]
|
|
255
|
+
)
|
|
256
|
+
self.setPolygon(poly)
|
|
257
|
+
self.setTransformOriginPoint(self._w / 2.0, self._h / 2.0)
|
|
258
|
+
self._update_custom_handles()
|
|
259
|
+
|
|
260
|
+
def _update_custom_handles(self) -> None:
|
|
261
|
+
if not (self._head_handle and self._body_handle):
|
|
262
|
+
return
|
|
263
|
+
head_w = self._head_width()
|
|
264
|
+
shaft_top, shaft_bottom = self._shaft_bounds()
|
|
265
|
+
tail_width = self._w - head_w
|
|
266
|
+
margin = DIVIDER_HANDLE_DIAMETER / 2.0
|
|
267
|
+
head_pos = QtCore.QPointF(self._w - head_w, shaft_top - margin)
|
|
268
|
+
body_pos = QtCore.QPointF(max(0.0, tail_width / 2.0), shaft_bottom + margin)
|
|
269
|
+
self._head_handle.setPos(head_pos)
|
|
270
|
+
self._body_handle.setPos(body_pos)
|
|
271
|
+
|
|
272
|
+
def _handle_special_drag(self, role: str, event: QtWidgets.QGraphicsSceneMouseEvent) -> None:
|
|
273
|
+
scene_pos = event.scenePos()
|
|
274
|
+
if not (event.modifiers() & QtCore.Qt.KeyboardModifier.AltModifier):
|
|
275
|
+
scene_pos = snap_to_grid(self, scene_pos)
|
|
276
|
+
local = self.mapFromScene(scene_pos)
|
|
277
|
+
if role == "head":
|
|
278
|
+
self._apply_head_drag(local.x())
|
|
279
|
+
else:
|
|
280
|
+
self._apply_body_drag(local.y())
|
|
281
|
+
|
|
282
|
+
def _apply_head_drag(self, local_x: float) -> None:
|
|
283
|
+
w = self._w
|
|
284
|
+
if w <= 0:
|
|
285
|
+
return
|
|
286
|
+
min_tail = max(8.0, min(w - 8.0, w * 0.1))
|
|
287
|
+
min_head = max(8.0, w * 0.1)
|
|
288
|
+
max_x = max(min_tail, w - min_head)
|
|
289
|
+
min_x = min_tail
|
|
290
|
+
if max_x < min_x:
|
|
291
|
+
min_x = max_x = w / 2.0
|
|
292
|
+
clamped_x = max(min_x, min(local_x, max_x))
|
|
293
|
+
ratio = (w - clamped_x) / w
|
|
294
|
+
self.set_head_ratio(ratio)
|
|
295
|
+
|
|
296
|
+
def _apply_body_drag(self, local_y: float) -> None:
|
|
297
|
+
h = self._h
|
|
298
|
+
if h <= 0:
|
|
299
|
+
return
|
|
300
|
+
center = h / 2.0
|
|
301
|
+
min_shaft = max(8.0, h * 0.15)
|
|
302
|
+
min_bottom = center + min_shaft / 2.0
|
|
303
|
+
clamped_y = max(min_bottom, min(local_y, h))
|
|
304
|
+
shaft_height = (clamped_y - center) * 2.0
|
|
305
|
+
ratio = shaft_height / h
|
|
306
|
+
self.set_shaft_ratio(ratio)
|
|
307
|
+
|
|
308
|
+
def update_handles(self): # type: ignore[override]
|
|
309
|
+
super().update_handles()
|
|
310
|
+
if self._handles:
|
|
311
|
+
tip: QtCore.QPointF | None = None
|
|
312
|
+
poly = self.polygon()
|
|
313
|
+
if len(poly) >= 4:
|
|
314
|
+
tip = QtCore.QPointF(poly[3])
|
|
315
|
+
else:
|
|
316
|
+
tip = QtCore.QPointF(self._w, self._h / 2.0)
|
|
317
|
+
for handle in self._handles:
|
|
318
|
+
if getattr(handle, "_direction", None) == "right":
|
|
319
|
+
handle.setPos(tip)
|
|
320
|
+
break
|
|
321
|
+
self._update_custom_handles()
|
|
322
|
+
|
|
323
|
+
def paint(self, painter, option, widget=None):
|
|
324
|
+
opt = QtWidgets.QStyleOptionGraphicsItem(option)
|
|
325
|
+
opt.state &= ~QtWidgets.QStyle.StateFlag.State_Selected
|
|
326
|
+
super().paint(painter, opt, widget)
|
|
327
|
+
if _should_draw_selection(self):
|
|
328
|
+
painter.save()
|
|
329
|
+
painter.setPen(PEN_SELECTED)
|
|
330
|
+
painter.setBrush(QtCore.Qt.BrushStyle.NoBrush)
|
|
331
|
+
painter.drawRect(self.boundingRect())
|
|
332
|
+
painter.restore()
|
|
333
|
+
|
|
334
|
+
def show_handles_once(self):
|
|
335
|
+
if not self._head_handle:
|
|
336
|
+
self._head_handle = BlockArrowHandle(self, "head")
|
|
337
|
+
self._head_handle.setZValue(1.0)
|
|
338
|
+
if not self._body_handle:
|
|
339
|
+
self._body_handle = BlockArrowHandle(self, "body")
|
|
340
|
+
self._body_handle.setZValue(1.0)
|
|
341
|
+
self._update_custom_handles()
|
|
342
|
+
|
|
343
|
+
def show_handles(self): # type: ignore[override]
|
|
344
|
+
self.show_handles_once()
|
|
345
|
+
super().show_handles()
|
|
346
|
+
if self._head_handle:
|
|
347
|
+
self._head_handle.show()
|
|
348
|
+
if self._body_handle:
|
|
349
|
+
self._body_handle.show()
|
|
350
|
+
|
|
351
|
+
def hide_handles(self): # type: ignore[override]
|
|
352
|
+
super().hide_handles()
|
|
353
|
+
if self._head_handle:
|
|
354
|
+
self._head_handle.hide()
|
|
355
|
+
if self._body_handle:
|
|
356
|
+
self._body_handle.hide()
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
__all__ = ["BlockArrowHandle", "BlockArrowItem", "DiamondItem", "TriangleItem"]
|
items/shapes/rects.py
ADDED
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
"""Rectangular shapes and related handles."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from PySide6 import QtCore, QtGui, QtWidgets
|
|
6
|
+
|
|
7
|
+
from constants import DEFAULT_FILL, PEN_NORMAL, PEN_SELECTED
|
|
8
|
+
from ..base import (
|
|
9
|
+
DIVIDER_HANDLE_COLOR,
|
|
10
|
+
DIVIDER_HANDLE_DIAMETER,
|
|
11
|
+
ResizableItem,
|
|
12
|
+
_should_draw_selection,
|
|
13
|
+
)
|
|
14
|
+
from ..labels import ShapeLabelMixin
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class SplitDividerHandle(QtWidgets.QGraphicsEllipseItem):
|
|
18
|
+
"""Drag handle for the horizontal split in :class:`SplitRoundedRectItem`."""
|
|
19
|
+
|
|
20
|
+
def __init__(self, parent: "SplitRoundedRectItem"):
|
|
21
|
+
radius = DIVIDER_HANDLE_DIAMETER / 2.0
|
|
22
|
+
super().__init__(
|
|
23
|
+
-radius,
|
|
24
|
+
-radius,
|
|
25
|
+
DIVIDER_HANDLE_DIAMETER,
|
|
26
|
+
DIVIDER_HANDLE_DIAMETER,
|
|
27
|
+
parent,
|
|
28
|
+
)
|
|
29
|
+
self.setBrush(DIVIDER_HANDLE_COLOR)
|
|
30
|
+
self.setPen(QtGui.QPen(QtCore.Qt.PenStyle.NoPen))
|
|
31
|
+
self.setAcceptedMouseButtons(QtCore.Qt.MouseButton.LeftButton)
|
|
32
|
+
self.setCursor(QtCore.Qt.CursorShape.SizeVerCursor)
|
|
33
|
+
self.setFlag(QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIgnoresTransformations)
|
|
34
|
+
self._parent_was_movable = False
|
|
35
|
+
|
|
36
|
+
def mousePressEvent(self, event: QtWidgets.QGraphicsSceneMouseEvent):
|
|
37
|
+
parent = self.parentItem()
|
|
38
|
+
if parent is not None:
|
|
39
|
+
flags = parent.flags()
|
|
40
|
+
self._parent_was_movable = bool(
|
|
41
|
+
flags & QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsMovable
|
|
42
|
+
)
|
|
43
|
+
if self._parent_was_movable:
|
|
44
|
+
parent.setFlag(
|
|
45
|
+
QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsMovable,
|
|
46
|
+
False,
|
|
47
|
+
)
|
|
48
|
+
event.accept()
|
|
49
|
+
|
|
50
|
+
def mouseMoveEvent(self, event: QtWidgets.QGraphicsSceneMouseEvent):
|
|
51
|
+
parent = self.parentItem()
|
|
52
|
+
if parent is not None:
|
|
53
|
+
parent._set_divider_from_scene_pos(event.scenePos()) # type: ignore[attr-defined]
|
|
54
|
+
event.accept()
|
|
55
|
+
|
|
56
|
+
def mouseReleaseEvent(self, event: QtWidgets.QGraphicsSceneMouseEvent):
|
|
57
|
+
parent = self.parentItem()
|
|
58
|
+
if parent is not None and self._parent_was_movable:
|
|
59
|
+
parent.setFlag(
|
|
60
|
+
QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsMovable,
|
|
61
|
+
True,
|
|
62
|
+
)
|
|
63
|
+
self._parent_was_movable = False
|
|
64
|
+
event.accept()
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class RectItem(ShapeLabelMixin, ResizableItem, QtWidgets.QGraphicsRectItem):
|
|
68
|
+
def __init__(self, x, y, w, h, rx: float = 0.0, ry: float = 0.0):
|
|
69
|
+
QtWidgets.QGraphicsRectItem.__init__(self, 0, 0, w, h)
|
|
70
|
+
ResizableItem.__init__(self)
|
|
71
|
+
self.setPos(x, y)
|
|
72
|
+
self.setTransformOriginPoint(w / 2.0, h / 2.0)
|
|
73
|
+
self.setFlags(
|
|
74
|
+
QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsMovable
|
|
75
|
+
| QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsSelectable
|
|
76
|
+
| QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemSendsGeometryChanges
|
|
77
|
+
| QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsFocusable
|
|
78
|
+
)
|
|
79
|
+
self.rx = rx
|
|
80
|
+
self.ry = ry
|
|
81
|
+
self._init_shape_label()
|
|
82
|
+
self.setPen(PEN_NORMAL)
|
|
83
|
+
self.setBrush(DEFAULT_FILL)
|
|
84
|
+
|
|
85
|
+
def _label_base_rect(self) -> QtCore.QRectF:
|
|
86
|
+
return QtCore.QRectF(self.rect())
|
|
87
|
+
|
|
88
|
+
def setRect(self, x: float, y: float, w: float, h: float) -> None: # type: ignore[override]
|
|
89
|
+
QtWidgets.QGraphicsRectItem.setRect(self, x, y, w, h)
|
|
90
|
+
self._update_label_geometry()
|
|
91
|
+
|
|
92
|
+
def setPen(self, pen): # type: ignore[override]
|
|
93
|
+
super().setPen(pen)
|
|
94
|
+
self._update_label_color()
|
|
95
|
+
|
|
96
|
+
def mouseDoubleClickEvent(
|
|
97
|
+
self, event: QtWidgets.QGraphicsSceneMouseEvent
|
|
98
|
+
) -> None: # type: ignore[override]
|
|
99
|
+
if event.button() == QtCore.Qt.MouseButton.LeftButton:
|
|
100
|
+
self._begin_label_edit()
|
|
101
|
+
event.accept()
|
|
102
|
+
return
|
|
103
|
+
super().mouseDoubleClickEvent(event)
|
|
104
|
+
|
|
105
|
+
def paint(self, painter, option, widget=None):
|
|
106
|
+
opt = QtWidgets.QStyleOptionGraphicsItem(option)
|
|
107
|
+
opt.state &= ~QtWidgets.QStyle.StateFlag.State_Selected
|
|
108
|
+
if self.rx or self.ry:
|
|
109
|
+
painter.setPen(self.pen())
|
|
110
|
+
painter.setBrush(self.brush())
|
|
111
|
+
painter.drawRoundedRect(self.rect(), self.rx, self.ry)
|
|
112
|
+
else:
|
|
113
|
+
super().paint(painter, opt, widget)
|
|
114
|
+
if _should_draw_selection(self):
|
|
115
|
+
painter.save()
|
|
116
|
+
painter.setPen(PEN_SELECTED)
|
|
117
|
+
painter.setBrush(QtCore.Qt.BrushStyle.NoBrush)
|
|
118
|
+
painter.drawRect(self.boundingRect())
|
|
119
|
+
painter.restore()
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class SplitRoundedRectItem(ResizableItem, QtWidgets.QGraphicsRectItem):
|
|
123
|
+
def __init__(
|
|
124
|
+
self,
|
|
125
|
+
x: float,
|
|
126
|
+
y: float,
|
|
127
|
+
w: float,
|
|
128
|
+
h: float,
|
|
129
|
+
rx: float = 15.0,
|
|
130
|
+
ry: float | None = None,
|
|
131
|
+
):
|
|
132
|
+
QtWidgets.QGraphicsRectItem.__init__(self, 0, 0, w, h)
|
|
133
|
+
ResizableItem.__init__(self)
|
|
134
|
+
self.setPos(x, y)
|
|
135
|
+
self.setTransformOriginPoint(w / 2.0, h / 2.0)
|
|
136
|
+
self.setFlags(
|
|
137
|
+
QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsMovable
|
|
138
|
+
| QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsSelectable
|
|
139
|
+
| QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemSendsGeometryChanges
|
|
140
|
+
| QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsFocusable
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
self.rx = rx
|
|
144
|
+
self.ry = ry if ry is not None else rx
|
|
145
|
+
self._split_ratio = 1.0 / 3.0
|
|
146
|
+
self._top_brush = QtGui.QBrush(QtGui.QColor("#f6e3b0"))
|
|
147
|
+
self._bottom_brush = QtGui.QBrush(DEFAULT_FILL)
|
|
148
|
+
self._divider_pen = QtGui.QPen(QtGui.QColor("#777"), 1.5)
|
|
149
|
+
self._divider_pen.setCosmetic(True)
|
|
150
|
+
|
|
151
|
+
self.setPen(PEN_NORMAL)
|
|
152
|
+
|
|
153
|
+
self._divider_handle = SplitDividerHandle(self)
|
|
154
|
+
self._divider_handle.setZValue(1.0)
|
|
155
|
+
self._divider_handle.hide()
|
|
156
|
+
self._update_divider_handle()
|
|
157
|
+
|
|
158
|
+
def _handle_margin(self) -> float:
|
|
159
|
+
bounds = self._divider_handle.boundingRect()
|
|
160
|
+
return max(bounds.width(), bounds.height()) / 2.0
|
|
161
|
+
|
|
162
|
+
def _line_y(self) -> float:
|
|
163
|
+
rect = self.rect()
|
|
164
|
+
return rect.top() + rect.height() * self._split_ratio
|
|
165
|
+
|
|
166
|
+
def divider_ratio(self) -> float:
|
|
167
|
+
return self._split_ratio
|
|
168
|
+
|
|
169
|
+
def set_divider_ratio(self, ratio: float) -> None:
|
|
170
|
+
rect = self.rect()
|
|
171
|
+
if rect.height() <= 0:
|
|
172
|
+
self._split_ratio = 0.5
|
|
173
|
+
else:
|
|
174
|
+
clamped = max(0.0, min(1.0, ratio))
|
|
175
|
+
target = rect.top() + rect.height() * clamped
|
|
176
|
+
self.set_divider_y(target)
|
|
177
|
+
|
|
178
|
+
def set_divider_y(self, y: float) -> None:
|
|
179
|
+
rect = self.rect()
|
|
180
|
+
if rect.height() <= 0:
|
|
181
|
+
self._split_ratio = 0.5
|
|
182
|
+
else:
|
|
183
|
+
margin = self._handle_margin()
|
|
184
|
+
min_y = rect.top() + margin
|
|
185
|
+
max_y = rect.bottom() - margin
|
|
186
|
+
if max_y < min_y:
|
|
187
|
+
y = rect.center().y()
|
|
188
|
+
else:
|
|
189
|
+
y = min(max(y, min_y), max_y)
|
|
190
|
+
self._split_ratio = (y - rect.top()) / rect.height()
|
|
191
|
+
self.update()
|
|
192
|
+
self._update_divider_handle()
|
|
193
|
+
|
|
194
|
+
def _update_divider_handle(self) -> None:
|
|
195
|
+
rect = self.rect()
|
|
196
|
+
self._divider_handle.setPos(rect.center().x(), self._line_y())
|
|
197
|
+
|
|
198
|
+
def _set_divider_from_scene_pos(self, scene_pos: QtCore.QPointF) -> None:
|
|
199
|
+
local = self.mapFromScene(scene_pos)
|
|
200
|
+
self.set_divider_y(local.y())
|
|
201
|
+
|
|
202
|
+
def topBrush(self) -> QtGui.QBrush:
|
|
203
|
+
return QtGui.QBrush(self._top_brush)
|
|
204
|
+
|
|
205
|
+
def setTopBrush(self, brush: QtGui.QBrush | QtGui.QColor) -> None:
|
|
206
|
+
self._top_brush = QtGui.QBrush(brush)
|
|
207
|
+
self.update()
|
|
208
|
+
|
|
209
|
+
def bottomBrush(self) -> QtGui.QBrush:
|
|
210
|
+
return QtGui.QBrush(self._bottom_brush)
|
|
211
|
+
|
|
212
|
+
def setBottomBrush(self, brush: QtGui.QBrush | QtGui.QColor) -> None:
|
|
213
|
+
self._bottom_brush = QtGui.QBrush(brush)
|
|
214
|
+
self.update()
|
|
215
|
+
|
|
216
|
+
def brush(self) -> QtGui.QBrush: # type: ignore[override]
|
|
217
|
+
return self.bottomBrush()
|
|
218
|
+
|
|
219
|
+
def setBrush(self, brush: QtGui.QBrush | QtGui.QColor) -> None: # type: ignore[override]
|
|
220
|
+
self.setBottomBrush(brush)
|
|
221
|
+
|
|
222
|
+
def setPen(self, pen: QtGui.QPen | QtGui.QColor) -> None: # type: ignore[override]
|
|
223
|
+
qpen = QtGui.QPen(pen)
|
|
224
|
+
QtWidgets.QGraphicsRectItem.setPen(self, qpen)
|
|
225
|
+
divider_width = max(1.0, qpen.widthF() * 0.75)
|
|
226
|
+
self._divider_pen = QtGui.QPen(qpen.color(), divider_width)
|
|
227
|
+
self._divider_pen.setCosmetic(True)
|
|
228
|
+
self.update()
|
|
229
|
+
|
|
230
|
+
def show_handles(self): # type: ignore[override]
|
|
231
|
+
super().show_handles()
|
|
232
|
+
self._divider_handle.show()
|
|
233
|
+
self._update_divider_handle()
|
|
234
|
+
|
|
235
|
+
def hide_handles(self): # type: ignore[override]
|
|
236
|
+
super().hide_handles()
|
|
237
|
+
self._divider_handle.hide()
|
|
238
|
+
|
|
239
|
+
def update_handles(self): # type: ignore[override]
|
|
240
|
+
super().update_handles()
|
|
241
|
+
self._update_divider_handle()
|
|
242
|
+
|
|
243
|
+
def setRect(self, x: float, y: float, w: float, h: float) -> None: # type: ignore[override]
|
|
244
|
+
QtWidgets.QGraphicsRectItem.setRect(self, x, y, w, h)
|
|
245
|
+
self._update_divider_handle()
|
|
246
|
+
|
|
247
|
+
def paint(self, painter, option, widget=None): # type: ignore[override]
|
|
248
|
+
rect = self.rect()
|
|
249
|
+
painter.save()
|
|
250
|
+
painter.setRenderHint(QtGui.QPainter.RenderHint.Antialiasing, True)
|
|
251
|
+
|
|
252
|
+
rx = max(0.0, min(self.rx, rect.width() / 2.0, 50.0))
|
|
253
|
+
ry = max(0.0, min(self.ry, rect.height() / 2.0, 50.0))
|
|
254
|
+
|
|
255
|
+
base_path = QtGui.QPainterPath()
|
|
256
|
+
if rx > 0.0 or ry > 0.0:
|
|
257
|
+
base_path.addRoundedRect(rect, rx, ry)
|
|
258
|
+
else:
|
|
259
|
+
base_path.addRect(rect)
|
|
260
|
+
|
|
261
|
+
line_y = self._line_y()
|
|
262
|
+
|
|
263
|
+
top_clip = QtGui.QPainterPath()
|
|
264
|
+
top_clip.addRect(rect.left(), rect.top(), rect.width(), max(0.0, line_y - rect.top()))
|
|
265
|
+
top_path = base_path.intersected(top_clip)
|
|
266
|
+
if not top_path.isEmpty():
|
|
267
|
+
painter.fillPath(top_path, self._top_brush)
|
|
268
|
+
|
|
269
|
+
bottom_clip = QtGui.QPainterPath()
|
|
270
|
+
bottom_clip.addRect(
|
|
271
|
+
rect.left(),
|
|
272
|
+
line_y,
|
|
273
|
+
rect.width(),
|
|
274
|
+
max(0.0, rect.bottom() - line_y),
|
|
275
|
+
)
|
|
276
|
+
bottom_path = base_path.intersected(bottom_clip)
|
|
277
|
+
if not bottom_path.isEmpty():
|
|
278
|
+
painter.fillPath(bottom_path, self._bottom_brush)
|
|
279
|
+
|
|
280
|
+
painter.setBrush(QtCore.Qt.BrushStyle.NoBrush)
|
|
281
|
+
painter.setPen(self.pen())
|
|
282
|
+
if rx > 0.0 or ry > 0.0:
|
|
283
|
+
painter.drawRoundedRect(rect, rx, ry)
|
|
284
|
+
else:
|
|
285
|
+
painter.drawRect(rect)
|
|
286
|
+
|
|
287
|
+
painter.setPen(self._divider_pen)
|
|
288
|
+
painter.drawLine(rect.left(), line_y, rect.right(), line_y)
|
|
289
|
+
painter.restore()
|
|
290
|
+
|
|
291
|
+
if _should_draw_selection(self):
|
|
292
|
+
painter.save()
|
|
293
|
+
painter.setPen(PEN_SELECTED)
|
|
294
|
+
painter.setBrush(QtCore.Qt.BrushStyle.NoBrush)
|
|
295
|
+
painter.drawRect(self.boundingRect())
|
|
296
|
+
painter.restore()
|
|
297
|
+
|
|
298
|
+
def shape(self) -> QtGui.QPainterPath: # type: ignore[override]
|
|
299
|
+
rect = self.rect()
|
|
300
|
+
path = QtGui.QPainterPath()
|
|
301
|
+
rx = max(0.0, min(self.rx, rect.width() / 2.0, 50.0))
|
|
302
|
+
ry = max(0.0, min(self.ry, rect.height() / 2.0, 50.0))
|
|
303
|
+
if rx > 0.0 or ry > 0.0:
|
|
304
|
+
path.addRoundedRect(rect, rx, ry)
|
|
305
|
+
else:
|
|
306
|
+
path.addRect(rect)
|
|
307
|
+
return path
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
__all__ = ["RectItem", "SplitDividerHandle", "SplitRoundedRectItem"]
|