v0.29.0 21b3150

native_popup_window.go RawHistoryFolder

1package ui
2
3import (
4	"context"
5	"fmt"
6	"sync"
7
8	"github.com/bnema/dumber/internal/application/port"
9	"github.com/bnema/dumber/internal/domain/entity"
10	"github.com/bnema/dumber/internal/logging"
11	"github.com/bnema/dumber/internal/ui/coordinator/content"
12	"github.com/bnema/dumber/internal/ui/layout"
13	"github.com/bnema/dumber/internal/ui/window"
14	"github.com/bnema/puregotk/v4/gtk"
15)
16
17type nativePopupWindow struct {
18	popupID        port.WebViewID
19	parentPaneID   entity.PaneID
20	parentWindowID string
21	popupWindow    *window.PopupWindow
22	webView        port.WebView
23	closeOnce      sync.Once
24}
25
26type nativePopupDestroyer interface {
27	Destroy()
28}
29
30var newPopupWindow = window.NewPopup
31
32func destroyFailedNativePopupSetup(popupShell nativePopupDestroyer, wv port.WebView) {
33	if wv != nil && !wv.IsDestroyed() {
34		wv.Destroy()
35	}
36	if popupShell != nil {
37		popupShell.Destroy()
38	}
39}
40
41func prepareNativePopupContentWidget(widget layout.Widget) (*gtk.Widget, error) {
42	if widget == nil {
43		return nil, fmt.Errorf("failed to wrap native popup webview widget")
44	}
45	gtkWidget := widget.GtkWidget()
46	if gtkWidget == nil {
47		return nil, fmt.Errorf("failed to wrap native popup webview widget")
48	}
49	widget.SetHexpand(true)
50	widget.SetVexpand(true)
51	return gtkWidget, nil
52}
53
54func (a *App) ensureNativePopupWindows() {
55	if a.nativePopupWindows == nil {
56		a.nativePopupWindows = make(map[port.WebViewID]*nativePopupWindow)
57	}
58}
59
60func (a *App) openNativePopupWindow(ctx context.Context, input content.NativePopupInput) error {
61	if a == nil || a.gtkApp == nil {
62		return fmt.Errorf("gtk application not available for native popup")
63	}
64	if input.PopupWebView == nil {
65		return fmt.Errorf("native popup webview is nil")
66	}
67
68	popupShell, err := newPopupWindow(ctx, a.gtkApp)
69	if err != nil {
70		destroyFailedNativePopupSetup(nil, input.PopupWebView)
71		return err
72	}
73	if a.contentCoord == nil {
74		destroyFailedNativePopupSetup(popupShell, input.PopupWebView)
75		return fmt.Errorf("content coordinator not available for native popup")
76	}
77	widget := a.contentCoord.WrapWidget(ctx, input.PopupWebView)
78	gtkWidget, err := prepareNativePopupContentWidget(widget)
79	if err != nil {
80		destroyFailedNativePopupSetup(popupShell, input.PopupWebView)
81		return err
82	}
83	popupShell.SetContent(gtkWidget)
84
85	parentWindowID := ""
86	if bw := a.browserWindowForAnyPane(input.ParentPaneID); bw != nil {
87		parentWindowID = bw.id
88	}
89
90	popupID := input.PopupWebView.ID()
91	state := &nativePopupWindow{
92		popupID:        popupID,
93		parentPaneID:   input.ParentPaneID,
94		parentWindowID: parentWindowID,
95		popupWindow:    popupShell,
96		webView:        input.PopupWebView,
97	}
98	popupShell.SetTitle("Dumber")
99
100	a.ensureNativePopupWindows()
101	a.nativePopupWindows[popupID] = state
102
103	if popupShell.Window() != nil {
104		closeRequestCb := func(_ gtk.Window) bool {
105			a.releaseNativePopupWindow(popupID, false, false)
106			return false
107		}
108		popupShell.Window().ConnectCloseRequest(&closeRequestCb)
109	}
110
111	if aborter, ok := input.PopupWebView.(port.NativePopupHostAbortCapable); ok {
112		aborter.SetNativePopupHostAbort(func() {
113			a.dispatchOnMainThread(func() {
114				a.releaseNativePopupWindow(popupID, true, false)
115			})
116		})
117	}
118	if lifecycle, ok := input.PopupWebView.(port.PopupLifecycleCapable); ok {
119		lifecycle.SetOnReadyToShow(func() {
120			a.dispatchOnMainThread(func() {
121				a.showNativePopupWindow(popupID)
122			})
123		})
124		lifecycle.SetOnClose(func() {
125			a.dispatchOnMainThread(func() {
126				a.releaseNativePopupWindow(popupID, true, false)
127			})
128		})
129	} else {
130		popupShell.Show()
131	}
132	if oauthWV, ok := input.PopupWebView.(port.OAuthCallbackCapable); ok {
133		oauthWV.AddCloseCallback(func() {
134			a.dispatchOnMainThread(func() {
135				a.releaseNativePopupWindow(popupID, true, false)
136			})
137		})
138	}
139	if a.contentCoord != nil && input.ObserveOAuthAutoClose {
140		a.contentCoord.ObserveNativePopupAuth(ctx, input)
141	}
142
143	logging.FromContext(ctx).Info().
144		Uint64("popup_id", uint64(popupID)).
145		Str("target_uri", input.TargetURI).
146		Str("parent_pane_id", string(input.ParentPaneID)).
147		Msg("native popup host created")
148	return nil
149}
150
151func (a *App) showNativePopupWindow(popupID port.WebViewID) {
152	if a == nil || a.nativePopupWindows == nil {
153		return
154	}
155	if state := a.nativePopupWindows[popupID]; state != nil && state.popupWindow != nil {
156		state.popupWindow.Show()
157	}
158}
159
160func (a *App) releaseNativePopupWindow(popupID port.WebViewID, closeWindow, destroyWindow bool) {
161	if a == nil || a.nativePopupWindows == nil {
162		return
163	}
164	state := a.nativePopupWindows[popupID]
165	if state == nil {
166		return
167	}
168	state.closeOnce.Do(func() {
169		delete(a.nativePopupWindows, popupID)
170		if state.webView != nil && !state.webView.IsDestroyed() {
171			state.webView.Destroy()
172		}
173		if state.popupWindow != nil {
174			switch {
175			case destroyWindow:
176				state.popupWindow.Destroy()
177			case closeWindow:
178				state.popupWindow.Close()
179			}
180		}
181	})
182}

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 8

Tags 42

↑↓ moveenter openesc close