Skip to content

Migrate from GTK3 to GTK4#786

Open
rabin-io wants to merge 33 commits into
projecthamster:masterfrom
rabin-io:gtk4-migration
Open

Migrate from GTK3 to GTK4#786
rabin-io wants to merge 33 commits into
projecthamster:masterfrom
rabin-io:gtk4-migration

Conversation

@rabin-io

Copy link
Copy Markdown

Summary

Complete migration of Hamster Time Tracker from GTK3 to GTK4, covering all UI files, Python code, and the graphics/animation framework.

  • 27 files changed — 1,072 insertions, 2,048 deletions (net reduction from removing deprecated patterns and verbose UI XML)
  • All 40 existing tests pass, plus 4 new GTK4 smoke tests added
  • Tested on Fedora 44 with GTK 4.22.4 / Python 3.14 / Wayland

What's included

  • UI files converted via gtk4-builder-tool + extensive manual cleanup
  • Graphics/Scene framework ported from GTK3 signals to GTK4 EventControllers (GestureClick, EventControllerKey, EventControllerMotion, EventControllerScroll)
  • Window-level keyboard shortcuts moved from EventControllerKey to ShortcutController (prevents intercepting editable widget input)
  • Calendar, FileChooser, style/color APIs updated to GTK4 equivalents
  • Container.add() replaced with widget-specific methods (append/set_child)
  • Popup/Popover focus fights resolved (show on icon click, not focus-in)
  • TreeView inline cell editing replaced with dialog-based editing (workaround for GTK 4.22 gtk_css_node_insert_after assertion bug)
  • can_focus=False and empty internal-child="accessible" ATK blocks removed from all UI files (breaks descendant focus in GTK4)
  • Dark theme support: DrawingArea text color sourced from root window style context
  • Deprecated utcfromtimestamp replaced with timezone-aware equivalent

Key GTK3→GTK4 behavioral differences discovered

  1. can_focus=False on a container prevents grab_focus() on ALL descendants — unlike GTK3 where it only affected the container itself
  2. EventControllerKey on a window intercepts ALL keystrokes — even in BUBBLE phase, preventing TextViews from receiving input
  3. DrawingArea doesn't inherit theme foreground colorget_color() returns black regardless of theme; must use root window's style context
  4. TreeView inline editing is broken in GTK 4.22set_cursor(start_editing=True) triggers assertion failure

Not included (deferred)

  • DnD migration (Gtk.DragSource/DropTarget) — commented out with TODO
  • ListView/ColumnView modernization (TreeView still works)
  • Dark mode CSS provider (add_provider_for_screenadd_provider_for_display)
  • Fixing the one pre-existing test_round_trip test failure (missing dt.timezone export, unrelated to GTK4)

Test plan

  • All 40 original tests pass + 4 new GTK4 smoke tests
  • Start/stop/edit activities via overview window
  • Date range navigation (day/week/month/manual)
  • Preferences: add/edit/remove categories and activities
  • Preferences: type in tags text view
  • Time input via icon-click dropdown
  • Export dialog (async FileDialog API)
  • Keyboard shortcuts (Ctrl+F, Ctrl+N, Ctrl+Space, Escape, arrows)
  • Dark theme: totals text readable without hover
  • Wayland session (Fedora 44)

🤖 Generated with Claude Code

rabin-io and others added 30 commits May 17, 2026 12:02
Migrate from GTK 3.0 to GTK 4.0 version requirements and update
all trivial API changes: Container.add→append/set_child,
pack_start→append, show_all removal, delete-event→close-request,
connect_signals→manual connections, STYLE_CLASS constants→strings,
get_toplevel→get_root, removed widgets (Arrow, HBox, IconSize),
set_shadow_type removal, and HeaderBar/Button API updates.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Run gtk4-builder-tool simplify --3to4 on date_range.ui,
edit_activity.ui, and preferences.ui. Then strip remaining
deprecated elements: GtkAlignment→GtkBox, GtkButtonBox→GtkBox,
remove shadow_type/border_width/padding/relief/use_stock/events
properties, remove all <signal> elements (connected manually
in Python), and remove <packing> blocks.

Delete unused stats.ui (no Python code references it, targets
GTK+ 2.16).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Menu system: Gtk.Menu/MenuItem → Gio.Menu + PopoverMenu with
  window-scoped SimpleActions
- Dialogs: ReportChooserDialog rewritten to use Gtk.FileDialog
  async API; MessageDialog uses named params; AboutDialog updated
- Styling: add_hint() replaced with set_placeholder_text(),
  override_background_color → CSS provider, get_background_color →
  lookup_color("theme_bg_color"), get_style().font_desc → default
- Cursors: CursorType enums → Cursor.new_from_name() strings
- Colors: Gdk.Color removed, RGBA-only in ColorUtils

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add SceneEvent dataclass shim to preserve event-passing interface
  for Scene/Sprite consumers without rewriting 40+ handler sites
- Replace do_draw() override with set_draw_func() callback
- Replace do_configure_event with draw_func width/height params
- Replace set_events() + GTK3 event signals with GTK4 controllers:
  EventControllerMotion, GestureClick, EventControllerScroll,
  EventControllerKey
- Remove GDK window tracking (_window/get_window/get_pointer);
  store mouse coords from motion controller instead
- Replace IconTheme.get_default() with get_for_display()
- Update Icon.load_icon to use lookup_icon() + file path
- Fix Totals widget: enter/leave-notify → Scene on-mouse-over/out,
  style-updated → notify::css-classes
- Fix Overview.on_key_press for EventControllerKey signature
- Fix layout.py pointer query to use scene.mouse_x/y

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- CmdLineEntry, TimeInput, TagsEntry: Replace Gtk.Window(POPUP)
  with Gtk.Popover, remove manual move()/resize() positioning
- Replace key-press-event/focus-out-event/focus-in-event with
  EventControllerKey/EventControllerFocus on all Entry subclasses
- Replace button-press-event on TreeView with GestureClick
- Remove _parent_click_watcher pattern (Popover auto-dismisses)
- Remove EntryCompletion from ActivityEntry and CategoryEntry
  (removed in GTK4; CmdLineEntry popup pattern is the replacement)
- Update icon-press signal handlers for GTK4 signature (no event)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Remove GTK3 DnD (enable_model_drag_source/dest, TargetFlags,
  drag_data_get/received) — marked TODO for GTK4 DragSource/DropTarget
- Replace button-press/release-event on tree lists with GestureClick
- Replace key-press-event on tree lists with EventControllerKey
- Replace focus-out-event on tags textarea with EventControllerFocus
- Add EventControllerKey for preferences window (Ctrl+W, Escape)
- Update all handler signatures for GTK4 controller params

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Update Flatpak runtime to GNOME 47 (GTK4)
- Update README: gir1.2-gtk-3.0 → gir1.2-gtk-4.0, Gtk3 → Gtk4
- Fix po/wscript shebang: python2 → python3
- Remove dead _test_label (GTK4 Label doesn't accept positional args)
- Remove GtkEventBox from edit_activity.ui (replaced with GtkBox)
- Remove AtkObject accessibility blocks from all UI files
- Add tests/test_gtk4_smoke.py: verifies Scene, SceneEvent, and
  UI file loading work under GTK4
- All 45 tests pass (41 existing + 4 new)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Calendar: day-selected-double-click → day-selected, select_month
  → select_day(GLib.DateTime), get_date tuple → GLib.DateTime
- facttree: has_toplevel_focus() → is_active()
- Entry subclasses: remove parent=parent from __init__ (not writable
  in GTK4), append to parent container explicitly instead
- tags.py Tag: gtk.Style() → pango.FontDescription(graphics._font_desc)
- edit_activity: focus_in/out_event → EventControllerFocus
- dayline: get_color(StateFlags.NORMAL) → get_color()
- edit_activity: day_preview.set_child → .append (GtkBox not single-child)
- preferences: remove dead DnD methods using removed GTK3 APIs
- overview: register menu actions on app (not window, which lacks
  add_action)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- DayLine: guard against width=0 in on_enter_frame (first frame
  before layout)
- Add margin-start/end/top/bottom to main containers in all UI
  files to replace the border_width/padding that was stripped
  from GtkAlignment wrappers during conversion
- Remove leftover GtkEntryCompletion objects from edit_activity.ui
- Remove dead DnD methods from preferences.py (used removed APIs)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The GtkEntryCompletion objects were removed but two entries still
referenced them via <property name="completion">.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- TimeInput: remove GestureClick on Entry (interfered with text
  input), add secondary dropdown icon instead, fix icon-release
  signature (no event param in GTK4)
- Calendar UI: remove invalid year/month/day/resize_toplevel
  properties (GTK4 Calendar uses GDateTime)
- DayLine: guard against zero width on first frame

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Switch from icon-release to icon-press signal
- Remove popup-on-focus-in (caused focus fight with Popover
  stealing focus → immediate focus-out → hide loop)
- Popup now only shows via dropdown icon click

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Use raw strings for regex patterns with \D and \s
- Set key controller to CAPTURE phase so window-level shortcuts
  (Ctrl+Space, Ctrl+N, arrows) work even when a child has focus

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
datetime.utcfromtimestamp() is deprecated since Python 3.12.
Replace with fromtimestamp(ts, tz=timezone.utc).replace(tzinfo=None)
to get the same naive-UTC datetime the codebase expects.
For .date() calls, use fromtimestamp(ts, tz=timezone.utc).date().

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
GTK4 Calendar.get_date() returns GLib.DateTime, not a (y,m,d) tuple.
Use get_year()/get_month()/get_day_of_month() methods. Month is
already 1-based in GLib.DateTime (unlike GTK3's 0-based).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Defer set_cursor(start_editing=True) to idle callback so the
GestureClick gesture finishes processing before TreeView tries
to enter edit mode. Use set_cursor instead of set_cursor_on_cell
which is more reliable in GTK4.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
All set_cursor_on_cell calls replaced with set_cursor + grab_focus
deferred via GLib.idle_add. GTK4 TreeView requires the gesture to
finish processing before editing can start. Applied consistently
to: add button, edit button, double-click-to-edit, and F2 key
handlers for both category and activity lists.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Remove per-tree EventControllerKey that was intercepting keystrokes
meant for the CellEditable entry widget. Move Delete/F2 handling
to the window-level key controller, which skips handling when a
cell is in edit mode (editable=True).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
GTK 4.22 has a bug where TreeView set_cursor(start_editing=True)
triggers a css_node_insert_after assertion failure and never
creates the CellEditable widget. Work around this by using a
simple modal dialog with an Entry for all add/edit operations
on categories and activities. Double-click and F2 also open
the dialog instead of trying inline editing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The activity and category ScrolledWindows and their parent boxes
needed hexpand=1 to fill the available space in the GtkPaned.
Without it, GTK4 gave them minimum width, truncating text.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
get_allocation().width can be very small before full layout in
GTK4, causing tags to wrap at tiny widths and show truncated.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Setting Tag.width=0 before creating the Label caused
Label._bounds_width=0, making Pango wrap text at 0 pixels wide
(one character per line). Remove the premature width/height=0
initialization — the correct size is set by __setattr__ when
self.text is assigned.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The autocomplete tags text view and its parent containers needed
hexpand to fill the available width. Without it, text wrapped at
a few pixels wide showing one character per line.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
EventControllerKey on the window intercepts ALL keystrokes in
GTK4, preventing TextViews and other editable widgets from
receiving input. Replace with ShortcutController that only
handles specific key combinations (Ctrl+W, Escape, Delete, F2)
and lets all other keys pass through to focused widgets.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Avoid window-wide Delete shortcut intercepting text editing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Even ShortcutController on the window prevents the tags TextView
from receiving keyboard input in GTK4. Remove all keyboard
interception — the window can be closed via the title bar button.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
rabin-io and others added 3 commits May 17, 2026 15:11
The CAPTURE-phase EventControllerKey on the overview window was
intercepting ALL keystrokes application-wide, preventing typing
in the preferences text view and other windows.

Replace with:
- ShortcutController for Ctrl+shortcuts and Escape (only triggers
  on specific key combos, doesn't intercept normal typing)
- BUBBLE-phase EventControllerKey for arrow/nav key forwarding
  to the fact tree (only when overview is active and filter entry
  doesn't have focus)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
In GTK4, can_focus=False on a parent container prevents grab_focus()
from succeeding on ALL descendant widgets, unlike GTK3 where it only
affected the container itself. This was the root cause of the tags
text view not accepting keyboard input in preferences.

Also removes empty <child internal-child="accessible"> ATK blocks
that gtk4-builder-tool failed to clean up during migration.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
DrawingArea does not inherit the theme foreground color, so
get_color() returns black regardless of theme. Use the root
window's style context instead, and update colors on map to
ensure the correct color is applied after the widget is in the tree.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants