detached a5cfffc

fix(workspace): prevent GTK focus chain warnings during pane close by clearing grandparent focus before reparenting

commit
a5cfffcc32bf7c879d347871bb185a8899511fb8
author
Brice · · 2025-10-04 19:31 UTC
committer
Brice · · 2025-10-05 13:54 UTC
parent
a06c390
signature
unsigned
3 files · +69 -1
M internal/app/browser/workspace_pane_ops.go +33 -1
@@ -910,7 +910,7 @@ func (wm *WorkspaceManager) swapContainers(grand *paneNode, sibling *paneNode) {
910910
911911 if webkit.WidgetIsValid(grand.container) && webkit.WidgetIsValid(sibling.container) {
912912 // GTK4 PanedSetStartChild/EndChild auto-unparent from current parent
913- // Log current parent for debugging, but this is normal GTK4 behavior
913+ // Focus was already cleared before unparenting closed pane
914914 if parent := webkit.WidgetGetParent(sibling.container); parent != 0 && parent != grand.container {
915915 log.Printf("[workspace] widget %#x reparenting from %#x to %#x (GTK4 will auto-unparent)", sibling.container, parent, grand.container)
916916 }
@@ -999,6 +999,17 @@ func (wm *WorkspaceManager) cascadePromotion(singleChildPaned *paneNode) {
999999 webkit.WidgetSetVExpand(onlyChild.container, true)
10001000 webkit.WidgetQueueAllocate(onlyChild.container)
10011001 webkit.WidgetQueueAllocate(greatGrandparent.container)
1002+ wm.scheduleIdleGuarded(func() bool {
1003+ if onlyChild == nil || !onlyChild.widgetValid {
1004+ return false
1005+ }
1006+ if onlyChild.container != 0 && webkit.WidgetIsValid(onlyChild.container) {
1007+ webkit.WidgetShow(onlyChild.container)
1008+ webkit.WidgetQueueResize(onlyChild.container)
1009+ webkit.WidgetQueueDraw(onlyChild.container)
1010+ }
1011+ return false
1012+ }, onlyChild)
10021013 }
10031014
10041015 // Cleanup the now-orphaned paned
@@ -1160,6 +1171,12 @@ func (wm *WorkspaceManager) closePane(node *paneNode) (*paneNode, error) {
11601171 sibling := wm.getSibling(node)
11611172 grandparent := parent.parent
11621173
1174+ // Clear grandparent's focus child BEFORE unparenting closed pane
1175+ // GTK propagates focus events upward, so we must clear grandparent focus first
1176+ if grandparent != nil && grandparent.container != 0 && webkit.WidgetIsValid(grandparent.container) {
1177+ webkit.WidgetSetFocusChild(grandparent.container, 0)
1178+ }
1179+
11631180 // Unparent the closed pane's container from the paned BEFORE promoting sibling
11641181 if node.container != 0 && webkit.WidgetIsValid(node.container) {
11651182 if containerParent := webkit.WidgetGetParent(node.container); containerParent == parent.container {
@@ -1179,6 +1196,21 @@ func (wm *WorkspaceManager) closePane(node *paneNode) (*paneNode, error) {
11791196 // PanedSetStartChild/EndChild and window.SetChild auto-unparent widgets
11801197 wm.swapContainers(grandparent, sibling)
11811198
1199+ // Reconnect the promoted sibling's rendering pipeline after GTK reparenting.
1200+ if sibling != nil && sibling.container != 0 {
1201+ wm.scheduleIdleGuarded(func() bool {
1202+ if sibling == nil || !sibling.widgetValid {
1203+ return false
1204+ }
1205+ if sibling.container != 0 && webkit.WidgetIsValid(sibling.container) {
1206+ webkit.WidgetShow(sibling.container)
1207+ webkit.WidgetQueueResize(sibling.container)
1208+ webkit.WidgetQueueDraw(sibling.container)
1209+ }
1210+ return false
1211+ }, sibling)
1212+ }
1213+
11821214 // STEP 7: Check if grandparent now has only one child (cascade promotion needed)
11831215 if grandparent != nil && ((grandparent.left == nil) != (grandparent.right == nil)) {
11841216 // Grandparent has exactly one child - it should be promoted too
@@ -910,7 +910,7 @@ func (wm *WorkspaceManager) swapContainers(grand *paneNode, sibling *paneNode) {
910 910
911 if webkit.WidgetIsValid(grand.container) && webkit.WidgetIsValid(sibling.container) {911 if webkit.WidgetIsValid(grand.container) && webkit.WidgetIsValid(sibling.container) {
912 // GTK4 PanedSetStartChild/EndChild auto-unparent from current parent912 // GTK4 PanedSetStartChild/EndChild auto-unparent from current parent
913- // Log current parent for debugging, but this is normal GTK4 behavior913+ // Focus was already cleared before unparenting closed pane
914 if parent := webkit.WidgetGetParent(sibling.container); parent != 0 && parent != grand.container {914 if parent := webkit.WidgetGetParent(sibling.container); parent != 0 && parent != grand.container {
915 log.Printf("[workspace] widget %#x reparenting from %#x to %#x (GTK4 will auto-unparent)", sibling.container, parent, grand.container)915 log.Printf("[workspace] widget %#x reparenting from %#x to %#x (GTK4 will auto-unparent)", sibling.container, parent, grand.container)
916 }916 }
@@ -999,6 +999,17 @@ func (wm *WorkspaceManager) cascadePromotion(singleChildPaned *paneNode) {
999 webkit.WidgetSetVExpand(onlyChild.container, true)999 webkit.WidgetSetVExpand(onlyChild.container, true)
1000 webkit.WidgetQueueAllocate(onlyChild.container)1000 webkit.WidgetQueueAllocate(onlyChild.container)
1001 webkit.WidgetQueueAllocate(greatGrandparent.container)1001 webkit.WidgetQueueAllocate(greatGrandparent.container)
1002+ wm.scheduleIdleGuarded(func() bool {
1003+ if onlyChild == nil || !onlyChild.widgetValid {
1004+ return false
1005+ }
1006+ if onlyChild.container != 0 && webkit.WidgetIsValid(onlyChild.container) {
1007+ webkit.WidgetShow(onlyChild.container)
1008+ webkit.WidgetQueueResize(onlyChild.container)
1009+ webkit.WidgetQueueDraw(onlyChild.container)
1010+ }
1011+ return false
1012+ }, onlyChild)
1002 }1013 }
1003 1014
1004 // Cleanup the now-orphaned paned1015 // Cleanup the now-orphaned paned
@@ -1160,6 +1171,12 @@ func (wm *WorkspaceManager) closePane(node *paneNode) (*paneNode, error) {
1160 sibling := wm.getSibling(node)1171 sibling := wm.getSibling(node)
1161 grandparent := parent.parent1172 grandparent := parent.parent
1162 1173
1174+ // Clear grandparent's focus child BEFORE unparenting closed pane
1175+ // GTK propagates focus events upward, so we must clear grandparent focus first
1176+ if grandparent != nil && grandparent.container != 0 && webkit.WidgetIsValid(grandparent.container) {
1177+ webkit.WidgetSetFocusChild(grandparent.container, 0)
1178+ }
1179+
1163 // Unparent the closed pane's container from the paned BEFORE promoting sibling1180 // Unparent the closed pane's container from the paned BEFORE promoting sibling
1164 if node.container != 0 && webkit.WidgetIsValid(node.container) {1181 if node.container != 0 && webkit.WidgetIsValid(node.container) {
1165 if containerParent := webkit.WidgetGetParent(node.container); containerParent == parent.container {1182 if containerParent := webkit.WidgetGetParent(node.container); containerParent == parent.container {
@@ -1179,6 +1196,21 @@ func (wm *WorkspaceManager) closePane(node *paneNode) (*paneNode, error) {
1179 // PanedSetStartChild/EndChild and window.SetChild auto-unparent widgets1196 // PanedSetStartChild/EndChild and window.SetChild auto-unparent widgets
1180 wm.swapContainers(grandparent, sibling)1197 wm.swapContainers(grandparent, sibling)
1181 1198
1199+ // Reconnect the promoted sibling's rendering pipeline after GTK reparenting.
1200+ if sibling != nil && sibling.container != 0 {
1201+ wm.scheduleIdleGuarded(func() bool {
1202+ if sibling == nil || !sibling.widgetValid {
1203+ return false
1204+ }
1205+ if sibling.container != 0 && webkit.WidgetIsValid(sibling.container) {
1206+ webkit.WidgetShow(sibling.container)
1207+ webkit.WidgetQueueResize(sibling.container)
1208+ webkit.WidgetQueueDraw(sibling.container)
1209+ }
1210+ return false
1211+ }, sibling)
1212+ }
1213+
1182 // STEP 7: Check if grandparent now has only one child (cascade promotion needed)1214 // STEP 7: Check if grandparent now has only one child (cascade promotion needed)
1183 if grandparent != nil && ((grandparent.left == nil) != (grandparent.right == nil)) {1215 if grandparent != nil && ((grandparent.left == nil) != (grandparent.right == nil)) {
1184 // Grandparent has exactly one child - it should be promoted too1216 // Grandparent has exactly one child - it should be promoted too
M pkg/webkit/workspace_widgets_cgo.go +18 -0
@@ -709,6 +709,24 @@ func WidgetGrabFocus(widget uintptr) {
709709 C.gtk_widget_grab_focus((*C.GtkWidget)(unsafe.Pointer(widget)))
710710 }
711711
712+// WidgetSetFocusChild sets or clears the focus child of a widget.
713+// Pass 0 as child to clear the focus child (prevents GTK focus chain warnings during reparenting).
714+func WidgetSetFocusChild(widget uintptr, child uintptr) {
715+ if widget == 0 {
716+ return
717+ }
718+ if !widgetIsValid(widget) {
719+ log.Printf("[workspace] WidgetSetFocusChild skipped invalid widget=%#x", widget)
720+ return
721+ }
722+ var childPtr *C.GtkWidget
723+ if child != 0 {
724+ childPtr = (*C.GtkWidget)(unsafe.Pointer(child))
725+ }
726+ C.gtk_widget_set_focus_child((*C.GtkWidget)(unsafe.Pointer(widget)), childPtr)
727+ log.Printf("[workspace] WidgetSetFocusChild widget=%#x child=%#x", widget, child)
728+}
729+
712730 // WidgetRef increments the reference count to keep the widget alive during reparenting.
713731 func WidgetRef(widget uintptr) bool {
714732 if widget == 0 {
@@ -709,6 +709,24 @@ func WidgetGrabFocus(widget uintptr) {
709 C.gtk_widget_grab_focus((*C.GtkWidget)(unsafe.Pointer(widget)))709 C.gtk_widget_grab_focus((*C.GtkWidget)(unsafe.Pointer(widget)))
710 }710 }
711 711
712+// WidgetSetFocusChild sets or clears the focus child of a widget.
713+// Pass 0 as child to clear the focus child (prevents GTK focus chain warnings during reparenting).
714+func WidgetSetFocusChild(widget uintptr, child uintptr) {
715+ if widget == 0 {
716+ return
717+ }
718+ if !widgetIsValid(widget) {
719+ log.Printf("[workspace] WidgetSetFocusChild skipped invalid widget=%#x", widget)
720+ return
721+ }
722+ var childPtr *C.GtkWidget
723+ if child != 0 {
724+ childPtr = (*C.GtkWidget)(unsafe.Pointer(child))
725+ }
726+ C.gtk_widget_set_focus_child((*C.GtkWidget)(unsafe.Pointer(widget)), childPtr)
727+ log.Printf("[workspace] WidgetSetFocusChild widget=%#x child=%#x", widget, child)
728+}
729+
712 // WidgetRef increments the reference count to keep the widget alive during reparenting.730 // WidgetRef increments the reference count to keep the widget alive during reparenting.
713 func WidgetRef(widget uintptr) bool {731 func WidgetRef(widget uintptr) bool {
714 if widget == 0 {732 if widget == 0 {
M pkg/webkit/workspace_widgets_stub.go +18 -0
@@ -279,6 +279,24 @@ func WidgetGrabFocus(widget uintptr) {
279279 }
280280 }
281281
282+// WidgetSetFocusChild sets or clears the focus child of a widget.
283+// Pass 0 as child to clear the focus child (prevents GTK focus chain warnings during reparenting).
284+func WidgetSetFocusChild(widget uintptr, child uintptr) {
285+ widgetMu.Lock()
286+ defer widgetMu.Unlock()
287+
288+ if widget == 0 {
289+ return
290+ }
291+
292+ stub, ok := widgetState[widget]
293+ if !ok || stub.isDestroyed {
294+ return
295+ }
296+
297+ stub.focusedChild = child
298+}
299+
282300 func WidgetRef(widget uintptr) bool {
283301 widgetMu.Lock()
284302 defer widgetMu.Unlock()
@@ -279,6 +279,24 @@ func WidgetGrabFocus(widget uintptr) {
279 }279 }
280 }280 }
281 281
282+// WidgetSetFocusChild sets or clears the focus child of a widget.
283+// Pass 0 as child to clear the focus child (prevents GTK focus chain warnings during reparenting).
284+func WidgetSetFocusChild(widget uintptr, child uintptr) {
285+ widgetMu.Lock()
286+ defer widgetMu.Unlock()
287+
288+ if widget == 0 {
289+ return
290+ }
291+
292+ stub, ok := widgetState[widget]
293+ if !ok || stub.isDestroyed {
294+ return
295+ }
296+
297+ stub.focusedChild = child
298+}
299+
282 func WidgetRef(widget uintptr) bool {300 func WidgetRef(widget uintptr) bool {
283 widgetMu.Lock()301 widgetMu.Lock()
284 defer widgetMu.Unlock()302 defer widgetMu.Unlock()

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 9

Tags 42

↑↓ moveenter openesc close