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