Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
diamondburned
GitHub Repository: diamondburned/gtkcord4
Path: blob/main/internal/window/quickswitcher/quickswitcher.go
366 views
1
package quickswitcher
2
3
import (
4
"context"
5
"fmt"
6
"log/slog"
7
8
"github.com/diamondburned/gotk4/pkg/gdk/v4"
9
"github.com/diamondburned/gotk4/pkg/gtk/v4"
10
"github.com/diamondburned/gotk4/pkg/pango"
11
"github.com/diamondburned/gotkit/app"
12
"github.com/diamondburned/gotkit/gtkutil"
13
"github.com/diamondburned/gotkit/gtkutil/cssutil"
14
"github.com/diamondburned/gotkit/gtkutil/textutil"
15
"libdb.so/dissent/internal/gtkcord"
16
)
17
18
// QuickSwitcher is a search box capable of looking up guilds and channels for
19
// quickly jumping to them. It replicates the Ctrl+K dialog of the desktop
20
// client.
21
type QuickSwitcher struct {
22
*gtk.Box
23
ctx gtkutil.Cancellable
24
text string
25
index index
26
27
search *gtk.SearchEntry
28
chosenFunc func()
29
30
entryScroll *gtk.ScrolledWindow
31
entryList *gtk.ListBox
32
entries []entry
33
}
34
35
type entry struct {
36
*gtk.ListBoxRow
37
indexItem indexItem
38
}
39
40
var qsCSS = cssutil.Applier("quickswitcher", `
41
.quickswitcher-search {
42
font-size: 1.15em;
43
margin: 0;
44
}
45
.quickswitcher-search image {
46
min-width: 32px;
47
min-height: 32px;
48
}
49
.quickswitcher-searchbar > revealer > box {
50
padding: 12px;
51
}
52
.quickswitcher-list {
53
font-size: 1.05em;
54
background: none;
55
margin: 8px;
56
margin-top: 0;
57
}
58
.quickswitcher-list > row {
59
padding: 4px 2px;
60
}
61
`)
62
63
// NewQuickSwitcher creates a new Quick Switcher instance.
64
func NewQuickSwitcher(ctx context.Context) *QuickSwitcher {
65
var qs QuickSwitcher
66
qs.index.update(ctx)
67
68
qs.search = gtk.NewSearchEntry()
69
qs.search.AddCSSClass("quickswitcher-search")
70
qs.search.SetHExpand(true)
71
qs.search.SetObjectProperty("placeholder-text", "Search")
72
qs.search.ConnectActivate(func() { qs.selectEntry() })
73
qs.search.ConnectNextMatch(func() { qs.moveDown() })
74
qs.search.ConnectPreviousMatch(func() { qs.moveUp() })
75
qs.search.ConnectSearchChanged(func() {
76
qs.text = qs.search.Text()
77
qs.do()
78
})
79
80
if qs.search.ObjectProperty("search-delay") != nil {
81
// Only GTK v4.8 and onwards.
82
qs.search.SetObjectProperty("search-delay", 100)
83
}
84
85
keyCtrl := gtk.NewEventControllerKey()
86
keyCtrl.ConnectKeyPressed(func(val, _ uint, state gdk.ModifierType) bool {
87
switch val {
88
case gdk.KEY_Up:
89
return qs.moveUp()
90
case gdk.KEY_Down, gdk.KEY_Tab:
91
return qs.moveDown()
92
default:
93
return false
94
}
95
})
96
qs.search.AddController(keyCtrl)
97
98
qs.entryList = gtk.NewListBox()
99
qs.entryList.AddCSSClass("quickswitcher-list")
100
qs.entryList.SetVExpand(true)
101
qs.entryList.SetSelectionMode(gtk.SelectionSingle)
102
qs.entryList.SetActivateOnSingleClick(true)
103
qs.entryList.SetPlaceholder(qsListPlaceholder())
104
qs.entryList.ConnectRowActivated(func(row *gtk.ListBoxRow) {
105
qs.choose(row.Index())
106
})
107
108
entryViewport := gtk.NewViewport(nil, nil)
109
entryViewport.SetScrollToFocus(true)
110
entryViewport.SetChild(qs.entryList)
111
112
qs.entryScroll = gtk.NewScrolledWindow()
113
qs.entryScroll.AddCSSClass("quickswitcher-scroll")
114
qs.entryScroll.SetPolicy(gtk.PolicyNever, gtk.PolicyAutomatic)
115
qs.entryScroll.SetChild(entryViewport)
116
qs.entryScroll.SetVExpand(true)
117
118
qs.Box = gtk.NewBox(gtk.OrientationVertical, 0)
119
qs.Box.SetVExpand(true)
120
qs.Box.Append(qs.search)
121
qs.Box.Append(qs.entryScroll)
122
123
qs.ctx = gtkutil.WithVisibility(ctx, qs.search)
124
qs.search.SetKeyCaptureWidget(qs)
125
126
qsCSS(qs.Box)
127
return &qs
128
}
129
130
func qsListLoading() gtk.Widgetter {
131
loading := gtk.NewSpinner()
132
loading.SetSizeRequest(24, 24)
133
loading.SetVAlign(gtk.AlignCenter)
134
loading.SetHAlign(gtk.AlignCenter)
135
loading.Start()
136
return loading
137
}
138
139
func qsListPlaceholder() gtk.Widgetter {
140
l := gtk.NewLabel("Where would you like to go?")
141
l.SetAttributes(textutil.Attrs(
142
pango.NewAttrScale(1.15),
143
))
144
l.SetVAlign(gtk.AlignCenter)
145
l.SetHAlign(gtk.AlignCenter)
146
return l
147
}
148
149
func (qs *QuickSwitcher) Clear() {
150
qs.search.SetText("")
151
qs.text = ""
152
qs.do()
153
}
154
155
func (qs *QuickSwitcher) do() {
156
for i, e := range qs.entries {
157
qs.entryList.Remove(e)
158
qs.entries[i] = entry{}
159
}
160
qs.entries = qs.entries[:0]
161
162
if qs.text == "" {
163
return
164
}
165
166
for _, match := range qs.index.search(qs.text) {
167
e := entry{
168
ListBoxRow: match.Row(qs.ctx.Take()),
169
indexItem: match,
170
}
171
172
qs.entries = append(qs.entries, e)
173
qs.entryList.Append(e)
174
}
175
176
if len(qs.entries) > 0 {
177
qs.entryList.SelectRow(qs.entries[0].ListBoxRow)
178
}
179
}
180
181
func (qs *QuickSwitcher) choose(n int) {
182
entry := qs.entries[n]
183
parent := gtk.BaseWidget(qs.Parent())
184
185
var ok bool
186
switch item := entry.indexItem.(type) {
187
case channelItem:
188
ok = parent.ActivateAction("app.open-channel", gtkcord.NewChannelIDVariant(item.ID))
189
case guildItem:
190
ok = parent.ActivateAction("app.open-guild", gtkcord.NewGuildIDVariant(item.ID))
191
}
192
if !ok {
193
slog.Error(
194
"failed to activate opening action from quick switcher",
195
"parent", fmt.Sprintf("%T", qs.Parent()),
196
"item", fmt.Sprintf("%T", entry.indexItem))
197
}
198
199
if qs.chosenFunc != nil {
200
qs.chosenFunc()
201
}
202
}
203
204
// ConnectChosen connects a function to be called when an entry is chosen.
205
func (qs *QuickSwitcher) ConnectChosen(f func()) {
206
if qs.chosenFunc != nil {
207
add := f
208
old := qs.chosenFunc
209
f = func() {
210
old()
211
add()
212
}
213
}
214
qs.chosenFunc = f
215
}
216
217
func (qs *QuickSwitcher) selectEntry() bool {
218
if len(qs.entries) == 0 {
219
return false
220
}
221
222
row := qs.entryList.SelectedRow()
223
if row == nil {
224
return false
225
}
226
227
qs.choose(row.Index())
228
return true
229
}
230
231
func (qs *QuickSwitcher) moveUp() bool { return qs.move(false) }
232
func (qs *QuickSwitcher) moveDown() bool { return qs.move(true) }
233
234
func (qs *QuickSwitcher) move(down bool) bool {
235
if len(qs.entries) == 0 {
236
return false
237
}
238
239
row := qs.entryList.SelectedRow()
240
if row == nil {
241
qs.entryList.SelectRow(qs.entries[0].ListBoxRow)
242
return true
243
}
244
245
ix := row.Index()
246
if down {
247
ix++
248
if ix == len(qs.entries) {
249
ix = 0
250
}
251
} else {
252
ix--
253
if ix == -1 {
254
ix = len(qs.entries) - 1
255
}
256
}
257
258
qs.entryList.SelectRow(qs.entries[ix].ListBoxRow)
259
260
// Steal focus. This is a hack to scroll to the selected item without having
261
// to manually calculate the coordinates.
262
var target gtk.Widgetter = qs.search
263
if focused := app.WindowFromContext(qs.ctx.Take()).Focus(); focused != nil {
264
target = focused
265
}
266
targetBase := gtk.BaseWidget(target)
267
qs.entries[ix].ListBoxRow.GrabFocus()
268
targetBase.GrabFocus()
269
270
return true
271
}
272
273