gmenu/cmd/gtkmenu/gui_list.go

372 lines
7.3 KiB
Go

package main
import (
"flag"
"fmt"
"html"
"log"
"os"
"strconv"
"strings"
"sync"
"code.rocketnine.space/tslocum/desktop"
"code.rocketnine.space/tslocum/gmenu/pkg/gmenu"
"github.com/gotk3/gotk3/glib"
"github.com/gotk3/gotk3/gtk"
"github.com/gotk3/gotk3/pango"
"github.com/kballard/go-shellquote"
)
const (
iconSize = 48
rowMargin = 4
labelMarginStart = 12
labelMarginTop = 4
labelMarginTopComment = 4
)
var execLabel *gtk.Label
func initList(container *gtk.Box) {
inputView = newTextView()
setNoExpand(&inputView.Widget)
inputView.SetProperty("accepts-tab", false)
inputView.SetProperty("wrap-mode", gtk.WRAP_CHAR)
inputView.SetProperty("cursor-visible", false)
inputView.SetProperty("left-margin", 2)
inputView.SetProperty("right-margin", 2)
buffer, err := inputView.GetBuffer()
if err != nil {
log.Fatal("failed to create input buffer:", err)
}
buffer.Connect("changed", func(tb *gtk.TextBuffer) bool {
gmenu.SetInput(strings.TrimSpace(textViewText(inputView)))
return false
})
if err != nil {
log.Fatal("failed to connect to changed event of input buffer:", err)
}
container.Add(inputView)
listScroll, err := gtk.ScrolledWindowNew(nil, nil)
if err != nil {
log.Fatal("failed to create ScrolledWindow:", err)
}
listScroll.SetHExpand(true)
listScroll.SetVExpand(false)
listBox, err = gtk.ListBoxNew()
if err != nil {
log.Fatal("failed to create ListBox:", err)
}
listBox.SetSelectionMode(gtk.SELECTION_BROWSE)
listBox.SetHExpand(false)
listBox.SetFocusOnClick(false)
listScroll.Add(listBox)
container.PackEnd(listScroll, true, true, 0)
if !config.HideAppIcons {
s, _ := container.GetScreen()
iconTheme, err = gtk.IconThemeGetForScreen(*s)
if err != nil {
log.Fatal("failed to get icon theme:", err)
}
}
lastEntry := len(gmenu.FilteredEntries) - 1
var (
entry *gmenu.ListEntry
wg = new(sync.WaitGroup)
)
for i, label := range gmenu.Names {
wg.Add(1)
glib.IdleAdd(rowInitFunc(i, label, entry, lastEntry, wg))
}
listBox.SetSortFunc(listSort)
listBox.SetFilterFunc(listFilter)
go func() {
wg.Wait()
glib.IdleAdd(func() {
listBox.Connect("row-activated", func(_ *gtk.ListBox, _ *gtk.ListBoxRow) {
err := listSelect(inputView, false)
if err != nil {
log.Fatal(err)
}
})
if err != nil {
log.Fatal("failed to connect to row-activated event of ListBox:", err)
}
})
}()
}
func initRow(container *gtk.Box, entry *gmenu.ListEntry, i int, lastEntry int) {
if !config.HideAppIcons {
img, _ := gtk.ImageNew()
img.SetSizeRequest(iconSize, iconSize)
container.PackStart(img, false, false, 0)
glib.IdleAdd(func() { loadIconImage(img, entry) })
}
labelContainer := newBox(gtk.ORIENTATION_VERTICAL)
labelContainer.SetMarginStart(labelMarginStart)
labelText := "Shell command"
if entry != nil && entry.Entry != nil {
labelText = entry.Label
}
l, err := gtk.LabelNew(fmt.Sprintf("<b>%s</b>", html.EscapeString(labelText)))
if err != nil {
log.Fatal("failed to create Label:", err)
}
l.SetUseMarkup(true)
setNoExpand(&l.Widget)
initLabel(l)
if !config.HideAppIcons && !config.HideAppDetails {
l.SetMarginTop(labelMarginTop)
}
if !config.HideAppDetails {
labelContainer.PackStart(l, false, false, 0)
} else {
labelContainer.PackStart(l, true, true, 0)
}
if config.HideAppDetails {
if i == lastEntry {
execLabel = l
}
} else if entry.Entry == nil || (entry.Entry != nil && entry.Comment != "") {
comment := ""
if entry.Entry != nil {
comment = entry.Comment
}
l, err := gtk.LabelNew(comment)
if err != nil {
log.Fatal("failed to create Label:", err)
}
setNoExpand(&l.Widget)
initLabel(l)
if !config.HideAppIcons {
l.SetMarginTop(labelMarginTopComment)
}
labelContainer.Add(l)
if i == lastEntry {
execLabel = l
}
}
setNoExpand(&labelContainer.Widget)
container.Add(labelContainer)
setNoExpand(&container.Widget)
}
func initLabel(l *gtk.Label) {
l.SetHAlign(gtk.ALIGN_START)
l.SetLineWrap(false)
l.SetSingleLineMode(true)
l.SetEllipsize(pango.ELLIPSIZE_END)
}
func updateList(input string) {
execLabel.SetText(input)
listBox.InvalidateFilter()
listBox.InvalidateSort()
listBox.SelectRow(listBox.GetRowAtIndex(0))
}
func newBox(orient gtk.Orientation) *gtk.Box {
box, err := gtk.BoxNew(orient, 0)
if err != nil {
log.Fatal("Unable to create box:", err)
}
return box
}
func newTextView() *gtk.TextView {
tv, err := gtk.TextViewNew()
if err != nil {
log.Fatal("Unable to create TextView:", err)
}
return tv
}
func textViewBuffer(tv *gtk.TextView) *gtk.TextBuffer {
buffer, err := tv.GetBuffer()
if err != nil {
log.Fatal("Unable to get TextView buffer:", err)
}
return buffer
}
func textViewText(tv *gtk.TextView) string {
buffer := textViewBuffer(tv)
start, end := buffer.GetBounds()
text, err := buffer.GetText(start, end, true)
if err != nil {
log.Fatal("Unable to get TextView text:", err)
}
return text
}
func setNoExpand(v *gtk.Widget) {
v.SetHExpand(false)
v.SetVExpand(false)
}
func rowID(row *gtk.ListBoxRow) int {
if row == nil {
return -1
}
name, err := row.GetName()
if err != nil || len(name) < 2 || name[0] != '#' {
return -1
}
id, err := strconv.Atoi(name[1:])
if err != nil {
return -1
}
return id
}
func selectedIndex() int {
if listBox == nil {
return -1
}
return rowID(listBox.GetSelectedRow())
}
func selectedEntry() *desktop.Entry {
i := selectedIndex()
if len(gmenu.FilteredEntries) == 0 || i < 0 || i > len(gmenu.FilteredEntries)-1 {
return nil
}
return gmenu.FilteredEntries[i].Entry
}
func listSelect(_ *gtk.TextView, runInTerminal bool) error {
gmenu.CloseInput()
var execute string
entry := selectedEntry()
if entry == nil {
execute = textViewText(inputView)
} else if entry.Type == desktop.Application {
if entry.Terminal {
runInTerminal = true
}
execute = entry.ExpandExec(shellquote.Join(flag.Args()...))
} else { // Type == desktop.Link
execute = shellquote.Join(config.BrowserCommand(), entry.URL)
}
path := ""
if entry != nil {
path = entry.Path
}
err := gmenu.Run(&config.Config, execute, path, runInTerminal, false)
if err != nil {
return err
}
os.Exit(0)
return nil
}
func listSort(row1 *gtk.ListBoxRow, row2 *gtk.ListBoxRow) int {
r1 := rowID(row1)
r2 := rowID(row2)
if r1 < 0 {
if gmenu.MatchEntry(r2) {
return 1
}
return -1
} else if r2 < 0 {
if gmenu.MatchEntry(r1) {
return -1
}
return 1
}
if gmenu.Sort(r1, r2) {
return -1
}
return 1
}
func listFilter(row *gtk.ListBoxRow) bool {
match := gmenu.MatchEntry(rowID(row))
row.SetSelectable(match)
return match
}
func rowInitFunc(i int, label string, entry *gmenu.ListEntry, lastEntry int, wg *sync.WaitGroup) func() {
return func() {
row, err := gtk.ListBoxRowNew()
if err != nil {
log.Fatal("failed to create ListBoxRow:", err)
}
if i == lastEntry {
entry = &gmenu.ListEntry{Entry: nil, Label: ""}
} else {
row.SetName("#" + strconv.Itoa(i))
entry = &gmenu.ListEntry{Entry: gmenu.Entries[i], Label: label}
}
container := newBox(gtk.ORIENTATION_HORIZONTAL)
container.SetMarginStart(rowMargin)
container.SetMarginTop(rowMargin)
container.SetMarginBottom(rowMargin)
container.SetMarginEnd(rowMargin)
initRow(container, entry, i, lastEntry)
row.Add(container)
listBox.Add(row)
listBox.SelectRow(listBox.GetRowAtIndex(0))
row.ShowAll()
wg.Done()
}
}