drawsvg-ui 0.5.4__tar.gz → 0.6.0__tar.gz

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.
Files changed (34) hide show
  1. {drawsvg_ui-0.5.4/src/drawsvg_ui.egg-info → drawsvg_ui-0.6.0}/PKG-INFO +5 -1
  2. {drawsvg_ui-0.5.4 → drawsvg_ui-0.6.0}/README.md +4 -0
  3. {drawsvg_ui-0.5.4 → drawsvg_ui-0.6.0}/pyproject.toml +2 -2
  4. {drawsvg_ui-0.5.4 → drawsvg_ui-0.6.0}/src/canvas_view.py +297 -105
  5. {drawsvg_ui-0.5.4 → drawsvg_ui-0.6.0}/src/constants.py +22 -15
  6. {drawsvg_ui-0.5.4 → drawsvg_ui-0.6.0/src/drawsvg_ui.egg-info}/PKG-INFO +5 -1
  7. {drawsvg_ui-0.5.4 → drawsvg_ui-0.6.0}/src/export_drawsvg.py +21 -10
  8. {drawsvg_ui-0.5.4 → drawsvg_ui-0.6.0}/src/import_drawsvg.py +14 -4
  9. {drawsvg_ui-0.5.4 → drawsvg_ui-0.6.0}/LICENSE +0 -0
  10. {drawsvg_ui-0.5.4 → drawsvg_ui-0.6.0}/setup.cfg +0 -0
  11. {drawsvg_ui-0.5.4 → drawsvg_ui-0.6.0}/src/app_info.py +0 -0
  12. {drawsvg_ui-0.5.4 → drawsvg_ui-0.6.0}/src/drawsvg_ui.egg-info/SOURCES.txt +0 -0
  13. {drawsvg_ui-0.5.4 → drawsvg_ui-0.6.0}/src/drawsvg_ui.egg-info/dependency_links.txt +0 -0
  14. {drawsvg_ui-0.5.4 → drawsvg_ui-0.6.0}/src/drawsvg_ui.egg-info/entry_points.txt +0 -0
  15. {drawsvg_ui-0.5.4 → drawsvg_ui-0.6.0}/src/drawsvg_ui.egg-info/requires.txt +0 -0
  16. {drawsvg_ui-0.5.4 → drawsvg_ui-0.6.0}/src/drawsvg_ui.egg-info/top_level.txt +0 -0
  17. {drawsvg_ui-0.5.4 → drawsvg_ui-0.6.0}/src/items/__init__.py +0 -0
  18. {drawsvg_ui-0.5.4 → drawsvg_ui-0.6.0}/src/items/base.py +0 -0
  19. {drawsvg_ui-0.5.4 → drawsvg_ui-0.6.0}/src/items/labels.py +0 -0
  20. {drawsvg_ui-0.5.4 → drawsvg_ui-0.6.0}/src/items/shapes/__init__.py +0 -0
  21. {drawsvg_ui-0.5.4 → drawsvg_ui-0.6.0}/src/items/shapes/curves.py +0 -0
  22. {drawsvg_ui-0.5.4 → drawsvg_ui-0.6.0}/src/items/shapes/lines.py +0 -0
  23. {drawsvg_ui-0.5.4 → drawsvg_ui-0.6.0}/src/items/shapes/polygons.py +0 -0
  24. {drawsvg_ui-0.5.4 → drawsvg_ui-0.6.0}/src/items/shapes/rects.py +0 -0
  25. {drawsvg_ui-0.5.4 → drawsvg_ui-0.6.0}/src/items/text.py +0 -0
  26. {drawsvg_ui-0.5.4 → drawsvg_ui-0.6.0}/src/items/widgets/__init__.py +0 -0
  27. {drawsvg_ui-0.5.4 → drawsvg_ui-0.6.0}/src/items/widgets/folder_tree.py +0 -0
  28. {drawsvg_ui-0.5.4 → drawsvg_ui-0.6.0}/src/main.py +0 -0
  29. {drawsvg_ui-0.5.4 → drawsvg_ui-0.6.0}/src/main_window.py +0 -0
  30. {drawsvg_ui-0.5.4 → drawsvg_ui-0.6.0}/src/palette.py +0 -0
  31. {drawsvg_ui-0.5.4 → drawsvg_ui-0.6.0}/src/properties_panel.py +0 -0
  32. {drawsvg_ui-0.5.4 → drawsvg_ui-0.6.0}/src/ui/__init__.py +0 -0
  33. {drawsvg_ui-0.5.4 → drawsvg_ui-0.6.0}/src/ui/main_window.ui +0 -0
  34. {drawsvg_ui-0.5.4 → drawsvg_ui-0.6.0}/src/ui/properties_panel.ui +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: drawsvg-ui
3
- Version: 0.5.4
3
+ Version: 0.6.0
4
4
  Summary: GUI for creating drawsvg scenes with PySide6
5
5
  Author-email: Taron686 <Taron686@users.noreply.github.com>
6
6
  License: GPL-3.0-or-later
@@ -33,6 +33,10 @@ Dynamic: license-file
33
33
 
34
34
  # DrawSVG UI
35
35
 
36
+ [![Tag](https://img.shields.io/github/v/tag/Taron686/drawsvg-ui?label=tag)](https://github.com/Taron686/drawsvg-ui/tags)
37
+ [![Build](https://github.com/Taron686/drawsvg-ui/actions/workflows/publish.yml/badge.svg?branch=main)](https://github.com/Taron686/drawsvg-ui/actions/workflows/publish.yml)
38
+ [![PyPI](https://img.shields.io/pypi/v/drawsvg-ui.svg?label=pypi)](https://pypi.org/project/drawsvg-ui/)
39
+
36
40
  This repository provides a graphical user interface designed to make it easier to create files with the [drawsvg](https://pypi.org/project/drawsvg/) library.
37
41
  Instead of writing raw Python code by hand, you can visually place, move, and edit shapes on a canvas, then export your work as a ready-to-use `drawsvg` file. The UI runs on PySide6 and comes with a property inspector, snapping/grid helpers and a set of ready-made shapes.
38
42
 
@@ -1,5 +1,9 @@
1
1
  # DrawSVG UI
2
2
 
3
+ [![Tag](https://img.shields.io/github/v/tag/Taron686/drawsvg-ui?label=tag)](https://github.com/Taron686/drawsvg-ui/tags)
4
+ [![Build](https://github.com/Taron686/drawsvg-ui/actions/workflows/publish.yml/badge.svg?branch=main)](https://github.com/Taron686/drawsvg-ui/actions/workflows/publish.yml)
5
+ [![PyPI](https://img.shields.io/pypi/v/drawsvg-ui.svg?label=pypi)](https://pypi.org/project/drawsvg-ui/)
6
+
3
7
  This repository provides a graphical user interface designed to make it easier to create files with the [drawsvg](https://pypi.org/project/drawsvg/) library.
4
8
  Instead of writing raw Python code by hand, you can visually place, move, and edit shapes on a canvas, then export your work as a ready-to-use `drawsvg` file. The UI runs on PySide6 and comes with a property inspector, snapping/grid helpers and a set of ready-made shapes.
5
9
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "drawsvg-ui"
7
- version = "0.5.4"
7
+ version = "0.6.0"
8
8
  description = "GUI for creating drawsvg scenes with PySide6"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -65,7 +65,7 @@ include = ["items*", "ui*"]
65
65
  ui = ["*.ui"]
66
66
 
67
67
  [tool.bumpversion]
68
- current_version = "0.5.4"
68
+ current_version = "0.6.0"
69
69
  commit = true
70
70
  tag = true
71
71
 
@@ -14,7 +14,14 @@ from typing import Any, Callable
14
14
  from PySide6 import QtCore, QtGui, QtWidgets
15
15
  from PySide6.QtGui import QTransform
16
16
 
17
- from constants import PALETTE_MIME, SHAPES, DEFAULTS
17
+ from constants import (
18
+ DEFAULTS,
19
+ HOVER_PREVIEW_COLOR,
20
+ HOVER_PREVIEW_STRENGTH,
21
+ HOVER_PREVIEW_X_COUNT,
22
+ PALETTE_MIME,
23
+ SHAPES,
24
+ )
18
25
  from items import (
19
26
  BlockArrowItem,
20
27
  CurvyBracketItem,
@@ -224,15 +231,26 @@ class SceneHistory(QtCore.QObject):
224
231
  return
225
232
  self._timer.start()
226
233
 
227
- def capture_now(self) -> None:
228
- self._capture_snapshot()
229
-
230
- def undo(self) -> None:
231
- if not self.can_undo():
232
- return
233
- self._index -= 1
234
- self._apply_current_state()
235
- self._notify()
234
+ def capture_now(self) -> None:
235
+ self._capture_snapshot()
236
+
237
+ def _flush_pending_snapshot(self) -> None:
238
+ if self._ignore_changes or not self._timer.isActive():
239
+ return
240
+ # Only flush when we're on the latest state; otherwise we'd
241
+ # truncate redo history while navigating older states.
242
+ if self._index != len(self._states) - 1:
243
+ return
244
+ self._timer.stop()
245
+ self._capture_snapshot()
246
+
247
+ def undo(self) -> None:
248
+ self._flush_pending_snapshot()
249
+ if not self.can_undo():
250
+ return
251
+ self._index -= 1
252
+ self._apply_current_state()
253
+ self._notify()
236
254
 
237
255
  def redo(self) -> None:
238
256
  if not self.can_redo():
@@ -690,11 +708,12 @@ class CanvasView(QtWidgets.QGraphicsView):
690
708
  QtCore.QTimer.singleShot(0, self._fit_view_to_page)
691
709
  self._update_scene_rect()
692
710
 
693
- self._panning = False
694
- self._pan_start = QtCore.QPointF()
695
- self._prev_drag_mode = self.dragMode()
696
- self._right_button_pressed = False
697
- self._suppress_context_menu = False
711
+ self._panning = False
712
+ self._pan_start = QtCore.QPointF()
713
+ self._prev_drag_mode = self.dragMode()
714
+ self._right_button_pressed = False
715
+ self._suppress_context_menu = False
716
+ self._hover_preview_item: QtWidgets.QGraphicsItem | None = None
698
717
 
699
718
  self._history = SceneHistory(self)
700
719
  self._history.capture_initial_state()
@@ -723,37 +742,61 @@ class CanvasView(QtWidgets.QGraphicsView):
723
742
  return False
724
743
  return True
725
744
 
726
- def _item_sort_key(self, item: QtWidgets.QGraphicsItem) -> tuple[float, str, float, float]:
727
- shape = str(item.data(0)) if item.data(0) else item.__class__.__name__
728
- pos = item.pos()
729
- return (
730
- round(float(item.zValue()), 6),
731
- shape,
732
- round(float(pos.x()), 6),
733
- round(float(pos.y()), 6),
734
- )
735
-
736
- def _serialize_scene_state(self) -> dict[str, Any]:
737
- scene = self.scene()
738
- if scene is None:
739
- return {"items": [], "grid_visible": bool(self._show_grid)}
740
- items = [
741
- item
742
- for item in scene.items()
743
- if self._is_serializable_item(item) and item.parentItem() is None
744
- ]
745
- items.sort(key=self._item_sort_key)
746
- return {
747
- "items": [self._serialize_item(item) for item in items],
748
- "grid_visible": bool(self._show_grid),
749
- }
750
-
751
- def _serialize_item(self, item: QtWidgets.QGraphicsItem) -> dict[str, Any]:
752
- shape_value = item.data(0)
753
- shape = str(shape_value) if shape_value else item.__class__.__name__
754
- base: dict[str, Any] = {
755
- "shape": shape,
756
- "class": item.__class__.__name__,
745
+ def _stacking_indices(self) -> dict[int, int]:
746
+ scene = self.scene()
747
+ if scene is None:
748
+ return {}
749
+ try:
750
+ ordered = scene.items(QtCore.Qt.SortOrder.AscendingOrder)
751
+ except TypeError:
752
+ # Older bindings can miss the overload with explicit sort order.
753
+ ordered = list(reversed(scene.items()))
754
+ return {id(item): index for index, item in enumerate(ordered)}
755
+
756
+ def _item_sort_key(
757
+ self,
758
+ item: QtWidgets.QGraphicsItem,
759
+ stacking_indices: Mapping[int, int] | None = None,
760
+ ) -> tuple[float, int, str, float, float]:
761
+ shape = str(item.data(0)) if item.data(0) else item.__class__.__name__
762
+ pos = item.pos()
763
+ stack_index = -1
764
+ if stacking_indices is not None:
765
+ stack_index = int(stacking_indices.get(id(item), -1))
766
+ return (
767
+ round(float(item.zValue()), 6),
768
+ stack_index,
769
+ shape,
770
+ round(float(pos.x()), 6),
771
+ round(float(pos.y()), 6),
772
+ )
773
+
774
+ def _serialize_scene_state(self) -> dict[str, Any]:
775
+ scene = self.scene()
776
+ if scene is None:
777
+ return {"items": [], "grid_visible": bool(self._show_grid)}
778
+ stacking_indices = self._stacking_indices()
779
+ items = [
780
+ item
781
+ for item in scene.items()
782
+ if self._is_serializable_item(item) and item.parentItem() is None
783
+ ]
784
+ items.sort(key=lambda item: self._item_sort_key(item, stacking_indices))
785
+ return {
786
+ "items": [self._serialize_item(item, stacking_indices) for item in items],
787
+ "grid_visible": bool(self._show_grid),
788
+ }
789
+
790
+ def _serialize_item(
791
+ self,
792
+ item: QtWidgets.QGraphicsItem,
793
+ stacking_indices: Mapping[int, int] | None = None,
794
+ ) -> dict[str, Any]:
795
+ shape_value = item.data(0)
796
+ shape = str(shape_value) if shape_value else item.__class__.__name__
797
+ base: dict[str, Any] = {
798
+ "shape": shape,
799
+ "class": item.__class__.__name__,
757
800
  "pos": [float(item.pos().x()), float(item.pos().y())],
758
801
  "rotation": float(item.rotation()),
759
802
  "scale": float(item.scale()),
@@ -844,14 +887,18 @@ class CanvasView(QtWidgets.QGraphicsView):
844
887
  base["direction"] = item.text_direction()
845
888
  elif isinstance(item, FolderTreeItem):
846
889
  base["structure"] = item.structure()
847
- elif isinstance(item, GroupItem):
848
- children = [
849
- child
850
- for child in item.childItems()
851
- if self._is_serializable_item(child)
852
- ]
853
- children.sort(key=self._item_sort_key)
854
- base["children"] = [self._serialize_item(child) for child in children]
890
+ elif isinstance(item, GroupItem):
891
+ children = [
892
+ child
893
+ for child in item.childItems()
894
+ if self._is_serializable_item(child)
895
+ ]
896
+ children.sort(
897
+ key=lambda child: self._item_sort_key(child, stacking_indices)
898
+ )
899
+ base["children"] = [
900
+ self._serialize_item(child, stacking_indices) for child in children
901
+ ]
855
902
  else:
856
903
  width, height = self._item_dimensions(item)
857
904
  base["size"] = [width, height]
@@ -978,10 +1025,45 @@ class CanvasView(QtWidgets.QGraphicsView):
978
1025
  return float(width_attr), float(height_attr)
979
1026
  bounds = item.boundingRect()
980
1027
  return float(bounds.width()), float(bounds.height())
981
-
982
- def _notify_selection_snapshot(self) -> None:
983
- payload = self._build_selection_snapshot()
984
- self.selectionSnapshotChanged.emit(payload)
1028
+
1029
+ def _hover_preview_target_at(
1030
+ self, view_pos: QtCore.QPoint
1031
+ ) -> QtWidgets.QGraphicsItem | None:
1032
+ candidate = self.itemAt(view_pos)
1033
+ selectable = QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsSelectable
1034
+ while candidate is not None and not (candidate.flags() & selectable):
1035
+ candidate = candidate.parentItem()
1036
+ if candidate is None:
1037
+ return None
1038
+ if candidate.isSelected():
1039
+ return None
1040
+ if isinstance(candidate, LineItem):
1041
+ return None
1042
+ if not self._is_serializable_item(candidate):
1043
+ return None
1044
+ return candidate
1045
+
1046
+ def _set_hover_preview_item(self, item: QtWidgets.QGraphicsItem | None) -> None:
1047
+ if item is self._hover_preview_item:
1048
+ return
1049
+ self._hover_preview_item = item
1050
+ self.viewport().update()
1051
+
1052
+ def _clear_hover_preview(self) -> None:
1053
+ self._set_hover_preview_item(None)
1054
+
1055
+ def _notify_selection_snapshot(self) -> None:
1056
+ hover_item = self._hover_preview_item
1057
+ if hover_item is not None:
1058
+ try:
1059
+ if hover_item.isSelected():
1060
+ self._hover_preview_item = None
1061
+ self.viewport().update()
1062
+ except RuntimeError:
1063
+ self._hover_preview_item = None
1064
+ self.viewport().update()
1065
+ payload = self._build_selection_snapshot()
1066
+ self.selectionSnapshotChanged.emit(payload)
985
1067
 
986
1068
  def _on_scene_contents_changed(self, _changes) -> None:
987
1069
  scene = self.scene()
@@ -1414,12 +1496,13 @@ class CanvasView(QtWidgets.QGraphicsView):
1414
1496
  self.fitInView(padded, QtCore.Qt.AspectRatioMode.KeepAspectRatio)
1415
1497
  self.centerOn(self._page_item)
1416
1498
 
1417
- def clear_canvas(self):
1418
- """Remove all items from the scene."""
1419
- scene = self.scene()
1420
- for item in list(scene.items()):
1421
- if isinstance(item, A4PageItem):
1422
- continue
1499
+ def clear_canvas(self):
1500
+ """Remove all items from the scene."""
1501
+ self._clear_hover_preview()
1502
+ scene = self.scene()
1503
+ for item in list(scene.items()):
1504
+ if isinstance(item, A4PageItem):
1505
+ continue
1423
1506
  if item.__class__.__name__.endswith("Handle"):
1424
1507
  continue
1425
1508
  if item.parentItem() is not None:
@@ -1435,13 +1518,109 @@ class CanvasView(QtWidgets.QGraphicsView):
1435
1518
  return
1436
1519
  self._ensure_pages_for_items(scene.items())
1437
1520
 
1438
- def drawBackground(self, painter: QtGui.QPainter, rect: QtCore.QRectF):
1439
- super().drawBackground(painter, rect)
1440
-
1441
- def set_grid_visible(self, visible: bool):
1442
- self._show_grid = visible
1443
- for page in self._pages.values():
1444
- page.set_grid_visible(visible)
1521
+ def drawBackground(self, painter: QtGui.QPainter, rect: QtCore.QRectF):
1522
+ super().drawBackground(painter, rect)
1523
+
1524
+ def drawForeground(self, painter: QtGui.QPainter, rect: QtCore.QRectF) -> None:
1525
+ super().drawForeground(painter, rect)
1526
+
1527
+ del rect
1528
+
1529
+ strength = max(0.0, float(HOVER_PREVIEW_STRENGTH))
1530
+ marker_count = max(0, int(HOVER_PREVIEW_X_COUNT))
1531
+ if strength <= 0.0 or marker_count <= 0:
1532
+ return
1533
+
1534
+ item = self._hover_preview_item
1535
+ scene = self.scene()
1536
+ if item is None or scene is None:
1537
+ return
1538
+
1539
+ try:
1540
+ if item.scene() is not scene:
1541
+ self._hover_preview_item = None
1542
+ return
1543
+ except RuntimeError:
1544
+ self._hover_preview_item = None
1545
+ return
1546
+
1547
+ corners_poly = item.mapToScene(item.boundingRect())
1548
+ if len(corners_poly) < 4:
1549
+ return
1550
+ c0 = QtCore.QPointF(corners_poly[0])
1551
+ c1 = QtCore.QPointF(corners_poly[1])
1552
+ c2 = QtCore.QPointF(corners_poly[2])
1553
+ c3 = QtCore.QPointF(corners_poly[3])
1554
+
1555
+ def lerp(start: QtCore.QPointF, end: QtCore.QPointF, t: float) -> QtCore.QPointF:
1556
+ return QtCore.QPointF(
1557
+ start.x() + (end.x() - start.x()) * t,
1558
+ start.y() + (end.y() - start.y()) * t,
1559
+ )
1560
+
1561
+ transform = painter.worldTransform()
1562
+ sx = math.hypot(transform.m11(), transform.m21())
1563
+ sy = math.hypot(transform.m12(), transform.m22())
1564
+ lod = max((sx + sy) * 0.5, 1e-6)
1565
+ strength_for_size = max(0.5, strength)
1566
+ marker_half = 3.0 * strength_for_size / lod
1567
+
1568
+ base_color = QtGui.QColor(HOVER_PREVIEW_COLOR)
1569
+ if not base_color.isValid():
1570
+ base_color = QtGui.QColor("#14b5ff")
1571
+ outline_color = QtGui.QColor(base_color)
1572
+ outline_color.setAlpha(max(0, min(255, int(round(125 * strength)))))
1573
+ marker_color = QtGui.QColor(base_color)
1574
+ marker_color.setAlpha(max(0, min(255, int(round(220 * strength)))))
1575
+
1576
+ painter.save()
1577
+ painter.setRenderHint(QtGui.QPainter.RenderHint.Antialiasing, True)
1578
+
1579
+ outline_pen = QtGui.QPen(outline_color, max(0.6, 1.0 * strength_for_size))
1580
+ outline_pen.setCosmetic(True)
1581
+ painter.setPen(outline_pen)
1582
+ painter.setBrush(QtCore.Qt.BrushStyle.NoBrush)
1583
+ painter.drawPolygon(QtGui.QPolygonF([c0, c1, c2, c3]))
1584
+
1585
+ marker_pen = QtGui.QPen(marker_color, max(0.7, 1.3 * strength_for_size))
1586
+ marker_pen.setCosmetic(True)
1587
+ painter.setPen(marker_pen)
1588
+
1589
+ edges = ((c0, c1), (c1, c2), (c2, c3), (c3, c0))
1590
+ edge_lengths = [QtCore.QLineF(start, end).length() for start, end in edges]
1591
+ perimeter = sum(edge_lengths)
1592
+ if perimeter <= 1e-6:
1593
+ painter.restore()
1594
+ return
1595
+
1596
+ points: list[QtCore.QPointF] = []
1597
+ for idx in range(marker_count):
1598
+ distance = perimeter * (idx / marker_count)
1599
+ remaining = distance
1600
+ for edge_index, edge_length in enumerate(edge_lengths):
1601
+ if remaining <= edge_length or edge_index == len(edge_lengths) - 1:
1602
+ start, end = edges[edge_index]
1603
+ t = 0.0 if edge_length <= 1e-6 else remaining / edge_length
1604
+ points.append(lerp(start, end, t))
1605
+ break
1606
+ remaining -= edge_length
1607
+
1608
+ for point in points:
1609
+ painter.drawLine(
1610
+ QtCore.QPointF(point.x() - marker_half, point.y() - marker_half),
1611
+ QtCore.QPointF(point.x() + marker_half, point.y() + marker_half),
1612
+ )
1613
+ painter.drawLine(
1614
+ QtCore.QPointF(point.x() - marker_half, point.y() + marker_half),
1615
+ QtCore.QPointF(point.x() + marker_half, point.y() - marker_half),
1616
+ )
1617
+
1618
+ painter.restore()
1619
+
1620
+ def set_grid_visible(self, visible: bool):
1621
+ self._show_grid = visible
1622
+ for page in self._pages.values():
1623
+ page.set_grid_visible(visible)
1445
1624
  self.viewport().update()
1446
1625
  if hasattr(self, "_history"):
1447
1626
  self._history.mark_dirty()
@@ -1531,12 +1710,16 @@ class CanvasView(QtWidgets.QGraphicsView):
1531
1710
  else:
1532
1711
  return None
1533
1712
 
1534
- item.setData(0, normalized)
1535
- self.scene().addItem(item)
1536
- item.setSelected(True)
1537
- self._ensure_page_for_item(item, drop_reference)
1538
- self._update_scene_rect()
1539
- return item
1713
+ scene = self.scene()
1714
+ if scene is None:
1715
+ return None
1716
+ item.setData(0, normalized)
1717
+ scene.addItem(item)
1718
+ scene.clearSelection()
1719
+ item.setSelected(True)
1720
+ self._ensure_page_for_item(item, drop_reference)
1721
+ self._update_scene_rect()
1722
+ return item
1540
1723
 
1541
1724
  def add_shape_at_view_center(self, shape: str) -> QtWidgets.QGraphicsItem | None:
1542
1725
  normalized = shape.strip()
@@ -1635,13 +1818,15 @@ class CanvasView(QtWidgets.QGraphicsView):
1635
1818
  return
1636
1819
  super().mousePressEvent(event)
1637
1820
 
1638
- def mouseMoveEvent(self, event: QtGui.QMouseEvent):
1639
-
1640
- item = self.itemAt(event.position().toPoint())
1641
- if item and item.flags() & QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsMovable:
1642
- self.viewport().setCursor(QtCore.Qt.CursorShape.SizeAllCursor)
1643
- else:
1644
- self.viewport().setCursor(QtCore.Qt.CursorShape.ArrowCursor)
1821
+ def mouseMoveEvent(self, event: QtGui.QMouseEvent):
1822
+ hover_target = self._hover_preview_target_at(event.position().toPoint())
1823
+ self._set_hover_preview_item(hover_target)
1824
+
1825
+ item = self.itemAt(event.position().toPoint())
1826
+ if item and item.flags() & QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsMovable:
1827
+ self.viewport().setCursor(QtCore.Qt.CursorShape.SizeAllCursor)
1828
+ else:
1829
+ self.viewport().setCursor(QtCore.Qt.CursorShape.ArrowCursor)
1645
1830
 
1646
1831
  if self._panning or self._right_button_pressed:
1647
1832
  delta = event.position() - self._pan_start
@@ -1655,20 +1840,22 @@ class CanvasView(QtWidgets.QGraphicsView):
1655
1840
  self.viewport().setCursor(
1656
1841
  QtCore.Qt.CursorShape.ClosedHandCursor
1657
1842
  )
1658
- if self._panning:
1659
- self._pan_start = event.position()
1660
- hbar = self.horizontalScrollBar()
1661
- vbar = self.verticalScrollBar()
1662
- hbar.setValue(hbar.value() - int(delta.x()))
1663
- vbar.setValue(vbar.value() - int(delta.y()))
1843
+ if self._panning:
1844
+ self._clear_hover_preview()
1845
+ self._pan_start = event.position()
1846
+ hbar = self.horizontalScrollBar()
1847
+ vbar = self.verticalScrollBar()
1848
+ hbar.setValue(hbar.value() - int(delta.x()))
1849
+ vbar.setValue(vbar.value() - int(delta.y()))
1664
1850
  event.accept()
1665
1851
  return
1666
- if getattr(self, "_dup_source", None):
1667
- pos = self.mapToScene(event.position().toPoint())
1668
- delta = pos - self._dup_start
1669
- if self._dup_items is None:
1670
- # Only create clones after surpassing the threshold.
1671
- if delta.manhattanLength() < DUPLICATE_DRAG_THRESHOLD:
1852
+ if getattr(self, "_dup_source", None):
1853
+ self._clear_hover_preview()
1854
+ pos = self.mapToScene(event.position().toPoint())
1855
+ delta = pos - self._dup_start
1856
+ if self._dup_items is None:
1857
+ # Only create clones after surpassing the threshold.
1858
+ if delta.manhattanLength() < DUPLICATE_DRAG_THRESHOLD:
1672
1859
  event.accept()
1673
1860
  return
1674
1861
  self._dup_items = []
@@ -1684,9 +1871,13 @@ class CanvasView(QtWidgets.QGraphicsView):
1684
1871
  it.setSelected(True)
1685
1872
  for it, start in zip(self._dup_items, self._dup_orig):
1686
1873
  it.setPos(start + delta)
1687
- event.accept()
1688
- return
1689
- super().mouseMoveEvent(event)
1874
+ event.accept()
1875
+ return
1876
+ super().mouseMoveEvent(event)
1877
+
1878
+ def leaveEvent(self, event: QtCore.QEvent) -> None:
1879
+ self._clear_hover_preview()
1880
+ super().leaveEvent(event)
1690
1881
 
1691
1882
  def mouseReleaseEvent(self, event: QtGui.QMouseEvent):
1692
1883
  if event.button() == QtCore.Qt.MouseButton.RightButton:
@@ -2506,9 +2697,10 @@ class CanvasView(QtWidgets.QGraphicsView):
2506
2697
  items[idx + 1], items[idx] = items[idx], items[idx + 1]
2507
2698
  elif action == back_act:
2508
2699
  items.insert(0, items.pop(idx))
2509
- elif action == front_act:
2510
- items.append(items.pop(idx))
2511
- for z, it in enumerate(items):
2512
- it.setZValue(z)
2513
- else:
2514
- super().contextMenuEvent(event)
2700
+ elif action == front_act:
2701
+ items.append(items.pop(idx))
2702
+ for z, it in enumerate(items):
2703
+ it.setZValue(z)
2704
+ self._history.mark_dirty()
2705
+ else:
2706
+ super().contextMenuEvent(event)
@@ -1,16 +1,16 @@
1
- # drawsvg-ui
2
- # Copyright (C) 2025 Andreas Wambold
3
- #
4
- # This program is free software: you can redistribute it and/or modify
5
- # it under the terms of the GNU General Public License as published by
6
- # the Free Software Foundation, either version 3 of the License, or
7
- # (at your option) any later version.
8
-
9
- from PySide6 import QtCore, QtGui
10
-
11
- PALETTE_MIME = "application/x-drawsvg-shape"
12
- SHAPES = (
13
- "Rectangle",
1
+ # drawsvg-ui
2
+ # Copyright (C) 2025 Andreas Wambold
3
+ #
4
+ # This program is free software: you can redistribute it and/or modify
5
+ # it under the terms of the GNU General Public License as published by
6
+ # the Free Software Foundation, either version 3 of the License, or
7
+ # (at your option) any later version.
8
+
9
+ from PySide6 import QtCore, QtGui
10
+
11
+ PALETTE_MIME = "application/x-drawsvg-shape"
12
+ SHAPES = (
13
+ "Rectangle",
14
14
  "Rounded Rectangle",
15
15
  "Split Rounded Rectangle",
16
16
  "Ellipse",
@@ -46,8 +46,15 @@ SELECTED_COLOR = QtGui.QColor("#16CCFA")
46
46
  PEN_SELECTED = QtGui.QPen(SELECTED_COLOR, 1, QtCore.Qt.PenStyle.DashLine)
47
47
  PEN_SELECTED.setCosmetic(True)
48
48
  DEFAULT_FILL = QtGui.QBrush(QtCore.Qt.white)
49
- DEFAULT_TEXT_COLOR = QtGui.QColor("#000")
50
- DEFAULT_FONT_FAMILY = "Arial"
49
+ DEFAULT_TEXT_COLOR = QtGui.QColor("#000")
50
+ DEFAULT_FONT_FAMILY = "Arial"
51
+
52
+ # Hover preview for selectable items in the canvas.
53
+ # Increase/decrease strength for a stronger/weaker effect.
54
+ HOVER_PREVIEW_STRENGTH = 0.8
55
+ # Total number of X markers drawn around the hovered item's border.
56
+ HOVER_PREVIEW_X_COUNT = 16
57
+ HOVER_PREVIEW_COLOR = QtGui.QColor("#14b5ff")
51
58
 
52
59
  # Default dash patterns used when exporting/importing common pen styles.
53
60
  PEN_STYLE_DASH_ARRAYS = {
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: drawsvg-ui
3
- Version: 0.5.4
3
+ Version: 0.6.0
4
4
  Summary: GUI for creating drawsvg scenes with PySide6
5
5
  Author-email: Taron686 <Taron686@users.noreply.github.com>
6
6
  License: GPL-3.0-or-later
@@ -33,6 +33,10 @@ Dynamic: license-file
33
33
 
34
34
  # DrawSVG UI
35
35
 
36
+ [![Tag](https://img.shields.io/github/v/tag/Taron686/drawsvg-ui?label=tag)](https://github.com/Taron686/drawsvg-ui/tags)
37
+ [![Build](https://github.com/Taron686/drawsvg-ui/actions/workflows/publish.yml/badge.svg?branch=main)](https://github.com/Taron686/drawsvg-ui/actions/workflows/publish.yml)
38
+ [![PyPI](https://img.shields.io/pypi/v/drawsvg-ui.svg?label=pypi)](https://pypi.org/project/drawsvg-ui/)
39
+
36
40
  This repository provides a graphical user interface designed to make it easier to create files with the [drawsvg](https://pypi.org/project/drawsvg/) library.
37
41
  Instead of writing raw Python code by hand, you can visually place, move, and edit shapes on a canvas, then export your work as a ready-to-use `drawsvg` file. The UI runs on PySide6 and comes with a property inspector, snapping/grid helpers and a set of ready-made shapes.
38
42
 
@@ -1463,16 +1463,27 @@ def export_drawsvg_py(scene: QtWidgets.QGraphicsScene, parent: QtWidgets.QWidget
1463
1463
  except Exception:
1464
1464
  text_dir = None
1465
1465
 
1466
- text_x = x_top + doc_margin * s
1467
- text_y = y_top + doc_margin * s
1468
-
1469
- base_attrs = [
1470
- f"fill='{color.name()}'",
1471
- f"font_family='{font.family()}'",
1472
- "text_anchor='start'",
1473
- "dominant_baseline='text-before-edge'",
1474
- "alignment_baseline='text-before-edge'",
1475
- f"line_height={line_ratio:.6f}",
1466
+ text_left = x_top + doc_margin * s
1467
+ text_right = x_top + br.width() * s - doc_margin * s
1468
+ text_center = (text_left + text_right) / 2.0
1469
+ if h_align == "right":
1470
+ text_anchor = "end"
1471
+ text_x = text_right
1472
+ elif h_align == "center":
1473
+ text_anchor = "middle"
1474
+ text_x = text_center
1475
+ else:
1476
+ text_anchor = "start"
1477
+ text_x = text_left
1478
+ text_y = y_top + doc_margin * s
1479
+
1480
+ base_attrs = [
1481
+ f"fill='{color.name()}'",
1482
+ f"font_family='{font.family()}'",
1483
+ f"text_anchor='{text_anchor}'",
1484
+ "dominant_baseline='text-before-edge'",
1485
+ "alignment_baseline='text-before-edge'",
1486
+ f"line_height={line_ratio:.6f}",
1476
1487
  "xml__space='preserve'",
1477
1488
  f"data_doc_margin={doc_margin:.4f}",
1478
1489
  f"data_font_px={pixel_size:.4f}",
@@ -786,10 +786,20 @@ def import_drawsvg_py(scene: QtWidgets.QGraphicsScene, parent: QtWidgets.QWidget
786
786
  except Exception:
787
787
  pass
788
788
 
789
- doc_margin_scene = doc_margin * scale_factor
790
- x_pos = text_x - doc_margin_scene
791
- y_pos = text_y - doc_margin_scene
792
- item.setPos(x_pos, y_pos)
789
+ doc_margin_scene = doc_margin * scale_factor
790
+ box_w_scene = (
791
+ float(box_w) * scale_factor
792
+ if box_w is not None
793
+ else item.boundingRect().width() * scale_factor
794
+ )
795
+ if data_text_h == "right":
796
+ x_pos = text_x - (box_w_scene - doc_margin_scene)
797
+ elif data_text_h == "center":
798
+ x_pos = text_x - box_w_scene / 2.0
799
+ else:
800
+ x_pos = text_x - doc_margin_scene
801
+ y_pos = text_y - doc_margin_scene
802
+ item.setPos(x_pos, y_pos)
793
803
  br = item.boundingRect()
794
804
  item.setTransformOriginPoint(br.width() / 2.0, br.height() / 2.0)
795
805
  if "transform" in kwargs:
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes