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,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"]