renderer.go RawHistoryFolder

1package gtkmenu
2
3import (
4	"github.com/bnema/dumber/internal/application/port"
5	"github.com/bnema/puregotk/v4/gdk"
6	"github.com/bnema/puregotk/v4/gobject"
7	"github.com/bnema/puregotk/v4/gtk"
8)
9
10type renderItem struct {
11	item      port.MenuItem
12	separator bool
13}
14
15const (
16	contextMenuBoxClass       = "context-menu"
17	contextMenuPopoverClass   = "context-menu-popover"
18	contextMenuItemClass      = "context-menu-item"
19	contextMenuSeparatorClass = "context-menu-separator"
20)
21
22// Renderer shows GTK context menus on the provided anchor widget.
23type Renderer struct {
24	dispatch func(func())
25}
26
27// NewRenderer creates a new Renderer.
28func NewRenderer(dispatch func(func())) *Renderer {
29	return &Renderer{dispatch: dispatch}
30}
31
32func BuildButtons(items []port.MenuItem) []renderItem {
33	renderItems := make([]renderItem, 0, len(items))
34	for _, item := range items {
35		if isSeparator(item) {
36			renderItems = append(renderItems, renderItem{separator: true})
37			continue
38		}
39		renderItems = append(renderItems, renderItem{item: item})
40	}
41	return renderItems
42}
43
44func isSeparator(item port.MenuItem) bool {
45	return item.Action == "" && item.Label == ""
46}
47
48// Show renders a popover menu on the GTK thread.
49func (r *Renderer) Show(
50	items []port.MenuItem,
51	anchor *gtk.Widget,
52	x, y int32,
53	onSelect func(port.MenuItem),
54	onClose func(),
55) {
56	if r == nil {
57		return
58	}
59	run := func() {
60		showMenu(anchor, BuildButtons(items), x, y, onSelect, onClose)
61	}
62	if r.dispatch == nil {
63		run()
64		return
65	}
66	r.dispatch(run)
67}
68
69func showMenu(
70	anchor *gtk.Widget,
71	items []renderItem,
72	x, y int32,
73	onSelect func(port.MenuItem),
74	onClose func(),
75) {
76	if anchor == nil {
77		if onClose != nil {
78			onClose()
79		}
80		return
81	}
82	if !hasRenderableItems(items) {
83		if onClose != nil {
84			onClose()
85		}
86		return
87	}
88
89	box := gtk.NewBox(gtk.OrientationVerticalValue, 0)
90	box.AddCssClass(contextMenuBoxClass)
91
92	selected := false
93	var popover *gtk.Popover
94	parent := ChoosePopoverParent(anchor, anchor.GetParent())
95	popoverHost, overlay := createPopoverHost(parent, x, y)
96
97	for _, item := range items {
98		if item.separator {
99			sep := gtk.NewSeparator(gtk.OrientationHorizontalValue)
100			sep.AddCssClass(contextMenuSeparatorClass)
101			box.Append(&sep.Widget)
102			continue
103		}
104
105		btn := gtk.NewButton()
106		btn.SetLabel(item.item.Label)
107		btn.AddCssClass("flat")
108		btn.AddCssClass(contextMenuItemClass)
109
110		menuItem := item.item
111		clickCb := func(_ gtk.Button) {
112			selected = true
113			if onSelect != nil {
114				onSelect(menuItem)
115			}
116			if popover != nil {
117				popover.Popdown()
118			}
119		}
120		btn.ConnectClicked(&clickCb)
121		box.Append(&btn.Widget)
122	}
123
124	popover = gtk.NewPopover()
125	popover.AddCssClass(contextMenuPopoverClass)
126	popover.SetChild(&box.Widget)
127	attachPopover(popover, popoverHost, parent)
128	popover.SetHasArrow(false)
129	popover.SetAutohide(true)
130
131	if popoverHost == nil {
132		rect := PopoverPointingRect(x, y, func(srcX, srcY float64) (float64, float64, bool) {
133			if anchor == nil || parent == nil || anchor == parent {
134				return srcX, srcY, false
135			}
136			var destX, destY float64
137			if !anchor.TranslateCoordinates(parent, srcX, srcY, &destX, &destY) {
138				return srcX, srcY, false
139			}
140			return destX, destY, true
141		})
142		popover.SetPointingTo(rect)
143	}
144
145	closedCb := func(_ gtk.Popover) {
146		if !selected && onClose != nil {
147			onClose()
148		}
149		cleanupPopover(popover, popoverHost, overlay)
150	}
151	popover.ConnectClosed(&closedCb)
152
153	popupPopover(popover, popoverHost)
154}
155
156// ChoosePopoverParent picks the best parent for a GTK popover.
157func ChoosePopoverParent(anchor, parent *gtk.Widget) *gtk.Widget {
158	if parent != nil {
159		return parent
160	}
161	return anchor
162}
163
164// PopoverPointingRect builds the popover pointing rectangle.
165func PopoverPointingRect(
166	x, y int32,
167	translate func(srcX, srcY float64) (float64, float64, bool),
168) *gdk.Rectangle {
169	destX, destY := float64(x), float64(y)
170	if translate != nil {
171		if tx, ty, ok := translate(destX, destY); ok {
172			destX, destY = tx, ty
173		}
174	}
175	return &gdk.Rectangle{X: int32(destX), Y: int32(destY), Width: 1, Height: 1}
176}
177
178func createPopoverHost(parent *gtk.Widget, x, y int32) (*gtk.MenuButton, *gtk.Overlay) {
179	overlay := overlayFromWidget(parent)
180	if overlay == nil {
181		return nil, nil
182	}
183	host := gtk.NewMenuButton()
184	host.SetCanFocus(false)
185	host.SetHalign(gtk.AlignStartValue)
186	host.SetValign(gtk.AlignStartValue)
187	host.SetMarginStart(int(x))
188	host.SetMarginTop(int(y))
189	host.SetOpacity(0)
190	host.SetSizeRequest(1, 1)
191	host.SetVisible(true)
192	overlay.AddOverlay(&host.Widget)
193	overlay.SetClipOverlay(&host.Widget, false)
194	overlay.SetMeasureOverlay(&host.Widget, false)
195	return host, overlay
196}
197
198func overlayFromWidget(widget *gtk.Widget) *gtk.Overlay {
199	if widget == nil || widget.GoPointer() == 0 {
200		return nil
201	}
202	if !gobject.TypeCheckInstanceIsAPtr(widget.GoPointer(), gtk.OverlayGLibType()) {
203		return nil
204	}
205	return gtk.OverlayNewFromInternalPtr(widget.GoPointer())
206}
207
208func attachPopover(popover *gtk.Popover, host *gtk.MenuButton, parent *gtk.Widget) {
209	if host != nil {
210		host.SetPopover(popover)
211		return
212	}
213	popover.SetParent(parent)
214}
215
216func cleanupPopover(popover *gtk.Popover, host *gtk.MenuButton, overlay *gtk.Overlay) {
217	if host != nil && overlay != nil {
218		host.SetPopover(nil)
219		overlay.RemoveOverlay(&host.Widget)
220		return
221	}
222	popover.Unparent()
223}
224
225func popupPopover(popover *gtk.Popover, host *gtk.MenuButton) {
226	if host != nil {
227		host.Popup()
228		return
229	}
230	popover.Popup()
231}
232
233func hasRenderableItems(items []renderItem) bool {
234	for _, item := range items {
235		if !item.separator {
236			return true
237		}
238	}
239	return false
240}

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