Rickard's Projects

This site hosts my projects.

Projects

Recent events

2025-09-07 17:28 Rickard pushed to timeline

changeset:   8077:610f9fbce68b
user:        Rickard Lindberg <rickard@rickardlindberg.me>
date:        Sun Sep 07 16:55:15 2025 +0200
summary:     Event box drawer draws text exactly where scene tells it to

diff -r ada2e0b2fad1 -r 610f9fbce68b source/timelinelib/canvas/data/event.py
--- a/source/timelinelib/canvas/data/event.py Sun Sep 07 16:29:14 2025 +0200
+++ b/source/timelinelib/canvas/data/event.py Sun Sep 07 16:55:15 2025 +0200
@@ -301,9 +301,6 @@
 
     icon = property(get_icon, set_icon)
 
-    def has_edge_icons(self):
-        return self.get_fuzzy() or self.fuzzy_start or self. fuzzy_end or self.get_locked()
-
     def get_hyperlink(self):
         return self._immutable_value.hyperlink
 
diff -r ada2e0b2fad1 -r 610f9fbce68b source/timelinelib/canvas/eventboxdrawers/defaulteventboxdrawer.py
--- a/source/timelinelib/canvas/eventboxdrawers/defaulteventboxdrawer.py Sun Sep 07 16:29:14 2025 +0200
+++ b/source/timelinelib/canvas/eventboxdrawers/defaulteventboxdrawer.py Sun Sep 07 16:55:15 2025 +0200
@@ -195,23 +195,9 @@
 
     def _draw_normal_text(self, dc, rect, event):
         dc.SetClippingRegion(rect)
-        dc.DrawText(self.scene.get_event_text(event), self._calc_x_pos(dc, rect, event), self._calc_y_pos(rect))
+        dc.DrawText(self.scene.get_event_text(event), rect.X, rect.Y)
         dc.DestroyClippingRegion()
 
-    def _calc_x_pos(self, dc, rect, event):
-        inner_rect = rect
-        text_x = inner_rect.X
-        text_x = self._adjust_x_for_edge_icons(event, rect, text_x)
-        return text_x
-
-    def _adjust_x_for_edge_icons(self, event, rect, text_x):
-        if event.has_edge_icons():
-            text_x += rect.Height // 2
-        return text_x
-
-    def _calc_y_pos(self, rect):
-        return rect.Y
-
     def _set_text_foreground_color(self, dc, event_rects):
         if event_rects.text_outside_box():
             color = wx.BLACK
diff -r ada2e0b2fad1 -r 610f9fbce68b test/unit/canvas/eventboxdrawers/defaulteventboxdrawer.py
--- a/test/unit/canvas/eventboxdrawers/defaulteventboxdrawer.py Sun Sep 07 16:29:14 2025 +0200
+++ b/test/unit/canvas/eventboxdrawers/defaulteventboxdrawer.py Sun Sep 07 16:55:15 2025 +0200
@@ -60,26 +60,6 @@
         self.dc.SetClippingRegion.assert_called_with(Rect(0, 0, WIDTH, HEIGHT))
         self.dc.DrawText.assert_called_with(DEFAULT_TEXT, 0, 0)
 
-    def test_non_centered_text_is_left_ajusted_when_fuzzy(self):
-        WIDTH = 100
-        HEIGHT = 20
-        self.event.has_edge_icons.return_value = True
-        rect = Rect(0, 0, WIDTH, HEIGHT)
-        self.drawer.center_text = False
-        self.drawer._draw_text(self.dc, event_rects(rect, self.event))
-        self.dc.SetClippingRegion.assert_called_with(Rect(0, 0, WIDTH, HEIGHT))
-        self.dc.DrawText.assert_called_with(DEFAULT_TEXT, HEIGHT // 2, 0)
-
-    def test_non_centered_text_is_left_ajusted_when_locked(self):
-        WIDTH = 100
-        HEIGHT = 20
-        self.event.has_edge_icons.return_value = True
-        rect = Rect(0, 0, WIDTH, HEIGHT)
-        self.drawer.center_text = False
-        self.drawer._draw_text(self.dc, event_rects(rect, self.event))
-        self.dc.SetClippingRegion.assert_called_with(Rect(0, 0, WIDTH, HEIGHT))
-        self.dc.DrawText.assert_called_with(DEFAULT_TEXT, HEIGHT // 2, 0)
-
     def test_milestone_with_no_text_can_be_drawn(self):
         with self.wxapp():
             self.event.text = ""
@@ -105,7 +85,6 @@
         self.event.get_fuzzy.return_value = False
         self.event.get_locked.return_value = False
         self.event.get_text.return_value = DEFAULT_TEXT
-        self.event.has_edge_icons.return_value = False
         self.cat = Mock()
         self.cat.font_color = (0, 0, 0)
         self.event.get_category.return_value = self.cat

changeset:   8079:f7628af2fb74
tag:         tip
user:        Rickard Lindberg <rickard@rickardlindberg.me>
date:        Sun Sep 07 17:23:49 2025 +0200
summary:     Move creation of EventRects down the stack

diff -r 6cab3cd2a859 -r f7628af2fb74 source/timelinelib/canvas/drawing/scene.py
--- a/source/timelinelib/canvas/drawing/scene.py Sun Sep 07 17:12:23 2025 +0200
+++ b/source/timelinelib/canvas/drawing/scene.py Sun Sep 07 17:23:49 2025 +0200
@@ -260,13 +260,7 @@
                     self._extend_container_width_for_point_event(event_rects)
                 return event_rects
             else:
-                rect = self._calc_ideal_rect_for_non_period_event(event)
-                return EventRects(
-                    event=event,
-                    box_rect=rect,
-                    text_rect=rect.CloneDeflate(self._inner_padding, self._inner_padding),
-                    height_padding=self._outer_padding,
-                )
+                return self._calc_ideal_rect_for_non_period_event(event)
 
     def _extend_container_width_for_point_event(self, event_rects):
         """Make point events be enclosed by the container rectangle."""
@@ -357,17 +351,25 @@
             x = self.x_pos_for_time(event.mean_time())
             y0 = self.divider_y
             y1 = y0 + 10
-            return FloatingRect(x - 5, y1 - 5, 10, 10)
-        rw = self._calc_width_for_non_period_event(event)
-        rh = self._calc_height_for_non_period_event(event)
-        rx = self._calc_x_pos_for_non_period_event(event, rw)
-        ry = self._calc_y_pos_for_non_period_event(event, rh)
-        if event.is_milestone():
-            rect = Rect(rx, ry, rw, rh)
-            rect.SetWidth(rect.GetHeight())
-            rect.SetX(self._metrics.calc_x(event.get_time_period().start_time) - rect.Width // 2)
-            return FloatingRect(rect)
-        return self._calc_ideal_wx_rect(rx, ry, rw, rh)
+            rect = FloatingRect(x - 5, y1 - 5, 10, 10)
+        else:
+            rw = self._calc_width_for_non_period_event(event)
+            rh = self._calc_height_for_non_period_event(event)
+            rx = self._calc_x_pos_for_non_period_event(event, rw)
+            ry = self._calc_y_pos_for_non_period_event(event, rh)
+            if event.is_milestone():
+                rect = Rect(rx, ry, rw, rh)
+                rect.SetWidth(rect.GetHeight())
+                rect.SetX(self._metrics.calc_x(event.get_time_period().start_time) - rect.Width // 2)
+                rect = FloatingRect(rect)
+            else:
+                rect = self._calc_ideal_wx_rect(rx, ry, rw, rh)
+        return EventRects(
+            event=event,
+            box_rect=rect,
+            text_rect=rect.CloneDeflate(self._inner_padding, self._inner_padding),
+            height_padding=self._outer_padding,
+        )
 
     def _calc_width_for_non_period_event(self, event):
         tw, th = self._get_text_size(self.get_event_text(event))

2025-09-07 16:29 Rickard pushed to timeline

changeset:   8076:ada2e0b2fad1
user:        Rickard Lindberg <rickard@rickardlindberg.me>
date:        Sun Sep 07 16:29:14 2025 +0200
summary:     Handle centering of text in scene instead of in event box drawer

diff -r 65da552067f8 -r ada2e0b2fad1 source/timelinelib/canvas/drawing/scene.py
--- a/source/timelinelib/canvas/drawing/scene.py Sun Sep 07 12:38:37 2025 +0200
+++ b/source/timelinelib/canvas/drawing/scene.py Sun Sep 07 16:29:14 2025 +0200
@@ -306,6 +306,10 @@
             w = self._calc_width_for_non_period_event(event)
             text_rect.X -= w
             text_rect.Width = w
+        elif self.center_text():
+            tw, th = self._get_text_size(self.get_event_text(event))
+            text_rect.Width = min(text_rect.Width, tw)
+            text_rect.X = box_rect.X + box_rect.Width // 2 - text_rect.Width // 2
         return EventRects(
             event=event,
             box_rect=box_rect.CloneInflate(inflate, inflate),
diff -r 65da552067f8 -r ada2e0b2fad1 source/timelinelib/canvas/eventboxdrawers/defaulteventboxdrawer.py
--- a/source/timelinelib/canvas/eventboxdrawers/defaulteventboxdrawer.py Sun Sep 07 12:38:37 2025 +0200
+++ b/source/timelinelib/canvas/eventboxdrawers/defaulteventboxdrawer.py Sun Sep 07 16:29:14 2025 +0200
@@ -202,7 +202,6 @@
         inner_rect = rect
         text_x = inner_rect.X
         text_x = self._adjust_x_for_edge_icons(event, rect, text_x)
-        text_x = self._adjust_x_for_centered_text(dc, event, inner_rect, text_x)
         return text_x
 
     def _adjust_x_for_edge_icons(self, event, rect, text_x):
@@ -210,18 +209,9 @@
             text_x += rect.Height // 2
         return text_x
 
-    def _adjust_x_for_centered_text(self, dc, event, inner_rect, text_x):
-        if self.center_text:
-            text_x = self._center_text(dc, event, inner_rect, text_x)
-        return text_x
-
     def _calc_y_pos(self, rect):
         return rect.Y
 
-    def _center_text(self, dc, event, inner_rect, text_x):
-        width, _ = dc.GetTextExtent(self.scene.get_event_text(event))
-        return max(text_x, text_x + (inner_rect.width - width) // 2)
-
     def _set_text_foreground_color(self, dc, event_rects):
         if event_rects.text_outside_box():
             color = wx.BLACK
diff -r 65da552067f8 -r ada2e0b2fad1 test/unit/canvas/drawing/scene.py
--- a/test/unit/canvas/drawing/scene.py Sun Sep 07 12:38:37 2025 +0200
+++ b/test/unit/canvas/drawing/scene.py Sun Sep 07 16:29:14 2025 +0200
@@ -144,8 +144,18 @@
         self.assertEqual(event_rects.box_rect, wx.Rect(300, 50, 34, 12))
         self.assertEqual(event_rects.text_rect, wx.Rect(301, 51, 32, 10))
 
+    def test_event_text_centered_in_period_event(self):
+        self.appearance.set_center_event_texts(True)
+        self.given_displayed_period("1 Jan 2010", "31 Jan 2010")
+        self.given_visible_event_at("10 Jan 2010", "11 Jan 2010", text="xy")
+        self.when_scene_is_created()
+        event_rects, = self.scene.event_data
+        self.assertEqual(event_rects.box_rect, wx.Rect(300, 50, 34, 10))
+        self.assertEqual(event_rects.text_rect, wx.Rect(316, 50, 2, 10))
+
     def setUp(self):
         WxAppTestCase.setUp(self)
+        self.appearance = Appearance()
         self.db = MemoryDB()
         self.view_properties = ViewProperties()
         self.given_number_of_events_stackable_is(5)
@@ -201,7 +211,7 @@
     def when_scene_is_created(self):
         self.scene = TimelineScene(
             self.size, self.db, self.view_properties, self.get_text_size_fn,
-            Appearance())
+            self.appearance)
         self.scene.set_outer_padding(self.outer_padding)
         self.scene.set_inner_padding(self.inner_padding)
         self.scene.set_baseline_padding(self.baseline_padding)
diff -r 65da552067f8 -r ada2e0b2fad1 test/unit/canvas/eventboxdrawers/defaulteventboxdrawer.py
--- a/test/unit/canvas/eventboxdrawers/defaulteventboxdrawer.py Sun Sep 07 12:38:37 2025 +0200
+++ b/test/unit/canvas/eventboxdrawers/defaulteventboxdrawer.py Sun Sep 07 16:29:14 2025 +0200
@@ -50,15 +50,6 @@
         self.dc.SetClippingRegion.assert_called_with(Rect(0, 0, WIDTH, HEIGHT))
         self.dc.DrawText.assert_called_with(DEFAULT_TEXT, 0, 0)
 
-    def test_centered_text_is_not_left_aligned(self):
-        WIDTH = 100
-        HEIGHT = 20
-        rect = Rect(0, 0, WIDTH, HEIGHT)
-        self.drawer.center_text = True
-        self.drawer._draw_text(self.dc, event_rects(rect, self.event))
-        self.dc.SetClippingRegion.assert_called_with(Rect(0, 0, WIDTH, HEIGHT))
-        self.dc.DrawText.assert_called_with(DEFAULT_TEXT, (WIDTH - 50) // 2, 0)
-
     def test_centered_text_is_left_aligned_if_text_is_too_long_to_fit(self):
         WIDTH = 100
         HEIGHT = 20

2025-09-07 12:45 Rickard pushed to timeline

changeset:   8075:65da552067f8
user:        Rickard Lindberg <rickard@rickardlindberg.me>
date:        Sun Sep 07 12:38:37 2025 +0200
summary:     Handle text box padding in scene instead of in drawer

diff -r ca6d5ca61efd -r 65da552067f8 source/timelinelib/canvas/drawing/scene.py
--- a/source/timelinelib/canvas/drawing/scene.py Sun Sep 07 12:18:37 2025 +0200
+++ b/source/timelinelib/canvas/drawing/scene.py Sun Sep 07 12:38:37 2025 +0200
@@ -263,7 +263,7 @@
                 return EventRects(
                     event=event,
                     box_rect=rect,
-                    text_rect=rect.Clone(),
+                    text_rect=rect.CloneDeflate(self._inner_padding, self._inner_padding),
                     height_padding=self._outer_padding,
                 )
 
@@ -301,7 +301,7 @@
         else:
             inflate = 0
         box_rect = self._calc_ideal_wx_rect(rx, ry, rw, rh)
-        text_rect = box_rect.Clone()
+        text_rect = box_rect.CloneDeflate(self._inner_padding, self._inner_padding)
         if self._appearance.get_text_to_the_left_of_period_events() and not event.is_container() and not event.is_subevent():
             w = self._calc_width_for_non_period_event(event)
             text_rect.X -= w
diff -r ca6d5ca61efd -r 65da552067f8 source/timelinelib/canvas/eventboxdrawers/defaulteventboxdrawer.py
--- a/source/timelinelib/canvas/eventboxdrawers/defaulteventboxdrawer.py Sun Sep 07 12:18:37 2025 +0200
+++ b/source/timelinelib/canvas/eventboxdrawers/defaulteventboxdrawer.py Sun Sep 07 12:38:37 2025 +0200
@@ -31,7 +31,6 @@
 HANDLE_SIZE = 4
 HALF_HANDLE_SIZE = HANDLE_SIZE // 2
 DATA_INDICATOR_SIZE = 10
-INNER_PADDING = 3  # Space inside event box to text (pixels)
 GRAY = (200, 200, 200)
 BITMAPS = dict();
 
@@ -188,19 +187,19 @@
             self._draw_the_text(dc, event_rects)
 
     def _there_is_room_for_the_text(self, rect):
-        return deflate_rect(rect).Width > 0
+        return rect.Width > 0
 
     def _draw_the_text(self, dc, event_rects):
         self._set_text_foreground_color(dc, event_rects)
         self._draw_normal_text(dc, event_rects.text_rect, event_rects.event)
 
     def _draw_normal_text(self, dc, rect, event):
-        dc.SetClippingRegion(deflate_rect(rect))
+        dc.SetClippingRegion(rect)
         dc.DrawText(self.scene.get_event_text(event), self._calc_x_pos(dc, rect, event), self._calc_y_pos(rect))
         dc.DestroyClippingRegion()
 
     def _calc_x_pos(self, dc, rect, event):
-        inner_rect = deflate_rect(rect)
+        inner_rect = rect
         text_x = inner_rect.X
         text_x = self._adjust_x_for_edge_icons(event, rect, text_x)
         text_x = self._adjust_x_for_centered_text(dc, event, inner_rect, text_x)
@@ -217,7 +216,7 @@
         return text_x
 
     def _calc_y_pos(self, rect):
-        return deflate_rect(rect).Y
+        return rect.Y
 
     def _center_text(self, dc, event, inner_rect, text_x):
         width, _ = dc.GetTextExtent(self.scene.get_event_text(event))
@@ -270,10 +269,6 @@
         return get_bitmap(self.view_properties.get_fuzzy_icon())
 
 
-def deflate_rect(rect, dx=INNER_PADDING, dy=INNER_PADDING):
-    return rect.CloneDeflate(dx, dy)
-
-
 def center_point_with_offset(rect, dx=0, dy=0):
     y = rect.Y + rect.Height // 2 - dy
     x = rect.X + rect.Width // 2 - dx
diff -r ca6d5ca61efd -r 65da552067f8 test/unit/canvas/drawing/scene.py
--- a/test/unit/canvas/drawing/scene.py Sun Sep 07 12:18:37 2025 +0200
+++ b/test/unit/canvas/drawing/scene.py Sun Sep 07 12:38:37 2025 +0200
@@ -26,6 +26,8 @@
 from timelinelib.test.utils import gregorian_period
 from timelinelib.test.utils import human_time_to_gregorian
 
+import wx
+
 
 class describe_scene(WxAppTestCase):
 
@@ -124,6 +126,24 @@
         self.when_scene_is_created()
         self.assertEqual(self.scene.get_event_text(event), "✔hello")
 
+    def test_event_text_box_padding_point_event(self):
+        self.given_displayed_period("1 Jan 2010", "31 Jan 2010")
+        self.given_visible_event_at("10 Jan 2010")
+        self.inner_padding = 1
+        self.when_scene_is_created()
+        event_rects, = self.scene.event_data
+        self.assertEqual(event_rects.box_rect, wx.Rect(294, 38, 12, 12))
+        self.assertEqual(event_rects.text_rect, wx.Rect(295, 39, 10, 10))
+
+    def test_event_text_box_padding_period_event(self):
+        self.given_displayed_period("1 Jan 2010", "31 Jan 2010")
+        self.given_visible_event_at("10 Jan 2010", "11 Jan 2010")
+        self.inner_padding = 1
+        self.when_scene_is_created()
+        event_rects, = self.scene.event_data
+        self.assertEqual(event_rects.box_rect, wx.Rect(300, 50, 34, 12))
+        self.assertEqual(event_rects.text_rect, wx.Rect(301, 51, 32, 10))
+
     def setUp(self):
         WxAppTestCase.setUp(self)
         self.db = MemoryDB()
diff -r ca6d5ca61efd -r 65da552067f8 test/unit/canvas/eventboxdrawers/defaulteventboxdrawer.py
--- a/test/unit/canvas/eventboxdrawers/defaulteventboxdrawer.py Sun Sep 07 12:18:37 2025 +0200
+++ b/test/unit/canvas/eventboxdrawers/defaulteventboxdrawer.py Sun Sep 07 12:38:37 2025 +0200
@@ -22,7 +22,6 @@
 from timelinelib.canvas.drawing.rect import Rect
 from timelinelib.canvas.drawing.scene import EventRects
 from timelinelib.canvas.eventboxdrawers.defaulteventboxdrawer import DefaultEventBoxDrawer
-from timelinelib.canvas.eventboxdrawers.defaulteventboxdrawer import INNER_PADDING
 from timelinelib.canvas.eventboxdrawers.defaultmilestonedrawer import DefaultMilestoneDrawer
 from timelinelib.test.cases.unit import UnitTestCase
 
@@ -37,8 +36,8 @@
         self.drawer._draw_text(self.dc, event_rects(rect, self.event))
         self.assertEqual(self.dc.DrawText.call_count, 0)
 
-    def test_when_rect_has_inner_padding_width_text_is_not_drawn(self):
-        rect = Rect(0, 0, INNER_PADDING * 2, 0)
+    def test_when_rect_is_too_small_the_text_is_not_drawn(self):
+        rect = Rect(0, 0, 0, 0)
         self.drawer._draw_text(self.dc, event_rects(rect, self.event))
         self.assertEqual(self.dc.DrawText.call_count, 0)
 
@@ -48,8 +47,8 @@
         rect = Rect(0, 0, WIDTH, HEIGHT)
         self.drawer.center_text = False
         self.drawer._draw_text(self.dc, event_rects(rect, self.event))
-        self.dc.SetClippingRegion.assert_called_with(Rect(INNER_PADDING, INNER_PADDING, WIDTH - 2 * INNER_PADDING, HEIGHT - 2 * INNER_PADDING))
-        self.dc.DrawText.assert_called_with(DEFAULT_TEXT, INNER_PADDING, INNER_PADDING)
+        self.dc.SetClippingRegion.assert_called_with(Rect(0, 0, WIDTH, HEIGHT))
+        self.dc.DrawText.assert_called_with(DEFAULT_TEXT, 0, 0)
 
     def test_centered_text_is_not_left_aligned(self):
         WIDTH = 100
@@ -57,8 +56,8 @@
         rect = Rect(0, 0, WIDTH, HEIGHT)
         self.drawer.center_text = True
         self.drawer._draw_text(self.dc, event_rects(rect, self.event))
-        self.dc.SetClippingRegion.assert_called_with(Rect(INNER_PADDING, INNER_PADDING, WIDTH - 2 * INNER_PADDING, HEIGHT - 2 * INNER_PADDING))
-        self.dc.DrawText.assert_called_with(DEFAULT_TEXT, (WIDTH - 2 * INNER_PADDING - 50) // 2 + INNER_PADDING, INNER_PADDING)
+        self.dc.SetClippingRegion.assert_called_with(Rect(0, 0, WIDTH, HEIGHT))
+        self.dc.DrawText.assert_called_with(DEFAULT_TEXT, (WIDTH - 50) // 2, 0)
 
     def test_centered_text_is_left_aligned_if_text_is_too_long_to_fit(self):
         WIDTH = 100
@@ -67,8 +66,8 @@
         self.dc.GetTextExtent.return_value = (500, 0)
         self.drawer.center_text = True
         self.drawer._draw_text(self.dc, event_rects(rect, self.event))
-        self.dc.SetClippingRegion.assert_called_with(Rect(INNER_PADDING, INNER_PADDING, WIDTH - 2 * INNER_PADDING, HEIGHT - 2 * INNER_PADDING))
-        self.dc.DrawText.assert_called_with(DEFAULT_TEXT, INNER_PADDING, INNER_PADDING)
+        self.dc.SetClippingRegion.assert_called_with(Rect(0, 0, WIDTH, HEIGHT))
+        self.dc.DrawText.assert_called_with(DEFAULT_TEXT, 0, 0)
 
     def test_non_centered_text_is_left_ajusted_when_fuzzy(self):
         WIDTH = 100
@@ -77,8 +76,8 @@
         rect = Rect(0, 0, WIDTH, HEIGHT)
         self.drawer.center_text = False
         self.drawer._draw_text(self.dc, event_rects(rect, self.event))
-        self.dc.SetClippingRegion.assert_called_with(Rect(INNER_PADDING, INNER_PADDING, WIDTH - 2 * INNER_PADDING, HEIGHT - 2 * INNER_PADDING))
-        self.dc.DrawText.assert_called_with(DEFAULT_TEXT, INNER_PADDING + HEIGHT // 2, INNER_PADDING)
+        self.dc.SetClippingRegion.assert_called_with(Rect(0, 0, WIDTH, HEIGHT))
+        self.dc.DrawText.assert_called_with(DEFAULT_TEXT, HEIGHT // 2, 0)
 
     def test_non_centered_text_is_left_ajusted_when_locked(self):
         WIDTH = 100
@@ -87,8 +86,8 @@
         rect = Rect(0, 0, WIDTH, HEIGHT)
         self.drawer.center_text = False
         self.drawer._draw_text(self.dc, event_rects(rect, self.event))
-        self.dc.SetClippingRegion.assert_called_with(Rect(INNER_PADDING, INNER_PADDING, WIDTH - 2 * INNER_PADDING, HEIGHT - 2 * INNER_PADDING))
-        self.dc.DrawText.assert_called_with(DEFAULT_TEXT, INNER_PADDING + HEIGHT // 2, INNER_PADDING)
+        self.dc.SetClippingRegion.assert_called_with(Rect(0, 0, WIDTH, HEIGHT))
+        self.dc.DrawText.assert_called_with(DEFAULT_TEXT, HEIGHT // 2, 0)
 
     def test_milestone_with_no_text_can_be_drawn(self):
         with self.wxapp():

2025-09-07 12:19 Rickard pushed to timeline

changeset:   8074:ca6d5ca61efd
user:        Rickard Lindberg <rickard@rickardlindberg.me>
date:        Sun Sep 07 12:18:37 2025 +0200
summary:     Take checkmark event text into consideration in all places

diff -r be60762bfa81 -r ca6d5ca61efd source/timelinelib/canvas/drawing/scene.py
--- a/source/timelinelib/canvas/drawing/scene.py Sun Sep 07 08:58:49 2025 +0200
+++ b/source/timelinelib/canvas/drawing/scene.py Sun Sep 07 12:18:37 2025 +0200
@@ -59,6 +59,12 @@
         self.major_strip_data = []
         self.minor_strip_data = []
 
+    def get_event_text(self, event):
+        if event.get_progress() == 100 and self._view_properties.get_display_checkmark_on_events_done():
+            return "\u2714" + event.get_text()
+        else:
+            return event.get_text()
+
     @property
     def view_properties(self):
         return self._view_properties
@@ -314,7 +320,7 @@
         )
 
     def _calc_height_for_period_event(self, event):
-        return self._get_text_height(event.get_text()) + 2 * self._inner_padding
+        return self._get_text_height(self.get_event_text(event)) + 2 * self._inner_padding
 
     def _calc_x_pos_for_period_event(self, event):
         return self._metrics.calc_x(event.get_time_period().start_time)
@@ -354,7 +360,7 @@
         return self._calc_ideal_wx_rect(rx, ry, rw, rh)
 
     def _calc_width_for_non_period_event(self, event):
-        tw, th = self._get_text_size(event.get_text())
+        tw, th = self._get_text_size(self.get_event_text(event))
         rw = tw + 2 * self._inner_padding
         if event.has_data():
             rw += self._data_indicator_size // 3
@@ -363,7 +369,7 @@
         return rw
 
     def _calc_height_for_non_period_event(self, event):
-        return self._get_text_height(event.get_text()) + 2 * self._inner_padding
+        return self._get_text_height(self.get_event_text(event)) + 2 * self._inner_padding
 
     def _calc_x_pos_for_non_period_event(self, event, rw):
         if self._appearance.get_draw_period_events_to_right():
diff -r be60762bfa81 -r ca6d5ca61efd source/timelinelib/canvas/eventboxdrawers/defaulteventboxdrawer.py
--- a/source/timelinelib/canvas/eventboxdrawers/defaulteventboxdrawer.py Sun Sep 07 08:58:49 2025 +0200
+++ b/source/timelinelib/canvas/eventboxdrawers/defaulteventboxdrawer.py Sun Sep 07 12:18:37 2025 +0200
@@ -196,15 +196,9 @@
 
     def _draw_normal_text(self, dc, rect, event):
         dc.SetClippingRegion(deflate_rect(rect))
-        dc.DrawText(self._get_text(event), self._calc_x_pos(dc, rect, event), self._calc_y_pos(rect))
+        dc.DrawText(self.scene.get_event_text(event), self._calc_x_pos(dc, rect, event), self._calc_y_pos(rect))
         dc.DestroyClippingRegion()
 
-    def _get_text(self, event):
-        if event.get_progress() == 100 and self.view_properties.get_display_checkmark_on_events_done():
-            return "\u2714" + event.get_text()
-        else:
-            return event.get_text()
-
     def _calc_x_pos(self, dc, rect, event):
         inner_rect = deflate_rect(rect)
         text_x = inner_rect.X
@@ -226,7 +220,7 @@
         return deflate_rect(rect).Y
 
     def _center_text(self, dc, event, inner_rect, text_x):
-        width, _ = dc.GetTextExtent(self._get_text(event))
+        width, _ = dc.GetTextExtent(self.scene.get_event_text(event))
         return max(text_x, text_x + (inner_rect.width - width) // 2)
 
     def _set_text_foreground_color(self, dc, event_rects):
diff -r be60762bfa81 -r ca6d5ca61efd test/unit/canvas/drawing/scene.py
--- a/test/unit/canvas/drawing/scene.py Sun Sep 07 08:58:49 2025 +0200
+++ b/test/unit/canvas/drawing/scene.py Sun Sep 07 12:18:37 2025 +0200
@@ -110,6 +110,20 @@
         self.when_scene_is_created()
         self.assertFalse(self.scene.event_data[0].event.ends_today)
 
+    def test_event_text_no_checkmark(self):
+        self.view_properties.set_display_checkmark_on_events_done(False)
+        self.given_displayed_period("1 Jan 2010", "31 Jan 2010")
+        event = self.given_visible_event_at("10 Jan 2010", progress=100, text="hello")
+        self.when_scene_is_created()
+        self.assertEqual(self.scene.get_event_text(event), "hello")
+
+    def test_event_text_checkmark(self):
+        self.view_properties.set_display_checkmark_on_events_done(True)
+        self.given_displayed_period("1 Jan 2010", "31 Jan 2010")
+        event = self.given_visible_event_at("10 Jan 2010", progress=100, text="hello")
+        self.when_scene_is_created()
+        self.assertEqual(self.scene.get_event_text(event), "✔hello")
+
     def setUp(self):
         WxAppTestCase.setUp(self)
         self.db = MemoryDB()
@@ -131,26 +145,29 @@
     def given_displayed_period(self, start, end):
         self.view_properties.displayed_period = gregorian_period(start, end)
 
-    def given_visible_event_at(self, start_time, end_time=None, ends_today=False):
-        self.given_event_at(start_time, end_time, visible=True, ends_today=ends_today)
+    def given_visible_event_at(self, start_time, end_time=None, **kwargs):
+        return self.given_event_at(start_time, end_time, visible=True, **kwargs)
 
     def given_hidden_event_at(self, time):
         self.given_event_at(time, visible=False)
 
-    def given_event_at(self, start_time, end_time=None, visible=True, ends_today=False):
+    def given_event_at(self, start_time, end_time=None, visible=True, ends_today=False, progress=None, text="event-text"):
         category = self.get_unique_category()
         if end_time is None:
             end_time = start_time
         event = Event().update(
             human_time_to_gregorian(start_time),
             human_time_to_gregorian(end_time),
-            "event-text",
+            text,
             category,
             ends_today=ends_today
         )
+        if progress is not None:
+            event.set_progress(progress)
         self.db.save_category(category)
         self.db.save_event(event)
         self.view_properties.set_category_visible(category, visible)
+        return event
 
     def get_unique_category(self):
         number = 1
diff -r be60762bfa81 -r ca6d5ca61efd test/unit/canvas/eventboxdrawers/defaulteventboxdrawer.py
--- a/test/unit/canvas/eventboxdrawers/defaulteventboxdrawer.py Sun Sep 07 08:58:49 2025 +0200
+++ b/test/unit/canvas/eventboxdrawers/defaulteventboxdrawer.py Sun Sep 07 12:18:37 2025 +0200
@@ -90,18 +90,6 @@
         self.dc.SetClippingRegion.assert_called_with(Rect(INNER_PADDING, INNER_PADDING, WIDTH - 2 * INNER_PADDING, HEIGHT - 2 * INNER_PADDING))
         self.dc.DrawText.assert_called_with(DEFAULT_TEXT, INNER_PADDING + HEIGHT // 2, INNER_PADDING)
 
-    def test_if_checkmark_is_to_be_used_it_is_placed_as_first_char_in_text(self):
-        WIDTH = 100
-        HEIGHT = 20
-        self.event.has_edge_icons.return_value = True
-        self.event.get_progress.return_value = 100
-        rect = Rect(0, 0, WIDTH, HEIGHT)
-        self.drawer.center_text = False
-        self.drawer.view_properties.get_display_checkmark_on_events_done.return_value = True
-        self.drawer._draw_text(self.dc, event_rects(rect, self.event))
-        self.dc.SetClippingRegion.assert_called_with(Rect(INNER_PADDING, INNER_PADDING, WIDTH - 2 * INNER_PADDING, HEIGHT - 2 * INNER_PADDING))
-        self.dc.DrawText.assert_called_with("\u2714" + DEFAULT_TEXT, INNER_PADDING + HEIGHT // 2, INNER_PADDING)
-
     def test_milestone_with_no_text_can_be_drawn(self):
         with self.wxapp():
             self.event.text = ""
@@ -118,6 +106,8 @@
     def setUp(self):
         self.drawer = DefaultEventBoxDrawer()
         self.drawer.view_properties = Mock()
+        self.drawer.scene = Mock()
+        self.drawer.scene.get_event_text = lambda event: event.get_text()
         self.dc = Mock()
         self.dc.GetTextExtent.return_value = (50, 0)
         self.event = Mock()

2025-09-07 09:42 Rickard pushed to timeline

changeset:   8072:0b7f7dbec26f
user:        Rickard Lindberg <rickard@rickardlindberg.me>
date:        Sun Sep 07 08:51:47 2025 +0200
summary:     Remove redundant condition

diff -r 05027d02057b -r 0b7f7dbec26f source/timelinelib/canvas/drawing/drawers/default.py
--- a/source/timelinelib/canvas/drawing/drawers/default.py Sun Sep 07 07:42:32 2025 +0200
+++ b/source/timelinelib/canvas/drawing/drawers/default.py Sun Sep 07 08:51:47 2025 +0200
@@ -380,7 +380,7 @@
                 continue
             if not event_rects.event.is_period():
                 self._draw_line(view_properties, event_rects.event, event_rects.box_rect)
-            elif not self.scene.never_show_period_events_as_point_events() and self._event_displayed_as_point_event(event_rects.box_rect):
+            elif self._event_displayed_as_point_event(event_rects.box_rect):
                 self._draw_line(view_properties, event_rects.event, event_rects.box_rect)
 
     def _event_displayed_as_point_event(self, rect):

changeset:   8073:be60762bfa81
user:        Rickard Lindberg <rickard@rickardlindberg.me>
date:        Sun Sep 07 08:58:49 2025 +0200
summary:     Draw short period events as 2 pixel wide period events if both settings text to the left and never period as point are set

diff -r 0b7f7dbec26f -r be60762bfa81 source/timelinelib/canvas/drawing/scene.py
--- a/source/timelinelib/canvas/drawing/scene.py Sun Sep 07 08:51:47 2025 +0200
+++ b/source/timelinelib/canvas/drawing/scene.py Sun Sep 07 08:58:49 2025 +0200
@@ -28,6 +28,7 @@
 
 FORWARD = 1
 BACKWARD = -1
+MIN_W_PERIOD_EVENT = 2
 
 
 class TimelineScene:
@@ -69,9 +70,16 @@
     def center_text(self):
         return self._appearance.get_center_event_texts()
 
-    def never_show_period_events_as_point_events(self):
+    def draw_short_period_event_as_symbol_below_divider_line(self):
         return self._appearance.get_never_show_period_events_as_point_events()
 
+    def draw_short_period_event_as_period_event(self):
+        return (
+            self._appearance.get_never_show_period_events_as_point_events()
+            and
+            self._appearance.get_text_to_the_left_of_period_events()
+        )
+
     def set_outer_padding(self, outer_padding):
         self._outer_padding = outer_padding
 
@@ -268,7 +276,13 @@
         return event.get_time_period().start_time > self._db.now
 
     def _display_as_period(self, event):
-        return self._metrics.calc_width(event.get_time_period()) > self._period_threshold
+        if event.is_period():
+            if self.draw_short_period_event_as_period_event():
+                return True
+            else:
+                return self._metrics.calc_width(event.get_time_period()) > self._period_threshold
+        else:
+            return False
 
     def _calc_ideal_rect_for_period_event(self, event):
         rw = self._calc_width_for_period_event(event)
@@ -294,8 +308,10 @@
         )
 
     def _calc_width_for_period_event(self, event):
-        min_w = 5 * self._outer_padding
-        return max(self._metrics.calc_width(event.get_time_period()), min_w)
+        return max(
+            self._metrics.calc_width(event.get_time_period()),
+            MIN_W_PERIOD_EVENT,
+        )
 
     def _calc_height_for_period_event(self, event):
         return self._get_text_height(event.get_text()) + 2 * self._inner_padding
@@ -321,7 +337,7 @@
         return self._metrics.half_height + self._baseline_padding + self._outer_padding
 
     def _calc_ideal_rect_for_non_period_event(self, event):
-        if self.never_show_period_events_as_point_events() and event.is_period():
+        if self.draw_short_period_event_as_symbol_below_divider_line() and event.is_period():
             x = self.x_pos_for_time(event.mean_time())
             y0 = self.divider_y
             y1 = y0 + 10
diff -r 0b7f7dbec26f -r be60762bfa81 source/timelinelib/canvas/eventboxdrawers/defaulteventboxdrawer.py
--- a/source/timelinelib/canvas/eventboxdrawers/defaulteventboxdrawer.py Sun Sep 07 08:51:47 2025 +0200
+++ b/source/timelinelib/canvas/eventboxdrawers/defaulteventboxdrawer.py Sun Sep 07 08:58:49 2025 +0200
@@ -47,7 +47,7 @@
         self.center_text = scene.center_text()
         if event.is_milestone():
             DefaultMilestoneDrawer(rect, event, selected, view_properties).draw(dc)
-        elif scene.never_show_period_events_as_point_events() and rect.is_allowed_to_overlap() and event.is_period():
+        elif scene.draw_short_period_event_as_symbol_below_divider_line() and rect.is_allowed_to_overlap() and event.is_period():
             self._draw_period_event_as_symbol_below_divider_line(dc, rect, scene, event)
         else:
             self._draw_event_box(dc, event_rects, selected)

2025-09-07 07:55 Rickard pushed to timeline

changeset:   8070:2b0ae898ef7f
user:        Rickard Lindberg <rickard@rickardlindberg.me>
date:        Sun Sep 07 07:24:34 2025 +0200
summary:     Ensure a pen and brush is always used in gradienteventboxdrawer

diff -r 63572366b45e -r 2b0ae898ef7f source/timelinelib/canvas/eventboxdrawers/gradienteventboxdrawer.py
--- a/source/timelinelib/canvas/eventboxdrawers/gradienteventboxdrawer.py Sun Sep 07 06:26:04 2025 +0200
+++ b/source/timelinelib/canvas/eventboxdrawers/gradienteventboxdrawer.py Sun Sep 07 07:24:34 2025 +0200
@@ -29,12 +29,17 @@
 
     @mark.overrides
     def _draw_background(self, dc, rect, event):
-        dc.SetPen(self._get_pen(dc, event))
-        dc.DrawRectangle(rect)
+        self._draw_gradient_background(dc, rect, event)
+        self._draw_border(dc, rect, event)
+
+    def _draw_gradient_background(self, dc, rect, event):
         light_color = lighten_color(event.get_color())
         dark_color = darken_color(event.get_color(), factor=0.8)
-        dc.GradientFillLinear(deflate_rect(rect), light_color, dark_color, wx.SOUTH)
-
+        dc.SetPen(wx.TRANSPARENT_PEN)
+        dc.SetBrush(wx.BLACK_BRUSH)
+        dc.GradientFillLinear(rect, light_color, dark_color, wx.SOUTH)
 
-def deflate_rect(rect, dx=1, dy=1):
-    return rect.CloneDeflate(dx, dy)
+    def _draw_border(self, dc, rect, event):
+        dc.SetPen(self._get_pen(dc, event))
+        dc.SetBrush(wx.TRANSPARENT_BRUSH)
+        dc.DrawRectangle(rect)

changeset:   8071:05027d02057b
user:        Rickard Lindberg <rickard@rickardlindberg.me>
date:        Sun Sep 07 07:42:32 2025 +0200
summary:     Ensure a pen and brush is always used in othergradienteventboxdrawer

diff -r 2b0ae898ef7f -r 05027d02057b source/timelinelib/canvas/eventboxdrawers/othergradienteventboxdrawer.py
--- a/source/timelinelib/canvas/eventboxdrawers/othergradienteventboxdrawer.py Sun Sep 07 07:24:34 2025 +0200
+++ b/source/timelinelib/canvas/eventboxdrawers/othergradienteventboxdrawer.py Sun Sep 07 07:42:32 2025 +0200
@@ -57,11 +57,7 @@
         self._rect = rect
         self._light_color = lighten_color(self._event.get_color())
         self._dark_color = darken_color(self._event.get_color(), factor=0.8)
-        dc.SetPen(self._get_pen(dc, event))
         if self._fuzzy_edges:
-            if event.fuzzy_start or event.fuzzy_end:
-                dc.DrawLine(self._rect.GetTopLeft(), self._rect.GetTopRight())
-                dc.DrawLine(self._rect.GetBottomLeft(), self._rect.GetBottomRight())
             if event.fuzzy_start and event.fuzzy_end:
                 self._draw_fuzzy_background(dc, GRADIENT_STYLE_BOTH)
             elif event.fuzzy_start:
@@ -70,6 +66,10 @@
                 self._draw_fuzzy_background(dc, GRADIENT_STYLE_RIGHT)
             else:
                 self._draw_background_no_fuzzy_edges(dc)
+            if event.fuzzy_start or event.fuzzy_end:
+                dc.SetPen(self._get_pen(dc, event))
+                dc.DrawLine(self._rect.GetTopLeft(), self._rect.GetTopRight())
+                dc.DrawLine(self._rect.GetBottomLeft(), self._rect.GetBottomRight())
         else:
             self._draw_background_no_fuzzy_edges(dc)
 
@@ -87,13 +87,21 @@
             super(OtherGradientEventBoxDrawer, self)._draw_fuzzy_edges(dc, rect, event)
 
     def _draw_background_no_fuzzy_edges(self, dc):
-        dc.DrawRectangle(self._rect)
-        dc.GradientFillLinear(deflate_rect(self._rect), self._light_color, self._dark_color, wx.WEST)
+        dc.SetPen(wx.TRANSPARENT_PEN)
+        dc.SetBrush(wx.BLACK_BRUSH)
+        dc.GradientFillLinear(self._rect, self._light_color, self._dark_color, wx.WEST)
+        self._draw_border(dc, self._rect, self._event)
+
+    def _draw_border(self, dc, rect, event):
+        dc.SetPen(self._get_pen(dc, event))
+        dc.SetBrush(wx.TRANSPARENT_BRUSH)
+        dc.DrawRectangle(rect)
 
     def _draw_fuzzy_background(self, dc, style):
         gc = create_gc(dc)
         brush = self._create_gradient_brush(gc, style)
         gc.SetBrush(brush)
+        gc.SetPen(wx.TRANSPARENT_PEN)
         gc.DrawRectangle(*self._rect)
 
     def _create_gradient_brush(self, gc, style):
@@ -120,7 +128,3 @@
             stops.Add(c1, 1)
         return gc.CreateLinearGradientBrush(self._rect.x, self._rect.y, self._rect.x + self._rect.width,
                                             self._rect.y + self._rect.height, stops)
-
-
-def deflate_rect(rect, dx=1, dy=1):
-    return rect.CloneDeflate(dx, dy)

2025-09-07 06:33 Rickard pushed to timeline

changeset:   8069:63572366b45e
user:        Rickard Lindberg <rickard@rickardlindberg.me>
date:        Sun Sep 07 06:26:04 2025 +0200
summary:     Make left handle more centered

diff -r c245cf55f35c -r 63572366b45e source/timelinelib/canvas/eventboxdrawers/handlerect.py
--- a/source/timelinelib/canvas/eventboxdrawers/handlerect.py Sun Sep 07 06:09:07 2025 +0200
+++ b/source/timelinelib/canvas/eventboxdrawers/handlerect.py Sun Sep 07 06:26:04 2025 +0200
@@ -54,7 +54,7 @@
               |                  |
               +------------------+
         """
-        x = (2 * (rect.x - self.x) - self.width) // 2
+        x = (2 * (rect.x - self.x) - self.width) // 2 + 1
         self.Offset(x, self._get_y(rect))
 
     def _translate_to_right_edge(self, rect):

2025-09-07 06:12 Rickard pushed to timeline

changeset:   8068:c245cf55f35c
user:        Rickard Lindberg <rickard@rickardlindberg.me>
date:        Sun Sep 07 06:09:07 2025 +0200
summary:     More logical use of clipping region

diff -r 5d2dffe1e692 -r c245cf55f35c source/timelinelib/canvas/drawing/drawers/default.py
--- a/source/timelinelib/canvas/drawing/drawers/default.py Sat Sep 06 19:16:59 2025 +0200
+++ b/source/timelinelib/canvas/drawing/drawers/default.py Sun Sep 07 06:09:07 2025 +0200
@@ -478,18 +478,15 @@
     def _draw_events(self, view_properties):
         """Draw all event boxes and the text inside them."""
         self._scroll_events_vertically(view_properties)
-        self.dc.DestroyClippingRegion()
         self._draw_lines_to_non_period_events(view_properties)
         for event_rects in self.scene.event_data:
             self.dc.SetFont(self._event_text_font)
             self._draw_box(event_rects, view_properties)
 
     def _draw_box(self, event_rects, view_properties):
-        self.dc.SetClippingRegion(event_rects.bounding_rect)
         self._event_box_drawer.draw(self.dc, self.scene, event_rects, view_properties)
         if EXTENDED_CONTAINER_STRATEGY.enabled() and event_rects.event.is_subevent() and not event_rects.event.is_period():
             self._draw_time_marker_for_point_event(event_rects.box_rect)
-        self.dc.DestroyClippingRegion()
 
     def _draw_time_marker_for_point_event(self, rect):
         pen = self.dc.GetPen()
diff -r 5d2dffe1e692 -r c245cf55f35c source/timelinelib/canvas/eventboxdrawers/defaulteventboxdrawer.py
--- a/source/timelinelib/canvas/eventboxdrawers/defaulteventboxdrawer.py Sat Sep 06 19:16:59 2025 +0200
+++ b/source/timelinelib/canvas/eventboxdrawers/defaulteventboxdrawer.py Sun Sep 07 06:09:07 2025 +0200
@@ -53,7 +53,6 @@
             self._draw_event_box(dc, event_rects, selected)
 
     def _draw_period_event_as_symbol_below_divider_line(self, dc, rect, scene, event):
-        dc.DestroyClippingRegion()
         x = rect.X + rect.Width // 2
         y0 = scene.divider_y
         y1 = rect.Y + rect.Height // 2
@@ -84,7 +83,6 @@
         pen = self._get_thin_border_pen(event)
         if self.view_properties.is_highlighted(event):
             if self.view_properties.get_highlight_count(event) % 2 == 0:
-                dc.DestroyClippingRegion()
                 pen = self._get_thick_border_pen(event)
         return pen
 
@@ -126,19 +124,15 @@
         return brush
 
     def _draw_fuzzy_start(self, dc, rect):
-        self._inflate_clipping_region(dc, rect)
         dc.DrawBitmap(self._get_fuzzy_bitmap(), rect.x - 4, rect.y + 4, True)
 
     def _draw_fuzzy_end(self, dc, rect):
-        self._inflate_clipping_region(dc, rect)
         dc.DrawBitmap(self._get_fuzzy_bitmap(), rect.x + rect.width - 8, rect.y + 4, True)
 
     def _draw_locked_start(self, dc, rect):
-        self._inflate_clipping_region(dc, rect)
         dc.DrawBitmap(self._get_lock_bitmap(), rect.x - 7, rect.y + 3, True)
 
     def _draw_locked_end(self, dc, rect):
-        self._inflate_clipping_region(dc, rect)
         dc.DrawBitmap(self._get_lock_bitmap(), rect.x + rect.width - 8, rect.y + 3, True)
 
     def _draw_progress_box(self, dc, rect, event):
@@ -184,7 +178,9 @@
         )
         dc.SetBrush(self._get_balloon_indicator_brush(event))
         dc.SetPen(wx.TRANSPARENT_PEN)
+        dc.SetClippingRegion(rect)
         dc.DrawPolygon(points)
+        dc.DestroyClippingRegion()
 
     def _draw_text(self, dc, event_rects):
         # Ensure that we can't draw content outside inner rectangle
@@ -197,11 +193,11 @@
     def _draw_the_text(self, dc, event_rects):
         self._set_text_foreground_color(dc, event_rects)
         self._draw_normal_text(dc, event_rects.text_rect, event_rects.event)
-        dc.DestroyClippingRegion()
 
     def _draw_normal_text(self, dc, rect, event):
-        self._set_clipping_rect(dc, rect)
+        dc.SetClippingRegion(deflate_rect(rect))
         dc.DrawText(self._get_text(event), self._calc_x_pos(dc, rect, event), self._calc_y_pos(rect))
+        dc.DestroyClippingRegion()
 
     def _get_text(self, event):
         if event.get_progress() == 100 and self.view_properties.get_display_checkmark_on_events_done():
@@ -209,9 +205,6 @@
         else:
             return event.get_text()
 
-    def _set_clipping_rect(self, dc, rect):
-        dc.SetClippingRegion(deflate_rect(rect))
-
     def _calc_x_pos(self, dc, rect, event):
         inner_rect = deflate_rect(rect)
         text_x = inner_rect.X
@@ -257,14 +250,11 @@
             dc.SetPen(pen)
             dc.DrawRectangle(small_rect)
 
-        dc.SetClippingRegion(rect)
         draw_frame_around_event()
         self._draw_all_handles(dc, rect)
-        dc.DestroyClippingRegion()
 
     @staticmethod
     def _draw_all_handles(dc, rect):
-        dc.DestroyClippingRegion()
         HandleRect(rect, LEFT_HANDLE).draw(dc)
         HandleRect(rect, MIDDLE_HANDLE).draw(dc)
         HandleRect(rect, RIGHT_HANDLE).draw(dc)
@@ -276,11 +266,6 @@
                 bitmap = wx.ArtProvider.GetBitmap(wx.ART_MISSING_IMAGE, wx.ART_MENU)
             dc.DrawBitmap(bitmap, rect.x + rect.width - 14, rect.y + 4, True)
 
-    @staticmethod
-    def _inflate_clipping_region(dc, rect):
-        dc.DestroyClippingRegion()
-        dc.SetClippingRegion(rect.CloneInflate(10, 0))
-
     def _get_hyperlink_bitmap(self):
         return get_bitmap(self.view_properties.get_hyperlink_icon())
 
diff -r 5d2dffe1e692 -r c245cf55f35c source/timelinelib/canvas/eventboxdrawers/defaultmilestonedrawer.py
--- a/source/timelinelib/canvas/eventboxdrawers/defaultmilestonedrawer.py Sat Sep 06 19:16:59 2025 +0200
+++ b/source/timelinelib/canvas/eventboxdrawers/defaultmilestonedrawer.py Sun Sep 07 06:09:07 2025 +0200
@@ -34,7 +34,6 @@
         self._view_properties = view_properties
 
     def draw(self, dc):
-        dc.DestroyClippingRegion()
         self._draw_rectangle(dc)
         self.draw_label(dc)
         if self._selected:

2025-09-06 19:21 Rickard pushed to timeline

changeset:   8067:5d2dffe1e692
user:        Rickard Lindberg <rickard@rickardlindberg.me>
date:        Sat Sep 06 19:16:59 2025 +0200
summary:     Remove unused method replace_box_rect

diff -r b0b392259e91 -r 5d2dffe1e692 source/timelinelib/canvas/drawing/scene.py
--- a/source/timelinelib/canvas/drawing/scene.py Fri Sep 05 07:18:07 2025 +0200
+++ b/source/timelinelib/canvas/drawing/scene.py Sat Sep 06 19:16:59 2025 +0200
@@ -558,14 +558,6 @@
         self.text_rect = text_rect
         self.height_padding = height_padding
 
-    def replace_box_rect(self, box_rect):
-        return EventRects(
-            event=self.event,
-            box_rect=box_rect,
-            text_rect=self.text_rect,
-            height_padding=self.height_padding,
-        )
-
     @property
     def bounding_rect(self):
         """

2025-09-06 13:23 Rickard pushed to blog

commit 95478c832e363d835ab891b3e41132b9d0379ded
Author: Rickard Lindberg <rickard@rickardlindberg.me>
Date:   Sat Sep 6 12:02:28 2025 +0200

    Newsletter August 2025

diff --git a/blog.py b/blog.py
index 8dd47b9..b10f4c0 100755
--- a/blog.py
+++ b/blog.py
@@ -514,6 +514,11 @@ class Post:
     def write(self, root, bibs, posts):
         output_dir = os.path.join(root, self.html_root())
         os.makedirs(output_dir, exist_ok=True)
+        for png in glob.glob(os.path.join(os.path.dirname(self.path), "*.png")):
+            shutil.copy(
+                src=png,
+                dst=os.path.join(output_dir, os.path.basename(png))
+            )
         with open(os.path.join(output_dir, self.html_name()), "w") as f:
             f.write(self.html_content(bibs, posts))
 
diff --git a/posts/2025/09/04/newsletter-august/garmin-overview.png b/posts/2025/09/04/newsletter-august/garmin-overview.png
new file mode 100644
index 0000000..2fe7f68
Binary files /dev/null and b/posts/2025/09/04/newsletter-august/garmin-overview.png differ
diff --git a/posts/2025/09/04/newsletter-august/garmin-run.png b/posts/2025/09/04/newsletter-august/garmin-run.png
new file mode 100644
index 0000000..2b39d43
Binary files /dev/null and b/posts/2025/09/04/newsletter-august/garmin-run.png differ
diff --git a/posts/2025/09/04/newsletter-august/garmin-strava.png b/posts/2025/09/04/newsletter-august/garmin-strava.png
new file mode 100644
index 0000000..9e729bd
Binary files /dev/null and b/posts/2025/09/04/newsletter-august/garmin-strava.png differ
diff --git a/posts/2025/09/04/newsletter-august/garmin-watch.png b/posts/2025/09/04/newsletter-august/garmin-watch.png
new file mode 100644
index 0000000..2e49eba
Binary files /dev/null and b/posts/2025/09/04/newsletter-august/garmin-watch.png differ
diff --git a/posts/2025/09/04/newsletter-august/post.md b/posts/2025/09/04/newsletter-august/post.md
new file mode 100644
index 0000000..a4e3220
--- /dev/null
+++ b/posts/2025/09/04/newsletter-august/post.md
@@ -0,0 +1,106 @@
+---
+date: 2025-09-06 13:14:00
+title: Newsletter August 2025: More Programming
+tags: newsletter,timeline
+---
+
+This month I've been programming on two hobby projects. I have made progress on
+both as well as enjoyed the craft of programming.
+
+## Garmin
+
+I record my runs with a Garmin watch and heart rate monitor.
+
+![Picture of my Garmin watch.](garmin-watch.png)
+
+I analyze the data they gather in the Garmin Connect and Strava apps.
+
+![Screenshots of Garmin Connect and Strava.](garmin-strava.png)
+
+This month I wanted to analyze if I have become faster over time given the same
+effort. For example, I wanted to know if my average pace at an effort of 135
+BPM in heart rate had increased or not.
+
+I looked at using Garmin Connect and Strava for this, but couldn't find a way
+to do it. I pay for Strava, but use the free version of Garmin Connect. I
+found that Garmin had recently launched a paid subscription called Garmin
+Connect+ that should, among other things, allow you to do more custom graphs.
+
+But I was not keen on paying for something that maybe supports what I want. And
+what about when I want to extract some other data from my runs, and Garmin
+doesn't support it? Why is it so difficult to analyze data that I own in a way
+that I want?
+
+That's when the familiar though of "How hard can it be to do myself?" hit me
+and I started researching. I tried to connect my Garmin watch to my computer.
+It showed up as a USB device. I looked through the files and found a folder
+with all my activities. There they were. These are the sources I would need to
+analyze my runs. In a file format called FIT.
+
+I started researching how to process FIT files. Eventually I found that Garmin
+has a Python SDK [garmin-fit-sdk](https://pypi.org/project/garmin-fit-sdk/).
+Nice!
+
+Once I figured out how to process the files (the most uncertain part of this
+project) it was just a matter of drawing some graphs I though. And it was, but
+as usual it takes more time than anticipated. But this time, not too long. So I
+managed to build something that serves my needs. Here is the main screen:
+
+![Screenshots of my Garmin app showing an overview of my
+runs.](garmin-overview.png)
+
+The top part shows an overview of all my runs.  My Garmin watch has limited
+memory, so only the most recent activities are stored. But because I have used
+the Garmin Connect app for a while, I had uploaded my activities there and was
+able to download them again and import into my application.
+
+The bottom part is that custom graph that I wanted. I think I can see a trend
+towards faster average paces. Nice! I can click on a specific run here and a
+different graph will open:
+
+![Screenshots of my Garmin app showing a specific run.](garmin-run.png)
+
+It shows the standard pace and heart rate graphs for a specific run.
+
+I have now stopped paying for Strava, and I think I will also leave their
+platform soon. For me, it has become more of a distraction than something that
+provides value.
+
+## Timeline
+
+This month I also did some more work on
+[Timeline](https://projects.rickardlindberg.me/timeline/). The new workflow
+that [my own code hosting
+platform](post:2025/05/02/newsletter-april-projects/post.md) provides with the
+addition of [building the Windows exe in
+Wine](post:2025/07/07/newsletter-july/post.md) is just wonderful. I make a
+change to the code, commit it, and push it. The push triggers a build of the
+project website and binaries. If the build succeeds, everything is published
+and the push commands finishes with a success code. I can share the new version
+instantly. If the build fails, nothing gets published and I have to fix the
+error found in the output of the push command.
+
+This month a request came in on the mailing list asking if event texts could be
+drawn outside the event box instead of inside. Here is what the tutorial
+timeline looks like today:
+
+![Screenshots of tutorial timeline with text drawn inside the event
+box.](timeline-text-inside.png)
+
+I thought this was an interesting problem to work on, so I had a look.
+Implementing this feature required extensive changes in the logic of how a
+timeline is drawn.  I spent quite a bit of time trying to understand the
+drawing code which I hadn't touched in years. I made many refactorings that I
+think made the code better in addition to supporting the new feature. Here is
+how it turned out:
+
+![Screenshots of tutorial timeline with text drawn outside the event
+box.](timeline-text-outside.png)
+
+In some timelines, this rendering works better, and there is now a setting in
+Timeline that allows you to choose which one you want.
+
+This work felt rewarding for many reasons. It was an example of a well
+functioning feedback loop involving a user. The feedback loop was shorter
+because of technical improvements in the code hosting platform. And I got do a
+fun programming task.
diff --git a/posts/2025/09/04/newsletter-august/timeline-text-inside.png b/posts/2025/09/04/newsletter-august/timeline-text-inside.png
new file mode 100644
index 0000000..93ec4ba
Binary files /dev/null and b/posts/2025/09/04/newsletter-august/timeline-text-inside.png differ
diff --git a/posts/2025/09/04/newsletter-august/timeline-text-outside.png b/posts/2025/09/04/newsletter-august/timeline-text-outside.png
new file mode 100644
index 0000000..9e41e48
Binary files /dev/null and b/posts/2025/09/04/newsletter-august/timeline-text-outside.png differ