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.
items/text.py ADDED
@@ -0,0 +1,331 @@
1
+ """Text and grouping items."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from PySide6 import QtCore, QtGui, QtWidgets
6
+
7
+ from .base import ResizableItem, ResizeHandle, RotationHandle, _should_draw_selection
8
+ from constants import PEN_SELECTED, DEFAULT_TEXT_COLOR, DEFAULT_FONT_FAMILY
9
+
10
+
11
+ class TextItem(ResizableItem, QtWidgets.QGraphicsTextItem):
12
+ """Editable text item whose bounding box stays user-controlled."""
13
+
14
+ _MIN_DIMENSION = 10.0
15
+ _VALID_H_ALIGN = ("left", "center", "right")
16
+ _VALID_V_ALIGN = ("top", "middle", "bottom")
17
+ _VALID_DIRECTIONS = {
18
+ "ltr": QtCore.Qt.LayoutDirection.LeftToRight,
19
+ "rtl": QtCore.Qt.LayoutDirection.RightToLeft,
20
+ }
21
+
22
+ def __init__(self, x, y, w, h):
23
+ QtWidgets.QGraphicsTextItem.__init__(self, "Text")
24
+ self._box_size = QtCore.QSizeF(1.0, 1.0)
25
+ self._auto_size = False
26
+ self._text_h_align = "left"
27
+ self._text_v_align = "top"
28
+ self._text_direction = "ltr"
29
+ self._content_offset = QtCore.QPointF()
30
+ ResizableItem.__init__(self)
31
+ font = QtGui.QFont(DEFAULT_FONT_FAMILY)
32
+ font.setPointSizeF(24.0)
33
+ super().setFont(font)
34
+ self.setDefaultTextColor(DEFAULT_TEXT_COLOR)
35
+ self.setTextInteractionFlags(QtCore.Qt.TextInteractionFlag.NoTextInteraction)
36
+ self.setFlags(
37
+ QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsMovable
38
+ | QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsSelectable
39
+ | QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemSendsGeometryChanges
40
+ | QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsFocusable,
41
+ )
42
+ base_rect = QtWidgets.QGraphicsTextItem.boundingRect(self)
43
+ width = float(w) if w and w > 0.0 else base_rect.width()
44
+ height = float(h) if h and h > 0.0 else base_rect.height()
45
+ self._auto_size = not (w and w > 0.0 and h and h > 0.0)
46
+ self._set_box_size(width, height, update_origin=True, from_init=True)
47
+ self.setPos(x, y)
48
+
49
+ def setPlainText(self, text: str) -> None: # type: ignore[override]
50
+ super().setPlainText(text)
51
+ if self._auto_size:
52
+ content_rect = QtWidgets.QGraphicsTextItem.boundingRect(self)
53
+ self._set_box_size(
54
+ content_rect.width(), content_rect.height(), update_origin=True
55
+ )
56
+ self._auto_size = False
57
+ else:
58
+ self._update_document_constraints()
59
+ self._update_transform_origin()
60
+ if _should_draw_selection(self):
61
+ self.update_handles()
62
+ self.update()
63
+
64
+ def setFont(self, font: QtGui.QFont) -> None: # type: ignore[override]
65
+ super().setFont(font)
66
+ self._update_document_constraints()
67
+ self._update_transform_origin()
68
+ if _should_draw_selection(self):
69
+ self.update_handles()
70
+ self.update()
71
+
72
+ def paint(self, painter, option, widget=None):
73
+ rect = self.boundingRect()
74
+ painter.save()
75
+ painter.setClipRect(rect)
76
+ painter.translate(self._content_offset)
77
+ opt = QtWidgets.QStyleOptionGraphicsItem(option)
78
+ opt.state &= ~QtWidgets.QStyle.StateFlag.State_Selected
79
+ super().paint(painter, opt, widget)
80
+ painter.restore()
81
+ if _should_draw_selection(self):
82
+ painter.save()
83
+ painter.setPen(PEN_SELECTED)
84
+ painter.setBrush(QtCore.Qt.BrushStyle.NoBrush)
85
+ painter.drawRect(rect)
86
+ painter.restore()
87
+
88
+ def mouseDoubleClickEvent(self, event):
89
+ self.setTextInteractionFlags(QtCore.Qt.TextInteractionFlag.TextEditorInteraction)
90
+ self.setFocus()
91
+ super().mouseDoubleClickEvent(event)
92
+
93
+ def focusOutEvent(self, event):
94
+ super().focusOutEvent(event)
95
+ self.setTextInteractionFlags(QtCore.Qt.TextInteractionFlag.NoTextInteraction)
96
+ self.setPlainText(self.toPlainText())
97
+
98
+ def set_document_margin(self, margin: float) -> None:
99
+ """Adjust the QTextDocument margin and keep layout in sync."""
100
+
101
+ doc = self.document()
102
+ if doc is None:
103
+ return
104
+ doc.setDocumentMargin(max(0.0, float(margin)))
105
+ self._update_document_constraints()
106
+ self._update_transform_origin()
107
+ if _should_draw_selection(self):
108
+ self.update_handles()
109
+ self.update()
110
+
111
+ def set_size(self, width: float, height: float, adjust_origin: bool = True) -> None:
112
+ """Resize the bounding box without scaling the text."""
113
+
114
+ self._auto_size = False
115
+ self._set_box_size(width, height, update_origin=adjust_origin)
116
+
117
+ def text_alignment(self) -> tuple[str, str]:
118
+ return self._text_h_align, self._text_v_align
119
+
120
+ def set_text_alignment(
121
+ self,
122
+ *,
123
+ horizontal: str | None = None,
124
+ vertical: str | None = None,
125
+ ) -> None:
126
+ changed = False
127
+ if horizontal in self._VALID_H_ALIGN and horizontal != self._text_h_align:
128
+ self._text_h_align = horizontal
129
+ changed = True
130
+ if vertical in self._VALID_V_ALIGN and vertical != self._text_v_align:
131
+ self._text_v_align = vertical
132
+ changed = True
133
+ if changed:
134
+ self._update_document_constraints()
135
+ self._update_transform_origin()
136
+ if _should_draw_selection(self):
137
+ self.update_handles()
138
+ self.update()
139
+
140
+ def text_direction(self) -> str:
141
+ return self._text_direction
142
+
143
+ def set_text_direction(self, direction: str) -> None:
144
+ if direction not in self._VALID_DIRECTIONS:
145
+ return
146
+ if direction == self._text_direction:
147
+ return
148
+ self._text_direction = direction
149
+ self._update_document_constraints()
150
+ self._update_transform_origin()
151
+ if _should_draw_selection(self):
152
+ self.update_handles()
153
+ self.update()
154
+
155
+ def boundingRect(self) -> QtCore.QRectF: # type: ignore[override]
156
+ return QtCore.QRectF(
157
+ 0.0,
158
+ 0.0,
159
+ self._box_size.width(),
160
+ self._box_size.height(),
161
+ )
162
+
163
+ def shape(self) -> QtGui.QPainterPath: # type: ignore[override]
164
+ path = QtGui.QPainterPath()
165
+ path.addRect(self.boundingRect())
166
+ return path
167
+
168
+ def _set_box_size(
169
+ self,
170
+ width: float,
171
+ height: float,
172
+ *,
173
+ update_origin: bool,
174
+ from_init: bool = False,
175
+ ) -> None:
176
+ width = max(self._MIN_DIMENSION, float(width))
177
+ height = max(self._MIN_DIMENSION, float(height))
178
+ prev_width = self._box_size.width()
179
+ prev_height = self._box_size.height()
180
+ size_changed = (
181
+ abs(prev_width - width) > 1e-3 or abs(prev_height - height) > 1e-3
182
+ )
183
+ if not from_init:
184
+ self.prepareGeometryChange()
185
+ self._box_size = QtCore.QSizeF(width, height)
186
+ self._update_document_constraints(adjust_layout=size_changed and not from_init)
187
+ if update_origin:
188
+ self._update_transform_origin()
189
+ if not from_init and _should_draw_selection(self):
190
+ self.update_handles()
191
+ if not from_init:
192
+ self.update()
193
+ snapped = self._snap_position_value(QtCore.QPointF(self.pos()))
194
+ if isinstance(snapped, QtCore.QPointF) and snapped != self.pos():
195
+ QtWidgets.QGraphicsTextItem.setPos(self, snapped)
196
+
197
+ def _update_document_constraints(self, *, adjust_layout: bool = False) -> None:
198
+ doc = self.document()
199
+ if doc is None:
200
+ return
201
+ margin = doc.documentMargin()
202
+ available_width = self._box_size.width() - 2.0 * margin
203
+ text_width = available_width if available_width > 0.0 else -1.0
204
+ page_width = (
205
+ available_width if available_width > 0.0 else self._box_size.width()
206
+ )
207
+ doc_height = max(self._MIN_DIMENSION, self._box_size.height())
208
+ self.setTextWidth(text_width)
209
+ doc.setPageSize(QtCore.QSizeF(max(0.0, page_width), doc_height))
210
+ if adjust_layout:
211
+ doc.adjustSize()
212
+ self.setTextWidth(text_width)
213
+ doc.setPageSize(QtCore.QSizeF(max(0.0, page_width), doc_height))
214
+ self._apply_text_alignment()
215
+ self._update_content_offset()
216
+
217
+ def _apply_text_alignment(self) -> None:
218
+ doc = self.document()
219
+ if doc is None:
220
+ return
221
+ option = doc.defaultTextOption()
222
+ alignment = option.alignment()
223
+ alignment &= ~(
224
+ QtCore.Qt.AlignmentFlag.AlignLeft
225
+ | QtCore.Qt.AlignmentFlag.AlignRight
226
+ | QtCore.Qt.AlignmentFlag.AlignHCenter
227
+ )
228
+ if self._text_h_align == "right":
229
+ alignment |= QtCore.Qt.AlignmentFlag.AlignRight
230
+ elif self._text_h_align == "center":
231
+ alignment |= QtCore.Qt.AlignmentFlag.AlignHCenter
232
+ else:
233
+ alignment |= QtCore.Qt.AlignmentFlag.AlignLeft
234
+ option.setAlignment(alignment)
235
+ option.setTextDirection(self._VALID_DIRECTIONS[self._text_direction])
236
+ doc.setDefaultTextOption(option)
237
+
238
+ def _update_content_offset(self) -> None:
239
+ content_rect = QtWidgets.QGraphicsTextItem.boundingRect(self)
240
+ box = self._box_size
241
+
242
+ left = content_rect.left()
243
+ top = content_rect.top()
244
+ right = content_rect.right()
245
+ bottom = content_rect.bottom()
246
+
247
+ # Horizontal offset
248
+ if self._text_h_align == "right":
249
+ dx = box.width() - right
250
+ elif self._text_h_align == "center":
251
+ dx = (box.width() - content_rect.width()) * 0.5 - left
252
+ else:
253
+ dx = -left
254
+ min_dx = -left
255
+ max_dx = box.width() - right
256
+ dx = max(min_dx, min(dx, max_dx))
257
+
258
+ # Vertical offset
259
+ if self._text_v_align == "bottom":
260
+ dy = box.height() - bottom
261
+ elif self._text_v_align == "middle":
262
+ dy = (box.height() - content_rect.height()) * 0.5 - top
263
+ else:
264
+ dy = -top
265
+ min_dy = -top
266
+ max_dy = box.height() - bottom
267
+ dy = max(min_dy, min(dy, max_dy))
268
+
269
+ self._content_offset = QtCore.QPointF(dx, dy)
270
+
271
+ def _update_transform_origin(self) -> None:
272
+ self.setTransformOriginPoint(
273
+ self._box_size.width() / 2.0, self._box_size.height() / 2.0
274
+ )
275
+
276
+
277
+ class GroupItem(ResizableItem, QtWidgets.QGraphicsItemGroup):
278
+ """Group of multiple items with shared handles."""
279
+
280
+ def __init__(self):
281
+ QtWidgets.QGraphicsItemGroup.__init__(self)
282
+ ResizableItem.__init__(self)
283
+ self.setFlags(
284
+ QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsMovable
285
+ | QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsSelectable
286
+ | QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemSendsGeometryChanges
287
+ | QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsFocusable
288
+ )
289
+ self.setData(0, "Group")
290
+ self.setHandlesChildEvents(False)
291
+
292
+ def _handle_rect(self) -> QtCore.QRectF: # type: ignore[override]
293
+ tight = self._contentRect()
294
+ if not tight.isNull():
295
+ return tight
296
+ return QtWidgets.QGraphicsItemGroup.boundingRect(self)
297
+
298
+ def _contentRect(self) -> QtCore.QRectF:
299
+ rect = QtCore.QRectF()
300
+ first = True
301
+ for child in self.childItems():
302
+ if isinstance(child, (ResizeHandle, RotationHandle)):
303
+ continue
304
+ child_rect = child.mapToParent(child.boundingRect()).boundingRect()
305
+ rect = child_rect if first else rect.united(child_rect)
306
+ first = False
307
+ return rect if not first else QtCore.QRectF()
308
+
309
+ def update_handles(self): # type: ignore[override]
310
+ rect = self._handle_rect()
311
+ if not rect.isNull():
312
+ self.setTransformOriginPoint(rect.center())
313
+ else:
314
+ self.setTransformOriginPoint(QtCore.QPointF())
315
+ super().update_handles()
316
+
317
+ def paint(self, painter, option, widget=None):
318
+ if _should_draw_selection(self):
319
+ painter.save()
320
+ painter.setPen(PEN_SELECTED)
321
+ painter.setBrush(QtCore.Qt.BrushStyle.NoBrush)
322
+ tight = self._contentRect()
323
+ if not tight.isNull():
324
+ half = PEN_SELECTED.widthF() * 0.5
325
+ painter.drawRect(tight.adjusted(half, half, -half, -half))
326
+ else:
327
+ painter.drawRect(self.boundingRect())
328
+ painter.restore()
329
+
330
+
331
+ __all__ = ["GroupItem", "TextItem"]
@@ -0,0 +1,5 @@
1
+ """Widget-like item exports."""
2
+
3
+ from .folder_tree import FolderTreeBranchDot, FolderTreeItem, FolderTreeNode
4
+
5
+ __all__ = ["FolderTreeBranchDot", "FolderTreeItem", "FolderTreeNode"]