Path: blob/main/internal/window/quickswitcher/quickswitcher.go
366 views
package quickswitcher12import (3"context"4"fmt"5"log/slog"67"github.com/diamondburned/gotk4/pkg/gdk/v4"8"github.com/diamondburned/gotk4/pkg/gtk/v4"9"github.com/diamondburned/gotk4/pkg/pango"10"github.com/diamondburned/gotkit/app"11"github.com/diamondburned/gotkit/gtkutil"12"github.com/diamondburned/gotkit/gtkutil/cssutil"13"github.com/diamondburned/gotkit/gtkutil/textutil"14"libdb.so/dissent/internal/gtkcord"15)1617// QuickSwitcher is a search box capable of looking up guilds and channels for18// quickly jumping to them. It replicates the Ctrl+K dialog of the desktop19// client.20type QuickSwitcher struct {21*gtk.Box22ctx gtkutil.Cancellable23text string24index index2526search *gtk.SearchEntry27chosenFunc func()2829entryScroll *gtk.ScrolledWindow30entryList *gtk.ListBox31entries []entry32}3334type entry struct {35*gtk.ListBoxRow36indexItem indexItem37}3839var qsCSS = cssutil.Applier("quickswitcher", `40.quickswitcher-search {41font-size: 1.15em;42margin: 0;43}44.quickswitcher-search image {45min-width: 32px;46min-height: 32px;47}48.quickswitcher-searchbar > revealer > box {49padding: 12px;50}51.quickswitcher-list {52font-size: 1.05em;53background: none;54margin: 8px;55margin-top: 0;56}57.quickswitcher-list > row {58padding: 4px 2px;59}60`)6162// NewQuickSwitcher creates a new Quick Switcher instance.63func NewQuickSwitcher(ctx context.Context) *QuickSwitcher {64var qs QuickSwitcher65qs.index.update(ctx)6667qs.search = gtk.NewSearchEntry()68qs.search.AddCSSClass("quickswitcher-search")69qs.search.SetHExpand(true)70qs.search.SetObjectProperty("placeholder-text", "Search")71qs.search.ConnectActivate(func() { qs.selectEntry() })72qs.search.ConnectNextMatch(func() { qs.moveDown() })73qs.search.ConnectPreviousMatch(func() { qs.moveUp() })74qs.search.ConnectSearchChanged(func() {75qs.text = qs.search.Text()76qs.do()77})7879if qs.search.ObjectProperty("search-delay") != nil {80// Only GTK v4.8 and onwards.81qs.search.SetObjectProperty("search-delay", 100)82}8384keyCtrl := gtk.NewEventControllerKey()85keyCtrl.ConnectKeyPressed(func(val, _ uint, state gdk.ModifierType) bool {86switch val {87case gdk.KEY_Up:88return qs.moveUp()89case gdk.KEY_Down, gdk.KEY_Tab:90return qs.moveDown()91default:92return false93}94})95qs.search.AddController(keyCtrl)9697qs.entryList = gtk.NewListBox()98qs.entryList.AddCSSClass("quickswitcher-list")99qs.entryList.SetVExpand(true)100qs.entryList.SetSelectionMode(gtk.SelectionSingle)101qs.entryList.SetActivateOnSingleClick(true)102qs.entryList.SetPlaceholder(qsListPlaceholder())103qs.entryList.ConnectRowActivated(func(row *gtk.ListBoxRow) {104qs.choose(row.Index())105})106107entryViewport := gtk.NewViewport(nil, nil)108entryViewport.SetScrollToFocus(true)109entryViewport.SetChild(qs.entryList)110111qs.entryScroll = gtk.NewScrolledWindow()112qs.entryScroll.AddCSSClass("quickswitcher-scroll")113qs.entryScroll.SetPolicy(gtk.PolicyNever, gtk.PolicyAutomatic)114qs.entryScroll.SetChild(entryViewport)115qs.entryScroll.SetVExpand(true)116117qs.Box = gtk.NewBox(gtk.OrientationVertical, 0)118qs.Box.SetVExpand(true)119qs.Box.Append(qs.search)120qs.Box.Append(qs.entryScroll)121122qs.ctx = gtkutil.WithVisibility(ctx, qs.search)123qs.search.SetKeyCaptureWidget(qs)124125qsCSS(qs.Box)126return &qs127}128129func qsListLoading() gtk.Widgetter {130loading := gtk.NewSpinner()131loading.SetSizeRequest(24, 24)132loading.SetVAlign(gtk.AlignCenter)133loading.SetHAlign(gtk.AlignCenter)134loading.Start()135return loading136}137138func qsListPlaceholder() gtk.Widgetter {139l := gtk.NewLabel("Where would you like to go?")140l.SetAttributes(textutil.Attrs(141pango.NewAttrScale(1.15),142))143l.SetVAlign(gtk.AlignCenter)144l.SetHAlign(gtk.AlignCenter)145return l146}147148func (qs *QuickSwitcher) Clear() {149qs.search.SetText("")150qs.text = ""151qs.do()152}153154func (qs *QuickSwitcher) do() {155for i, e := range qs.entries {156qs.entryList.Remove(e)157qs.entries[i] = entry{}158}159qs.entries = qs.entries[:0]160161if qs.text == "" {162return163}164165for _, match := range qs.index.search(qs.text) {166e := entry{167ListBoxRow: match.Row(qs.ctx.Take()),168indexItem: match,169}170171qs.entries = append(qs.entries, e)172qs.entryList.Append(e)173}174175if len(qs.entries) > 0 {176qs.entryList.SelectRow(qs.entries[0].ListBoxRow)177}178}179180func (qs *QuickSwitcher) choose(n int) {181entry := qs.entries[n]182parent := gtk.BaseWidget(qs.Parent())183184var ok bool185switch item := entry.indexItem.(type) {186case channelItem:187ok = parent.ActivateAction("app.open-channel", gtkcord.NewChannelIDVariant(item.ID))188case guildItem:189ok = parent.ActivateAction("app.open-guild", gtkcord.NewGuildIDVariant(item.ID))190}191if !ok {192slog.Error(193"failed to activate opening action from quick switcher",194"parent", fmt.Sprintf("%T", qs.Parent()),195"item", fmt.Sprintf("%T", entry.indexItem))196}197198if qs.chosenFunc != nil {199qs.chosenFunc()200}201}202203// ConnectChosen connects a function to be called when an entry is chosen.204func (qs *QuickSwitcher) ConnectChosen(f func()) {205if qs.chosenFunc != nil {206add := f207old := qs.chosenFunc208f = func() {209old()210add()211}212}213qs.chosenFunc = f214}215216func (qs *QuickSwitcher) selectEntry() bool {217if len(qs.entries) == 0 {218return false219}220221row := qs.entryList.SelectedRow()222if row == nil {223return false224}225226qs.choose(row.Index())227return true228}229230func (qs *QuickSwitcher) moveUp() bool { return qs.move(false) }231func (qs *QuickSwitcher) moveDown() bool { return qs.move(true) }232233func (qs *QuickSwitcher) move(down bool) bool {234if len(qs.entries) == 0 {235return false236}237238row := qs.entryList.SelectedRow()239if row == nil {240qs.entryList.SelectRow(qs.entries[0].ListBoxRow)241return true242}243244ix := row.Index()245if down {246ix++247if ix == len(qs.entries) {248ix = 0249}250} else {251ix--252if ix == -1 {253ix = len(qs.entries) - 1254}255}256257qs.entryList.SelectRow(qs.entries[ix].ListBoxRow)258259// Steal focus. This is a hack to scroll to the selected item without having260// to manually calculate the coordinates.261var target gtk.Widgetter = qs.search262if focused := app.WindowFromContext(qs.ctx.Take()).Focus(); focused != nil {263target = focused264}265targetBase := gtk.BaseWidget(target)266qs.entries[ix].ListBoxRow.GrabFocus()267targetBase.GrabFocus()268269return true270}271272273