timeline

A cross-platform application for displaying and navigating events on a timeline.

Home

Source code

hg clone static-http://projects.rickardlindberg.me/scm/timeline

Website

https://projects.rickardlindberg.me/timeline

Recent events

2025-08-24 12:22 Rickard pushed to timeline

changeset:   8063:deead63fc19f
user:        Rickard Lindberg <rickard@rickardlindberg.me>
date:        Sun Aug 24 12:12:25 2025 +0200
summary:     Ensure listening on initial appearance

diff -r c2fde191bf90 -r deead63fc19f source/timelinelib/canvas/timelinecanvascontroller.py
--- a/source/timelinelib/canvas/timelinecanvascontroller.py Sun Aug 24 12:06:13 2025 +0200
+++ b/source/timelinelib/canvas/timelinecanvascontroller.py Sun Aug 24 12:12:25 2025 +0200
@@ -48,6 +48,7 @@
         """
         self._debug_enabled = debug_enabled
         self._appearance = Appearance()
+        self._appearance.listen_for_any(self._redraw_timeline)
         self._monitoring = Monitoring()
         self._view = view
         self._drawing_algorithm = self._set_drawing_algorithm(drawer)

changeset:   8064:c8aee09d8e20
tag:         tip
user:        Rickard Lindberg <rickard@rickardlindberg.me>
date:        Sun Aug 24 12:22:38 2025 +0200
summary:     Write changelog for text to the left of period events

diff -r deead63fc19f -r c8aee09d8e20 documentation/changelog.rst
--- a/documentation/changelog.rst Sun Aug 24 12:12:25 2025 +0200
+++ b/documentation/changelog.rst Sun Aug 24 12:22:38 2025 +0200
@@ -9,6 +9,16 @@
 
 * Beta versions: |betas|_
 
+New features, enhancements:
+
+* ``Text to the left of period events``
+  This is a new option (available in Preferences/General) that makes the event
+  text of period events appear to the left of the event box. Implementing this
+  feature required extensive changes in the logic of how a timeline is drawn.
+  Some details were deliberately changed because I think it yielded a better
+  result. Some details might have accidentally changed. If you think the
+  appearance has changed for the worse, please let us know.
+
 Fixed crash reports and bugs:
 
 * ``Image.Scale(): argument 1 has unexpected type 'float'``

2025-08-24 12:06 Rickard pushed to timeline

changeset:   8061:25841312ce26
parent:      8058:2d20dc22fc76
user:        Rickard Lindberg <rickard@rickardlindberg.me>
date:        Sun Aug 24 12:04:28 2025 +0200
summary:     Add config for text to the left of period events

diff -r 2d20dc22fc76 -r 25841312ce26 source/timelinelib/canvas/appearance.py
--- a/source/timelinelib/canvas/appearance.py Sun Aug 24 11:22:39 2025 +0200
+++ b/source/timelinelib/canvas/appearance.py Sun Aug 24 12:04:28 2025 +0200
@@ -59,6 +59,7 @@
         self._build_property("time_scale_pos", 1)
         self._build_property("use_bold_nowline", False)
         self._build_property("milestone_as_circle", False)
+        self._build_property("text_to_the_left_of_period_events", False)
 
     def _build_property(self, name, initial_value):
 
diff -r 2d20dc22fc76 -r 25841312ce26 source/timelinelib/canvas/drawing/scene.py
--- a/source/timelinelib/canvas/drawing/scene.py Sun Aug 24 11:22:39 2025 +0200
+++ b/source/timelinelib/canvas/drawing/scene.py Sun Aug 24 12:04:28 2025 +0200
@@ -282,6 +282,10 @@
             inflate = 0
         box_rect = self._calc_ideal_wx_rect(rx, ry, rw, rh)
         text_rect = box_rect.Clone()
+        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
+            text_rect.Width = w
         return EventRects(
             event=event,
             box_rect=box_rect.CloneInflate(inflate, inflate),
diff -r 2d20dc22fc76 -r 25841312ce26 source/timelinelib/config/dotfile.py
--- a/source/timelinelib/config/dotfile.py Sun Aug 24 11:22:39 2025 +0200
+++ b/source/timelinelib/config/dotfile.py Sun Aug 24 12:04:28 2025 +0200
@@ -348,6 +348,7 @@
     {'name': 'use_sidebar_filter_hint', 'default': 'True'},
     {'name': 'real_time_redraw_interval_navigate', 'default': 'False'},
     {'name': 'milestone_as_circle', 'default': 'False'},
+    {'name': 'text_to_the_left_of_period_events', 'default': 'False'},
 )
 INT_CONFIGS = (
     {'name': 'sidebar_width', 'default': '200'},
diff -r 2d20dc22fc76 -r 25841312ce26 source/timelinelib/wxgui/components/timelinepanelguicreator.py
--- a/source/timelinelib/wxgui/components/timelinepanelguicreator.py Sun Aug 24 11:22:39 2025 +0200
+++ b/source/timelinelib/wxgui/components/timelinepanelguicreator.py Sun Aug 24 12:04:28 2025 +0200
@@ -159,6 +159,7 @@
             appearance.set_time_scale_pos(self._config.time_scale_pos)
             appearance.set_use_bold_nowline(self._config.use_bold_nowline)
             appearance.set_milestone_as_circle(self._config.milestone_as_circle)
+            appearance.set_text_to_the_left_of_period_events(self._config.text_to_the_left_of_period_events)
         self._config.listen_for_any(update_appearance)
         update_appearance()
 
diff -r 2d20dc22fc76 -r 25841312ce26 source/timelinelib/wxgui/dialogs/preferences/controller.py
--- a/source/timelinelib/wxgui/dialogs/preferences/controller.py Sun Aug 24 11:22:39 2025 +0200
+++ b/source/timelinelib/wxgui/dialogs/preferences/controller.py Sun Aug 24 12:04:28 2025 +0200
@@ -193,6 +193,9 @@
     def on_milestone_as_circle(self, evt):
         self.config.milestone_as_circle = self.view.GetMilestoneAsCircle()
 
+    def on_text_to_the_left_of_period_events(self, evt):
+        self.config.text_to_the_left_of_period_events = self.view.GetTextToTheLeftOfPeriodEvents()
+
     def _set_initial_values(self):
         self.view.SetOpenRecentCheckboxValue(self.config.open_recent_at_startup)
         self.view.SetInertialScrollingCheckboxValue(self.config.use_inertial_scrolling)
@@ -240,6 +243,7 @@
         self.view.SetUseSidebarTextColoring(self.config.use_sidebar_text_coloring)
         self.view.SetUseSidebarFilterHint(self.config.use_sidebar_filter_hint)
         self.view.SetMilestoneAsCircle(self.config.milestone_as_circle)
+        self.view.SetTextToTheLeftOfPeriodEvents(self.config.text_to_the_left_of_period_events)
 
     def _week_index(self, week):
         for (i, w) in self.weeks_map:
diff -r 2d20dc22fc76 -r 25841312ce26 source/timelinelib/wxgui/dialogs/preferences/view.py
--- a/source/timelinelib/wxgui/dialogs/preferences/view.py Sun Aug 24 11:22:39 2025 +0200
+++ b/source/timelinelib/wxgui/dialogs/preferences/view.py Sun Aug 24 12:04:28 2025 +0200
@@ -87,6 +87,11 @@
                             event_EVT_CHECKBOX="on_milestone_as_circle"
                             label="$(milestone_as_circle)"
                         />
+                        <CheckBox
+                            name="text_to_the_left_of_period_events"
+                            event_EVT_CHECKBOX="on_text_to_the_left_of_period_events"
+                            label="$(text_to_the_left_of_period_events)"
+                        />
                         <Button
                             name="select_tab_order"
                             event_EVT_BUTTON="on_tab_order_click"
@@ -510,6 +515,7 @@
             "use_sidebar_text_coloring_text": _("Use colored text in sidebar instead of colored boxes"),
             "use_sidebar_filter_hint_text": _("Use hint in sidebar filter textbox instead of label"),
             "milestone_as_circle": _("Show milestone as a circle"),
+            "text_to_the_left_of_period_events": _("Text to the left of period events"),
         }, title=_("Preferences"))
         self.controller.on_init(config, ExperimentalFeatures())
         self.font_sizer.Layout()
@@ -629,6 +635,12 @@
     def SetMilestoneAsCircle(self, value):
         self.milestone_as_circle_checkbox.SetValue(value)
 
+    def GetTextToTheLeftOfPeriodEvents(self):
+        return self.text_to_the_left_of_period_events.GetValue()
+
+    def SetTextToTheLeftOfPeriodEvents(self, value):
+        self.text_to_the_left_of_period_events.SetValue(value)
+
     def SetWorkdayLength(self, value):
         self.workday_length_choices.Select(value - 1)
 
diff -r 2d20dc22fc76 -r 25841312ce26 translations/timeline.pot
--- a/translations/timeline.pot Sun Aug 24 11:22:39 2025 +0200
+++ b/translations/timeline.pot Sun Aug 24 12:04:28 2025 +0200
@@ -8,7 +8,7 @@
 msgstr ""
 "Project-Id-Version: PACKAGE VERSION\n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2025-08-24 10:32+0200\n"
+"POT-Creation-Date: 2025-08-24 11:53+0200\n"
 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
 "Language-Team: LANGUAGE <LL@li.org>\n"
@@ -834,8 +834,8 @@
 msgstr ""
 
 #: source\timelinelib\config\shortcut.py:45
-#: source\timelinelib\wxgui\components\timelinepanelguicreator.py:258
-#: source\timelinelib\wxgui\dialogs\preferences\view.py:485
+#: source\timelinelib\wxgui\components\timelinepanelguicreator.py:259
+#: source\timelinelib\wxgui\dialogs\preferences\view.py:490
 msgid "Edit"
 msgstr ""
 
@@ -851,7 +851,7 @@
 #: source\timelinelib\config\shortcut.py:48
 #: source\timelinelib\config\shortcut.py:51
 #: source\timelinelib\config\shortcut.py:52
-#: source\timelinelib\wxgui\dialogs\preferences\view.py:495
+#: source\timelinelib\wxgui\dialogs\preferences\view.py:500
 msgid "Navigate"
 msgstr ""
 
@@ -1326,7 +1326,7 @@
 #: source\timelinelib\help\pages.py:181
 #: source\timelinelib\plugin\plugins\exporters\timelineexporter.py:76
 #: source\timelinelib\wxgui\dialogs\export\view.py:77
-#: source\timelinelib\wxgui\dialogs\preferences\view.py:477
+#: source\timelinelib\wxgui\dialogs\preferences\view.py:482
 msgid "Events"
 msgstr ""
 
@@ -1451,7 +1451,7 @@
 msgstr ""
 
 #: source\timelinelib\help\pages.py:276
-#: source\timelinelib\wxgui\dialogs\preferences\view.py:486
+#: source\timelinelib\wxgui\dialogs\preferences\view.py:491
 msgid "Experimental Features"
 msgstr ""
 
@@ -2181,7 +2181,7 @@
 msgstr ""
 
 #: source\timelinelib\wxgui\components\categorytree.py:221
-#: source\timelinelib\wxgui\components\timelinepanelguicreator.py:252
+#: source\timelinelib\wxgui\components\timelinepanelguicreator.py:253
 msgid "Delete"
 msgstr ""
 
@@ -2386,51 +2386,51 @@
 msgstr ""
 
 #: source\timelinelib\wxgui\components\timelinepanelguicreator.py:91
-#: source\timelinelib\wxgui\dialogs\preferences\view.py:503
+#: source\timelinelib\wxgui\dialogs\preferences\view.py:508
 #: source\timelinelib\wxgui\frames\mainframe\menus\viewmenu.py:43
 #: source\timelinelib\wxgui\frames\mainframe\toolbarcreator.py:46
 #: source\timelinelib\wxgui\frames\mainframe\toolbarcreator.py:56
 msgid "Center"
 msgstr ""
 
-#: source\timelinelib\wxgui\components\timelinepanelguicreator.py:259
+#: source\timelinelib\wxgui\components\timelinepanelguicreator.py:260
 msgid "Duplicate..."
 msgstr ""
 
-#: source\timelinelib\wxgui\components\timelinepanelguicreator.py:261
-msgid "Measure Duration of Events..."
-msgstr ""
-
 #: source\timelinelib\wxgui\components\timelinepanelguicreator.py:262
-msgid "Mark Event as Done"
+msgid "Measure Duration of Events..."
 msgstr ""
 
 #: source\timelinelib\wxgui\components\timelinepanelguicreator.py:263
-msgid "Select Category..."
+msgid "Mark Event as Done"
 msgstr ""
 
 #: source\timelinelib\wxgui\components\timelinepanelguicreator.py:264
+msgid "Select Category..."
+msgstr ""
+
+#: source\timelinelib\wxgui\components\timelinepanelguicreator.py:265
 msgid "Select Container..."
 msgstr ""
 
-#: source\timelinelib\wxgui\components\timelinepanelguicreator.py:267
+#: source\timelinelib\wxgui\components\timelinepanelguicreator.py:268
 msgid "Sticky Balloon"
 msgstr ""
 
-#: source\timelinelib\wxgui\components\timelinepanelguicreator.py:274
+#: source\timelinelib\wxgui\components\timelinepanelguicreator.py:275
 msgid "Goto URL"
 msgstr ""
 
-#: source\timelinelib\wxgui\components\timelinepanelguicreator.py:305
+#: source\timelinelib\wxgui\components\timelinepanelguicreator.py:306
 #, python-format
 msgid "Are you sure you want to delete %d events?"
 msgstr ""
 
-#: source\timelinelib\wxgui\components\timelinepanelguicreator.py:308
+#: source\timelinelib\wxgui\components\timelinepanelguicreator.py:309
 msgid "Are you sure you want to delete this event?"
 msgstr ""
 
-#: source\timelinelib\wxgui\components\timelinepanelguicreator.py:356
+#: source\timelinelib\wxgui\components\timelinepanelguicreator.py:357
 #, python-format
 msgid "%s events hidden"
 msgstr ""
@@ -3005,7 +3005,7 @@
 msgstr ""
 
 #: source\timelinelib\wxgui\dialogs\preferences\controller.py:73
-#: source\timelinelib\wxgui\dialogs\preferences\controller.py:216
+#: source\timelinelib\wxgui\dialogs\preferences\controller.py:219
 msgid "Current"
 msgstr ""
 
@@ -3013,228 +3013,232 @@
 msgid "Default year, mont and day must have a numeric value"
 msgstr ""
 
-#: source\timelinelib\wxgui\dialogs\preferences\view.py:458
+#: source\timelinelib\wxgui\dialogs\preferences\view.py:463
 msgid "General"
 msgstr ""
 
-#: source\timelinelib\wxgui\dialogs\preferences\view.py:459
-msgid "Open most recent timeline on startup"
-msgstr ""
-
-#: source\timelinelib\wxgui\dialogs\preferences\view.py:460
-msgid "Use inertial scrolling"
-msgstr ""
-
-#: source\timelinelib\wxgui\dialogs\preferences\view.py:461
-msgid "Never show period Events as point Events"
-msgstr ""
-
-#: source\timelinelib\wxgui\dialogs\preferences\view.py:462
-msgid "Center Event texts"
-msgstr ""
-
-#: source\timelinelib\wxgui\dialogs\preferences\view.py:463
-msgid "Uncheck time checkbox for new events"
-msgstr ""
-
 #: source\timelinelib\wxgui\dialogs\preferences\view.py:464
-msgid "Balloon text below icon"
+msgid "Open most recent timeline on startup"
 msgstr ""
 
 #: source\timelinelib\wxgui\dialogs\preferences\view.py:465
-msgid "Filter items in listbox export, on categories"
+msgid "Use inertial scrolling"
 msgstr ""
 
 #: source\timelinelib\wxgui\dialogs\preferences\view.py:466
-msgid "Select Event Editor Tab Order"
+msgid "Never show period Events as point Events"
 msgstr ""
 
 #: source\timelinelib\wxgui\dialogs\preferences\view.py:467
-msgid "Select Date format"
+msgid "Center Event texts"
 msgstr ""
 
 #: source\timelinelib\wxgui\dialogs\preferences\view.py:468
-msgid "Date && Time"
+msgid "Uncheck time checkbox for new events"
 msgstr ""
 
 #: source\timelinelib\wxgui\dialogs\preferences\view.py:469
-msgid "Week start on:"
-msgstr ""
-
-#: source\timelinelib\wxgui\dialogs\preferences\view.py:470
-msgid "Monday"
+msgid "Balloon text below icon"
 msgstr ""
 
 #: source\timelinelib\wxgui\dialogs\preferences\view.py:470
-msgid "Sunday"
+msgid "Filter items in listbox export, on categories"
 msgstr ""
 
 #: source\timelinelib\wxgui\dialogs\preferences\view.py:471
-msgid "Workday Length in hours:"
+msgid "Select Event Editor Tab Order"
 msgstr ""
 
 #: source\timelinelib\wxgui\dialogs\preferences\view.py:472
-msgid "Fonts"
+msgid "Select Date format"
 msgstr ""
 
 #: source\timelinelib\wxgui\dialogs\preferences\view.py:473
-msgid "Colours"
+msgid "Date && Time"
 msgstr ""
 
 #: source\timelinelib\wxgui\dialogs\preferences\view.py:474
-msgid "Major Strips:"
+msgid "Week start on:"
 msgstr ""
 
 #: source\timelinelib\wxgui\dialogs\preferences\view.py:475
-msgid "Minor Strips:"
+msgid "Monday"
+msgstr ""
+
+#: source\timelinelib\wxgui\dialogs\preferences\view.py:475
+msgid "Sunday"
 msgstr ""
 
 #: source\timelinelib\wxgui\dialogs\preferences\view.py:476
-msgid "Balloons:"
+msgid "Workday Length in hours:"
+msgstr ""
+
+#: source\timelinelib\wxgui\dialogs\preferences\view.py:477
+msgid "Fonts"
 msgstr ""
 
 #: source\timelinelib\wxgui\dialogs\preferences\view.py:478
-msgid "Eras"
+msgid "Colours"
 msgstr ""
 
 #: source\timelinelib\wxgui\dialogs\preferences\view.py:479
-msgid "Event Editor"
+msgid "Major Strips:"
 msgstr ""
 
 #: source\timelinelib\wxgui\dialogs\preferences\view.py:480
-msgid "Icons"
+msgid "Minor Strips:"
 msgstr ""
 
 #: source\timelinelib\wxgui\dialogs\preferences\view.py:481
-msgid "Fuzzy icon"
-msgstr ""
-
-#: source\timelinelib\wxgui\dialogs\preferences\view.py:482
-msgid "Locked icon"
+msgid "Balloons:"
 msgstr ""
 
 #: source\timelinelib\wxgui\dialogs\preferences\view.py:483
-msgid "Hyperlink icon"
+msgid "Eras"
 msgstr ""
 
 #: source\timelinelib\wxgui\dialogs\preferences\view.py:484
-msgid "Legends:"
+msgid "Event Editor"
+msgstr ""
+
+#: source\timelinelib\wxgui\dialogs\preferences\view.py:485
+msgid "Icons"
+msgstr ""
+
+#: source\timelinelib\wxgui\dialogs\preferences\view.py:486
+msgid "Fuzzy icon"
 msgstr ""
 
 #: source\timelinelib\wxgui\dialogs\preferences\view.py:487
-msgid "Minor strip divider line:"
+msgid "Locked icon"
 msgstr ""
 
 #: source\timelinelib\wxgui\dialogs\preferences\view.py:488
-msgid "Major strip divider line:"
+msgid "Hyperlink icon"
 msgstr ""
 
 #: source\timelinelib\wxgui\dialogs\preferences\view.py:489
-msgid "Now line:"
-msgstr ""
-
-#: source\timelinelib\wxgui\dialogs\preferences\view.py:490
-msgid "Weekends:"
-msgstr ""
-
-#: source\timelinelib\wxgui\dialogs\preferences\view.py:491
-msgid "Use bold line"
+msgid "Legends:"
 msgstr ""
 
 #: source\timelinelib\wxgui\dialogs\preferences\view.py:492
-msgid "Background"
+msgid "Minor strip divider line:"
 msgstr ""
 
 #: source\timelinelib\wxgui\dialogs\preferences\view.py:493
-msgid "Vertical space between Events (px)"
+msgid "Major strip divider line:"
 msgstr ""
 
 #: source\timelinelib\wxgui\dialogs\preferences\view.py:494
-msgid ""
-"Real time redraw interval (s). Real time redraw is disabled if value is 0"
+msgid "Now line:"
+msgstr ""
+
+#: source\timelinelib\wxgui\dialogs\preferences\view.py:495
+msgid "Weekends:"
 msgstr ""
 
 #: source\timelinelib\wxgui\dialogs\preferences\view.py:496
-msgid "Colorize weekends"
+msgid "Use bold line"
 msgstr ""
 
 #: source\timelinelib\wxgui\dialogs\preferences\view.py:497
-msgid "Skip s in decade text"
+msgid "Background"
 msgstr ""
 
 #: source\timelinelib\wxgui\dialogs\preferences\view.py:498
-msgid "Display checkmark when events are done"
+msgid "Vertical space between Events (px)"
 msgstr ""
 
 #: source\timelinelib\wxgui\dialogs\preferences\view.py:499
-msgid "Never use time precision for events"
-msgstr ""
-
-#: source\timelinelib\wxgui\dialogs\preferences\view.py:500
-msgid "Use second precision for time"
+msgid ""
+"Real time redraw interval (s). Real time redraw is disabled if value is 0"
 msgstr ""
 
 #: source\timelinelib\wxgui\dialogs\preferences\view.py:501
-msgid "Legend Position"
+msgid "Colorize weekends"
 msgstr ""
 
 #: source\timelinelib\wxgui\dialogs\preferences\view.py:502
-msgid "Bottom-Left"
-msgstr ""
-
-#: source\timelinelib\wxgui\dialogs\preferences\view.py:502
-msgid "Top-Left"
-msgstr ""
-
-#: source\timelinelib\wxgui\dialogs\preferences\view.py:502
-msgid "Top-Right"
-msgstr ""
-
-#: source\timelinelib\wxgui\dialogs\preferences\view.py:502
-msgid "Bottom-Right"
+msgid "Skip s in decade text"
 msgstr ""
 
 #: source\timelinelib\wxgui\dialogs\preferences\view.py:503
-msgid "Top"
-msgstr ""
-
-#: source\timelinelib\wxgui\dialogs\preferences\view.py:503
-msgid "Bottom"
+msgid "Display checkmark when events are done"
 msgstr ""
 
 #: source\timelinelib\wxgui\dialogs\preferences\view.py:504
-msgid "Time scale position"
+msgid "Never use time precision for events"
 msgstr ""
 
 #: source\timelinelib\wxgui\dialogs\preferences\view.py:505
-msgid "Default Year"
+msgid "Use second precision for time"
 msgstr ""
 
 #: source\timelinelib\wxgui\dialogs\preferences\view.py:506
-msgid "Default Month"
+msgid "Legend Position"
+msgstr ""
+
+#: source\timelinelib\wxgui\dialogs\preferences\view.py:507
+msgid "Bottom-Left"
 msgstr ""
 
 #: source\timelinelib\wxgui\dialogs\preferences\view.py:507
-msgid "Default Day"
+msgid "Top-Left"
+msgstr ""
+
+#: source\timelinelib\wxgui\dialogs\preferences\view.py:507
+msgid "Top-Right"
+msgstr ""
+
+#: source\timelinelib\wxgui\dialogs\preferences\view.py:507
+msgid "Bottom-Right"
+msgstr ""
+
+#: source\timelinelib\wxgui\dialogs\preferences\view.py:508
+msgid "Top"
 msgstr ""
 
 #: source\timelinelib\wxgui\dialogs\preferences\view.py:508
-msgid "Use date default values"
+msgid "Bottom"
+msgstr ""
+
+#: source\timelinelib\wxgui\dialogs\preferences\view.py:509
+msgid "Time scale position"
 msgstr ""
 
 #: source\timelinelib\wxgui\dialogs\preferences\view.py:510
-msgid "Use colored text in sidebar instead of colored boxes"
+msgid "Default Year"
 msgstr ""
 
 #: source\timelinelib\wxgui\dialogs\preferences\view.py:511
-msgid "Use hint in sidebar filter textbox instead of label"
+msgid "Default Month"
 msgstr ""
 
 #: source\timelinelib\wxgui\dialogs\preferences\view.py:512
-msgid "Show milestone as a circle"
+msgid "Default Day"
 msgstr ""
 
 #: source\timelinelib\wxgui\dialogs\preferences\view.py:513
+msgid "Use date default values"
+msgstr ""
+
+#: source\timelinelib\wxgui\dialogs\preferences\view.py:515
+msgid "Use colored text in sidebar instead of colored boxes"
+msgstr ""
+
+#: source\timelinelib\wxgui\dialogs\preferences\view.py:516
+msgid "Use hint in sidebar filter textbox instead of label"
+msgstr ""
+
+#: source\timelinelib\wxgui\dialogs\preferences\view.py:517
+msgid "Show milestone as a circle"
+msgstr ""
+
+#: source\timelinelib\wxgui\dialogs\preferences\view.py:518
+msgid "Text to the left of period events"
+msgstr ""
+
+#: source\timelinelib\wxgui\dialogs\preferences\view.py:519
 #: source\timelinelib\wxgui\frames\mainframe\menus\editmenu.py:34
 msgid "Preferences"
 msgstr ""

changeset:   8062:c2fde191bf90
parent:      8061:25841312ce26
parent:      8060:3f04f6bd113d
user:        Rickard Lindberg <rickard@rickardlindberg.me>
date:        Sun Aug 24 12:06:13 2025 +0200
summary:     Merge

diff -r 25841312ce26 -r c2fde191bf90 documentation/changelog.rst
--- a/documentation/changelog.rst Sun Aug 24 12:04:28 2025 +0200
+++ b/documentation/changelog.rst Sun Aug 24 12:06:13 2025 +0200
@@ -11,7 +11,12 @@
 
 Fixed crash reports and bugs:
 
-* ``TypeError: DC.DrawCircle(): arguments did not match any overloaded call: overload 1: argument 1 has unexpected type 'float' overload 2: argument 1 has unexpected type 'float'``
+* ``Image.Scale(): argument 1 has unexpected type 'float'``
+  This makes it impossible to load icons in events.
+  The error appears after the upgrade to a newer version of wxPython.
+  The API now expects integers instead of flotas
+
+* ``TypeError: DC.DrawCircle(): arguments did not match any overloaded call overload 1: argument 1 has unexpected type 'float' overload 2: argument 1 has unexpected type 'float'``
   This was a problem with the feature "Milestones can be drawn as circles"
   introduced in version 2.11.0. The ``DC.DrawCircle`` API expects integers, but
   we didn't ensure that.
diff -r 25841312ce26 -r c2fde191bf90 source/timelinelib/wxgui/components/propertyeditors/iconeditor.py
--- a/source/timelinelib/wxgui/components/propertyeditors/iconeditor.py Sun Aug 24 12:04:28 2025 +0200
+++ b/source/timelinelib/wxgui/components/propertyeditors/iconeditor.py Sun Aug 24 12:06:13 2025 +0200
@@ -43,7 +43,7 @@
                     factor = float(H) / float(h)
                     w = w * factor
                     h = h * factor
-                image = image.Scale(w, h, wx.IMAGE_QUALITY_HIGH)
+                image = image.Scale(int(w), int(h), wx.IMAGE_QUALITY_HIGH)
                 return image.ConvertToBitmap()
         except:
             pass

2025-08-24 11:45 Roger pushed to timeline

changeset:   8059:5f4b15207f8d
parent:      8019:8d6eb11fd6ba
user:        roger <roger@rolidata.se>
date:        Sun Aug 24 11:40:11 2025 +0200
summary:     Fixed problem caused by change of argument types in wxPython API Image.Scale()

diff -r 8d6eb11fd6ba -r 5f4b15207f8d documentation/changelog.rst
--- a/documentation/changelog.rst Sun Aug 10 07:59:56 2025 +0200
+++ b/documentation/changelog.rst Sun Aug 24 11:40:11 2025 +0200
@@ -11,7 +11,12 @@
 
 Fixed crash reports and bugs:
 
-* ``TypeError: DC.DrawCircle(): arguments did not match any overloaded call: overload 1: argument 1 has unexpected type 'float' overload 2: argument 1 has unexpected type 'float'``
+* ``Image.Scale(): argument 1 has unexpected type 'float'``
+  This makes it impossible to load icons in events.
+  The error appears after the upgrade to a newer version of wxPython.
+  The API now expects integers instead of flotas
+
+* ``TypeError: DC.DrawCircle(): arguments did not match any overloaded call overload 1: argument 1 has unexpected type 'float' overload 2: argument 1 has unexpected type 'float'``
   This was a problem with the feature "Milestones can be drawn as circles"
   introduced in version 2.11.0. The ``DC.DrawCircle`` API expects integers, but
   we didn't ensure that.
diff -r 8d6eb11fd6ba -r 5f4b15207f8d source/timelinelib/wxgui/components/propertyeditors/iconeditor.py
--- a/source/timelinelib/wxgui/components/propertyeditors/iconeditor.py Sun Aug 10 07:59:56 2025 +0200
+++ b/source/timelinelib/wxgui/components/propertyeditors/iconeditor.py Sun Aug 24 11:40:11 2025 +0200
@@ -43,7 +43,7 @@
                     factor = float(H) / float(h)
                     w = w * factor
                     h = h * factor
-                image = image.Scale(w, h, wx.IMAGE_QUALITY_HIGH)
+                image = image.Scale(int(w), int(h), wx.IMAGE_QUALITY_HIGH)
                 return image.ConvertToBitmap()
         except:
             pass

changeset:   8060:3f04f6bd113d
parent:      8059:5f4b15207f8d
parent:      8058:2d20dc22fc76
user:        roger <roger@rolidata.se>
date:        Sun Aug 24 11:45:23 2025 +0200
summary:     Fixed problem caused by change of argument types in wxPython API Image.Scale()

diff -r 5f4b15207f8d -r 3f04f6bd113d source/timelinelib/canvas/drawing/drawers/default.py
--- a/source/timelinelib/canvas/drawing/drawers/default.py Sun Aug 24 11:40:11 2025 +0200
+++ b/source/timelinelib/canvas/drawing/drawers/default.py Sun Aug 24 11:45:23 2025 +0200
@@ -25,7 +25,6 @@
 from timelinelib.canvas.drawing.drawers.nowline import NowLine
 from timelinelib.canvas.drawing.interface import Drawer
 from timelinelib.canvas.drawing.scene import TimelineScene
-from timelinelib.features.experimental.experimentalfeatures import EXTENDED_CONTAINER_HEIGHT
 from timelinelib.features.experimental.experimentalfeatures import EXTENDED_CONTAINER_STRATEGY
 from timelinelib.utils import unique_based_on_eq
 from timelinelib.wxgui.components.font import Font
@@ -119,16 +118,16 @@
             "above_stretch": 0,
             "below_stretch": 0,
         }
-        for (evt, rect) in self.scene.event_data:
-            if rect.Y < self.scene.divider_y:
+        for event_rects in self.scene.event_data:
+            if event_rects.box_rect.Y < self.scene.divider_y:
                 measurements["above_stretch"] = max(
                     measurements["above_stretch"],
-                    self.scene.divider_y - rect.Y
+                    self.scene.divider_y - event_rects.box_rect.Y
                 )
             else:
                 measurements["below_stretch"] = max(
                     measurements["below_stretch"],
-                    rect.Bottom - self.scene.divider_y
+                    event_rects.box_rect.Bottom - self.scene.divider_y
                 )
         measurements["above_stretch"] += self.scene.get_baseline_padding() * 2
         measurements["below_stretch"] += self.scene.get_baseline_padding() * 2
@@ -153,8 +152,6 @@
         self.colorize_weekends = appearance.get_colorize_weekends()
         self.outer_padding = OUTER_PADDING
         self.outer_padding = appearance.get_vertical_space_between_events()
-        if EXTENDED_CONTAINER_HEIGHT.enabled():
-            self.outer_padding += EXTENDED_CONTAINER_HEIGHT.get_extra_outer_padding_to_avoid_vertical_overlapping()
         self.appearance = appearance
         self.dc = dc
         self.time_type = timeline.get_time_type()
@@ -231,12 +228,11 @@
         return self.snap(start), self.snap(end)
 
     def event_at(self, x, y, alt_down=False):
-        for (event, rect) in reversed(self.scene.event_data):
-            if event.is_container():
-                rect = self._adjust_container_rect_for_hittest(rect)
+        for event_rects in reversed(self.scene.event_data):
+            rect = event_rects.box_rect
             if rect.Contains(wx.Point(x, y)):
-                self.set_horizontal_mouse_position_factor(event, x)
-                return event
+                self.set_horizontal_mouse_position_factor(event_rects.event, x)
+                return event_rects.event
         return None
 
     def set_horizontal_mouse_position_factor(self, event, x):
@@ -249,32 +245,26 @@
 
     def get_events_in_rect(self, rect):
         return [
-            event
-            for (event, rect)
+            event_rects.event
+            for event_rects
             in self.scene.event_data
-            if rect.Intersects(rect)
+            if event_rects.box_rect.Intersects(rect)
         ]
 
-    def _adjust_container_rect_for_hittest(self, rect):
-        if EXTENDED_CONTAINER_HEIGHT.enabled():
-            return EXTENDED_CONTAINER_HEIGHT.get_vertical_larger_box_rect(rect)
-        else:
-            return rect
-
     def event_with_rect_at(self, x, y, view_properties=None):
-        for (event, rect) in self.scene.event_data:
-            if view_properties.is_selected(event):
-                if rect.Contains(wx.Point(x, y)):
-                    if event.is_container():
-                        return event, rect
+        for event_rects in self.scene.event_data:
+            if view_properties.is_selected(event_rects.event):
+                if event_rects.box_rect.Contains(wx.Point(x, y)):
+                    if event_rects.event.is_container():
+                        return event_rects.event, event_rects.box_rect
                     else:
-                        return event, rect
+                        return event_rects.event, event_rects.box_rect
         return None
 
     def event_rect(self, evt):
-        for (event, rect) in self.scene.event_data:
-            if evt == event:
-                return rect
+        for event_rects in self.scene.event_data:
+            if evt == event_rects.event:
+                return event_rects.box_rect
         return None
 
     def balloon_at(self, x, y):
@@ -385,13 +375,13 @@
         DividerLine(self).draw()
 
     def _draw_lines_to_non_period_events(self, view_properties):
-        for (event, rect) in self.scene.event_data:
-            if event.is_milestone():
+        for event_rects in self.scene.event_data:
+            if event_rects.event.is_milestone():
                 continue
-            if not event.is_period():
-                self._draw_line(view_properties, event, rect)
-            elif not self.scene.never_show_period_events_as_point_events() and self._event_displayed_as_point_event(rect):
-                self._draw_line(view_properties, event, rect)
+            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):
+                self._draw_line(view_properties, event_rects.event, event_rects.box_rect)
 
     def _event_displayed_as_point_event(self, rect):
         return self.scene.divider_y > rect.Y
@@ -433,10 +423,10 @@
         return y
 
     def _get_container_y(self, subevent):
-        for (event, rect) in self.scene.event_data:
-            if event.is_container():
-                if event is subevent.container:
-                    return rect.y - 1
+        for event_rects in self.scene.event_data:
+            if event_rects.event.is_container():
+                if event_rects.event is subevent.container:
+                    return event_rects.box_rect.y - 1
         return self.scene.divider_y
 
     def _set_line_color(self, view_properties, event):
@@ -452,9 +442,9 @@
 
     def _extract_categories(self):
         return sort_categories(unique_based_on_eq(
-            event.category
-            for (event, _) in self.scene.event_data
-            if event.category
+            event_rects.event.category
+            for event_rects in self.scene.event_data
+            if event_rects.event.category
         ))
 
     def _draw_legend(self, view_properties, categories):
@@ -468,46 +458,37 @@
         collection = []
         amount = view_properties.hscroll_amount
         if amount != 0:
-            for (event, rect) in self.scene.event_data:
-                if rect.Y < self.scene.divider_y:
-                    self._scroll_point_events(amount, event, rect, collection)
+            for event_rects in self.scene.event_data:
+                if event_rects.box_rect.Y < self.scene.divider_y:
+                    self._scroll_point_events(amount, event_rects, collection)
                 else:
-                    self._scroll_period_events(amount, event, rect, collection)
+                    self._scroll_period_events(amount, event_rects, collection)
             self.scene.event_data = collection
 
-    def _scroll_point_events(self, amount, event, rect, collection):
-        rect.Y += amount
-        if rect.Y < self.scene.divider_y - rect.height:
-            collection.append((event, rect))
+    def _scroll_point_events(self, amount, event_rects, collection):
+        event_rects.move_y(amount)
+        if event_rects.box_rect.Y < self.scene.divider_y - event_rects.box_rect.height:
+            collection.append(event_rects)
 
-    def _scroll_period_events(self, amount, event, rect, collection):
-        rect.Y -= amount
-        if rect.Y > self.scene.divider_y + rect.height:
-            collection.append((event, rect))
+    def _scroll_period_events(self, amount, event_rects, collection):
+        event_rects.move_y(-amount)
+        if event_rects.box_rect.Y > self.scene.divider_y + event_rects.box_rect.height:
+            collection.append(event_rects)
 
     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, rect) in self.scene.event_data:
+        for event_rects in self.scene.event_data:
             self.dc.SetFont(self._event_text_font)
-            if event.is_container():
-                self._draw_container(event, rect, view_properties)
-            else:
-                self._draw_box(rect, event, view_properties)
+            self._draw_box(event_rects, view_properties)
 
-    def _draw_container(self, event, rect, view_properties):
-        box_rect = rect.CloneInflate(2, 2)
-        if EXTENDED_CONTAINER_HEIGHT.enabled():
-            box_rect = EXTENDED_CONTAINER_HEIGHT.get_vertical_larger_box_rect(rect)
-        self._draw_box(box_rect, event, view_properties)
-
-    def _draw_box(self, rect, event, view_properties):
-        self.dc.SetClippingRegion(rect)
-        self._event_box_drawer.draw(self.dc, self.scene, rect, event, view_properties)
-        if EXTENDED_CONTAINER_STRATEGY.enabled() and event.is_subevent() and not event.is_period():
-            self._draw_time_marker_for_point_event(rect)
+    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):
@@ -524,13 +505,13 @@
         top_event = None
         top_rect = None
         self.dc.SetTextForeground(BLACK)
-        for (event, rect) in self.scene.event_data:
-            if event.get_data("description") is not None or event.get_data("icon") is not None:
-                sticky = view_properties.event_has_sticky_balloon(event)
-                if view_properties.event_is_hovered(event) or sticky:
+        for event_rects in self.scene.event_data:
+            if event_rects.event.get_data("description") is not None or event_rects.event.get_data("icon") is not None:
+                sticky = view_properties.event_has_sticky_balloon(event_rects.event)
+                if view_properties.event_is_hovered(event_rects.event) or sticky:
                     if not sticky:
-                        top_event, top_rect = event, rect
-                    self._draw_ballon(event, rect, sticky)
+                        top_event, top_rect = event_rects.event, event_rects.box_rect
+                    self._draw_ballon(event_rects.event, event_rects.box_rect, sticky)
         # Make the unsticky balloon appear on top
         if top_event is not None:
             self._draw_ballon(top_event, top_rect, False)
diff -r 5f4b15207f8d -r 3f04f6bd113d source/timelinelib/canvas/drawing/scene.py
--- a/source/timelinelib/canvas/drawing/scene.py Sun Aug 24 11:40:11 2025 +0200
+++ b/source/timelinelib/canvas/drawing/scene.py Sun Aug 24 11:45:23 2025 +0200
@@ -18,11 +18,12 @@
 
 import wx
 
+from timelinelib.canvas.data import TimePeriod
+from timelinelib.canvas.drawing.rect import FloatingRect
+from timelinelib.canvas.drawing.rect import Rect
 from timelinelib.canvas.drawing.utils import Metrics
-from timelinelib.canvas.data import TimePeriod
+from timelinelib.features.experimental.experimentalfeatures import EXTENDED_CONTAINER_HEIGHT
 from timelinelib.features.experimental.experimentalfeatures import EXTENDED_CONTAINER_STRATEGY
-from timelinelib.canvas.drawing.rect import Rect
-from timelinelib.canvas.drawing.rect import FloatingRect
 
 
 FORWARD = 1
@@ -36,6 +37,11 @@
         self._view_properties = view_properties
         self._get_text_size_fn = get_text_size_fn
         self._appearance = appearance
+        self._container_padding = 2
+        if EXTENDED_CONTAINER_HEIGHT.enabled():
+            self._container_title_height = self._get_text_height("")
+        else:
+            self._container_title_height = 0
         self._outer_padding = 5
         self._inner_padding = 3
         self._baseline_padding = 15
@@ -111,7 +117,6 @@
         return self._metrics.calc_width(time_period)
 
     def get_closest_overlapping_event(self, selected_event, up=True):
-        self._inflate_event_rects_to_get_right_dimensions_for_overlap_calculations()
         rect = self._get_event_rect(selected_event)
         # self._get_event_rect() returns None if the selected event isn't visible.
         # (The selected event can be scrolled out of view). In this case the period
@@ -128,14 +133,10 @@
             evt = self._get_overlapping_event(period, direction, selected_event, rect)
             return evt, direction
 
-    def _inflate_event_rects_to_get_right_dimensions_for_overlap_calculations(self):
-        for (_, rect) in self.event_data:
-            rect.Inflate(self._outer_padding, self._outer_padding)
-
     def _get_event_rect(self, event):
-        for (evt, rect) in self.event_data:
-            if evt == event:
-                return rect
+        for event_rects in self.event_data:
+            if event_rects.event == event:
+                return event_rects.box_rect
 
     def _event_rect_drawn_as_period(self, event_rect):
         return event_rect.Y >= self.divider_y
@@ -173,24 +174,24 @@
     @staticmethod
     def _get_next_overlapping_event(event_data, selected_event):
         selected_event_found = False
-        for (e, _) in event_data:
-            if not selected_event.is_subevent() and e.is_subevent():
+        for event_rects in event_data:
+            if not selected_event.is_subevent() and event_rects.event.is_subevent():
                 continue
             if selected_event_found:
-                return e
+                return event_rects.event
             else:
-                if e == selected_event:
+                if event_rects.event == selected_event:
                     selected_event_found = True
 
     @staticmethod
     def _get_prev_overlapping_event(event_data, selected_event):
         prev_event = None
-        for (e, _) in event_data:
-            if not selected_event.is_subevent() and e.is_subevent():
+        for event_rects in event_data:
+            if not selected_event.is_subevent() and event_rects.event.is_subevent():
                 continue
-            if e == selected_event:
+            if event_rects.event == selected_event:
                 return prev_event
-            prev_event = e
+            prev_event = event_rects.event
 
     def _calc_event_sizes_and_positions(self):
         self.events_from_db = self._db.get_events(self._view_properties.displayed_period)
@@ -218,21 +219,16 @@
 
     def _calc_event_rects(self, events):
         self.event_data = self._calc_non_overlapping_event_rects(events)
-        self._deflate_rects(self.event_data)
         return self.event_data
 
     def _calc_non_overlapping_event_rects(self, events):
         self.event_data = []
         for event in events:
-            rect = self._create_ideal_rect_for_event(event)
-            self._prevent_overlapping_by_adjusting_rect_y(event, rect)
-            self.event_data.append((event, rect))
+            event_rects = self._create_ideal_rect_for_event(event)
+            self._prevent_overlapping_by_adjusting_rect_y(event_rects)
+            self.event_data.append(event_rects)
         return self.event_data
 
-    def _deflate_rects(self, event_data):
-        for (_, rect) in event_data:
-            rect.Deflate(self._outer_padding, self._outer_padding)
-
     def _create_ideal_rect_for_event(self, event):
         self._reset_ends_today_when_start_date_is_in_future(event)
         if event.ends_today:
@@ -241,21 +237,28 @@
             return self._calc_ideal_rect_for_period_event(event)
         else:
             if self._is_subevent_in_extended_container_strategy(event):
-                rect = self._calc_ideal_rect_for_period_event(event)
+                event_rects = self._calc_ideal_rect_for_period_event(event)
                 rw = self._calc_width_for_non_period_event(event)
-                rect.SetWidth(rw)
+                event_rects.box_rect.SetWidth(rw)
+                event_rects.text_rect.SetWidth(rw)
                 if not event.is_period():
-                    self._extend_container_width_for_point_event(event, rect)
-                return rect
+                    self._extend_container_width_for_point_event(event_rects)
+                return event_rects
             else:
-                return self._calc_ideal_rect_for_non_period_event(event)
+                rect = self._calc_ideal_rect_for_non_period_event(event)
+                return EventRects(
+                    event=event,
+                    box_rect=rect,
+                    text_rect=rect.Clone(),
+                    height_padding=self._outer_padding,
+                )
 
-    def _extend_container_width_for_point_event(self, subevent, subevent_rect):
+    def _extend_container_width_for_point_event(self, event_rects):
         """Make point events be enclosed by the container rectangle."""
-        event, container_rect = self._get_container_data_for_subevent(subevent)
-        right_x_pos_diff = subevent_rect.GetRight() - container_rect.GetRight()
+        container_event_rects = self._get_container_data_for_subevent(event_rects.event)
+        right_x_pos_diff = event_rects.bounding_rect.GetRight() - container_event_rects.box_rect.GetRight()
         if right_x_pos_diff > 0:
-            container_rect.SetRight(container_rect.GetRight() + right_x_pos_diff)
+            container_event_rects.extend_width(right_x_pos_diff)
 
     def _reset_ends_today_when_start_date_is_in_future(self, event):
         if event.ends_today and self._start_date_is_in_future(event):
@@ -272,17 +275,29 @@
         rh = self._calc_height_for_period_event(event)
         rx = self._calc_x_pos_for_period_event(event)
         ry = self._calc_y_pos_for_period_event(event)
-        return self._calc_ideal_wx_rect(rx, ry, rw, rh)
+        if event.is_container():
+            rh += self._container_title_height
+            inflate = self._container_padding
+        else:
+            inflate = 0
+        box_rect = self._calc_ideal_wx_rect(rx, ry, rw, rh)
+        text_rect = box_rect.Clone()
+        return EventRects(
+            event=event,
+            box_rect=box_rect.CloneInflate(inflate, inflate),
+            text_rect=text_rect.CloneInflate(inflate, inflate),
+            height_padding=self._outer_padding,
+        )
 
     def _calc_width_for_period_event(self, event):
         min_w = 5 * self._outer_padding
-        return max(self._metrics.calc_width(event.get_time_period()) + 2 * self._outer_padding, min_w)
+        return max(self._metrics.calc_width(event.get_time_period()), min_w)
 
     def _calc_height_for_period_event(self, event):
-        return self._get_text_height(event.get_text()) + 2 * self._inner_padding + 2 * self._outer_padding
+        return self._get_text_height(event.get_text()) + 2 * self._inner_padding
 
     def _calc_x_pos_for_period_event(self, event):
-        return self._metrics.calc_x(event.get_time_period().start_time) - self._outer_padding
+        return self._metrics.calc_x(event.get_time_period().start_time)
 
     def _calc_y_pos_for_period_event(self, event):
         if self._is_subevent_in_extended_container_strategy(event):
@@ -291,22 +306,22 @@
             if event.is_period():
                 return self._get_container_ry(event)
             else:
-                return self._metrics.half_height - self._baseline_padding
+                return self._metrics.half_height - self._baseline_padding - self._outer_padding
         else:
-            return self._metrics.half_height + self._baseline_padding
+            return self._metrics.half_height + self._baseline_padding + self._outer_padding
 
     def _get_container_ry(self, subevent):
-        for (event, rect) in self.event_data:
-            if event == subevent.container:
-                return rect.y
-        return self._metrics.half_height + self._baseline_padding
+        for event_rect in self.event_data:
+            if event_rect.event == subevent.container:
+                return event_rect.box_rect.y
+        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():
             x = self.x_pos_for_time(event.mean_time())
             y0 = self.divider_y
             y1 = y0 + 10
-            return FloatingRect(x - 5, y1 - 5, 10, 10).CloneInflate(self._outer_padding, self._outer_padding)
+            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)
@@ -320,7 +335,7 @@
 
     def _calc_width_for_non_period_event(self, event):
         tw, th = self._get_text_size(event.get_text())
-        rw = tw + 2 * self._inner_padding + 2 * self._outer_padding
+        rw = tw + 2 * self._inner_padding
         if event.has_data():
             rw += self._data_indicator_size // 3
         if event.get_fuzzy() or event.get_locked():
@@ -328,11 +343,11 @@
         return rw
 
     def _calc_height_for_non_period_event(self, event):
-        return self._get_text_height(event.get_text()) + 2 * self._inner_padding + 2 * self._outer_padding
+        return self._get_text_height(event.get_text()) + 2 * self._inner_padding
 
     def _calc_x_pos_for_non_period_event(self, event, rw):
         if self._appearance.get_draw_period_events_to_right():
-            return self._metrics.calc_x(event.get_time_period().start_time) - self._outer_padding
+            return self._metrics.calc_x(event.get_time_period().start_time)
         else:
             return self._metrics.calc_x(event.mean_time()) - rw // 2
 
@@ -340,7 +355,7 @@
         if event.is_milestone():
             return self._metrics.half_height - rh // 2
         else:
-            return self._metrics.half_height - rh - self._baseline_padding
+            return self._metrics.half_height - rh - self._baseline_padding - self._outer_padding
 
     def _get_text_size(self, text):
         if len(text) > 0:
@@ -397,92 +412,125 @@
         return time.is_weekend_day
 
     def get_hidden_event_count(self):
-        visible_events_count = len([rect for _, rect in self.event_data
-                                    if rect.Y < self.height and (rect.Y + rect.Height) > 0])
+        visible_events_count = len([
+            event_rects
+            for event_rects
+            in self.event_data
+            if event_rects.box_rect.Y < self.height and (event_rects.box_rect.Y + event_rects.box_rect.Height) > 0
+        ])
         return len(self.events_from_db) - visible_events_count
 
-    def _prevent_overlapping_by_adjusting_rect_y(self, event, event_rect):
-        if event_rect.is_allowed_to_overlap():
+    def _prevent_overlapping_by_adjusting_rect_y(self, event_rects):
+        if event_rects.box_rect.is_allowed_to_overlap():
             return
-        if self._is_subevent_in_extended_container_strategy(event):
-            self._adjust_subevent_rect(event, event_rect)
-        elif event.is_subevent() and self._display_as_period(event):
-            self._adjust_subevent_rect(event, event_rect)
+        if self._is_subevent_in_extended_container_strategy(event_rects.event):
+            self._adjust_subevent_rect(event_rects)
+        elif event_rects.event.is_subevent() and self._display_as_period(event_rects.event):
+            self._adjust_subevent_rect(event_rects)
         else:
-            if self._display_as_period(event):
-                self._adjust_period_rect(event_rect)
+            if self._display_as_period(event_rects.event):
+                self._adjust_period_rect(event_rects)
             else:
-                self._adjust_point_rect(event_rect)
+                self._adjust_point_rect(event_rects)
 
-    def _adjust_period_rect(self, event_rect):
-        rect = self._get_overlapping_period_rect_with_largest_y(event_rect)
+    def _adjust_period_rect(self, event_rects):
+        rect = self._get_overlapping_period_rect_with_largest_y(event_rects.bounding_rect)
         if rect is not None:
-            event_rect.Y = rect.Y + rect.height
+            event_rects.set_bounding_y(rect.Y + rect.height)
 
-    def _adjust_subevent_rect(self, subevent, event_rect):
-        rect = self._get_overlapping_subevent_rect_with_largest_y(subevent, event_rect)
+    def _adjust_subevent_rect(self, event_rects):
+        container_event_rects = self._get_container_data_for_subevent(event_rects.event)
+        event_rects.set_bounding_y(
+            container_event_rects.box_rect.Y
+            +
+            self._container_padding
+            +
+            self._container_title_height
+            -
+            event_rects.height_padding
+        )
+        rect = self._get_overlapping_subevent_rect_with_largest_y(event_rects)
         if rect is not None:
-            event_rect.Y = rect.Y + rect.height
-            self._adjust_container_rect_height(subevent, event_rect)
+            event_rects.set_bounding_y(rect.Y + rect.height)
+            subevent_box_bottom = event_rects.box_rect.Bottom
+            container_box_bottom = container_event_rects.box_rect.Bottom
+            diff = subevent_box_bottom - container_box_bottom + self._container_padding
+            if diff > 0:
+                container_event_rects.extend_box_height(diff)
 
-    def _adjust_container_rect_height(self, subevent, event_rect):
-        container_evt, container_rect = self._get_container_data_for_subevent(subevent)
-        _, th = self._get_text_size(container_evt.get_text())
-        rh = th + 2 * (self._inner_padding + self._outer_padding)
-        h = event_rect.Y - container_rect.Y + rh
-        if container_rect.height < h:
-            container_rect.Height = h
-
-    def _get_overlapping_subevent_rect_with_largest_y(self, subevent, event_rect):
-        event_data = self._get_list_with_overlapping_subevents(subevent, event_rect)
+    def _get_overlapping_subevent_rect_with_largest_y(self, event_rects):
+        event_data = self._get_list_with_overlapping_subevents(event_rects)
         rect_with_largest_y = None
-        for (_, rect) in event_data:
-            if rect_with_largest_y is None or rect.Y > rect_with_largest_y.Y:
-                rect_with_largest_y = rect
+        for event_rects in event_data:
+            if rect_with_largest_y is None or event_rects.bounding_rect.Y > rect_with_largest_y.Y:
+                rect_with_largest_y = event_rects.bounding_rect
         return rect_with_largest_y
 
     def _get_overlapping_period_rect_with_largest_y(self, event_rect):
         event_data = self._get_list_with_overlapping_period_events(event_rect)
         rect_with_largest_yh = None
-        for (_, rect) in event_data:
-            if rect_with_largest_yh is None or rect.Y + rect.Height > rect_with_largest_yh.Y + rect_with_largest_yh.Height:
-                rect_with_largest_yh = rect
+        for event_rects in event_data:
+            if rect_with_largest_yh is None or event_rects.bounding_rect.Y + event_rects.bounding_rect.Height > rect_with_largest_yh.Y + rect_with_largest_yh.Height:
+                rect_with_largest_yh = event_rects.bounding_rect
         return rect_with_largest_yh
 
     def _get_list_with_overlapping_period_events(self, event_rect):
-        return [(event, rect) for (event, rect) in self.event_data
-                if (self._rects_overlap(event_rect, rect) and
-                    rect.Y >= self.divider_y)]
+        return [
+            event_rects
+            for event_rects
+            in self.event_data
+            if (
+                self._rects_overlap(event_rect, event_rects.bounding_rect)
+                and
+                event_rects.bounding_rect.Y >= self.divider_y
+            )
+        ]
 
-    def _get_list_with_overlapping_subevents(self, subevent, event_rect):
-        ls = [(event, rect) for (event, rect) in self.event_data
-              if (event.is_subevent() and
-                  event.container is subevent.container and
-                  self._rects_overlap(event_rect, rect) and
-                  rect.Y >= self.divider_y)]
-        return ls
+    def _get_list_with_overlapping_subevents(self, event_rects):
+        return [
+            inner_event_rects
+            for inner_event_rects
+            in self.event_data
+            if (
+                inner_event_rects.event.is_subevent()
+                and
+                inner_event_rects.event.container is event_rects.event.container
+                and
+                self._rects_overlap(event_rects.bounding_rect, inner_event_rects.bounding_rect)
+                and
+                inner_event_rects.bounding_rect.Y >= self.divider_y
+            )
+        ]
 
-    def _adjust_point_rect(self, event_rect):
-        rect = self._get_overlapping_point_rect_with_smallest_y(event_rect)
+    def _adjust_point_rect(self, event_rects):
+        rect = self._get_overlapping_point_rect_with_smallest_y(event_rects.bounding_rect)
         if rect is not None:
-            event_rect.Y = rect.Y - event_rect.height
+            event_rects.set_bounding_y(rect.Y - event_rects.bounding_rect.height)
 
     def _get_overlapping_point_rect_with_smallest_y(self, event_rect):
         event_data = self._get_list_with_overlapping_point_events(event_rect)
         rect_with_smallest_y = None
-        for (_, rect) in event_data:
-            if rect_with_smallest_y is None or rect.Y < rect_with_smallest_y.Y:
-                rect_with_smallest_y = rect
+        for event_rects in event_data:
+            if rect_with_smallest_y is None or event_rects.bounding_rect.Y < rect_with_smallest_y.Y:
+                rect_with_smallest_y = event_rects.bounding_rect
         return rect_with_smallest_y
 
     def _get_list_with_overlapping_point_events(self, event_rect):
-        return [(event, rect) for (event, rect) in self.event_data
-                if (self._rects_overlap(event_rect, rect) and
-                    rect.Y < self.divider_y) and
-                    not rect.is_allowed_to_overlap()]
+        return [
+            event_rects
+            for event_rects
+            in self.event_data
+            if (
+                self._rects_overlap(event_rect, event_rects.bounding_rect)
+                and
+                event_rects.bounding_rect.Y < self.divider_y
+                and
+                not event_rects.box_rect.is_allowed_to_overlap()
+            )
+        ]
 
     def _rects_overlap(self, rect1, rect2):
-        REMOVE_X_PADDING = 2 + self._outer_padding * 2
+        REMOVE_X_PADDING = 2
         return (rect2.x + REMOVE_X_PADDING <= rect1.x + rect1.width and
                 rect1.x + REMOVE_X_PADDING <= rect2.x + rect2.width)
 
@@ -490,4 +538,79 @@
         return EXTENDED_CONTAINER_STRATEGY.enabled() and event.is_subevent()
 
     def _get_container_data_for_subevent(self, subevent):
-        return [(event, rect) for (event, rect) in self.event_data if event is subevent.container][0]
+        return [
+            event_rects
+            for event_rects
+            in self.event_data
+            if event_rects.event is subevent.container
+        ][0]
+
+
+class EventRects:
+
+    def __init__(self, event, box_rect, text_rect, height_padding):
+        self.event = event
+        self.box_rect = box_rect
+        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):
+        """
+        >>> box_rect = Rect(0, 0, 10, 10)
+        >>> text_rect = Rect(10, 10, 10, 10)
+        >>> event_rects = EventRects(event=None, box_rect=box_rect, text_rect=text_rect, height_padding=5)
+        >>> event_rects.bounding_rect
+        wx.Rect(0, -5, 20, 30)
+        """
+        min_x = min([self.box_rect.X, self.text_rect.X])
+        min_y = min([self.box_rect.Y, self.text_rect.Y])
+        max_right = max([self.box_rect.Right, self.text_rect.Right])
+        max_bottom = max([self.box_rect.Bottom, self.text_rect.Bottom])
+        return Rect(
+            x=min_x,
+            y=min_y,
+            width=max_right-min_x+1,
+            height=max_bottom-min_y+1,
+        ).CloneInflate(0, self.height_padding)
+
+    def set_bounding_y(self, y):
+        """
+        >>> box_rect = Rect(0, 0, 10, 5)
+        >>> text_rect = Rect(0, 0, 10, 5)
+        >>> event_rects = EventRects(event=None, box_rect=box_rect, text_rect=text_rect, height_padding=5)
+
+        >>> event_rects.box_rect
+        wx.Rect(0, 0, 10, 5)
+        >>> event_rects.bounding_rect
+        wx.Rect(0, -5, 10, 15)
+
+        >>> event_rects.set_bounding_y(5)
+        >>> event_rects.box_rect
+        wx.Rect(0, 10, 10, 5)
+        >>> event_rects.bounding_rect
+        wx.Rect(0, 5, 10, 15)
+        """
+        self.move_y(y - self.bounding_rect.Y)
+
+    def extend_box_height(self, height):
+        self.box_rect.Height += height
+
+    def extend_width(self, widht):
+        self.box_rect.Width += widht
+        self.text_rect.Width += widht
+
+    def move_y(self, amount):
+        self.box_rect.Y += amount
+        self.text_rect.Y += amount
+
+    def text_outside_box(self):
+        return self.text_rect.X < self.box_rect.X
diff -r 5f4b15207f8d -r 3f04f6bd113d source/timelinelib/canvas/eventboxdrawers/defaulteventboxdrawer.py
--- a/source/timelinelib/canvas/eventboxdrawers/defaulteventboxdrawer.py Sun Aug 24 11:40:11 2025 +0200
+++ b/source/timelinelib/canvas/eventboxdrawers/defaulteventboxdrawer.py Sun Aug 24 11:45:23 2025 +0200
@@ -22,7 +22,6 @@
 
 from timelinelib.canvas.drawing.utils import black_solid_pen, black_solid_brush, get_colour, darken_color
 from timelinelib.config.paths import EVENT_ICONS_DIR
-from timelinelib.features.experimental.experimentalfeatures import EXTENDED_CONTAINER_HEIGHT
 from timelinelib.canvas.eventboxdrawers.defaultmilestonedrawer import DefaultMilestoneDrawer
 from timelinelib.canvas.eventboxdrawers.handlerect import HandleRect, MIDDLE_HANDLE, LEFT_HANDLE, RIGHT_HANDLE
 from timelinelib.wxgui.utils import load_bitmap
@@ -39,7 +38,9 @@
 
 class DefaultEventBoxDrawer:
 
-    def draw(self, dc, scene, rect, event, view_properties):
+    def draw(self, dc, scene, event_rects, view_properties):
+        rect = event_rects.box_rect
+        event = event_rects.event
         self.scene = scene
         self.view_properties = view_properties
         selected = view_properties.is_selected(event)
@@ -49,7 +50,7 @@
         elif scene.never_show_period_events_as_point_events() 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, rect, event, selected)
+            self._draw_event_box(dc, event_rects, selected)
 
     def _draw_period_event_as_symbol_below_divider_line(self, dc, rect, scene, event):
         dc.DestroyClippingRegion()
@@ -61,12 +62,14 @@
         dc.DrawLine(x, y0, x, y1)
         dc.DrawCircle(x, y1, 2)
 
-    def _draw_event_box(self, dc, rect, event, selected):
+    def _draw_event_box(self, dc, event_rects, selected):
+        event = event_rects.event
+        rect = event_rects.box_rect
         self._draw_background(dc, rect, event)
         self._draw_fuzzy_edges(dc, rect, event)
         self._draw_locked_edges(dc, rect, event)
         self._draw_progress_box(dc, rect, event)
-        self._draw_text(dc, rect, event)
+        self._draw_text(dc, event_rects)
         self._draw_contents_indicator(dc, event, rect)
         self._draw_locked_edges(dc, rect, event)
         self._draw_selection_handles(dc, event, rect, selected)
@@ -183,20 +186,17 @@
         dc.SetPen(wx.TRANSPARENT_PEN)
         dc.DrawPolygon(points)
 
-    def _draw_text(self, dc, rect, event):
+    def _draw_text(self, dc, event_rects):
         # Ensure that we can't draw content outside inner rectangle
-        if self._there_is_room_for_the_text(rect):
-            self._draw_the_text(dc, rect, event)
+        if self._there_is_room_for_the_text(event_rects.text_rect):
+            self._draw_the_text(dc, event_rects)
 
     def _there_is_room_for_the_text(self, rect):
         return deflate_rect(rect).Width > 0
 
-    def _draw_the_text(self, dc, rect, event):
-        self._set_text_foreground_color(dc, event)
-        if event.is_container() and EXTENDED_CONTAINER_HEIGHT.enabled():
-            EXTENDED_CONTAINER_HEIGHT.draw_container_text_top_adjusted(event.get_text(), dc, rect)
-        else:
-            self._draw_normal_text(dc, rect, event)
+    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):
@@ -236,11 +236,15 @@
         width, _ = dc.GetTextExtent(self._get_text(event))
         return max(text_x, text_x + (inner_rect.width - width) // 2)
 
-    def _set_text_foreground_color(self, dc, event):
-        try:
-            dc.SetTextForeground(get_colour(event.get_category().font_color))
-        except AttributeError:
-            dc.SetTextForeground(wx.BLACK)
+    def _set_text_foreground_color(self, dc, event_rects):
+        if event_rects.text_outside_box():
+            color = wx.BLACK
+        else:
+            try:
+                color = get_colour(event_rects.event.get_category().font_color)
+            except AttributeError:
+                color = wx.BLACK
+        dc.SetTextForeground(color)
 
     def _draw_handles(self, dc, event, rect):
 
diff -r 5f4b15207f8d -r 3f04f6bd113d source/timelinelib/features/experimental/experimentalfeaturecontainersize.py
--- a/source/timelinelib/features/experimental/experimentalfeaturecontainersize.py Sun Aug 24 11:40:11 2025 +0200
+++ b/source/timelinelib/features/experimental/experimentalfeaturecontainersize.py Sun Aug 24 11:45:23 2025 +0200
@@ -16,46 +16,17 @@
 # along with Timeline.  If not, see <http://www.gnu.org/licenses/>.
 
 
-import wx
 from timelinelib.features.experimental.experimentalfeature import ExperimentalFeature
-from timelinelib.wxgui.components.font import Font
-from timelinelib.canvas.drawing.rect import Rect
 
 
 CONFIG_NAME = "Extend Container height"
 DISPLAY_NAME = _("Extend Container height")
 DESCRIPTION = _("""
               Extend the height of a container so that the container name becomes visible.
-
-              This also has the side effect that ordinary events come farther apart in
-              the vertical direction.
-
-              The font for the container name has a fixed size when you zoom vertically (Alt + Mouse wheel)
               """)
-Y_OFFSET = -16
-PADDING = 12
-OUTER_PAADING = 4
-TEXT_OFFSET = -2
-INNER_PADDING = 3
-FONT_SIZE = 8
 
 
 class ExperimentalFeatureContainerSize(ExperimentalFeature):
 
     def __init__(self):
         ExperimentalFeature.__init__(self, DISPLAY_NAME, DESCRIPTION, CONFIG_NAME)
-
-    def get_extra_outer_padding_to_avoid_vertical_overlapping(self):
-        return OUTER_PAADING
-
-    def get_vertical_larger_box_rect(self, rect):
-        return Rect(rect.X - 2, rect.Y - 2 - PADDING, rect.Width + 4, rect.Height + 4 + PADDING)
-
-    def draw_container_text_top_adjusted(self, text, dc, rect):
-        old_font = dc.GetFont()
-        dc.SetFont(Font(FONT_SIZE))
-        dc.SetClippingRegion(Rect(rect.X, rect.Y + Y_OFFSET, rect.Width, rect.Height))
-        text_x = rect.X + INNER_PADDING
-        text_y = rect.Y + INNER_PADDING + TEXT_OFFSET
-        dc.DrawText(text, text_x, text_y)
-        dc.SetFont(old_font)
diff -r 5f4b15207f8d -r 3f04f6bd113d test/unit/canvas/drawing/scene.py
--- a/test/unit/canvas/drawing/scene.py Sun Aug 24 11:40:11 2025 +0200
+++ b/test/unit/canvas/drawing/scene.py Sun Aug 24 11:45:23 2025 +0200
@@ -55,39 +55,39 @@
         self.given_visible_event_at("5 Jan 2010")
         self.given_visible_event_at("5 Jan 2010")
         self.when_scene_is_created()
-        self.assertTrue(self.scene.event_data[0][1].Y >
-                        self.scene.event_data[1][1].Y)
+        self.assertTrue(self.scene.event_data[0].bounding_rect.Y >
+                        self.scene.event_data[1].bounding_rect.Y)
 
     def test_point_events_on_different_dates_has_same_y_positions(self):
         self.given_displayed_period("1 Jan 2010", "10 Jan 2010")
         self.given_visible_event_at("2 Jan 2010")
         self.given_visible_event_at("9 Jan 2010")
         self.when_scene_is_created()
-        self.assertEqual(self.scene.event_data[0][1].Y,
-                         self.scene.event_data[1][1].Y)
+        self.assertEqual(self.scene.event_data[0].bounding_rect.Y,
+                         self.scene.event_data[1].bounding_rect.Y)
 
     def test_period_events_with_same_period_has_different_y_positions(self):
         self.given_displayed_period("1 Jan 2010", "12 Jan 2010")
         self.given_visible_event_at("2 Jan 2010", "10 Jan 2010")
         self.given_visible_event_at("2 Jan 2010", "10 Jan 2010")
         self.when_scene_is_created()
-        self.assertTrue(self.scene.event_data[0][1].Y <
-                        self.scene.event_data[1][1].Y)
+        self.assertTrue(self.scene.event_data[0].bounding_rect.Y <
+                        self.scene.event_data[1].bounding_rect.Y)
 
     def test_period_events_with_different_periods_has_same_y_positions(self):
         self.given_displayed_period("1 Jan 2010", "12 Jan 2010")
         self.given_visible_event_at("2 Jan 2010", "3 Jan 2010")
         self.given_visible_event_at("8 Jan 2010", "10 Jan 2010")
         self.when_scene_is_created()
-        self.assertEqual(self.scene.event_data[0][1].Y,
-                         self.scene.event_data[1][1].Y)
+        self.assertEqual(self.scene.event_data[0].bounding_rect.Y,
+                         self.scene.event_data[1].bounding_rect.Y)
 
     def test_long_periods_are_not_drawn_very_far_outside_screen(self):
         self.given_displayed_period("1 Jan 50 12:00", "1 Jan 50 13:00")
         self.given_visible_event_at("1 Jan 0", "1 Jan 1000")
         self.when_scene_is_created()
-        event_x = self.scene.event_data[0][1].X
-        event_width = self.scene.event_data[0][1].Width
+        event_x = self.scene.event_data[0].bounding_rect.X
+        event_width = self.scene.event_data[0].bounding_rect.Width
         event_right_x = event_x + event_width
         window_width = self.size[0]
         self.assertTrue(event_x > -self.MAX_OUTSIDE_SCREEN)
@@ -108,7 +108,7 @@
         self.given_visible_event_at("1 Jan 3017", "1 Feb 3017", ends_today=True)
         self.assertTrue(self.db.get_first_event().ends_today)
         self.when_scene_is_created()
-        self.assertFalse(self.scene.event_data[0][0].ends_today)
+        self.assertFalse(self.scene.event_data[0].event.ends_today)
 
     def setUp(self):
         WxAppTestCase.setUp(self)
diff -r 5f4b15207f8d -r 3f04f6bd113d test/unit/canvas/eventboxdrawers/defaulteventboxdrawer.py
--- a/test/unit/canvas/eventboxdrawers/defaulteventboxdrawer.py Sun Aug 24 11:40:11 2025 +0200
+++ b/test/unit/canvas/eventboxdrawers/defaulteventboxdrawer.py Sun Aug 24 11:45:23 2025 +0200
@@ -19,11 +19,12 @@
 import wx
 from unittest.mock import Mock
 
-from timelinelib.test.cases.unit import UnitTestCase
+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.canvas.drawing.rect import Rect
+from timelinelib.test.cases.unit import UnitTestCase
 
 
 DEFAULT_TEXT = "test"
@@ -33,12 +34,12 @@
 
     def test_when_rect_has_zero_width_text_is_not_drawn(self):
         rect = Rect(0, 0, 0, 0)
-        self.drawer._draw_text(self.dc, rect, self.event)
+        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)
-        self.drawer._draw_text(self.dc, rect, self.event)
+        self.drawer._draw_text(self.dc, event_rects(rect, self.event))
         self.assertEqual(self.dc.DrawText.call_count, 0)
 
     def test_non_centered_text_is_left_aligned(self):
@@ -46,7 +47,7 @@
         HEIGHT = 20
         rect = Rect(0, 0, WIDTH, HEIGHT)
         self.drawer.center_text = False
-        self.drawer._draw_text(self.dc, rect, self.event)
+        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)
 
@@ -55,7 +56,7 @@
         HEIGHT = 20
         rect = Rect(0, 0, WIDTH, HEIGHT)
         self.drawer.center_text = True
-        self.drawer._draw_text(self.dc, rect, self.event)
+        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)
 
@@ -65,7 +66,7 @@
         rect = Rect(0, 0, WIDTH, HEIGHT)
         self.dc.GetTextExtent.return_value = (500, 0)
         self.drawer.center_text = True
-        self.drawer._draw_text(self.dc, rect, self.event)
+        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)
 
@@ -75,7 +76,7 @@
         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, rect, self.event)
+        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)
 
@@ -85,7 +86,7 @@
         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, rect, self.event)
+        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)
 
@@ -97,7 +98,7 @@
         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, rect, self.event)
+        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)
 
@@ -128,3 +129,12 @@
         self.cat = Mock()
         self.cat.font_color = (0, 0, 0)
         self.event.get_category.return_value = self.cat
+
+
+def event_rects(rect, event):
+    return EventRects(
+        event=event,
+        box_rect=rect,
+        text_rect=rect.Clone(),
+        height_padding=0,
+    )
diff -r 5f4b15207f8d -r 3f04f6bd113d tools/generate-pot-file.py
--- a/tools/generate-pot-file.py Sun Aug 24 11:40:11 2025 +0200
+++ b/tools/generate-pot-file.py Sun Aug 24 11:45:23 2025 +0200
@@ -29,7 +29,17 @@
 
 
 def generate_pot_file():
-    subprocess.check_call(build_xgettext_command(parse_arguments()), cwd=ROOT_DIR)
+    arguments = parse_arguments()
+    subprocess.check_call(build_xgettext_command(arguments), cwd=ROOT_DIR)
+    lines = []
+    with open(arguments.outfile) as f:
+        for line in f:
+            if line.startswith("#: source"):
+                lines.append(line.replace("/", "\\"))
+            else:
+                lines.append(line)
+    with open(arguments.outfile, "w") as f:
+        f.write("".join(lines))
 
 
 def parse_arguments():
diff -r 5f4b15207f8d -r 3f04f6bd113d translations/timeline.pot
--- a/translations/timeline.pot Sun Aug 24 11:40:11 2025 +0200
+++ b/translations/timeline.pot Sun Aug 24 11:45:23 2025 +0200
@@ -8,7 +8,7 @@
 msgstr ""
 "Project-Id-Version: PACKAGE VERSION\n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2025-07-08 07:50+0000\n"
+"POT-Creation-Date: 2025-08-24 10:32+0200\n"
 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
 "Language-Team: LANGUAGE <LL@li.org>\n"
@@ -1109,22 +1109,15 @@
 msgid "Unable to delete backup dbfile '%s'."
 msgstr ""
 
-#: source\timelinelib\features\experimental\experimentalfeaturecontainersize.py:25
+#: source\timelinelib\features\experimental\experimentalfeaturecontainersize.py:23
 msgid "Extend Container height"
 msgstr ""
 
-#: source\timelinelib\features\experimental\experimentalfeaturecontainersize.py:26
+#: source\timelinelib\features\experimental\experimentalfeaturecontainersize.py:24
 msgid ""
 "\n"
 "              Extend the height of a container so that the container name "
 "becomes visible.\n"
-"\n"
-"              This also has the side effect that ordinary events come "
-"farther apart in\n"
-"              the vertical direction.\n"
-"\n"
-"              The font for the container name has a fixed size when you zoom "
-"vertically (Alt + Mouse wheel)\n"
 "              "
 msgstr ""
 
@@ -2175,74 +2168,74 @@
 msgstr ""
 
 #: source\timelinelib\wxgui\components\categorychoice.py:89
-#: source\timelinelib\wxgui\components\categorytree.py:502
+#: source\timelinelib\wxgui\components\categorytree.py:503
 msgid "Add Category"
 msgstr ""
 
-#: source\timelinelib\wxgui\components\categorytree.py:212
+#: source\timelinelib\wxgui\components\categorytree.py:213
 msgid "Edit..."
 msgstr ""
 
-#: source\timelinelib\wxgui\components\categorytree.py:216
+#: source\timelinelib\wxgui\components\categorytree.py:217
 msgid "Add..."
 msgstr ""
 
-#: source\timelinelib\wxgui\components\categorytree.py:220
+#: source\timelinelib\wxgui\components\categorytree.py:221
 #: source\timelinelib\wxgui\components\timelinepanelguicreator.py:252
 msgid "Delete"
 msgstr ""
 
-#: source\timelinelib\wxgui\components\categorytree.py:225
+#: source\timelinelib\wxgui\components\categorytree.py:226
 msgid "Check All"
 msgstr ""
 
-#: source\timelinelib\wxgui\components\categorytree.py:229
+#: source\timelinelib\wxgui\components\categorytree.py:230
 msgid "Check children"
 msgstr ""
 
-#: source\timelinelib\wxgui\components\categorytree.py:233
+#: source\timelinelib\wxgui\components\categorytree.py:234
 msgid "Check all children"
 msgstr ""
 
-#: source\timelinelib\wxgui\components\categorytree.py:237
+#: source\timelinelib\wxgui\components\categorytree.py:238
 msgid "Check all parents"
 msgstr ""
 
-#: source\timelinelib\wxgui\components\categorytree.py:241
+#: source\timelinelib\wxgui\components\categorytree.py:242
 msgid "Check parents for checked children"
 msgstr ""
 
-#: source\timelinelib\wxgui\components\categorytree.py:246
+#: source\timelinelib\wxgui\components\categorytree.py:247
 msgid "Uncheck All"
 msgstr ""
 
-#: source\timelinelib\wxgui\components\categorytree.py:250
+#: source\timelinelib\wxgui\components\categorytree.py:251
 msgid "Uncheck children"
 msgstr ""
 
-#: source\timelinelib\wxgui\components\categorytree.py:254
+#: source\timelinelib\wxgui\components\categorytree.py:255
 msgid "Uncheck all children"
 msgstr ""
 
-#: source\timelinelib\wxgui\components\categorytree.py:258
+#: source\timelinelib\wxgui\components\categorytree.py:259
 msgid "Uncheck all parents"
 msgstr ""
 
-#: source\timelinelib\wxgui\components\categorytree.py:496
+#: source\timelinelib\wxgui\components\categorytree.py:497
 msgid "Edit Category"
 msgstr ""
 
-#: source\timelinelib\wxgui\components\categorytree.py:508
+#: source\timelinelib\wxgui\components\categorytree.py:509
 #, python-format
 msgid "Are you sure you want to delete category '%s'?"
 msgstr ""
 
-#: source\timelinelib\wxgui\components\categorytree.py:511
+#: source\timelinelib\wxgui\components\categorytree.py:512
 #, python-format
 msgid "Events belonging to '%s' will no longer belong to a category."
 msgstr ""
 
-#: source\timelinelib\wxgui\components\categorytree.py:514
+#: source\timelinelib\wxgui\components\categorytree.py:515
 #, python-format
 msgid "Events belonging to '%s' will now belong to '%s'."
 msgstr ""

2025-08-24 11:39 Rickard pushed to timeline

changeset:   8058:2d20dc22fc76
user:        Rickard Lindberg <rickard@rickardlindberg.me>
date:        Sun Aug 24 11:22:39 2025 +0200
summary:     Support for drawing text outside event box

diff -r 6c38d6c35d6e -r 2d20dc22fc76 source/timelinelib/canvas/drawing/scene.py
--- a/source/timelinelib/canvas/drawing/scene.py Sun Aug 24 10:47:57 2025 +0200
+++ b/source/timelinelib/canvas/drawing/scene.py Sun Aug 24 11:22:39 2025 +0200
@@ -280,11 +280,12 @@
             inflate = self._container_padding
         else:
             inflate = 0
-        rect = self._calc_ideal_wx_rect(rx, ry, rw, rh)
+        box_rect = self._calc_ideal_wx_rect(rx, ry, rw, rh)
+        text_rect = box_rect.Clone()
         return EventRects(
             event=event,
-            box_rect=rect.CloneInflate(inflate, inflate),
-            text_rect=rect.CloneInflate(inflate, inflate),
+            box_rect=box_rect.CloneInflate(inflate, inflate),
+            text_rect=text_rect.CloneInflate(inflate, inflate),
             height_padding=self._outer_padding,
         )
 
@@ -610,3 +611,6 @@
     def move_y(self, amount):
         self.box_rect.Y += amount
         self.text_rect.Y += amount
+
+    def text_outside_box(self):
+        return self.text_rect.X < self.box_rect.X
diff -r 6c38d6c35d6e -r 2d20dc22fc76 source/timelinelib/canvas/eventboxdrawers/defaulteventboxdrawer.py
--- a/source/timelinelib/canvas/eventboxdrawers/defaulteventboxdrawer.py Sun Aug 24 10:47:57 2025 +0200
+++ b/source/timelinelib/canvas/eventboxdrawers/defaulteventboxdrawer.py Sun Aug 24 11:22:39 2025 +0200
@@ -65,12 +65,11 @@
     def _draw_event_box(self, dc, event_rects, selected):
         event = event_rects.event
         rect = event_rects.box_rect
-        text_rect = event_rects.text_rect
         self._draw_background(dc, rect, event)
         self._draw_fuzzy_edges(dc, rect, event)
         self._draw_locked_edges(dc, rect, event)
         self._draw_progress_box(dc, rect, event)
-        self._draw_text(dc, text_rect, event)
+        self._draw_text(dc, event_rects)
         self._draw_contents_indicator(dc, event, rect)
         self._draw_locked_edges(dc, rect, event)
         self._draw_selection_handles(dc, event, rect, selected)
@@ -187,17 +186,17 @@
         dc.SetPen(wx.TRANSPARENT_PEN)
         dc.DrawPolygon(points)
 
-    def _draw_text(self, dc, rect, event):
+    def _draw_text(self, dc, event_rects):
         # Ensure that we can't draw content outside inner rectangle
-        if self._there_is_room_for_the_text(rect):
-            self._draw_the_text(dc, rect, event)
+        if self._there_is_room_for_the_text(event_rects.text_rect):
+            self._draw_the_text(dc, event_rects)
 
     def _there_is_room_for_the_text(self, rect):
         return deflate_rect(rect).Width > 0
 
-    def _draw_the_text(self, dc, rect, event):
-        self._set_text_foreground_color(dc, event)
-        self._draw_normal_text(dc, rect, event)
+    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):
@@ -237,11 +236,15 @@
         width, _ = dc.GetTextExtent(self._get_text(event))
         return max(text_x, text_x + (inner_rect.width - width) // 2)
 
-    def _set_text_foreground_color(self, dc, event):
-        try:
-            dc.SetTextForeground(get_colour(event.get_category().font_color))
-        except AttributeError:
-            dc.SetTextForeground(wx.BLACK)
+    def _set_text_foreground_color(self, dc, event_rects):
+        if event_rects.text_outside_box():
+            color = wx.BLACK
+        else:
+            try:
+                color = get_colour(event_rects.event.get_category().font_color)
+            except AttributeError:
+                color = wx.BLACK
+        dc.SetTextForeground(color)
 
     def _draw_handles(self, dc, event, rect):
 
diff -r 6c38d6c35d6e -r 2d20dc22fc76 test/unit/canvas/eventboxdrawers/defaulteventboxdrawer.py
--- a/test/unit/canvas/eventboxdrawers/defaulteventboxdrawer.py Sun Aug 24 10:47:57 2025 +0200
+++ b/test/unit/canvas/eventboxdrawers/defaulteventboxdrawer.py Sun Aug 24 11:22:39 2025 +0200
@@ -19,11 +19,12 @@
 import wx
 from unittest.mock import Mock
 
-from timelinelib.test.cases.unit import UnitTestCase
+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.canvas.drawing.rect import Rect
+from timelinelib.test.cases.unit import UnitTestCase
 
 
 DEFAULT_TEXT = "test"
@@ -33,12 +34,12 @@
 
     def test_when_rect_has_zero_width_text_is_not_drawn(self):
         rect = Rect(0, 0, 0, 0)
-        self.drawer._draw_text(self.dc, rect, self.event)
+        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)
-        self.drawer._draw_text(self.dc, rect, self.event)
+        self.drawer._draw_text(self.dc, event_rects(rect, self.event))
         self.assertEqual(self.dc.DrawText.call_count, 0)
 
     def test_non_centered_text_is_left_aligned(self):
@@ -46,7 +47,7 @@
         HEIGHT = 20
         rect = Rect(0, 0, WIDTH, HEIGHT)
         self.drawer.center_text = False
-        self.drawer._draw_text(self.dc, rect, self.event)
+        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)
 
@@ -55,7 +56,7 @@
         HEIGHT = 20
         rect = Rect(0, 0, WIDTH, HEIGHT)
         self.drawer.center_text = True
-        self.drawer._draw_text(self.dc, rect, self.event)
+        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)
 
@@ -65,7 +66,7 @@
         rect = Rect(0, 0, WIDTH, HEIGHT)
         self.dc.GetTextExtent.return_value = (500, 0)
         self.drawer.center_text = True
-        self.drawer._draw_text(self.dc, rect, self.event)
+        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)
 
@@ -75,7 +76,7 @@
         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, rect, self.event)
+        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)
 
@@ -85,7 +86,7 @@
         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, rect, self.event)
+        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)
 
@@ -97,7 +98,7 @@
         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, rect, self.event)
+        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)
 
@@ -128,3 +129,12 @@
         self.cat = Mock()
         self.cat.font_color = (0, 0, 0)
         self.event.get_category.return_value = self.cat
+
+
+def event_rects(rect, event):
+    return EventRects(
+        event=event,
+        box_rect=rect,
+        text_rect=rect.Clone(),
+        height_padding=0,
+    )

2025-08-24 10:49 Rickard pushed to timeline

changeset:   8051:f691895e6a36
user:        Rickard Lindberg <rickard@rickardlindberg.me>
date:        Sun Aug 24 08:24:37 2025 +0200
summary:     Move creation of EventRects further down the call stack

diff -r 18cc2c9d5ee2 -r f691895e6a36 source/timelinelib/canvas/drawing/scene.py
--- a/source/timelinelib/canvas/drawing/scene.py Sun Aug 24 07:59:31 2025 +0200
+++ b/source/timelinelib/canvas/drawing/scene.py Sun Aug 24 08:24:37 2025 +0200
@@ -218,13 +218,7 @@
     def _calc_non_overlapping_event_rects(self, events):
         self.event_data = []
         for event in events:
-            rect = self._create_ideal_rect_for_event(event)
-            event_rects = EventRects(
-                event=event,
-                box_rect=rect,
-                text_rect=rect.Clone(),
-                height_padding=self._outer_padding,
-            )
+            event_rects = self._create_ideal_rect_for_event(event)
             self._prevent_overlapping_by_adjusting_rect_y(event_rects)
             self.event_data.append(event_rects)
         return self.event_data
@@ -234,7 +228,13 @@
         if event.ends_today:
             event.set_end_time(self._db.now)
         if self._display_as_period(event):
-            return self._calc_ideal_rect_for_period_event(event)
+            rect = self._calc_ideal_rect_for_period_event(event)
+            return EventRects(
+                event=event,
+                box_rect=rect,
+                text_rect=rect.Clone(),
+                height_padding=self._outer_padding,
+            )
         else:
             if self._is_subevent_in_extended_container_strategy(event):
                 rect = self._calc_ideal_rect_for_period_event(event)
@@ -242,9 +242,20 @@
                 rect.SetWidth(rw)
                 if not event.is_period():
                     self._extend_container_width_for_point_event(event, rect)
-                return rect
+                return EventRects(
+                    event=event,
+                    box_rect=rect,
+                    text_rect=rect.Clone(),
+                    height_padding=self._outer_padding,
+                )
             else:
-                return self._calc_ideal_rect_for_non_period_event(event)
+                rect = self._calc_ideal_rect_for_non_period_event(event)
+                return EventRects(
+                    event=event,
+                    box_rect=rect,
+                    text_rect=rect.Clone(),
+                    height_padding=self._outer_padding,
+                )
 
     def _extend_container_width_for_point_event(self, subevent, subevent_rect):
         """Make point events be enclosed by the container rectangle."""

changeset:   8057:6c38d6c35d6e
user:        Rickard Lindberg <rickard@rickardlindberg.me>
date:        Sun Aug 24 10:47:57 2025 +0200
summary:     Fix text rect for point event in extended container

diff -r bd3d0f52b6e4 -r 6c38d6c35d6e source/timelinelib/canvas/drawing/scene.py
--- a/source/timelinelib/canvas/drawing/scene.py Sun Aug 24 10:35:30 2025 +0200
+++ b/source/timelinelib/canvas/drawing/scene.py Sun Aug 24 10:47:57 2025 +0200
@@ -240,6 +240,7 @@
                 event_rects = self._calc_ideal_rect_for_period_event(event)
                 rw = self._calc_width_for_non_period_event(event)
                 event_rects.box_rect.SetWidth(rw)
+                event_rects.text_rect.SetWidth(rw)
                 if not event.is_period():
                     self._extend_container_width_for_point_event(event_rects)
                 return event_rects

2025-08-24 08:04 Rickard pushed to timeline

changeset:   8048:cfe7be006c13
user:        Rickard Lindberg <rickard@rickardlindberg.me>
date:        Sun Aug 24 07:50:24 2025 +0200
summary:     Implement set_bounding_y in terms of move_y

diff -r 6ea1f3b8bad5 -r cfe7be006c13 source/timelinelib/canvas/drawing/scene.py
--- a/source/timelinelib/canvas/drawing/scene.py Sat Aug 23 20:17:15 2025 +0200
+++ b/source/timelinelib/canvas/drawing/scene.py Sun Aug 24 07:50:24 2025 +0200
@@ -572,9 +572,7 @@
         >>> event_rects.bounding_rect
         wx.Rect(0, 5, 10, 15)
         """
-        diff = self.bounding_rect.Y - y
-        self.box_rect.Y -= diff
-        self.text_rect.Y -= diff
+        self.move_y(y - self.bounding_rect.Y)
 
     def extend_bounding_height(self, height):
         self.box_rect.Height += height

changeset:   8050:18cc2c9d5ee2
user:        Rickard Lindberg <rickard@rickardlindberg.me>
date:        Sun Aug 24 07:59:31 2025 +0200
summary:     Fix misstake when checking is_allowed_to_overlap

diff -r c8fdd8e75b29 -r 18cc2c9d5ee2 source/timelinelib/canvas/drawing/scene.py
--- a/source/timelinelib/canvas/drawing/scene.py Sun Aug 24 07:58:07 2025 +0200
+++ b/source/timelinelib/canvas/drawing/scene.py Sun Aug 24 07:59:31 2025 +0200
@@ -498,7 +498,7 @@
                 and
                 event_rects.bounding_rect.Y < self.divider_y
                 and
-                not event_rects.bounding_rect.is_allowed_to_overlap()
+                not event_rects.box_rect.is_allowed_to_overlap()
             )
         ]
 

2025-08-23 20:43 Rickard pushed to timeline

changeset:   8020:00063458b083
user:        Rickard Lindberg <rickard@rickardlindberg.me>
date:        Sat Aug 23 15:07:15 2025 +0200
summary:     Introduce EventRects

diff -r 8d6eb11fd6ba -r 00063458b083 source/timelinelib/canvas/drawing/scene.py
--- a/source/timelinelib/canvas/drawing/scene.py Sun Aug 10 07:59:56 2025 +0200
+++ b/source/timelinelib/canvas/drawing/scene.py Sat Aug 23 15:07:15 2025 +0200
@@ -226,7 +226,7 @@
         for event in events:
             rect = self._create_ideal_rect_for_event(event)
             self._prevent_overlapping_by_adjusting_rect_y(event, rect)
-            self.event_data.append((event, rect))
+            self.event_data.append(EventRects(event=event, rect=rect))
         return self.event_data
 
     def _deflate_rects(self, event_data):
@@ -491,3 +491,42 @@
 
     def _get_container_data_for_subevent(self, subevent):
         return [(event, rect) for (event, rect) in self.event_data if event is subevent.container][0]
+
+
+class EventRects:
+
+    def __init__(self, event, rect):
+        self.event = event
+        self.rect = rect
+
+    def __iter__(self):
+        """
+        EventRects can unpack into an event and a rect for backwards
+        compatibility:
+
+        >>> event = object()
+        >>> rect = object()
+        >>> unpacked_event, unpacked_rect = EventRects(event, rect)
+        >>> unpacked_event is event
+        True
+        >>> unpacked_rect is rect
+        True
+        """
+        return iter(self.to_tuple())
+
+    def __getitem__(self, key):
+        """
+        EventRects can be indexed on its unpacked values:
+
+        >>> event = object()
+        >>> rect = object()
+        >>> event_rects = EventRects(event, rect)
+        >>> event_rects[0] is event
+        True
+        >>> event_rects[1] is rect
+        True
+        """
+        return self.to_tuple()[key]
+
+    def to_tuple(self):
+        return (self.event, self.rect)

changeset:   8047:6ea1f3b8bad5
user:        Rickard Lindberg <rickard@rickardlindberg.me>
date:        Sat Aug 23 20:17:15 2025 +0200
summary:     Draw text inside text rect instead of box rect

diff -r 2179d111f0cc -r 6ea1f3b8bad5 source/timelinelib/canvas/drawing/drawers/default.py
--- a/source/timelinelib/canvas/drawing/drawers/default.py Sat Aug 23 19:56:56 2025 +0200
+++ b/source/timelinelib/canvas/drawing/drawers/default.py Sat Aug 23 20:17:15 2025 +0200
@@ -495,21 +495,25 @@
         for event_rects in self.scene.event_data:
             self.dc.SetFont(self._event_text_font)
             if event_rects.event.is_container():
-                self._draw_container(event_rects.event, event_rects.box_rect, view_properties)
+                self._draw_container(event_rects, view_properties)
             else:
-                self._draw_box(event_rects.box_rect, event_rects.event, view_properties)
+                self._draw_box(event_rects, view_properties)
 
-    def _draw_container(self, event, rect, view_properties):
+    def _draw_container(self, event_rects, view_properties):
+        rect = event_rects.box_rect
         box_rect = rect.CloneInflate(2, 2)
         if EXTENDED_CONTAINER_HEIGHT.enabled():
             box_rect = EXTENDED_CONTAINER_HEIGHT.get_vertical_larger_box_rect(rect)
-        self._draw_box(box_rect, event, view_properties)
+        self._draw_box(
+            event_rects=event_rects.replace_box_rect(box_rect),
+            view_properties=view_properties,
+        )
 
-    def _draw_box(self, rect, event, view_properties):
-        self.dc.SetClippingRegion(rect)
-        self._event_box_drawer.draw(self.dc, self.scene, rect, event, view_properties)
-        if EXTENDED_CONTAINER_STRATEGY.enabled() and event.is_subevent() and not event.is_period():
-            self._draw_time_marker_for_point_event(rect)
+    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):
diff -r 2179d111f0cc -r 6ea1f3b8bad5 source/timelinelib/canvas/drawing/scene.py
--- a/source/timelinelib/canvas/drawing/scene.py Sat Aug 23 19:56:56 2025 +0200
+++ b/source/timelinelib/canvas/drawing/scene.py Sat Aug 23 20:17:15 2025 +0200
@@ -222,7 +222,7 @@
             event_rects = EventRects(
                 event=event,
                 box_rect=rect,
-                text_rect=rect,
+                text_rect=rect.Clone(),
                 height_padding=self._outer_padding,
             )
             self._prevent_overlapping_by_adjusting_rect_y(event_rects)
@@ -527,6 +527,14 @@
         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):
         """
@@ -549,8 +557,9 @@
 
     def set_bounding_y(self, y):
         """
-        >>> rect = Rect(0, 0, 10, 5)
-        >>> event_rects = EventRects(event=None, box_rect=rect, text_rect=rect, height_padding=5)
+        >>> box_rect = Rect(0, 0, 10, 5)
+        >>> text_rect = Rect(0, 0, 10, 5)
+        >>> event_rects = EventRects(event=None, box_rect=box_rect, text_rect=text_rect, height_padding=5)
 
         >>> event_rects.box_rect
         wx.Rect(0, 0, 10, 5)
@@ -565,12 +574,16 @@
         """
         diff = self.bounding_rect.Y - y
         self.box_rect.Y -= diff
+        self.text_rect.Y -= diff
 
     def extend_bounding_height(self, height):
         self.box_rect.Height += height
+        self.text_rect.Height += height
 
     def extend_width(self, widht):
         self.box_rect.Width += widht
+        self.text_rect.Width += widht
 
     def move_y(self, amount):
         self.box_rect.Y += amount
+        self.text_rect.Y += amount
diff -r 2179d111f0cc -r 6ea1f3b8bad5 source/timelinelib/canvas/eventboxdrawers/defaulteventboxdrawer.py
--- a/source/timelinelib/canvas/eventboxdrawers/defaulteventboxdrawer.py Sat Aug 23 19:56:56 2025 +0200
+++ b/source/timelinelib/canvas/eventboxdrawers/defaulteventboxdrawer.py Sat Aug 23 20:17:15 2025 +0200
@@ -39,7 +39,9 @@
 
 class DefaultEventBoxDrawer:
 
-    def draw(self, dc, scene, rect, event, view_properties):
+    def draw(self, dc, scene, event_rects, view_properties):
+        rect = event_rects.box_rect
+        event = event_rects.event
         self.scene = scene
         self.view_properties = view_properties
         selected = view_properties.is_selected(event)
@@ -49,7 +51,7 @@
         elif scene.never_show_period_events_as_point_events() 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, rect, event, selected)
+            self._draw_event_box(dc, event_rects, selected)
 
     def _draw_period_event_as_symbol_below_divider_line(self, dc, rect, scene, event):
         dc.DestroyClippingRegion()
@@ -61,12 +63,15 @@
         dc.DrawLine(x, y0, x, y1)
         dc.DrawCircle(x, y1, 2)
 
-    def _draw_event_box(self, dc, rect, event, selected):
+    def _draw_event_box(self, dc, event_rects, selected):
+        event = event_rects.event
+        rect = event_rects.box_rect
+        text_rect = event_rects.text_rect
         self._draw_background(dc, rect, event)
         self._draw_fuzzy_edges(dc, rect, event)
         self._draw_locked_edges(dc, rect, event)
         self._draw_progress_box(dc, rect, event)
-        self._draw_text(dc, rect, event)
+        self._draw_text(dc, text_rect, event)
         self._draw_contents_indicator(dc, event, rect)
         self._draw_locked_edges(dc, rect, event)
         self._draw_selection_handles(dc, event, rect, selected)

2025-08-10 08:01 Rickard pushed to timeline

changeset:   8019:8d6eb11fd6ba
user:        Rickard Lindberg <rickard@rickardlindberg.me>
date:        Sun Aug 10 07:59:56 2025 +0200
summary:     Fix formatting issue in changelog.

diff -r b66471810a7c -r 8d6eb11fd6ba documentation/changelog.rst
--- a/documentation/changelog.rst Sun Aug 10 07:53:05 2025 +0200
+++ b/documentation/changelog.rst Sun Aug 10 07:59:56 2025 +0200
@@ -11,9 +11,7 @@
 
 Fixed crash reports and bugs:
 
-* ``TypeError: DC.DrawCircle(): arguments did not match any overloaded call:
-overload 1: argument 1 has unexpected type 'float' overload 2: argument 1 has
-unexpected type 'float'``
+* ``TypeError: DC.DrawCircle(): arguments did not match any overloaded call: overload 1: argument 1 has unexpected type 'float' overload 2: argument 1 has unexpected type 'float'``
   This was a problem with the feature "Milestones can be drawn as circles"
   introduced in version 2.11.0. The ``DC.DrawCircle`` API expects integers, but
   we didn't ensure that.

2025-08-10 07:54 Rickard pushed to timeline

changeset:   8017:0d3bdb916bad
user:        Rickard Lindberg <rickard@rickardlindberg.me>
date:        Sun Aug 10 07:51:14 2025 +0200
summary:     Ensure arguments to DC.DrawCircle are integers.

diff -r e36e19c5df16 -r 0d3bdb916bad documentation/changelog.rst
--- a/documentation/changelog.rst Sat Aug 09 20:37:42 2025 +0200
+++ b/documentation/changelog.rst Sun Aug 10 07:51:14 2025 +0200
@@ -9,6 +9,15 @@
 
 * Beta versions: |betas|_
 
+Fixed crash reports and bugs:
+
+* ``TypeError: DC.DrawCircle(): arguments did not match any overloaded call:
+overload 1: argument 1 has unexpected type 'float' overload 2: argument 1 has
+unexpected type 'float'``
+  This was a problem with the feature "Milestones can be drawn as circles"
+  introduced in version 2.11.0. The ``DC.DrawCircle`` API expects integers, but
+  we didn't ensure that.
+
 Version 2.11.0
 --------------
 **Released on 9 August 2025.**
diff -r e36e19c5df16 -r 0d3bdb916bad source/timelinelib/canvas/eventboxdrawers/defaultmilestonedrawer.py
--- a/source/timelinelib/canvas/eventboxdrawers/defaultmilestonedrawer.py Sat Aug 09 20:37:42 2025 +0200
+++ b/source/timelinelib/canvas/eventboxdrawers/defaultmilestonedrawer.py Sun Aug 10 07:51:14 2025 +0200
@@ -52,7 +52,11 @@
         dc.DrawRectangle(self._rect)
 
     def _draw_circle_background(self, dc):
-        dc.DrawCircle(self._rect.X + self._rect.Width / 2, self._rect.Y + self._rect.Height / 2, self._rect.Width / 2)
+        dc.DrawCircle(
+            self._rect.X + self._rect.Width // 2,
+            self._rect.Y + self._rect.Height // 2,
+            self._rect.Width // 2
+        )
 
     def draw_label(self, dc):
         label = self._event.text[0] if self._event.text else " "

changeset:   8018:b66471810a7c
user:        Rickard Lindberg <rickard@rickardlindberg.me>
date:        Sun Aug 10 07:53:05 2025 +0200
summary:     Draw milestones at the correct x-coordinate.

diff -r 0d3bdb916bad -r b66471810a7c documentation/changelog.rst
--- a/documentation/changelog.rst Sun Aug 10 07:51:14 2025 +0200
+++ b/documentation/changelog.rst Sun Aug 10 07:53:05 2025 +0200
@@ -18,6 +18,9 @@
   introduced in version 2.11.0. The ``DC.DrawCircle`` API expects integers, but
   we didn't ensure that.
 
+* Milestones were drawn slightly off center because of an incorrect
+  calculation.
+
 Version 2.11.0
 --------------
 **Released on 9 August 2025.**
diff -r 0d3bdb916bad -r b66471810a7c source/timelinelib/canvas/drawing/scene.py
--- a/source/timelinelib/canvas/drawing/scene.py Sun Aug 10 07:51:14 2025 +0200
+++ b/source/timelinelib/canvas/drawing/scene.py Sun Aug 10 07:53:05 2025 +0200
@@ -314,7 +314,7 @@
         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) - rw // 2)
+            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)
 

2025-08-09 20:37 Rickard pushed to timeline

changeset:   8016:e36e19c5df16
user:        Rickard Lindberg <rickard@rickardlindberg.me>
date:        Sat Aug 09 20:37:42 2025 +0200
summary:     Add removed instruction to upload binaries to SF as well.

diff -r 87cd10564450 -r e36e19c5df16 documentation/release-instructions.rst
--- a/documentation/release-instructions.rst Sat Aug 09 20:29:19 2025 +0200
+++ b/documentation/release-instructions.rst Sat Aug 09 20:37:42 2025 +0200
@@ -63,3 +63,9 @@
 1. Send email to thetimelineproj-user@lists.sourceforge.net
 2. Post news to SF (http://sourceforge.net/p/thetimelineproj/news/?source=navbar)
    (login required)
+
+Upload binaries to SF as well
+-----------------------------
+
+Ensure that the exe file has "Default Download For" Windows checkbox checked
+and ensure that the the zip file has all the others checked.