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
properties_panel.py
ADDED
|
@@ -0,0 +1,1406 @@
|
|
|
1
|
+
"""Interactive properties panel that allows editing item attributes."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import math
|
|
6
|
+
import weakref
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any, Callable, TYPE_CHECKING
|
|
9
|
+
|
|
10
|
+
from PySide6 import QtCore, QtGui, QtWidgets
|
|
11
|
+
from PySide6.QtUiTools import QUiLoader
|
|
12
|
+
|
|
13
|
+
from items import (
|
|
14
|
+
BlockArrowItem,
|
|
15
|
+
CurvyBracketItem,
|
|
16
|
+
DiamondItem,
|
|
17
|
+
EllipseItem,
|
|
18
|
+
LineItem,
|
|
19
|
+
RectItem,
|
|
20
|
+
ShapeLabelMixin,
|
|
21
|
+
SplitRoundedRectItem,
|
|
22
|
+
TextItem,
|
|
23
|
+
TriangleItem,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
if TYPE_CHECKING: # pragma: no cover - only for typing
|
|
27
|
+
from canvas_view import CanvasView
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
Number = float
|
|
31
|
+
|
|
32
|
+
_UI_PATH = Path(__file__).resolve().parent / "ui" / "properties_panel.ui"
|
|
33
|
+
|
|
34
|
+
class ColorButton(QtWidgets.QToolButton):
|
|
35
|
+
"""Tool button that displays and edits a QColor."""
|
|
36
|
+
|
|
37
|
+
colorChanged = QtCore.Signal(QtGui.QColor)
|
|
38
|
+
|
|
39
|
+
def __init__(self, color: QtGui.QColor | str | None = None, parent: QtWidgets.QWidget | None = None) -> None:
|
|
40
|
+
super().__init__(parent)
|
|
41
|
+
self._color = QtGui.QColor()
|
|
42
|
+
self.setAutoRaise(True)
|
|
43
|
+
self.setCursor(QtCore.Qt.CursorShape.PointingHandCursor)
|
|
44
|
+
self.setToolButtonStyle(QtCore.Qt.ToolButtonStyle.ToolButtonTextBesideIcon)
|
|
45
|
+
self.setMinimumHeight(26)
|
|
46
|
+
self.setSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed)
|
|
47
|
+
self.clicked.connect(self._choose_color)
|
|
48
|
+
self.setColor(color or QtGui.QColor("#000000"))
|
|
49
|
+
|
|
50
|
+
def color(self) -> QtGui.QColor:
|
|
51
|
+
return QtGui.QColor(self._color)
|
|
52
|
+
|
|
53
|
+
def setColor(self, value: QtGui.QColor | str | None) -> None:
|
|
54
|
+
if isinstance(value, QtGui.QColor):
|
|
55
|
+
color = QtGui.QColor(value)
|
|
56
|
+
elif isinstance(value, str):
|
|
57
|
+
color = QtGui.QColor(value)
|
|
58
|
+
else:
|
|
59
|
+
color = QtGui.QColor("#00000000")
|
|
60
|
+
if not color.isValid():
|
|
61
|
+
color = QtGui.QColor("#00000000")
|
|
62
|
+
if self._color == color:
|
|
63
|
+
return
|
|
64
|
+
self._color = color
|
|
65
|
+
self._update_visuals()
|
|
66
|
+
|
|
67
|
+
def _choose_color(self) -> None:
|
|
68
|
+
dialog = QtWidgets.QColorDialog(self._color, self)
|
|
69
|
+
dialog.setOption(QtWidgets.QColorDialog.ColorDialogOption.ShowAlphaChannel, True)
|
|
70
|
+
if dialog.exec():
|
|
71
|
+
new_color = dialog.currentColor()
|
|
72
|
+
if new_color.isValid():
|
|
73
|
+
if self._color != new_color:
|
|
74
|
+
self._color = QtGui.QColor(new_color)
|
|
75
|
+
self._update_visuals()
|
|
76
|
+
self.colorChanged.emit(QtGui.QColor(self._color))
|
|
77
|
+
|
|
78
|
+
def _update_visuals(self) -> None:
|
|
79
|
+
rgba = self._color.name(QtGui.QColor.HexArgb)
|
|
80
|
+
text = self._color.name(QtGui.QColor.HexRgb) if self._color.alphaF() >= 0.99 else rgba
|
|
81
|
+
self.setText(text.upper())
|
|
82
|
+
luminance = 0.2126 * self._color.redF() + 0.7152 * self._color.greenF() + 0.0722 * self._color.blueF()
|
|
83
|
+
foreground = "#000000" if luminance > 0.6 or self._color.alphaF() < 0.5 else "#ffffff"
|
|
84
|
+
self.setStyleSheet(
|
|
85
|
+
"QToolButton {"
|
|
86
|
+
f"background-color: {rgba};"
|
|
87
|
+
"border: 1px solid #666;"
|
|
88
|
+
f"color: {foreground};"
|
|
89
|
+
"padding: 3px 8px;"
|
|
90
|
+
"}"
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class PlainTextEditor(QtWidgets.QPlainTextEdit):
|
|
95
|
+
"""Plain text editor that emits editingFinished on focus loss or Ctrl+Enter."""
|
|
96
|
+
|
|
97
|
+
editingFinished = QtCore.Signal()
|
|
98
|
+
|
|
99
|
+
def focusOutEvent(self, event: QtGui.QFocusEvent) -> None: # type: ignore[override]
|
|
100
|
+
super().focusOutEvent(event)
|
|
101
|
+
self.editingFinished.emit()
|
|
102
|
+
|
|
103
|
+
def keyPressEvent(self, event: QtGui.QKeyEvent) -> None: # type: ignore[override]
|
|
104
|
+
if event.key() in (QtCore.Qt.Key.Key_Return, QtCore.Qt.Key.Key_Enter) and (
|
|
105
|
+
event.modifiers() & QtCore.Qt.KeyboardModifier.ControlModifier
|
|
106
|
+
):
|
|
107
|
+
self.editingFinished.emit()
|
|
108
|
+
event.accept()
|
|
109
|
+
return
|
|
110
|
+
super().keyPressEvent(event)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class _PropertiesUiLoader(QUiLoader):
|
|
114
|
+
"""QUiLoader that embeds the .ui file into an existing QWidget instance."""
|
|
115
|
+
|
|
116
|
+
def __init__(self, baseinstance: QtWidgets.QWidget) -> None:
|
|
117
|
+
super().__init__(baseinstance)
|
|
118
|
+
self._baseinstance = baseinstance
|
|
119
|
+
self.registerCustomWidget(ColorButton)
|
|
120
|
+
self.registerCustomWidget(PlainTextEditor)
|
|
121
|
+
|
|
122
|
+
def createWidget(
|
|
123
|
+
self,
|
|
124
|
+
class_name: str,
|
|
125
|
+
parent: QtWidgets.QWidget | None = None,
|
|
126
|
+
name: str = "",
|
|
127
|
+
) -> QtWidgets.QWidget:
|
|
128
|
+
if parent is None and self._baseinstance is not None:
|
|
129
|
+
return self._baseinstance
|
|
130
|
+
widget = super().createWidget(class_name, parent, name)
|
|
131
|
+
if self._baseinstance is not None and name:
|
|
132
|
+
setattr(self._baseinstance, name, widget)
|
|
133
|
+
return widget
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class PropertyBinding(QtCore.QObject):
|
|
137
|
+
"""Connects a UI widget with getter/setter callables."""
|
|
138
|
+
|
|
139
|
+
def __init__(
|
|
140
|
+
self,
|
|
141
|
+
widget: QtWidgets.QWidget,
|
|
142
|
+
getter: Callable[[], Any],
|
|
143
|
+
setter: Callable[[Any], bool | None],
|
|
144
|
+
change_signal: QtCore.SignalInstance,
|
|
145
|
+
read_from_widget: Callable[[QtWidgets.QWidget], Any],
|
|
146
|
+
write_to_widget: Callable[[QtWidgets.QWidget, Any], None],
|
|
147
|
+
change_callback: Callable[[], None],
|
|
148
|
+
) -> None:
|
|
149
|
+
super().__init__(widget)
|
|
150
|
+
self.widget = widget
|
|
151
|
+
self._getter = getter
|
|
152
|
+
self._setter = setter
|
|
153
|
+
self._read = read_from_widget
|
|
154
|
+
self._write = write_to_widget
|
|
155
|
+
self._change_callback = change_callback
|
|
156
|
+
self._guard = False
|
|
157
|
+
change_signal.connect(self._on_widget_changed)
|
|
158
|
+
|
|
159
|
+
def refresh(self) -> None:
|
|
160
|
+
if self.widget is None:
|
|
161
|
+
return
|
|
162
|
+
value = self._getter()
|
|
163
|
+
self._guard = True
|
|
164
|
+
try:
|
|
165
|
+
blocker = QtCore.QSignalBlocker(self.widget)
|
|
166
|
+
self._write(self.widget, value)
|
|
167
|
+
finally:
|
|
168
|
+
self._guard = False
|
|
169
|
+
|
|
170
|
+
def _on_widget_changed(self, *args: Any) -> None:
|
|
171
|
+
if self._guard or self.widget is None:
|
|
172
|
+
return
|
|
173
|
+
self._guard = True
|
|
174
|
+
try:
|
|
175
|
+
value = self._read(self.widget)
|
|
176
|
+
changed = self._setter(value)
|
|
177
|
+
finally:
|
|
178
|
+
self._guard = False
|
|
179
|
+
if changed is not False:
|
|
180
|
+
self._change_callback()
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
class PropertiesPanel(QtWidgets.QWidget):
|
|
184
|
+
"""Interactive properties panel that keeps the canvas in sync with edits."""
|
|
185
|
+
|
|
186
|
+
def __init__(
|
|
187
|
+
self,
|
|
188
|
+
canvas: CanvasView | None = None,
|
|
189
|
+
parent: QtWidgets.QWidget | None = None,
|
|
190
|
+
) -> None:
|
|
191
|
+
super().__init__(parent)
|
|
192
|
+
self.setMinimumWidth(260)
|
|
193
|
+
self._canvas = canvas
|
|
194
|
+
self._current_item: QtWidgets.QGraphicsItem | None = None
|
|
195
|
+
self._object_bindings: list[PropertyBinding] = []
|
|
196
|
+
self._text_bindings: list[PropertyBinding] = []
|
|
197
|
+
self._latest_object_data: dict[str, Any] = {}
|
|
198
|
+
self._latest_text_data: dict[str, Any] = {}
|
|
199
|
+
self._half_width_widgets: list[weakref.ReferenceType[QtWidgets.QWidget]] = []
|
|
200
|
+
self._spacer_visible = False
|
|
201
|
+
|
|
202
|
+
self._load_ui()
|
|
203
|
+
self.setMinimumWidth(260) # loader resets widget properties
|
|
204
|
+
self.setSizePolicy(
|
|
205
|
+
QtWidgets.QSizePolicy.Policy.Preferred,
|
|
206
|
+
QtWidgets.QSizePolicy.Policy.Expanding,
|
|
207
|
+
)
|
|
208
|
+
self._layout = self.layout()
|
|
209
|
+
if self._layout is None:
|
|
210
|
+
self._layout = QtWidgets.QVBoxLayout(self)
|
|
211
|
+
self.setLayout(self._layout)
|
|
212
|
+
|
|
213
|
+
self._empty_spacer = QtWidgets.QSpacerItem(
|
|
214
|
+
0,
|
|
215
|
+
0,
|
|
216
|
+
QtWidgets.QSizePolicy.Policy.Minimum,
|
|
217
|
+
QtWidgets.QSizePolicy.Policy.Expanding,
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
self._title_label = self._require_widget(QtWidgets.QLabel, "titleLabel")
|
|
221
|
+
title_font = self._title_label.font()
|
|
222
|
+
title_font.setBold(True)
|
|
223
|
+
self._title_label.setFont(title_font)
|
|
224
|
+
|
|
225
|
+
self._info_label = self._require_widget(QtWidgets.QLabel, "infoLabel")
|
|
226
|
+
|
|
227
|
+
self._tab_widget = self._require_widget(QtWidgets.QTabWidget, "tabWidget")
|
|
228
|
+
self._tab_widget.setDocumentMode(True)
|
|
229
|
+
self._tab_widget.setMovable(False)
|
|
230
|
+
self._tab_widget.setSizePolicy(
|
|
231
|
+
QtWidgets.QSizePolicy.Policy.Preferred,
|
|
232
|
+
QtWidgets.QSizePolicy.Policy.Expanding,
|
|
233
|
+
)
|
|
234
|
+
if self._layout is not None:
|
|
235
|
+
index = self._layout.indexOf(self._tab_widget)
|
|
236
|
+
if index >= 0:
|
|
237
|
+
self._layout.setStretch(index, 1)
|
|
238
|
+
|
|
239
|
+
self._cache_object_widgets()
|
|
240
|
+
self._cache_text_widgets()
|
|
241
|
+
self._initialize_combobox_options()
|
|
242
|
+
self._initialize_half_width_tracking()
|
|
243
|
+
self._reset_object_sections()
|
|
244
|
+
self._reset_text_sections()
|
|
245
|
+
self._set_tab_widget_active(False)
|
|
246
|
+
self._set_empty_spacer_visible(True)
|
|
247
|
+
|
|
248
|
+
def resizeEvent(self, event: QtGui.QResizeEvent) -> None: # type: ignore[override]
|
|
249
|
+
super().resizeEvent(event)
|
|
250
|
+
self._update_half_width_widgets()
|
|
251
|
+
|
|
252
|
+
def _load_ui(self) -> None:
|
|
253
|
+
loader = _PropertiesUiLoader(self)
|
|
254
|
+
ui_file = QtCore.QFile(str(_UI_PATH))
|
|
255
|
+
if not ui_file.open(QtCore.QIODevice.OpenModeFlag.ReadOnly):
|
|
256
|
+
raise RuntimeError(f"Could not open UI file: {_UI_PATH}")
|
|
257
|
+
try:
|
|
258
|
+
if loader.load(ui_file) is None:
|
|
259
|
+
raise RuntimeError(f"Failed to load UI from {_UI_PATH}")
|
|
260
|
+
finally:
|
|
261
|
+
ui_file.close()
|
|
262
|
+
|
|
263
|
+
def _require_widget(
|
|
264
|
+
self,
|
|
265
|
+
widget_type: type[QtWidgets.QWidget],
|
|
266
|
+
object_name: str,
|
|
267
|
+
) -> QtWidgets.QWidget:
|
|
268
|
+
widget = self.findChild(widget_type, object_name)
|
|
269
|
+
if widget is None:
|
|
270
|
+
raise RuntimeError(f"Missing widget '{object_name}' in properties panel UI")
|
|
271
|
+
return widget
|
|
272
|
+
|
|
273
|
+
def _cache_object_widgets(self) -> None:
|
|
274
|
+
self._group_transform = self._require_widget(QtWidgets.QGroupBox, "groupTransform")
|
|
275
|
+
self._group_size = self._require_widget(QtWidgets.QGroupBox, "groupSize")
|
|
276
|
+
self._group_rounded = self._require_widget(QtWidgets.QGroupBox, "groupRoundedCorners")
|
|
277
|
+
self._group_sections = self._require_widget(QtWidgets.QGroupBox, "groupSections")
|
|
278
|
+
self._group_stroke = self._require_widget(QtWidgets.QGroupBox, "groupStroke")
|
|
279
|
+
self._group_fill = self._require_widget(QtWidgets.QGroupBox, "groupFill")
|
|
280
|
+
self._group_arrow_shape = self._require_widget(QtWidgets.QGroupBox, "groupArrowShape")
|
|
281
|
+
self._group_bracket = self._require_widget(QtWidgets.QGroupBox, "groupBracket")
|
|
282
|
+
self._group_line_arrows = self._require_widget(QtWidgets.QGroupBox, "groupLineArrows")
|
|
283
|
+
|
|
284
|
+
self._spin_pos_x = self._require_widget(QtWidgets.QDoubleSpinBox, "spinPosX")
|
|
285
|
+
self._spin_pos_y = self._require_widget(QtWidgets.QDoubleSpinBox, "spinPosY")
|
|
286
|
+
self._spin_rotation = self._require_widget(QtWidgets.QDoubleSpinBox, "spinRotation")
|
|
287
|
+
self._spin_scale = self._require_widget(QtWidgets.QDoubleSpinBox, "spinScale")
|
|
288
|
+
self._spin_z_value = self._require_widget(QtWidgets.QDoubleSpinBox, "spinZValue")
|
|
289
|
+
self._spin_width = self._require_widget(QtWidgets.QDoubleSpinBox, "spinWidth")
|
|
290
|
+
self._spin_height = self._require_widget(QtWidgets.QDoubleSpinBox, "spinHeight")
|
|
291
|
+
self._spin_radius_x = self._require_widget(QtWidgets.QDoubleSpinBox, "spinRadiusX")
|
|
292
|
+
self._spin_radius_y = self._require_widget(QtWidgets.QDoubleSpinBox, "spinRadiusY")
|
|
293
|
+
self._color_top_fill = self._require_widget(ColorButton, "colorTopFill")
|
|
294
|
+
self._color_bottom_fill = self._require_widget(ColorButton, "colorBottomFill")
|
|
295
|
+
self._spin_divider_ratio = self._require_widget(QtWidgets.QDoubleSpinBox, "spinDividerRatio")
|
|
296
|
+
self._color_stroke = self._require_widget(ColorButton, "colorStroke")
|
|
297
|
+
self._spin_stroke_width = self._require_widget(QtWidgets.QDoubleSpinBox, "spinStrokeWidth")
|
|
298
|
+
self._color_fill = self._require_widget(ColorButton, "colorFill")
|
|
299
|
+
self._spin_head_ratio = self._require_widget(QtWidgets.QDoubleSpinBox, "spinHeadRatio")
|
|
300
|
+
self._spin_shaft_ratio = self._require_widget(QtWidgets.QDoubleSpinBox, "spinShaftRatio")
|
|
301
|
+
self._spin_hook_depth = self._require_widget(QtWidgets.QDoubleSpinBox, "spinHookDepth")
|
|
302
|
+
self._check_arrow_start = self._require_widget(QtWidgets.QCheckBox, "checkArrowStart")
|
|
303
|
+
self._check_arrow_end = self._require_widget(QtWidgets.QCheckBox, "checkArrowEnd")
|
|
304
|
+
self._spin_arrow_length = self._require_widget(QtWidgets.QDoubleSpinBox, "spinArrowLength")
|
|
305
|
+
self._spin_arrow_width = self._require_widget(QtWidgets.QDoubleSpinBox, "spinArrowWidth")
|
|
306
|
+
|
|
307
|
+
self._object_groups = [
|
|
308
|
+
self._group_transform,
|
|
309
|
+
self._group_size,
|
|
310
|
+
self._group_rounded,
|
|
311
|
+
self._group_sections,
|
|
312
|
+
self._group_stroke,
|
|
313
|
+
self._group_fill,
|
|
314
|
+
self._group_arrow_shape,
|
|
315
|
+
self._group_bracket,
|
|
316
|
+
self._group_line_arrows,
|
|
317
|
+
]
|
|
318
|
+
|
|
319
|
+
def _cache_text_widgets(self) -> None:
|
|
320
|
+
self._group_label = self._require_widget(QtWidgets.QGroupBox, "groupLabel")
|
|
321
|
+
self._group_text_content = self._require_widget(QtWidgets.QGroupBox, "groupTextContent")
|
|
322
|
+
self._group_text_format = self._require_widget(QtWidgets.QGroupBox, "groupTextFormat")
|
|
323
|
+
|
|
324
|
+
self._line_label_text = self._require_widget(QtWidgets.QLineEdit, "lineLabelText")
|
|
325
|
+
self._combo_label_font = self._require_widget(QtWidgets.QFontComboBox, "comboLabelFont")
|
|
326
|
+
self._spin_label_font_size = self._require_widget(QtWidgets.QDoubleSpinBox, "spinLabelFontSize")
|
|
327
|
+
self._color_label_font = self._require_widget(ColorButton, "colorLabelFont")
|
|
328
|
+
self._button_label_color_default = self._require_widget(QtWidgets.QToolButton, "buttonLabelColorDefault")
|
|
329
|
+
self._combo_label_horizontal = self._require_widget(QtWidgets.QComboBox, "comboLabelHorizontal")
|
|
330
|
+
self._combo_label_vertical = self._require_widget(QtWidgets.QComboBox, "comboLabelVertical")
|
|
331
|
+
|
|
332
|
+
self._plain_text_content = self._require_widget(PlainTextEditor, "plainTextContent")
|
|
333
|
+
self._combo_text_font = self._require_widget(QtWidgets.QFontComboBox, "comboTextFont")
|
|
334
|
+
self._spin_text_font_size = self._require_widget(QtWidgets.QDoubleSpinBox, "spinTextFontSize")
|
|
335
|
+
self._color_text_font = self._require_widget(ColorButton, "colorTextFont")
|
|
336
|
+
self._spin_text_padding = self._require_widget(QtWidgets.QDoubleSpinBox, "spinTextPadding")
|
|
337
|
+
self._combo_text_horizontal = self._require_widget(QtWidgets.QComboBox, "comboTextHorizontal")
|
|
338
|
+
self._combo_text_vertical = self._require_widget(QtWidgets.QComboBox, "comboTextVertical")
|
|
339
|
+
self._combo_text_direction = self._require_widget(QtWidgets.QComboBox, "comboTextDirection")
|
|
340
|
+
|
|
341
|
+
self._text_groups = [self._group_label, self._group_text_content, self._group_text_format]
|
|
342
|
+
|
|
343
|
+
def _initialize_combobox_options(self) -> None:
|
|
344
|
+
alignment_options = [("Left", "left"), ("Centered", "center"), ("Right", "right")]
|
|
345
|
+
vertical_options = [("Top", "top"), ("Center", "middle"), ("Bottom", "bottom")]
|
|
346
|
+
self._configure_combobox(self._combo_label_horizontal, alignment_options)
|
|
347
|
+
self._configure_combobox(self._combo_label_vertical, vertical_options)
|
|
348
|
+
self._configure_combobox(self._combo_text_horizontal, alignment_options)
|
|
349
|
+
self._configure_combobox(self._combo_text_vertical, vertical_options)
|
|
350
|
+
self._configure_combobox(
|
|
351
|
+
self._combo_text_direction,
|
|
352
|
+
[("Left to Right", "ltr"), ("Right to Left", "rtl")],
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
def _configure_combobox(
|
|
356
|
+
self,
|
|
357
|
+
combo: QtWidgets.QComboBox,
|
|
358
|
+
options: list[tuple[str, Any]],
|
|
359
|
+
) -> None:
|
|
360
|
+
combo.clear()
|
|
361
|
+
combo.setInsertPolicy(QtWidgets.QComboBox.InsertPolicy.NoInsert)
|
|
362
|
+
combo.setSizeAdjustPolicy(QtWidgets.QComboBox.SizeAdjustPolicy.AdjustToContents)
|
|
363
|
+
combo.setMinimumContentsLength(1)
|
|
364
|
+
combo.setMaxVisibleItems(16)
|
|
365
|
+
combo.setEditable(False)
|
|
366
|
+
for text, value in options:
|
|
367
|
+
combo.addItem(text, value)
|
|
368
|
+
|
|
369
|
+
def _initialize_half_width_tracking(self) -> None:
|
|
370
|
+
widgets = [
|
|
371
|
+
self._line_label_text,
|
|
372
|
+
self._combo_label_font,
|
|
373
|
+
self._combo_label_horizontal,
|
|
374
|
+
self._combo_label_vertical,
|
|
375
|
+
self._combo_text_font,
|
|
376
|
+
self._combo_text_horizontal,
|
|
377
|
+
self._combo_text_vertical,
|
|
378
|
+
self._combo_text_direction,
|
|
379
|
+
]
|
|
380
|
+
for widget in widgets:
|
|
381
|
+
self._track_half_width_widget(widget)
|
|
382
|
+
|
|
383
|
+
def _reset_object_sections(self) -> None:
|
|
384
|
+
for group in self._object_groups:
|
|
385
|
+
group.hide()
|
|
386
|
+
|
|
387
|
+
def _reset_text_sections(self) -> None:
|
|
388
|
+
for group in self._text_groups:
|
|
389
|
+
group.hide()
|
|
390
|
+
|
|
391
|
+
# ------------------------------------------------------------------ #
|
|
392
|
+
# Public API #
|
|
393
|
+
# ------------------------------------------------------------------ #
|
|
394
|
+
|
|
395
|
+
def _half_width_limit(self) -> int:
|
|
396
|
+
base = self.width()
|
|
397
|
+
if base <= 0:
|
|
398
|
+
base = self.minimumWidth()
|
|
399
|
+
if base <= 0:
|
|
400
|
+
base = 200
|
|
401
|
+
return max(40,80)
|
|
402
|
+
|
|
403
|
+
def _track_half_width_widget(self, widget: QtWidgets.QWidget) -> None:
|
|
404
|
+
if widget is None:
|
|
405
|
+
return
|
|
406
|
+
for ref in self._half_width_widgets:
|
|
407
|
+
if ref() is widget:
|
|
408
|
+
break
|
|
409
|
+
else:
|
|
410
|
+
self._half_width_widgets.append(weakref.ref(widget))
|
|
411
|
+
self._set_widget_half_width(widget)
|
|
412
|
+
|
|
413
|
+
def _set_widget_half_width(self, widget: QtWidgets.QWidget | None) -> None:
|
|
414
|
+
if widget is None:
|
|
415
|
+
return
|
|
416
|
+
widget.setMaximumWidth(self._half_width_limit())
|
|
417
|
+
|
|
418
|
+
def _update_half_width_widgets(self) -> None:
|
|
419
|
+
limit = self._half_width_limit()
|
|
420
|
+
alive_refs: list[weakref.ReferenceType[QtWidgets.QWidget]] = []
|
|
421
|
+
for ref in self._half_width_widgets:
|
|
422
|
+
widget = ref()
|
|
423
|
+
if widget is None:
|
|
424
|
+
continue
|
|
425
|
+
widget.setMaximumWidth(limit)
|
|
426
|
+
alive_refs.append(ref)
|
|
427
|
+
self._half_width_widgets = alive_refs
|
|
428
|
+
|
|
429
|
+
def clear(self) -> None:
|
|
430
|
+
self._title_label.setText("Properties")
|
|
431
|
+
self._info_label.setText("No object selected.")
|
|
432
|
+
self._info_label.show()
|
|
433
|
+
self._current_item = None
|
|
434
|
+
self._latest_object_data = {}
|
|
435
|
+
self._latest_text_data = {}
|
|
436
|
+
self._clear_bindings()
|
|
437
|
+
self._reset_object_sections()
|
|
438
|
+
self._reset_text_sections()
|
|
439
|
+
self._set_tab_widget_active(False)
|
|
440
|
+
self._set_empty_spacer_visible(True)
|
|
441
|
+
|
|
442
|
+
def show_multi_selection(self, count: int) -> None:
|
|
443
|
+
self._title_label.setText("Properties")
|
|
444
|
+
self._info_label.setText(f"{count} objects selected.")
|
|
445
|
+
self._info_label.show()
|
|
446
|
+
self._current_item = None
|
|
447
|
+
self._latest_object_data = {}
|
|
448
|
+
self._latest_text_data = {}
|
|
449
|
+
self._clear_bindings()
|
|
450
|
+
self._reset_object_sections()
|
|
451
|
+
self._reset_text_sections()
|
|
452
|
+
self._set_tab_widget_active(False)
|
|
453
|
+
self._set_empty_spacer_visible(True)
|
|
454
|
+
|
|
455
|
+
def update_snapshot(self, payload: object) -> None:
|
|
456
|
+
if not isinstance(payload, dict):
|
|
457
|
+
self.clear()
|
|
458
|
+
return
|
|
459
|
+
|
|
460
|
+
selection_type = payload.get("selection_type")
|
|
461
|
+
if selection_type == "single":
|
|
462
|
+
item = payload.get("item")
|
|
463
|
+
if not isinstance(item, QtWidgets.QGraphicsItem):
|
|
464
|
+
self.clear()
|
|
465
|
+
return
|
|
466
|
+
title = str(payload.get("title", "Object"))
|
|
467
|
+
object_data = payload.get("object_data")
|
|
468
|
+
if not isinstance(object_data, dict):
|
|
469
|
+
object_data = {}
|
|
470
|
+
text_data = payload.get("text_data")
|
|
471
|
+
if not isinstance(text_data, dict):
|
|
472
|
+
text_data = {}
|
|
473
|
+
self._show_single(item, title, object_data, text_data)
|
|
474
|
+
elif selection_type == "multi":
|
|
475
|
+
count = int(payload.get("count", 0))
|
|
476
|
+
self.show_multi_selection(count)
|
|
477
|
+
else:
|
|
478
|
+
self.clear()
|
|
479
|
+
|
|
480
|
+
# ------------------------------------------------------------------ #
|
|
481
|
+
# Internal helpers #
|
|
482
|
+
# ------------------------------------------------------------------ #
|
|
483
|
+
|
|
484
|
+
def _show_single(
|
|
485
|
+
self,
|
|
486
|
+
item: QtWidgets.QGraphicsItem,
|
|
487
|
+
title: str,
|
|
488
|
+
object_data: dict[str, Any],
|
|
489
|
+
text_data: dict[str, Any],
|
|
490
|
+
) -> None:
|
|
491
|
+
self._title_label.setText(f"Properties – {title}")
|
|
492
|
+
self._info_label.hide()
|
|
493
|
+
|
|
494
|
+
if item is not self._current_item:
|
|
495
|
+
self._current_item = item
|
|
496
|
+
self._rebuild_for_item(item, object_data, text_data)
|
|
497
|
+
else:
|
|
498
|
+
self._latest_object_data = dict(object_data)
|
|
499
|
+
self._latest_text_data = dict(text_data)
|
|
500
|
+
self._refresh_active_bindings()
|
|
501
|
+
|
|
502
|
+
if hasattr(self._tab_widget, "setCurrentIndex") and self._tab_widget.currentIndex() == -1:
|
|
503
|
+
self._tab_widget.setCurrentIndex(0)
|
|
504
|
+
self._set_tab_widget_active(True)
|
|
505
|
+
self._set_empty_spacer_visible(False)
|
|
506
|
+
|
|
507
|
+
def _rebuild_for_item(
|
|
508
|
+
self,
|
|
509
|
+
item: QtWidgets.QGraphicsItem,
|
|
510
|
+
object_data: dict[str, Any],
|
|
511
|
+
text_data: dict[str, Any],
|
|
512
|
+
) -> None:
|
|
513
|
+
self._latest_object_data = dict(object_data)
|
|
514
|
+
self._latest_text_data = dict(text_data)
|
|
515
|
+
self._clear_bindings()
|
|
516
|
+
self._reset_object_sections()
|
|
517
|
+
self._reset_text_sections()
|
|
518
|
+
self._build_object_section(item, object_data)
|
|
519
|
+
self._build_text_section(item)
|
|
520
|
+
self._update_text_tab_visibility()
|
|
521
|
+
self._refresh_active_bindings()
|
|
522
|
+
|
|
523
|
+
def _clear_bindings(self) -> None:
|
|
524
|
+
for binding in self._object_bindings:
|
|
525
|
+
binding.deleteLater()
|
|
526
|
+
for binding in self._text_bindings:
|
|
527
|
+
binding.deleteLater()
|
|
528
|
+
self._object_bindings.clear()
|
|
529
|
+
self._text_bindings.clear()
|
|
530
|
+
|
|
531
|
+
def _bindings_for(self, group: str) -> list[PropertyBinding]:
|
|
532
|
+
return self._object_bindings if group == "object" else self._text_bindings
|
|
533
|
+
|
|
534
|
+
def _refresh_active_bindings(self) -> None:
|
|
535
|
+
for binding in self._object_bindings:
|
|
536
|
+
binding.refresh()
|
|
537
|
+
for binding in self._text_bindings:
|
|
538
|
+
binding.refresh()
|
|
539
|
+
|
|
540
|
+
def _set_tab_widget_active(self, active: bool) -> None:
|
|
541
|
+
if self._layout is None:
|
|
542
|
+
return
|
|
543
|
+
tab_index = self._layout.indexOf(self._tab_widget)
|
|
544
|
+
self._tab_widget.show()
|
|
545
|
+
self._tab_widget.setEnabled(active)
|
|
546
|
+
if not active:
|
|
547
|
+
self._tab_widget.setCurrentIndex(0)
|
|
548
|
+
if tab_index >= 0:
|
|
549
|
+
self._layout.setStretch(tab_index, 1)
|
|
550
|
+
self._layout.invalidate()
|
|
551
|
+
|
|
552
|
+
def _set_empty_spacer_visible(self, visible: bool) -> None:
|
|
553
|
+
if self._layout is None or self._empty_spacer is None:
|
|
554
|
+
return
|
|
555
|
+
if visible and not self._spacer_visible:
|
|
556
|
+
self._layout.addItem(self._empty_spacer)
|
|
557
|
+
self._spacer_visible = True
|
|
558
|
+
elif not visible and self._spacer_visible:
|
|
559
|
+
self._layout.removeItem(self._empty_spacer)
|
|
560
|
+
self._spacer_visible = False
|
|
561
|
+
|
|
562
|
+
def _after_property_change(self) -> None:
|
|
563
|
+
if self._canvas is not None:
|
|
564
|
+
self._canvas.history().mark_dirty()
|
|
565
|
+
self._refresh_active_bindings()
|
|
566
|
+
|
|
567
|
+
@staticmethod
|
|
568
|
+
def _set_double_spin_value(spin: QtWidgets.QDoubleSpinBox, value: Any) -> None:
|
|
569
|
+
try:
|
|
570
|
+
target = float(value)
|
|
571
|
+
except (TypeError, ValueError):
|
|
572
|
+
return
|
|
573
|
+
if not math.isfinite(target):
|
|
574
|
+
return
|
|
575
|
+
precision = 10 ** (-spin.decimals())
|
|
576
|
+
if abs(spin.value() - target) > precision / 2.0:
|
|
577
|
+
spin.setValue(target)
|
|
578
|
+
|
|
579
|
+
def _set_check_state(self, box: QtWidgets.QCheckBox, value: Any) -> None:
|
|
580
|
+
target = bool(value)
|
|
581
|
+
if box.isChecked() != target:
|
|
582
|
+
box.setChecked(target)
|
|
583
|
+
|
|
584
|
+
@staticmethod
|
|
585
|
+
def _set_combo_value(combo: QtWidgets.QComboBox, value: Any) -> None:
|
|
586
|
+
index = combo.findData(value)
|
|
587
|
+
if index < 0:
|
|
588
|
+
return
|
|
589
|
+
if combo.currentIndex() != index:
|
|
590
|
+
combo.setCurrentIndex(index)
|
|
591
|
+
@staticmethod
|
|
592
|
+
def _set_line_edit_text(edit: QtWidgets.QLineEdit, value: Any) -> None:
|
|
593
|
+
text = "" if value is None else str(value)
|
|
594
|
+
if edit.text() != text:
|
|
595
|
+
edit.setText(text)
|
|
596
|
+
|
|
597
|
+
@staticmethod
|
|
598
|
+
def _set_plain_text(editor: PlainTextEditor, value: Any) -> None:
|
|
599
|
+
text = "" if value is None else str(value)
|
|
600
|
+
if editor.toPlainText() != text:
|
|
601
|
+
cursor = editor.textCursor()
|
|
602
|
+
position = cursor.position()
|
|
603
|
+
editor.blockSignals(True)
|
|
604
|
+
editor.setPlainText(text)
|
|
605
|
+
cursor = editor.textCursor()
|
|
606
|
+
cursor.setPosition(min(position, len(text)))
|
|
607
|
+
editor.setTextCursor(cursor)
|
|
608
|
+
editor.blockSignals(False)
|
|
609
|
+
|
|
610
|
+
def _bind_double_spin(
|
|
611
|
+
self,
|
|
612
|
+
spin: QtWidgets.QDoubleSpinBox,
|
|
613
|
+
getter: Callable[[], Number],
|
|
614
|
+
setter: Callable[[Number], bool | None],
|
|
615
|
+
*,
|
|
616
|
+
group: str = "object",
|
|
617
|
+
) -> None:
|
|
618
|
+
binding = PropertyBinding(
|
|
619
|
+
spin,
|
|
620
|
+
getter,
|
|
621
|
+
setter,
|
|
622
|
+
spin.valueChanged,
|
|
623
|
+
lambda w: float(w.value()),
|
|
624
|
+
self._set_double_spin_value,
|
|
625
|
+
self._after_property_change,
|
|
626
|
+
)
|
|
627
|
+
binding.refresh()
|
|
628
|
+
self._bindings_for(group).append(binding)
|
|
629
|
+
|
|
630
|
+
def _bind_checkbox(
|
|
631
|
+
self,
|
|
632
|
+
box: QtWidgets.QCheckBox,
|
|
633
|
+
getter: Callable[[], bool],
|
|
634
|
+
setter: Callable[[bool], bool | None],
|
|
635
|
+
*,
|
|
636
|
+
group: str = "object",
|
|
637
|
+
) -> None:
|
|
638
|
+
binding = PropertyBinding(
|
|
639
|
+
box,
|
|
640
|
+
getter,
|
|
641
|
+
setter,
|
|
642
|
+
box.toggled,
|
|
643
|
+
lambda w: bool(w.isChecked()),
|
|
644
|
+
self._set_check_state,
|
|
645
|
+
self._after_property_change,
|
|
646
|
+
)
|
|
647
|
+
binding.refresh()
|
|
648
|
+
self._bindings_for(group).append(binding)
|
|
649
|
+
|
|
650
|
+
def _bind_combobox(
|
|
651
|
+
self,
|
|
652
|
+
combo: QtWidgets.QComboBox,
|
|
653
|
+
getter: Callable[[], Any],
|
|
654
|
+
setter: Callable[[Any], bool | None],
|
|
655
|
+
*,
|
|
656
|
+
group: str = "object",
|
|
657
|
+
) -> None:
|
|
658
|
+
binding = PropertyBinding(
|
|
659
|
+
combo,
|
|
660
|
+
getter,
|
|
661
|
+
setter,
|
|
662
|
+
combo.currentIndexChanged,
|
|
663
|
+
lambda w: w.currentData(),
|
|
664
|
+
self._set_combo_value,
|
|
665
|
+
self._after_property_change,
|
|
666
|
+
)
|
|
667
|
+
binding.refresh()
|
|
668
|
+
self._bindings_for(group).append(binding)
|
|
669
|
+
|
|
670
|
+
def _bind_line_edit(
|
|
671
|
+
self,
|
|
672
|
+
edit: QtWidgets.QLineEdit,
|
|
673
|
+
getter: Callable[[], str],
|
|
674
|
+
setter: Callable[[str], bool | None],
|
|
675
|
+
*,
|
|
676
|
+
group: str = "object",
|
|
677
|
+
) -> None:
|
|
678
|
+
binding = PropertyBinding(
|
|
679
|
+
edit,
|
|
680
|
+
getter,
|
|
681
|
+
setter,
|
|
682
|
+
edit.editingFinished,
|
|
683
|
+
lambda w: w.text(),
|
|
684
|
+
self._set_line_edit_text,
|
|
685
|
+
self._after_property_change,
|
|
686
|
+
)
|
|
687
|
+
binding.refresh()
|
|
688
|
+
self._bindings_for(group).append(binding)
|
|
689
|
+
|
|
690
|
+
def _bind_plain_text(
|
|
691
|
+
self,
|
|
692
|
+
editor: PlainTextEditor,
|
|
693
|
+
getter: Callable[[], str],
|
|
694
|
+
setter: Callable[[str], bool | None],
|
|
695
|
+
*,
|
|
696
|
+
group: str = "object",
|
|
697
|
+
) -> None:
|
|
698
|
+
binding = PropertyBinding(
|
|
699
|
+
editor,
|
|
700
|
+
getter,
|
|
701
|
+
setter,
|
|
702
|
+
editor.editingFinished,
|
|
703
|
+
lambda w: w.toPlainText(),
|
|
704
|
+
self._set_plain_text,
|
|
705
|
+
self._after_property_change,
|
|
706
|
+
)
|
|
707
|
+
binding.refresh()
|
|
708
|
+
self._bindings_for(group).append(binding)
|
|
709
|
+
|
|
710
|
+
def _bind_font_combobox(
|
|
711
|
+
self,
|
|
712
|
+
combo: QtWidgets.QFontComboBox,
|
|
713
|
+
getter: Callable[[], QtGui.QFont],
|
|
714
|
+
setter: Callable[[QtGui.QFont], bool | None],
|
|
715
|
+
*,
|
|
716
|
+
group: str = "object",
|
|
717
|
+
) -> None:
|
|
718
|
+
binding = PropertyBinding(
|
|
719
|
+
combo,
|
|
720
|
+
getter,
|
|
721
|
+
setter,
|
|
722
|
+
combo.currentFontChanged,
|
|
723
|
+
lambda w: w.currentFont(),
|
|
724
|
+
lambda w, value: w.setCurrentFont(value),
|
|
725
|
+
self._after_property_change,
|
|
726
|
+
)
|
|
727
|
+
binding.refresh()
|
|
728
|
+
self._bindings_for(group).append(binding)
|
|
729
|
+
|
|
730
|
+
def _bind_color_field(
|
|
731
|
+
self,
|
|
732
|
+
color_button: ColorButton,
|
|
733
|
+
getter: Callable[[], QtGui.QColor],
|
|
734
|
+
setter: Callable[[QtGui.QColor], bool | None],
|
|
735
|
+
*,
|
|
736
|
+
group: str = "object",
|
|
737
|
+
reset_button: QtWidgets.QAbstractButton | None = None,
|
|
738
|
+
resetter: Callable[[], bool | None] | None = None,
|
|
739
|
+
) -> None:
|
|
740
|
+
binding = PropertyBinding(
|
|
741
|
+
color_button,
|
|
742
|
+
getter,
|
|
743
|
+
setter,
|
|
744
|
+
color_button.colorChanged,
|
|
745
|
+
lambda w: w.color(),
|
|
746
|
+
lambda w, value: w.setColor(value),
|
|
747
|
+
self._after_property_change,
|
|
748
|
+
)
|
|
749
|
+
binding.refresh()
|
|
750
|
+
self._bindings_for(group).append(binding)
|
|
751
|
+
|
|
752
|
+
if reset_button is not None and resetter is not None:
|
|
753
|
+
def on_reset() -> None:
|
|
754
|
+
changed = resetter()
|
|
755
|
+
if changed is False:
|
|
756
|
+
binding.refresh()
|
|
757
|
+
else:
|
|
758
|
+
self._after_property_change()
|
|
759
|
+
|
|
760
|
+
previous = getattr(reset_button, "_properties_panel_reset_callback", None)
|
|
761
|
+
if previous is not None:
|
|
762
|
+
try:
|
|
763
|
+
reset_button.clicked.disconnect(previous)
|
|
764
|
+
except (TypeError, RuntimeError):
|
|
765
|
+
pass
|
|
766
|
+
reset_button._properties_panel_reset_callback = on_reset # type: ignore[attr-defined]
|
|
767
|
+
reset_button.clicked.connect(on_reset)
|
|
768
|
+
|
|
769
|
+
# ------------------------------------------------------------------ #
|
|
770
|
+
# Object section #
|
|
771
|
+
# ------------------------------------------------------------------ #
|
|
772
|
+
|
|
773
|
+
def _build_object_section(self, item: QtWidgets.QGraphicsItem, object_data: dict[str, Any]) -> None:
|
|
774
|
+
self._group_transform.show()
|
|
775
|
+
self._bind_double_spin(
|
|
776
|
+
self._spin_pos_x,
|
|
777
|
+
lambda: float(item.pos().x()),
|
|
778
|
+
lambda value: self._set_item_pos(item, x=value),
|
|
779
|
+
)
|
|
780
|
+
self._bind_double_spin(
|
|
781
|
+
self._spin_pos_y,
|
|
782
|
+
lambda: float(item.pos().y()),
|
|
783
|
+
lambda value: self._set_item_pos(item, y=value),
|
|
784
|
+
)
|
|
785
|
+
self._bind_double_spin(
|
|
786
|
+
self._spin_rotation,
|
|
787
|
+
lambda: float(item.rotation()),
|
|
788
|
+
lambda value: self._set_item_rotation(item, value),
|
|
789
|
+
)
|
|
790
|
+
self._bind_double_spin(
|
|
791
|
+
self._spin_scale,
|
|
792
|
+
lambda: float(item.scale()),
|
|
793
|
+
lambda value: self._set_item_scale(item, value),
|
|
794
|
+
)
|
|
795
|
+
self._bind_double_spin(
|
|
796
|
+
self._spin_z_value,
|
|
797
|
+
lambda: float(item.zValue()),
|
|
798
|
+
lambda value: self._set_item_z(item, value),
|
|
799
|
+
)
|
|
800
|
+
|
|
801
|
+
size_value = object_data.get("size")
|
|
802
|
+
if isinstance(size_value, (list, tuple)) and len(size_value) == 2:
|
|
803
|
+
self._group_size.show()
|
|
804
|
+
self._bind_double_spin(
|
|
805
|
+
self._spin_width,
|
|
806
|
+
lambda item=item: float(self._item_dimensions(item)[0]),
|
|
807
|
+
lambda value: self._set_item_size(item, width=value),
|
|
808
|
+
)
|
|
809
|
+
self._bind_double_spin(
|
|
810
|
+
self._spin_height,
|
|
811
|
+
lambda item=item: float(self._item_dimensions(item)[1]),
|
|
812
|
+
lambda value: self._set_item_size(item, height=value),
|
|
813
|
+
)
|
|
814
|
+
|
|
815
|
+
if isinstance(item, (RectItem, SplitRoundedRectItem)) or hasattr(item, "rx"):
|
|
816
|
+
self._group_rounded.show()
|
|
817
|
+
self._bind_double_spin(
|
|
818
|
+
self._spin_radius_x,
|
|
819
|
+
lambda: float(getattr(item, "rx", 0.0)),
|
|
820
|
+
lambda value: self._set_corner_radius(item, "rx", value),
|
|
821
|
+
)
|
|
822
|
+
self._bind_double_spin(
|
|
823
|
+
self._spin_radius_y,
|
|
824
|
+
lambda: float(getattr(item, "ry", getattr(item, "rx", 0.0))),
|
|
825
|
+
lambda value: self._set_corner_radius(item, "ry", value),
|
|
826
|
+
)
|
|
827
|
+
|
|
828
|
+
if isinstance(item, SplitRoundedRectItem):
|
|
829
|
+
self._group_sections.show()
|
|
830
|
+
self._bind_color_field(
|
|
831
|
+
self._color_top_fill,
|
|
832
|
+
lambda: item.topBrush().color(),
|
|
833
|
+
lambda color: self._set_split_brush_color(item, "top", color),
|
|
834
|
+
)
|
|
835
|
+
self._bind_color_field(
|
|
836
|
+
self._color_bottom_fill,
|
|
837
|
+
lambda: item.bottomBrush().color(),
|
|
838
|
+
lambda color: self._set_split_brush_color(item, "bottom", color),
|
|
839
|
+
)
|
|
840
|
+
self._bind_double_spin(
|
|
841
|
+
self._spin_divider_ratio,
|
|
842
|
+
lambda: float(item.divider_ratio()),
|
|
843
|
+
lambda value: self._set_split_ratio(item, value),
|
|
844
|
+
)
|
|
845
|
+
|
|
846
|
+
if hasattr(item, "pen") and not isinstance(item, TextItem):
|
|
847
|
+
self._group_stroke.show()
|
|
848
|
+
self._bind_color_field(
|
|
849
|
+
self._color_stroke,
|
|
850
|
+
lambda: item.pen().color(),
|
|
851
|
+
lambda color: self._set_pen_color(item, color),
|
|
852
|
+
)
|
|
853
|
+
self._bind_double_spin(
|
|
854
|
+
self._spin_stroke_width,
|
|
855
|
+
lambda: float(item.pen().widthF()),
|
|
856
|
+
lambda value: self._set_pen_width(item, value),
|
|
857
|
+
)
|
|
858
|
+
|
|
859
|
+
if hasattr(item, "brush") and not isinstance(item, LineItem):
|
|
860
|
+
self._group_fill.show()
|
|
861
|
+
self._bind_color_field(
|
|
862
|
+
self._color_fill,
|
|
863
|
+
lambda: item.brush().color(),
|
|
864
|
+
lambda color: self._set_brush_color(item, color),
|
|
865
|
+
)
|
|
866
|
+
|
|
867
|
+
if isinstance(item, BlockArrowItem):
|
|
868
|
+
self._group_arrow_shape.show()
|
|
869
|
+
self._bind_double_spin(
|
|
870
|
+
self._spin_head_ratio,
|
|
871
|
+
item.head_ratio,
|
|
872
|
+
lambda value: self._set_block_arrow_ratio(item, "head", value),
|
|
873
|
+
)
|
|
874
|
+
self._bind_double_spin(
|
|
875
|
+
self._spin_shaft_ratio,
|
|
876
|
+
item.shaft_ratio,
|
|
877
|
+
lambda value: self._set_block_arrow_ratio(item, "shaft", value),
|
|
878
|
+
)
|
|
879
|
+
|
|
880
|
+
if isinstance(item, CurvyBracketItem):
|
|
881
|
+
self._group_bracket.show()
|
|
882
|
+
self._bind_double_spin(
|
|
883
|
+
self._spin_hook_depth,
|
|
884
|
+
item.hook_ratio,
|
|
885
|
+
lambda value: self._set_bracket_hook_ratio(item, value),
|
|
886
|
+
)
|
|
887
|
+
|
|
888
|
+
if isinstance(item, LineItem):
|
|
889
|
+
self._group_line_arrows.show()
|
|
890
|
+
self._bind_checkbox(
|
|
891
|
+
self._check_arrow_start,
|
|
892
|
+
lambda: bool(item.arrow_start),
|
|
893
|
+
lambda value: self._toggle_line_arrow(item, "start", value),
|
|
894
|
+
)
|
|
895
|
+
self._bind_checkbox(
|
|
896
|
+
self._check_arrow_end,
|
|
897
|
+
lambda: bool(item.arrow_end),
|
|
898
|
+
lambda value: self._toggle_line_arrow(item, "end", value),
|
|
899
|
+
)
|
|
900
|
+
self._bind_double_spin(
|
|
901
|
+
self._spin_arrow_length,
|
|
902
|
+
item.arrow_head_length,
|
|
903
|
+
lambda value: self._set_line_arrow_metric(item, "length", value),
|
|
904
|
+
)
|
|
905
|
+
self._bind_double_spin(
|
|
906
|
+
self._spin_arrow_width,
|
|
907
|
+
item.arrow_head_width,
|
|
908
|
+
lambda value: self._set_line_arrow_metric(item, "width", value),
|
|
909
|
+
)
|
|
910
|
+
|
|
911
|
+
# ------------------------------------------------------------------ #
|
|
912
|
+
# Text section #
|
|
913
|
+
# ------------------------------------------------------------------ #
|
|
914
|
+
|
|
915
|
+
def _build_text_section(self, item: QtWidgets.QGraphicsItem) -> None:
|
|
916
|
+
if isinstance(item, TextItem):
|
|
917
|
+
self._build_text_item_section(item)
|
|
918
|
+
elif isinstance(item, ShapeLabelMixin):
|
|
919
|
+
self._build_label_section(item)
|
|
920
|
+
|
|
921
|
+
def _build_label_section(self, item: ShapeLabelMixin) -> None:
|
|
922
|
+
self._group_label.show()
|
|
923
|
+
self._bind_line_edit(
|
|
924
|
+
self._line_label_text,
|
|
925
|
+
item.label_text,
|
|
926
|
+
lambda value: self._set_label_text(item, value),
|
|
927
|
+
group="text",
|
|
928
|
+
)
|
|
929
|
+
self._combo_label_font.setCurrentFont(item.label_item().font())
|
|
930
|
+
self._bind_font_combobox(
|
|
931
|
+
self._combo_label_font,
|
|
932
|
+
lambda: item.label_item().font(),
|
|
933
|
+
lambda font: self._set_label_font_family(item, font),
|
|
934
|
+
group="text",
|
|
935
|
+
)
|
|
936
|
+
self._bind_double_spin(
|
|
937
|
+
self._spin_label_font_size,
|
|
938
|
+
lambda: float(self._label_font_size(item)),
|
|
939
|
+
lambda value: self._set_label_font_size(item, value),
|
|
940
|
+
group="text",
|
|
941
|
+
)
|
|
942
|
+
self._bind_color_field(
|
|
943
|
+
self._color_label_font,
|
|
944
|
+
item.label_color,
|
|
945
|
+
lambda color: self._set_label_color(item, color),
|
|
946
|
+
group="text",
|
|
947
|
+
reset_button=self._button_label_color_default,
|
|
948
|
+
resetter=lambda: self._reset_label_color(item),
|
|
949
|
+
)
|
|
950
|
+
self._bind_combobox(
|
|
951
|
+
self._combo_label_horizontal,
|
|
952
|
+
lambda: item.label_alignment()[0],
|
|
953
|
+
lambda value: self._set_label_alignment(item, horizontal=value),
|
|
954
|
+
group="text",
|
|
955
|
+
)
|
|
956
|
+
self._bind_combobox(
|
|
957
|
+
self._combo_label_vertical,
|
|
958
|
+
lambda: item.label_alignment()[1],
|
|
959
|
+
lambda value: self._set_label_alignment(item, vertical=value),
|
|
960
|
+
group="text",
|
|
961
|
+
)
|
|
962
|
+
|
|
963
|
+
def _build_text_item_section(self, item: TextItem) -> None:
|
|
964
|
+
self._group_text_content.show()
|
|
965
|
+
self._bind_plain_text(
|
|
966
|
+
self._plain_text_content,
|
|
967
|
+
item.toPlainText,
|
|
968
|
+
lambda value: self._set_text_item_content(item, value),
|
|
969
|
+
group="text",
|
|
970
|
+
)
|
|
971
|
+
|
|
972
|
+
self._group_text_format.show()
|
|
973
|
+
self._combo_text_font.setCurrentFont(item.font())
|
|
974
|
+
self._bind_font_combobox(
|
|
975
|
+
self._combo_text_font,
|
|
976
|
+
lambda: item.font(),
|
|
977
|
+
lambda font: self._set_text_item_font_family(item, font),
|
|
978
|
+
group="text",
|
|
979
|
+
)
|
|
980
|
+
self._bind_double_spin(
|
|
981
|
+
self._spin_text_font_size,
|
|
982
|
+
lambda: float(self._text_point_size(item)),
|
|
983
|
+
lambda value: self._set_text_point_size(item, value),
|
|
984
|
+
group="text",
|
|
985
|
+
)
|
|
986
|
+
self._bind_color_field(
|
|
987
|
+
self._color_text_font,
|
|
988
|
+
item.defaultTextColor,
|
|
989
|
+
lambda color: self._set_text_color(item, color),
|
|
990
|
+
group="text",
|
|
991
|
+
)
|
|
992
|
+
self._bind_double_spin(
|
|
993
|
+
self._spin_text_padding,
|
|
994
|
+
lambda: float(item.document().documentMargin()) if item.document() else 0.0,
|
|
995
|
+
lambda value: self._set_text_margin(item, value),
|
|
996
|
+
group="text",
|
|
997
|
+
)
|
|
998
|
+
self._bind_combobox(
|
|
999
|
+
self._combo_text_horizontal,
|
|
1000
|
+
lambda: item.text_alignment()[0],
|
|
1001
|
+
lambda value: self._set_text_alignment(item, horizontal=value),
|
|
1002
|
+
group="text",
|
|
1003
|
+
)
|
|
1004
|
+
self._bind_combobox(
|
|
1005
|
+
self._combo_text_vertical,
|
|
1006
|
+
lambda: item.text_alignment()[1],
|
|
1007
|
+
lambda value: self._set_text_alignment(item, vertical=value),
|
|
1008
|
+
group="text",
|
|
1009
|
+
)
|
|
1010
|
+
self._bind_combobox(
|
|
1011
|
+
self._combo_text_direction,
|
|
1012
|
+
item.text_direction,
|
|
1013
|
+
lambda value: self._set_text_direction(item, value),
|
|
1014
|
+
group="text",
|
|
1015
|
+
)
|
|
1016
|
+
|
|
1017
|
+
def _update_text_tab_visibility(self) -> None:
|
|
1018
|
+
has_text = bool(self._text_bindings)
|
|
1019
|
+
if hasattr(self._tab_widget, "setTabVisible"):
|
|
1020
|
+
self._tab_widget.setTabVisible(1, has_text)
|
|
1021
|
+
else:
|
|
1022
|
+
self._tab_widget.setTabEnabled(1, has_text)
|
|
1023
|
+
if not has_text and self._tab_widget.currentIndex() == 1:
|
|
1024
|
+
self._tab_widget.setCurrentIndex(0)
|
|
1025
|
+
|
|
1026
|
+
# ------------------------------------------------------------------ #
|
|
1027
|
+
# Setter helpers #
|
|
1028
|
+
# ------------------------------------------------------------------ #
|
|
1029
|
+
|
|
1030
|
+
@staticmethod
|
|
1031
|
+
def _set_item_pos(item: QtWidgets.QGraphicsItem, *, x: Number | None = None, y: Number | None = None) -> bool:
|
|
1032
|
+
pos = item.pos()
|
|
1033
|
+
target_x = float(x) if x is not None else float(pos.x())
|
|
1034
|
+
target_y = float(y) if y is not None else float(pos.y())
|
|
1035
|
+
if math.isclose(pos.x(), target_x, abs_tol=0.1) and math.isclose(pos.y(), target_y, abs_tol=0.1):
|
|
1036
|
+
return False
|
|
1037
|
+
item.setPos(target_x, target_y)
|
|
1038
|
+
return True
|
|
1039
|
+
|
|
1040
|
+
@staticmethod
|
|
1041
|
+
def _set_item_rotation(item: QtWidgets.QGraphicsItem, angle: Number) -> bool:
|
|
1042
|
+
target = float(angle)
|
|
1043
|
+
if math.isclose(item.rotation(), target, abs_tol=0.1):
|
|
1044
|
+
return False
|
|
1045
|
+
item.setRotation(target)
|
|
1046
|
+
return True
|
|
1047
|
+
|
|
1048
|
+
@staticmethod
|
|
1049
|
+
def _set_item_scale(item: QtWidgets.QGraphicsItem, scale: Number) -> bool:
|
|
1050
|
+
target = max(0.01, float(scale))
|
|
1051
|
+
if math.isclose(item.scale(), target, rel_tol=1e-3, abs_tol=1e-3):
|
|
1052
|
+
return False
|
|
1053
|
+
item.setScale(target)
|
|
1054
|
+
return True
|
|
1055
|
+
|
|
1056
|
+
@staticmethod
|
|
1057
|
+
def _set_item_z(item: QtWidgets.QGraphicsItem, z_value: Number) -> bool:
|
|
1058
|
+
target = float(z_value)
|
|
1059
|
+
if math.isclose(item.zValue(), target, abs_tol=0.01):
|
|
1060
|
+
return False
|
|
1061
|
+
item.setZValue(target)
|
|
1062
|
+
return True
|
|
1063
|
+
|
|
1064
|
+
def _set_item_size(
|
|
1065
|
+
self,
|
|
1066
|
+
item: QtWidgets.QGraphicsItem,
|
|
1067
|
+
*,
|
|
1068
|
+
width: Number | None = None,
|
|
1069
|
+
height: Number | None = None,
|
|
1070
|
+
) -> bool:
|
|
1071
|
+
current_w, current_h = self._item_dimensions(item)
|
|
1072
|
+
target_w = float(width) if width is not None else current_w
|
|
1073
|
+
target_h = float(height) if height is not None else current_h
|
|
1074
|
+
target_w = max(1.0, target_w)
|
|
1075
|
+
target_h = max(1.0, target_h)
|
|
1076
|
+
if (
|
|
1077
|
+
math.isclose(current_w, target_w, rel_tol=1e-3, abs_tol=0.2)
|
|
1078
|
+
and math.isclose(current_h, target_h, rel_tol=1e-3, abs_tol=0.2)
|
|
1079
|
+
):
|
|
1080
|
+
return False
|
|
1081
|
+
old_top_left = item.mapToScene(QtCore.QPointF(0.0, 0.0))
|
|
1082
|
+
|
|
1083
|
+
if isinstance(item, QtWidgets.QGraphicsRectItem):
|
|
1084
|
+
rect = item.rect()
|
|
1085
|
+
item.setRect(rect.x(), rect.y(), target_w, target_h)
|
|
1086
|
+
elif isinstance(item, QtWidgets.QGraphicsEllipseItem):
|
|
1087
|
+
rect = item.rect()
|
|
1088
|
+
item.setRect(rect.x(), rect.y(), target_w, target_h)
|
|
1089
|
+
elif isinstance(item, BlockArrowItem):
|
|
1090
|
+
item._w = target_w # type: ignore[assignment]
|
|
1091
|
+
item._h = target_h # type: ignore[assignment]
|
|
1092
|
+
item._update_polygon() # type: ignore[attr-defined]
|
|
1093
|
+
item.setTransformOriginPoint(target_w / 2.0, target_h / 2.0)
|
|
1094
|
+
elif hasattr(item, "set_size") and callable(getattr(item, "set_size")):
|
|
1095
|
+
try:
|
|
1096
|
+
item.set_size(target_w, target_h) # type: ignore[misc]
|
|
1097
|
+
except TypeError:
|
|
1098
|
+
item.set_size(target_w, target_h, adjust_origin=True) # type: ignore[misc]
|
|
1099
|
+
else:
|
|
1100
|
+
base_bounds = item.boundingRect()
|
|
1101
|
+
scale_x = target_w / (base_bounds.width() or 1.0)
|
|
1102
|
+
scale_y = target_h / (base_bounds.height() or 1.0)
|
|
1103
|
+
item.setScale(max(scale_x, scale_y))
|
|
1104
|
+
|
|
1105
|
+
new_bounds = item.boundingRect()
|
|
1106
|
+
item.setTransformOriginPoint(new_bounds.center())
|
|
1107
|
+
new_top_left = item.mapToScene(QtCore.QPointF(0.0, 0.0))
|
|
1108
|
+
item.setPos(item.pos() + (old_top_left - new_top_left))
|
|
1109
|
+
if hasattr(item, "update_handles"):
|
|
1110
|
+
item.update_handles()
|
|
1111
|
+
return True
|
|
1112
|
+
|
|
1113
|
+
@staticmethod
|
|
1114
|
+
def _item_dimensions(item: QtWidgets.QGraphicsItem) -> tuple[float, float]:
|
|
1115
|
+
if isinstance(item, QtWidgets.QGraphicsRectItem):
|
|
1116
|
+
rect = item.rect()
|
|
1117
|
+
return float(rect.width()), float(rect.height())
|
|
1118
|
+
if isinstance(item, QtWidgets.QGraphicsEllipseItem):
|
|
1119
|
+
rect = item.rect()
|
|
1120
|
+
return float(rect.width()), float(rect.height())
|
|
1121
|
+
width_attr = getattr(item, "_w", None)
|
|
1122
|
+
height_attr = getattr(item, "_h", None)
|
|
1123
|
+
if isinstance(width_attr, (int, float)) and isinstance(height_attr, (int, float)):
|
|
1124
|
+
return float(width_attr), float(height_attr)
|
|
1125
|
+
bounds = item.boundingRect()
|
|
1126
|
+
return float(bounds.width()), float(bounds.height())
|
|
1127
|
+
|
|
1128
|
+
@staticmethod
|
|
1129
|
+
def _set_corner_radius(item: Any, attr: str, value: Number) -> bool:
|
|
1130
|
+
target = max(0.0, float(value))
|
|
1131
|
+
current = float(getattr(item, attr, 0.0))
|
|
1132
|
+
if math.isclose(current, target, abs_tol=0.1):
|
|
1133
|
+
return False
|
|
1134
|
+
setattr(item, attr, target)
|
|
1135
|
+
if hasattr(item, "update"):
|
|
1136
|
+
item.update()
|
|
1137
|
+
return True
|
|
1138
|
+
|
|
1139
|
+
@staticmethod
|
|
1140
|
+
def _set_pen_color(item: Any, color: QtGui.QColor) -> bool:
|
|
1141
|
+
qcolor = QtGui.QColor(color)
|
|
1142
|
+
if not qcolor.isValid():
|
|
1143
|
+
return False
|
|
1144
|
+
pen = QtGui.QPen(item.pen())
|
|
1145
|
+
if pen.color() == qcolor:
|
|
1146
|
+
return False
|
|
1147
|
+
pen.setColor(qcolor)
|
|
1148
|
+
item.setPen(pen)
|
|
1149
|
+
return True
|
|
1150
|
+
|
|
1151
|
+
@staticmethod
|
|
1152
|
+
def _set_pen_width(item: Any, width: Number) -> bool:
|
|
1153
|
+
target = max(0.05, float(width))
|
|
1154
|
+
pen = QtGui.QPen(item.pen())
|
|
1155
|
+
if math.isclose(pen.widthF(), target, abs_tol=0.05):
|
|
1156
|
+
return False
|
|
1157
|
+
pen.setWidthF(target)
|
|
1158
|
+
item.setPen(pen)
|
|
1159
|
+
return True
|
|
1160
|
+
|
|
1161
|
+
@staticmethod
|
|
1162
|
+
def _set_brush_color(item: Any, color: QtGui.QColor) -> bool:
|
|
1163
|
+
qcolor = QtGui.QColor(color)
|
|
1164
|
+
if not qcolor.isValid():
|
|
1165
|
+
return False
|
|
1166
|
+
brush = QtGui.QBrush(item.brush())
|
|
1167
|
+
if brush.style() == QtCore.Qt.BrushStyle.NoBrush or brush.color() != qcolor:
|
|
1168
|
+
brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern)
|
|
1169
|
+
brush.setColor(qcolor)
|
|
1170
|
+
item.setBrush(brush)
|
|
1171
|
+
return True
|
|
1172
|
+
return False
|
|
1173
|
+
|
|
1174
|
+
@staticmethod
|
|
1175
|
+
def _set_split_brush_color(item: SplitRoundedRectItem, which: str, color: QtGui.QColor) -> bool:
|
|
1176
|
+
qcolor = QtGui.QColor(color)
|
|
1177
|
+
if not qcolor.isValid():
|
|
1178
|
+
return False
|
|
1179
|
+
if which == "top":
|
|
1180
|
+
current = item.topBrush().color()
|
|
1181
|
+
if current == qcolor:
|
|
1182
|
+
return False
|
|
1183
|
+
item.setTopBrush(qcolor)
|
|
1184
|
+
else:
|
|
1185
|
+
current = item.bottomBrush().color()
|
|
1186
|
+
if current == qcolor:
|
|
1187
|
+
return False
|
|
1188
|
+
item.setBottomBrush(qcolor)
|
|
1189
|
+
item.update()
|
|
1190
|
+
return True
|
|
1191
|
+
|
|
1192
|
+
@staticmethod
|
|
1193
|
+
def _set_split_ratio(item: SplitRoundedRectItem, value: Number) -> bool:
|
|
1194
|
+
target = float(value)
|
|
1195
|
+
current = float(item.divider_ratio())
|
|
1196
|
+
if math.isclose(current, target, abs_tol=1e-3):
|
|
1197
|
+
return False
|
|
1198
|
+
item.set_divider_ratio(target)
|
|
1199
|
+
return True
|
|
1200
|
+
|
|
1201
|
+
@staticmethod
|
|
1202
|
+
def _set_block_arrow_ratio(item: BlockArrowItem, which: str, value: Number) -> bool:
|
|
1203
|
+
target = float(value)
|
|
1204
|
+
if which == "head":
|
|
1205
|
+
current = float(item.head_ratio())
|
|
1206
|
+
if math.isclose(current, target, abs_tol=1e-3):
|
|
1207
|
+
return False
|
|
1208
|
+
item.set_head_ratio(target)
|
|
1209
|
+
else:
|
|
1210
|
+
current = float(item.shaft_ratio())
|
|
1211
|
+
if math.isclose(current, target, abs_tol=1e-3):
|
|
1212
|
+
return False
|
|
1213
|
+
item.set_shaft_ratio(target)
|
|
1214
|
+
return True
|
|
1215
|
+
|
|
1216
|
+
@staticmethod
|
|
1217
|
+
def _set_bracket_hook_ratio(item: CurvyBracketItem, value: Number) -> bool:
|
|
1218
|
+
target = float(value)
|
|
1219
|
+
current = float(item.hook_ratio())
|
|
1220
|
+
if math.isclose(current, target, abs_tol=1e-3):
|
|
1221
|
+
return False
|
|
1222
|
+
item.set_hook_ratio(target)
|
|
1223
|
+
return True
|
|
1224
|
+
|
|
1225
|
+
@staticmethod
|
|
1226
|
+
def _toggle_line_arrow(item: LineItem, which: str, value: bool) -> bool:
|
|
1227
|
+
desired = bool(value)
|
|
1228
|
+
if which == "start":
|
|
1229
|
+
before = bool(item.arrow_start)
|
|
1230
|
+
if before == desired:
|
|
1231
|
+
return False
|
|
1232
|
+
item.set_arrow_start(desired)
|
|
1233
|
+
else:
|
|
1234
|
+
before = bool(item.arrow_end)
|
|
1235
|
+
if before == desired:
|
|
1236
|
+
return False
|
|
1237
|
+
item.set_arrow_end(desired)
|
|
1238
|
+
return True
|
|
1239
|
+
|
|
1240
|
+
@staticmethod
|
|
1241
|
+
def _set_line_arrow_metric(item: LineItem, which: str, value: Number) -> bool:
|
|
1242
|
+
target = max(0.1, float(value))
|
|
1243
|
+
if which == "length":
|
|
1244
|
+
before = float(item.arrow_head_length())
|
|
1245
|
+
if math.isclose(before, target, abs_tol=0.1):
|
|
1246
|
+
return False
|
|
1247
|
+
item.set_arrow_head_length(target)
|
|
1248
|
+
else:
|
|
1249
|
+
before = float(item.arrow_head_width())
|
|
1250
|
+
if math.isclose(before, target, abs_tol=0.1):
|
|
1251
|
+
return False
|
|
1252
|
+
item.set_arrow_head_width(target)
|
|
1253
|
+
return True
|
|
1254
|
+
|
|
1255
|
+
@staticmethod
|
|
1256
|
+
def _set_label_text(item: ShapeLabelMixin, value: str) -> bool:
|
|
1257
|
+
text = str(value)
|
|
1258
|
+
if item.label_text() == text:
|
|
1259
|
+
return False
|
|
1260
|
+
item.set_label_text(text)
|
|
1261
|
+
return True
|
|
1262
|
+
|
|
1263
|
+
@staticmethod
|
|
1264
|
+
def _set_label_font_family(item: ShapeLabelMixin, font: QtGui.QFont) -> bool:
|
|
1265
|
+
current_font = item.label_item().font()
|
|
1266
|
+
if current_font.family() == font.family():
|
|
1267
|
+
return False
|
|
1268
|
+
new_font = QtGui.QFont(current_font)
|
|
1269
|
+
new_font.setFamily(font.family())
|
|
1270
|
+
item.label_item().setFont(new_font)
|
|
1271
|
+
return True
|
|
1272
|
+
|
|
1273
|
+
@staticmethod
|
|
1274
|
+
def _label_font_size(item: ShapeLabelMixin) -> float:
|
|
1275
|
+
font = item.label_item().font()
|
|
1276
|
+
size = font.pixelSize()
|
|
1277
|
+
if size and size > 0:
|
|
1278
|
+
return float(size)
|
|
1279
|
+
point_size = font.pointSizeF()
|
|
1280
|
+
if point_size and point_size > 0:
|
|
1281
|
+
return float(point_size)
|
|
1282
|
+
return 16.0
|
|
1283
|
+
|
|
1284
|
+
@staticmethod
|
|
1285
|
+
def _set_label_font_size(item: ShapeLabelMixin, value: Number) -> bool:
|
|
1286
|
+
size = max(4.0, float(value))
|
|
1287
|
+
current = PropertiesPanel._label_font_size(item)
|
|
1288
|
+
if math.isclose(current, size, abs_tol=0.5):
|
|
1289
|
+
return False
|
|
1290
|
+
item.set_label_font_pixel_size(int(round(size)))
|
|
1291
|
+
return True
|
|
1292
|
+
|
|
1293
|
+
@staticmethod
|
|
1294
|
+
def _set_label_color(item: ShapeLabelMixin, color: QtGui.QColor) -> bool:
|
|
1295
|
+
qcolor = QtGui.QColor(color)
|
|
1296
|
+
if not qcolor.isValid():
|
|
1297
|
+
return False
|
|
1298
|
+
has_override = item.label_has_custom_color()
|
|
1299
|
+
current = item.label_color()
|
|
1300
|
+
if has_override and current == qcolor:
|
|
1301
|
+
return False
|
|
1302
|
+
item.set_label_color(qcolor)
|
|
1303
|
+
return True
|
|
1304
|
+
|
|
1305
|
+
@staticmethod
|
|
1306
|
+
def _reset_label_color(item: ShapeLabelMixin) -> bool:
|
|
1307
|
+
if not item.label_has_custom_color():
|
|
1308
|
+
return False
|
|
1309
|
+
item.reset_label_color()
|
|
1310
|
+
return True
|
|
1311
|
+
|
|
1312
|
+
@staticmethod
|
|
1313
|
+
def _set_label_alignment(
|
|
1314
|
+
item: ShapeLabelMixin,
|
|
1315
|
+
*,
|
|
1316
|
+
horizontal: str | None = None,
|
|
1317
|
+
vertical: str | None = None,
|
|
1318
|
+
) -> bool:
|
|
1319
|
+
before_h, before_v = item.label_alignment()
|
|
1320
|
+
item.set_label_alignment(horizontal=horizontal, vertical=vertical)
|
|
1321
|
+
after_h, after_v = item.label_alignment()
|
|
1322
|
+
return before_h != after_h or before_v != after_v
|
|
1323
|
+
|
|
1324
|
+
@staticmethod
|
|
1325
|
+
def _set_text_item_content(item: TextItem, value: str) -> bool:
|
|
1326
|
+
text = str(value)
|
|
1327
|
+
if item.toPlainText() == text:
|
|
1328
|
+
return False
|
|
1329
|
+
item.setPlainText(text)
|
|
1330
|
+
return True
|
|
1331
|
+
|
|
1332
|
+
@staticmethod
|
|
1333
|
+
def _set_text_item_font_family(item: TextItem, font: QtGui.QFont) -> bool:
|
|
1334
|
+
current = item.font()
|
|
1335
|
+
if current.family() == font.family():
|
|
1336
|
+
return False
|
|
1337
|
+
new_font = QtGui.QFont(current)
|
|
1338
|
+
new_font.setFamily(font.family())
|
|
1339
|
+
item.setFont(new_font)
|
|
1340
|
+
return True
|
|
1341
|
+
|
|
1342
|
+
@staticmethod
|
|
1343
|
+
def _text_point_size(item: TextItem) -> float:
|
|
1344
|
+
font = item.font()
|
|
1345
|
+
size = font.pointSizeF()
|
|
1346
|
+
if size and size > 0:
|
|
1347
|
+
return float(size)
|
|
1348
|
+
pixel = font.pixelSize()
|
|
1349
|
+
if pixel and pixel > 0:
|
|
1350
|
+
return float(pixel)
|
|
1351
|
+
return 12.0
|
|
1352
|
+
|
|
1353
|
+
@staticmethod
|
|
1354
|
+
def _set_text_point_size(item: TextItem, value: Number) -> bool:
|
|
1355
|
+
size = max(1.0, float(value))
|
|
1356
|
+
current = PropertiesPanel._text_point_size(item)
|
|
1357
|
+
if math.isclose(current, size, abs_tol=0.4):
|
|
1358
|
+
return False
|
|
1359
|
+
font = QtGui.QFont(item.font())
|
|
1360
|
+
font.setPointSizeF(size)
|
|
1361
|
+
if font.pointSizeF() <= 0.0:
|
|
1362
|
+
font.setPixelSize(int(round(size)))
|
|
1363
|
+
item.setFont(font)
|
|
1364
|
+
return True
|
|
1365
|
+
|
|
1366
|
+
@staticmethod
|
|
1367
|
+
def _set_text_color(item: TextItem, color: QtGui.QColor) -> bool:
|
|
1368
|
+
qcolor = QtGui.QColor(color)
|
|
1369
|
+
if not qcolor.isValid():
|
|
1370
|
+
return False
|
|
1371
|
+
current = item.defaultTextColor()
|
|
1372
|
+
if current == qcolor:
|
|
1373
|
+
return False
|
|
1374
|
+
item.setDefaultTextColor(qcolor)
|
|
1375
|
+
return True
|
|
1376
|
+
|
|
1377
|
+
@staticmethod
|
|
1378
|
+
def _set_text_margin(item: TextItem, value: Number) -> bool:
|
|
1379
|
+
margin = max(0.0, float(value))
|
|
1380
|
+
doc = item.document()
|
|
1381
|
+
if doc is None:
|
|
1382
|
+
return False
|
|
1383
|
+
current = float(doc.documentMargin())
|
|
1384
|
+
if math.isclose(current, margin, abs_tol=0.2):
|
|
1385
|
+
return False
|
|
1386
|
+
item.set_document_margin(margin)
|
|
1387
|
+
return True
|
|
1388
|
+
|
|
1389
|
+
@staticmethod
|
|
1390
|
+
def _set_text_alignment(
|
|
1391
|
+
item: TextItem,
|
|
1392
|
+
*,
|
|
1393
|
+
horizontal: str | None = None,
|
|
1394
|
+
vertical: str | None = None,
|
|
1395
|
+
) -> bool:
|
|
1396
|
+
before = item.text_alignment()
|
|
1397
|
+
item.set_text_alignment(horizontal=horizontal, vertical=vertical)
|
|
1398
|
+
after = item.text_alignment()
|
|
1399
|
+
return before != after
|
|
1400
|
+
|
|
1401
|
+
@staticmethod
|
|
1402
|
+
def _set_text_direction(item: TextItem, direction: str) -> bool:
|
|
1403
|
+
if item.text_direction() == direction:
|
|
1404
|
+
return False
|
|
1405
|
+
item.set_text_direction(direction)
|
|
1406
|
+
return True
|