This site hosts my projects.
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))
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
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():
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()
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)
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)
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):
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:
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):
"""
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.
+
+
+
+I analyze the data they gather in the Garmin Connect and Strava apps.
+
+
+
+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:
+
+
+
+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:
+
+
+
+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:
+
+
+
+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:
+
+
+
+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