Skip to content

Commit dff0269

Browse files
add highlighter
1 parent 907be42 commit dff0269

File tree

5 files changed

+70
-106
lines changed

5 files changed

+70
-106
lines changed
Lines changed: 2 additions & 0 deletions
Loading

data/resources.data.gresource.xml.in

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
<file preprocess="xml-stripblanks">icons/scalable/actions/text-insert2-symbolic.svg</file>
1212
<file preprocess="xml-stripblanks">icons/scalable/actions/pointer-primary-click-symbolic.svg</file>
1313
<file preprocess="xml-stripblanks">icons/scalable/actions/screenshooter-symbolic.svg</file>
14+
<file preprocess="xml-stripblanks">icons/scalable/actions/marker-symbolic.svg</file>
1415

1516
</gresource>
1617
</gresources>

gradia/ui/drawing_actions.py

Lines changed: 47 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
#Copyright (C) 2025 Alexander Vanhee
1+
# Copyright (C) 2025 Alexander Vanhee
22
#
33
# This program is free software: you can redistribute it and/or modify
44
# it under the terms of the GNU General Public License as published by
@@ -18,7 +18,6 @@
1818
from gi.repository import Gtk, Gdk, Gio, cairo, Pango, PangoCairo
1919
from enum import Enum
2020
import math
21-
import re
2221

2322
class DrawingMode(Enum):
2423
PEN = "pen"
@@ -28,38 +27,38 @@ class DrawingMode(Enum):
2827
CIRCLE = "circle"
2928
TEXT = "text"
3029
SELECT = "select"
30+
HIGHLIGHTER = "highlighter"
3131

3232
class DrawingAction:
33+
DEFAULT_PADDING = 0.02
34+
3335
def draw(self, cr: cairo.Context, image_to_widget_coords, scale: float):
3436
raise NotImplementedError
3537

3638
def get_bounds(self):
3739
raise NotImplementedError
3840

41+
def apply_padding(self, bounds, extra_padding=0.0):
42+
min_x, min_y, max_x, max_y = bounds
43+
padding = self.DEFAULT_PADDING + extra_padding
44+
return (min_x - padding, min_y - padding, max_x + padding, max_y + padding)
45+
3946
def contains_point(self, x, y):
4047
min_x, min_y, max_x, max_y = self.get_bounds()
41-
tolerance_x = (max_x - min_x) * 0.1 if (max_x - min_x) > 0 else 0.01
42-
tolerance_y = (max_y - min_y) * 0.1 if (max_y - min_y) > 0 else 0.01
43-
4448
if isinstance(self, (LineAction, ArrowAction)):
4549
px, py = x, y
4650
x1, y1 = self.start
4751
x2, y2 = self.end
48-
49-
line_len_sq = (x2 - x1)**2 + (y2 - y1)**2
52+
line_len_sq = (x2 - x1) ** 2 + (y2 - y1) ** 2
5053
if line_len_sq == 0:
51-
return math.hypot(px - x1, py - y1) < tolerance_x
52-
54+
return math.hypot(px - x1, py - y1) < self.DEFAULT_PADDING
5355
t = ((px - x1) * (x2 - x1) + (py - y1) * (y2 - y1)) / line_len_sq
5456
t = max(0, min(1, t))
55-
5657
closest_x = x1 + t * (x2 - x1)
5758
closest_y = y1 + t * (y2 - y1)
58-
5959
dist_sq = (px - closest_x)**2 + (py - closest_y)**2
6060
return dist_sq < (0.01 + self.width / 200.0)**2
61-
62-
return min_x - tolerance_x <= x <= max_x + tolerance_x and min_y - tolerance_y <= y <= max_y + tolerance_y
61+
return min_x <= x <= max_x and min_y <= y <= max_y
6362

6463
def translate(self, dx, dy):
6564
raise NotImplementedError
@@ -83,11 +82,9 @@ def draw(self, cr, image_to_widget_coords, scale):
8382

8483
def get_bounds(self):
8584
if not self.stroke:
86-
return 0, 0, 0, 0
87-
xs = [p[0] for p in self.stroke]
88-
ys = [p[1] for p in self.stroke]
89-
padding = self.pen_size / 200.0
90-
return min(xs) - padding, min(ys) - padding, max(xs) + padding, max(ys) + padding
85+
return (0, 0, 0, 0)
86+
xs, ys = zip(*self.stroke)
87+
return self.apply_padding((min(xs), min(ys), max(xs), max(ys)))
9188

9289
def translate(self, dx, dy):
9390
self.stroke = [(x + dx, y + dy) for x, y in self.stroke]
@@ -129,8 +126,7 @@ def get_bounds(self):
129126
max_x = max(self.start[0], self.end[0])
130127
min_y = min(self.start[1], self.end[1])
131128
max_y = max(self.start[1], self.end[1])
132-
padding = 0.025
133-
return min_x - padding, min_y - padding, max_x + padding, max_y + padding
129+
return self.apply_padding((min_x, min_y, max_x, max_y))
134130

135131
def translate(self, dx, dy):
136132
self.start = (self.start[0] + dx, self.start[1] + dy)
@@ -147,72 +143,38 @@ def __init__(self, position, text, color, font_size, font_family="Sans"):
147143
def draw(self, cr, image_to_widget_coords, scale):
148144
if not self.text.strip():
149145
return
150-
151146
x, y = image_to_widget_coords(*self.position)
152147
cr.set_source_rgba(*self.color)
153-
154148
layout = PangoCairo.create_layout(cr)
155149
font_desc = Pango.FontDescription()
156150
font_desc.set_family(self.font_family)
157151
font_desc.set_size(int(self.font_size * scale * Pango.SCALE))
158152
layout.set_font_description(font_desc)
159153
layout.set_text(self.text, -1)
160-
161-
ink_rect, logical_rect = layout.get_extents()
154+
_, logical_rect = layout.get_extents()
162155
text_width = logical_rect.width / Pango.SCALE
163156
text_height = logical_rect.height / Pango.SCALE
164-
165-
adjusted_x = x - (text_width / 2)
166-
adjusted_y = y - text_height
167-
168-
cr.move_to(adjusted_x, adjusted_y)
157+
cr.move_to(x - text_width / 2, y - text_height)
169158
PangoCairo.show_layout(cr, layout)
170159

171160
def get_bounds(self):
172161
char_width_factor = 0.6
173-
estimated_text_width_img_units = len(self.text) * self.font_size * char_width_factor / 800.0
174-
estimated_text_height_img_units = self.font_size / 400.0
175-
162+
width = len(self.text) * self.font_size * char_width_factor / 800.0
163+
height = self.font_size / 400.0
176164
x, y = self.position
177-
178-
min_x = x - estimated_text_width_img_units / 2
179-
max_x = x + estimated_text_width_img_units / 2
180-
min_y = y - estimated_text_height_img_units
181-
max_y = y
182-
183-
return (min_x, min_y, max_x, max_y)
165+
return self.apply_padding((x - width / 2, y - height, x + width / 2, y))
184166

185167
def translate(self, dx, dy):
186168
self.position = (self.position[0] + dx, self.position[1] + dy)
187169

188-
class LineAction(DrawingAction):
189-
def __init__(self, start, end, color, width):
190-
self.start = start
191-
self.end = end
192-
self.color = color
193-
self.width = width
194-
170+
class LineAction(ArrowAction):
195171
def draw(self, cr, image_to_widget_coords, scale):
196172
cr.set_source_rgba(*self.color)
197173
cr.set_line_width(self.width * scale)
198-
start_x, start_y = image_to_widget_coords(*self.start)
199-
end_x, end_y = image_to_widget_coords(*self.end)
200-
cr.move_to(start_x, start_y)
201-
cr.line_to(end_x, end_y)
174+
cr.move_to(*image_to_widget_coords(*self.start))
175+
cr.line_to(*image_to_widget_coords(*self.end))
202176
cr.stroke()
203177

204-
def get_bounds(self):
205-
min_x = min(self.start[0], self.end[0])
206-
max_x = max(self.start[0], self.end[0])
207-
min_y = min(self.start[1], self.end[1])
208-
max_y = max(self.start[1], self.end[1])
209-
padding = self.width / 200.0
210-
return min_x - padding, min_y - padding, max_x + padding, max_y + padding
211-
212-
def translate(self, dx, dy):
213-
self.start = (self.start[0] + dx, self.start[1] + dy)
214-
self.end = (self.end[0] + dx, self.end[1] + dy)
215-
216178
class RectAction(DrawingAction):
217179
def __init__(self, start, end, color, width, fill_color=None):
218180
self.start = start
@@ -224,74 +186,61 @@ def __init__(self, start, end, color, width, fill_color=None):
224186
def draw(self, cr, image_to_widget_coords, scale):
225187
x1, y1 = image_to_widget_coords(*self.start)
226188
x2, y2 = image_to_widget_coords(*self.end)
227-
rect_x = min(x1, x2)
228-
rect_y = min(y1, y2)
229-
rect_w = abs(x2 - x1)
230-
rect_h = abs(y2 - y1)
231-
189+
x, y = min(x1, x2), min(y1, y2)
190+
w, h = abs(x2 - x1), abs(y2 - y1)
232191
if self.fill_color:
233192
cr.set_source_rgba(*self.fill_color)
234-
cr.rectangle(rect_x, rect_y, rect_w, rect_h)
193+
cr.rectangle(x, y, w, h)
235194
cr.fill()
236-
237195
cr.set_source_rgba(*self.color)
238196
cr.set_line_width(self.width * scale)
239-
cr.rectangle(rect_x, rect_y, rect_w, rect_h)
197+
cr.rectangle(x, y, w, h)
240198
cr.stroke()
241199

242200
def get_bounds(self):
243201
min_x = min(self.start[0], self.end[0])
244202
max_x = max(self.start[0], self.end[0])
245203
min_y = min(self.start[1], self.end[1])
246204
max_y = max(self.start[1], self.end[1])
247-
padding = self.width / 200.0
248-
return min_x - padding, min_y - padding, max_x + padding, max_y + padding
205+
return self.apply_padding((min_x, min_y, max_x, max_y))
249206

250207
def translate(self, dx, dy):
251208
self.start = (self.start[0] + dx, self.start[1] + dy)
252209
self.end = (self.end[0] + dx, self.end[1] + dy)
253210

254-
class CircleAction(DrawingAction):
255-
def __init__(self, start, end, color, width, fill_color=None):
256-
self.start = start
257-
self.end = end
258-
self.color = color
259-
self.width = width
260-
self.fill_color = fill_color
261-
211+
class CircleAction(RectAction):
262212
def draw(self, cr, image_to_widget_coords, scale):
263213
x1, y1 = image_to_widget_coords(*self.start)
264214
x2, y2 = image_to_widget_coords(*self.end)
265-
cx = (x1 + x2) / 2
266-
cy = (y1 + y2) / 2
267-
rx = abs(x2 - x1) / 2
268-
ry = abs(y2 - y1) / 2
215+
cx, cy = (x1 + x2) / 2, (y1 + y2) / 2
216+
rx, ry = abs(x2 - x1) / 2, abs(y2 - y1) / 2
269217
if rx < 1e-3 or ry < 1e-3:
270218
return
271-
272219
cr.save()
273220
cr.translate(cx, cy)
274221
cr.scale(rx, ry)
275222
cr.arc(0, 0, 1, 0, 2 * math.pi)
276223
cr.restore()
277-
278224
if self.fill_color:
279225
cr.set_source_rgba(*self.fill_color)
280226
cr.fill_preserve()
281-
282227
cr.set_source_rgba(*self.color)
283228
cr.set_line_width(self.width * scale)
284229
cr.stroke()
285230

286-
def get_bounds(self):
287-
min_x = min(self.start[0], self.end[0])
288-
max_x = max(self.start[0], self.end[0])
289-
min_y = min(self.start[1], self.end[1])
290-
max_y = max(self.start[1], self.end[1])
291-
padding = self.width / 200.0
292-
return min_x - padding, min_y - padding, max_x + padding, max_y + padding
293-
294-
def translate(self, dx, dy):
295-
self.start = (self.start[0] + dx, self.start[1] + dy)
296-
self.end = (self.end[0] + dx, self.end[1] + dy)
231+
class HighlighterAction(StrokeAction):
232+
def draw(self, cr, image_to_widget_coords, scale):
233+
if len(self.stroke) < 2:
234+
return
235+
coords = [image_to_widget_coords(x, y) for x, y in self.stroke]
236+
cr.set_operator(cairo.Operator.MULTIPLY)
237+
cr.set_source_rgba(*self.color)
238+
cr.set_line_width(self.pen_size * scale)
239+
cr.set_line_cap(cairo.LineCap.BUTT)
240+
cr.move_to(*coords[0])
241+
for point in coords[1:]:
242+
cr.line_to(*point)
243+
cr.stroke()
244+
cr.set_operator(cairo.Operator.OVER)
245+
cr.set_line_cap(cairo.LineCap.ROUND)
297246

gradia/ui/drawing_overlay.py

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
DEFAULT_FONT_SIZE = 22.0
2828
DEFAULT_FONT_FAMILY = "Caveat"
2929
DEFAULT_PEN_COLOR = (1.0, 1.0, 1.0, 0.8)
30+
DEFAULT_HIGHLIGHTER_SIZE = 12.0
3031

3132
class DrawingOverlay(Gtk.DrawingArea):
3233
def __init__(self):
@@ -40,6 +41,7 @@ def __init__(self):
4041
self.font_size = DEFAULT_FONT_SIZE
4142
self.font_family = DEFAULT_FONT_FAMILY
4243
self.pen_color = DEFAULT_PEN_COLOR
44+
self.highlighter_size = DEFAULT_HIGHLIGHTER_SIZE
4345
self.fill_color = None
4446
self.is_drawing = False
4547
self.current_stroke = []
@@ -107,7 +109,6 @@ def _setup_actions(self):
107109
if hasattr(root, "add_action"):
108110
root.add_action(action)
109111

110-
111112
def remove_selected_action(self) -> bool :
112113
if self.selected_action and self.selected_action in self.actions:
113114
self.actions.remove(self.selected_action)
@@ -325,7 +326,7 @@ def _on_drag_begin(self, gesture, x, y):
325326

326327
self.is_drawing = True
327328
rel = self._widget_to_image_coords(x, y)
328-
if self.drawing_mode == DrawingMode.PEN:
329+
if self.drawing_mode == DrawingMode.PEN or self.drawing_mode == DrawingMode.HIGHLIGHTER:
329330
self.current_stroke = [rel]
330331
else:
331332
self.start_point = rel
@@ -351,7 +352,7 @@ def _on_drag_update(self, gesture, dx, dy):
351352
if not self.is_drawing:
352353
return
353354

354-
if self.drawing_mode == DrawingMode.PEN:
355+
if self.drawing_mode == DrawingMode.PEN or self.drawing_mode == DrawingMode.HIGHLIGHTER:
355356
self.current_stroke.append((rel_x, rel_y))
356357
else:
357358
self.end_point = (rel_x, rel_y)
@@ -371,14 +372,18 @@ def _on_drag_end(self, gesture, dx, dy):
371372

372373
self.is_drawing = False
373374
mode = self.drawing_mode
374-
if mode == DrawingMode.PEN and len(self.current_stroke) > 1:
375-
self.actions.append(StrokeAction(self.current_stroke.copy(), self.pen_color, self.pen_size))
375+
if (mode == DrawingMode.PEN or mode == DrawingMode.HIGHLIGHTER) and len(self.current_stroke) > 1:
376+
if mode == DrawingMode.PEN:
377+
self.actions.append(StrokeAction(self.current_stroke.copy(), self.pen_color, self.pen_size))
378+
else:
379+
highlighter_color = (self.pen_color[0], self.pen_color[1], self.pen_color[2], 0.3)
380+
self.actions.append(HighlighterAction(self.current_stroke.copy(), highlighter_color, self.highlighter_size))
376381
self.current_stroke.clear()
377382
elif self.start_point and self.end_point:
378383
if mode == DrawingMode.ARROW:
379384
self.actions.append(ArrowAction(self.start_point, self.end_point, self.pen_color, self.arrow_head_size, self.pen_size))
380385
elif mode == DrawingMode.LINE:
381-
self.actions.append(LineAction(self.start_point, self.end_point, self.pen_color, self.pen_size))
386+
self.actions.append(LineAction(self.start_point, self.end_point, self.pen_color, 0,self.pen_size))
382387
elif mode == DrawingMode.SQUARE:
383388
self.actions.append(RectAction(self.start_point, self.end_point, self.pen_color, self.pen_size, self.fill_color))
384389
elif mode == DrawingMode.CIRCLE:
@@ -400,7 +405,7 @@ def _on_motion(self, controller, x, y):
400405
else:
401406
name = "default"
402407
else:
403-
name = "crosshair" if self.drawing_mode == DrawingMode.PEN else "cell"
408+
name = "crosshair" if self.drawing_mode == DrawingMode.PEN or self.drawing_mode == DrawingMode.HIGHLIGHTER else "cell"
404409
if not self._is_point_in_image(x, y):
405410
name = "default"
406411
self.set_cursor(Gdk.Cursor.new_from_name(name, None))
@@ -420,11 +425,14 @@ def _on_draw(self, area, cr, width, height):
420425
cr.set_source_rgba(*self.pen_color)
421426
if self.drawing_mode == DrawingMode.PEN and len(self.current_stroke) > 1:
422427
StrokeAction(self.current_stroke, self.pen_color, self.pen_size).draw(cr, self._image_to_widget_coords, scale)
428+
elif self.drawing_mode == DrawingMode.HIGHLIGHTER and len(self.current_stroke) > 1:
429+
highlighter_color = (self.pen_color[0], self.pen_color[1], self.pen_color[2], 0.3)
430+
HighlighterAction(self.current_stroke, highlighter_color, self.highlighter_size).draw(cr, self._image_to_widget_coords, scale)
423431
elif self.start_point and self.end_point:
424432
if self.drawing_mode == DrawingMode.ARROW:
425433
ArrowAction(self.start_point, self.end_point, self.pen_color, self.arrow_head_size, self.pen_size).draw(cr, self._image_to_widget_coords, scale)
426434
elif self.drawing_mode == DrawingMode.LINE:
427-
LineAction(self.start_point, self.end_point, self.pen_color, self.pen_size).draw(cr, self._image_to_widget_coords, scale)
435+
LineAction(self.start_point, self.end_point, self.pen_color, 0, self.pen_size).draw(cr, self._image_to_widget_coords, scale)
428436
elif self.drawing_mode == DrawingMode.SQUARE:
429437
RectAction(self.start_point, self.end_point, self.pen_color, self.pen_size, self.fill_color).draw(cr, self._image_to_widget_coords, scale)
430438
elif self.drawing_mode == DrawingMode.CIRCLE:
@@ -501,6 +509,9 @@ def set_font_size(self, size):
501509
def set_font_family(self, family):
502510
self.font_family = family if family else "Sans"
503511

512+
def set_highlighter_size(self, s):
513+
self.highlighter_size = max(1.0, s)
514+
504515
def set_drawing_visible(self, v):
505516
self.set_visible(v)
506517

gradia/ui/ui_parts.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,7 @@ def create_drawing_tools_group() -> Adw.PreferencesGroup:
298298
(DrawingMode.ARROW, "arrow1-top-right-symbolic", 4, 0),
299299
(DrawingMode.SQUARE, "box-small-outline-symbolic", 0, 1),
300300
(DrawingMode.CIRCLE, "circle-outline-thick-symbolic", 1, 1),
301+
(DrawingMode.HIGHLIGHTER, "marker-symbolic", 2, 1),
301302
]
302303

303304
fill_sensitive_modes = {DrawingMode.SQUARE, DrawingMode.CIRCLE}

0 commit comments

Comments
 (0)