Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 33 additions & 8 deletions skitchg/canvas.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ def __init__(self, parent=None):
self._move_snapshot = [] # (item, pos) at mouse press in select mode
self._handle_drag = False # a reshape handle grabbed the mouse
self._body_drag = False # an annotation body grabbed the mouse
self._text_click = None # (item, press pos): text tool press on a block
self._auto_fit = True # refit on resize until the user zooms manually

# ------------------------------------------------------------------ image
Expand Down Expand Up @@ -215,11 +216,9 @@ def mousePressEvent(self, event):
pos = self.mapToScene(event.position().toPoint())

if self._editing_text is not None:
# Clicking outside the text being edited commits it.
if self._editing_text not in self.items(event.position().toPoint()):
self.commit_text_edit()
else:
return
# Clicking anywhere commits the text being edited. Pressing on it
# again grabs it for moving; a plain click reopens it for editing.
self.commit_text_edit()

# Reshape handles on a selected annotation always win over drawing a
# new item, no matter which tool is active.
Expand All @@ -236,8 +235,16 @@ def mousePressEvent(self, event):

if self.tool == "text":
item = self._editable_item_at(event.position().toPoint())
if item is not None:
self.start_text_edit(item)
body = next((i for i in self.items(event.position().toPoint())
if isinstance(i, AnnotationItem)), None)
if item is not None or body is not None:
# Drag to move the annotation; a click without dragging on a
# text block edits it.
self._text_click = (item, pos) if item is not None else None
self._body_drag = True
self._move_snapshot = [(i, QPointF(i.pos()))
for i in self.annotation_items()]
super().mousePressEvent(event)
else:
self._create_text(pos)
return
Expand Down Expand Up @@ -315,7 +322,15 @@ def mouseMoveEvent(self, event):
elif isinstance(item, PenItem):
item.add_point(pos)
elif isinstance(item, MarkerItem):
item.set_geometry(head=pos)
# The pointer follows the drag: the tip rides under the cursor and
# the head trails behind toward where the drag started. A tiny
# jitter keeps the default upright pin on the click point.
d = pos - self._drag_start
if math.hypot(d.x(), d.y()) < item.radius * 0.5:
item.set_geometry(tip=self._drag_start, head=self._drag_start
+ QPointF(0, -item.default_offset()))
else:
item.set_geometry(tip=pos, head=self._drag_start)
elif self._crop_overlay is not None:
self._crop_overlay.setRect(QRectF(self._drag_start, pos).normalized())

Expand All @@ -332,6 +347,16 @@ def mouseReleaseEvent(self, event):
if self._body_drag:
self._body_drag = False
super().mouseReleaseEvent(event)
click, self._text_click = self._text_click, None
if click is not None:
item, start = click
if (self.mapToScene(event.position().toPoint()) - start).manhattanLength() < MIN_DRAG:
# Just a click: undo any jitter and open it for editing.
for i, old_pos in self._move_snapshot:
i.setPos(old_pos)
self._move_snapshot = []
self.start_text_edit(item)
return
self._push_moves()
return

Expand Down
51 changes: 49 additions & 2 deletions skitchg/items.py
Original file line number Diff line number Diff line change
Expand Up @@ -505,7 +505,8 @@ class TextItem(AnnotationItem):
"""Bold annotation text with a white outline for readability.

Editing is a simple append/backspace model driven by the canvas:
click to place, type, Enter commits, Escape cancels.
click to place, type, Enter commits, Escape cancels. Once placed, drag
the block to move it and its corner handles to scale it.
"""

def __init__(self, pos, color, point_size, outline=True):
Expand All @@ -524,6 +525,7 @@ def font(self):
def set_text(self, text):
self.prepareGeometryChange()
self.text = text
self._layout_handles()
self.update()

def set_style(self, color=None, stroke_width=None, outline=None,
Expand All @@ -533,6 +535,7 @@ def set_style(self, color=None, stroke_width=None, outline=None,
self.prepareGeometryChange()
self.point_size = float(point_size)
super().set_style(color=color, outline=outline)
self._layout_handles()

def get_style(self):
return {
Expand All @@ -551,6 +554,50 @@ def _apply_geometry_state(self, state):
def _display_text(self):
return self.text + ("‸" if self.editing else "")

# ---- corner handles: drag to scale the text, opposite corner stays put ----

CORNERS = {"tl": "br", "tr": "bl", "bl": "tr", "br": "tl"}

def _text_rect(self):
metrics = QFontMetricsF(self.font())
width = max([metrics.horizontalAdvance(l) for l in self._lines()] + [10.0])
return QRectF(0, 0, width, metrics.lineSpacing() * len(self._lines()))

def _corner(self, role):
r = self._text_rect()
return {"tl": r.topLeft(), "tr": r.topRight(),
"bl": r.bottomLeft(), "br": r.bottomRight()}[role]

def handle_roles(self):
return list(self.CORNERS)

def handle_pos(self, role):
return self._corner(role)

def begin_handle_drag(self):
super().begin_handle_drag()
self._resize_start_size = self.point_size

def handle_moved(self, role, scene_pos):
start = self._drag_start_state
if start is None:
return
anchor_role = self.CORNERS[role]
# Measure against the geometry at drag start so the scale doesn't drift.
self._apply_geometry_state(start)
self.prepareGeometryChange()
self.setPos(start["pos"])
anchor = self.mapToScene(self._corner(anchor_role))
grabbed = self.mapToScene(self._corner(role))
d0 = math.hypot(grabbed.x() - anchor.x(), grabbed.y() - anchor.y())
d1 = math.hypot(scene_pos.x() - anchor.x(), scene_pos.y() - anchor.y())
if d0 < 1e-3:
return
self.point_size = max(6.0, min(400.0, self._resize_start_size * d1 / d0))
self.setPos(self.pos() + anchor - self.mapToScene(self._corner(anchor_role)))
self._layout_handles()
self.update()

def _lines(self):
return self._display_text().split("\n") or [""]

Expand Down Expand Up @@ -605,7 +652,7 @@ class MarkerItem(AnnotationItem):

The tail tip sits on the thing you're pointing at; the head holds an
auto-incremented number (editable, double-click). Drag while placing
to aim the tail in any direction.
and the tail follows the mouse: the tip ends up under the cursor.
"""

MAX_CHARS = 3
Expand Down
12 changes: 12 additions & 0 deletions tests/test_smoke.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,18 @@ def test_smoke():
text.set_style(point_size=40)
assert text.point_size == 40

# Text corner handle: dragging outward scales it, opposite corner fixed
anchor = text.mapToScene(text.handle_pos("tl"))
corner = text.mapToScene(text.handle_pos("br"))
text.begin_handle_drag()
text.handle_moved("br", anchor + (corner - anchor) * 1.5)
text.end_handle_drag()
assert abs(text.point_size - 60) < 0.01
moved = text.mapToScene(text.handle_pos("tl")) - anchor
assert abs(moved.x()) < 0.5 and abs(moved.y()) < 0.5
stack.undo()
assert text.point_size == 40

# Text edit flow: commit, discard-empty, cancel
canvas._create_text(QPointF(50, 50))
canvas._editing_text.set_text("typed")
Expand Down
Loading