Cache application icons

This commit is contained in:
Trevor Slocum 2019-08-02 20:40:02 -07:00
parent 1bb42b05cc
commit c1cfe822b0
5 changed files with 245 additions and 149 deletions

View File

@ -6,30 +6,30 @@ import (
"html"
"log"
"os"
"path"
"strconv"
"strings"
"github.com/gotk3/gotk3/pango"
"git.sr.ht/~tslocum/desktop"
"git.sr.ht/~tslocum/gmenu/pkg/gmenu"
"github.com/gotk3/gotk3/gdk"
"github.com/gotk3/gotk3/glib"
"github.com/gotk3/gotk3/gtk"
"github.com/gotk3/gotk3/pango"
"github.com/kballard/go-shellquote"
"github.com/tslocum/gotk3/gtk"
gtkfork "github.com/tslocum/gotk3/gtk"
)
const (
iconSize = 48
iconMargin = 4
iconMarginStart = 2
labelMarginStart = 4
iconMarginStart = 4
labelMarginStart = 8
labelMarginTop = 8
labelMarginTopComment = 4
)
var execLabel *gtk.Label
var (
execLabel *gtk.Label
)
func initList(container *gtk.Box) {
inputView = newTextView()
@ -60,8 +60,9 @@ func initList(container *gtk.Box) {
}
listBox.SetSelectionMode(gtk.SELECTION_BROWSE)
listBox.SetHExpand(false)
listBox.SetFocusOnClick(false)
_, err = listBox.Connect("button-press-event", func(listBox *gtkfork.ListBox, ev *gdk.Event) {
_, err = listBox.Connect("button-press-event", func(listBox *gtk.ListBox, ev *gdk.Event) {
mouseEvent := &gdk.EventButton{ev}
if mouseEvent.Type() == gdk.EVENT_2BUTTON_PRESS {
err := listSelect(inputView)
@ -77,126 +78,35 @@ func initList(container *gtk.Box) {
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
for i, entry := range gmenu.FilteredEntries {
// Capture variables
i := i
entry := entry
row, err := gtk.ListBoxRowNew()
if err != nil {
log.Fatal("failed to create ListBoxRow:", err)
}
row.SetFocusOnClick(false)
row.SetName("#" + strconv.Itoa(i))
container := newBox(gtk.ORIENTATION_HORIZONTAL)
if !config.HideAppIcons {
s, _ := container.GetScreen()
theme, err := gtk.IconThemeGetForScreen(*s)
if err != nil {
log.Fatal("failed to get icon theme:", err)
}
var (
pbuf *gdk.Pixbuf
img *gtk.Image
)
if entry.Entry != nil && entry.Icon != "" {
if path.IsAbs(entry.Icon) {
pbuf, err = gdk.PixbufNewFromFileAtSize(entry.Icon, iconSize, iconSize)
} else {
pbuf, err = theme.LoadIcon(entry.Icon, iconSize, gtk.ICON_LOOKUP_USE_BUILTIN)
}
}
if pbuf == nil || err != nil {
var icon string
if entry.Entry == nil {
icon = "utilities-terminal"
} else if entry.Type == desktop.Application {
icon = "application-x-executable"
} else {
icon = "text-html"
}
pbuf, err = theme.LoadIcon(icon, iconSize, gtk.ICON_LOOKUP_USE_BUILTIN)
}
if err != nil {
// Failed to load icon
img, err = gtk.ImageNew()
if err == nil {
img.SetSizeRequest(iconSize, iconSize)
}
} else {
if pbuf.GetWidth() != iconSize && pbuf.GetHeight() != iconSize {
pbuf, _ = pbuf.ScaleSimple(iconSize, iconSize, gdk.INTERP_BILINEAR)
}
img, err = gtk.ImageNewFromPixbuf(pbuf)
}
if err != nil {
log.Fatal("failed to create Icon:", err)
}
img.SetMarginStart(iconMarginStart)
img.SetMarginTop(iconMargin)
img.SetMarginEnd(iconMargin)
img.SetMarginBottom(iconMargin)
container.PackStart(img, false, false, 0)
}
labelContainer := newBox(gtk.ORIENTATION_VERTICAL)
labelContainer.SetMarginStart(labelMarginStart)
l, err := gtk.LabelNew(fmt.Sprintf("<b>%s</b>", html.EscapeString(entry.Label)))
if err != nil {
log.Fatal("failed to create Label:", err)
}
l.SetUseMarkup(true)
l.SetHAlign(gtk.ALIGN_START)
setNoExpand(&l.Widget)
l.SetLineWrap(false)
l.SetSingleLineMode(true)
l.SetEllipsize(pango.ELLIPSIZE_END)
if !config.HideAppIcons {
l.SetMarginTop(labelMarginTop)
}
labelContainer.PackStart(l, false, false, 0)
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)
}
l.SetHAlign(gtk.ALIGN_START)
setNoExpand(&l.Widget)
l.SetLineWrap(false)
l.SetSingleLineMode(true)
l.SetEllipsize(pango.ELLIPSIZE_END)
if !config.HideAppIcons {
l.SetMarginTop(labelMarginTopComment)
}
labelContainer.Add(l)
if i == lastEntry {
execLabel = l
}
}
setNoExpand(&labelContainer.Widget)
container.Add(labelContainer)
setNoExpand(&container.Widget)
row.Add(container)
initRow(container, entry, i, lastEntry)
listBox.Add(row)
}
@ -211,9 +121,103 @@ func initList(container *gtk.Box) {
log.Fatal("failed to create ListBox:", err)
}
listBox.SelectRow(listBox.GetRowAtIndex(0))
gmenu.SetInput("")
}
func initRow(container *gtk.Box, entry *gmenu.ListEntry, i int, lastEntry int) {
if !config.HideAppIcons {
img, _ := gtk.ImageNew()
img.SetSizeRequest(iconSize, iconSize)
glib.IdleAdd(func() { loadIconImage(img, entry) })
container.PackStart(img, false, false, 0)
}
labelContainer := newBox(gtk.ORIENTATION_VERTICAL)
labelContainer.SetMarginStart(labelMarginStart)
l, err := gtk.LabelNew(fmt.Sprintf("<b>%s</b>", html.EscapeString(entry.Label)))
if err != nil {
log.Fatal("failed to create Label:", err)
}
l.SetUseMarkup(true)
l.SetHAlign(gtk.ALIGN_START)
setNoExpand(&l.Widget)
l.SetLineWrap(false)
l.SetSingleLineMode(true)
l.SetEllipsize(pango.ELLIPSIZE_END)
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)
}
l.SetHAlign(gtk.ALIGN_START)
setNoExpand(&l.Widget)
l.SetLineWrap(false)
l.SetSingleLineMode(true)
l.SetEllipsize(pango.ELLIPSIZE_END)
if !config.HideAppIcons {
l.SetMarginTop(labelMarginTopComment)
}
labelContainer.Add(l)
if i == lastEntry {
execLabel = l
}
}
setNoExpand(&labelContainer.Widget)
container.Add(labelContainer)
setNoExpand(&container.Widget)
}
func loadIconImage(img *gtk.Image, entry *gmenu.ListEntry) {
var (
pbuf *gdk.Pixbuf
err error
)
if entry.Entry != nil && entry.Icon != "" {
pbuf, err = loadIcon(entry.Icon)
}
if pbuf == nil || err != nil {
pbuf, err = loadIcon(fallbackIcon(entry))
}
if err != nil {
log.Fatal("failed to create Icon:", err)
}
img.SetFromPixbuf(pbuf)
img.SetMarginStart(iconMarginStart)
img.SetMarginTop(iconMargin)
img.SetMarginEnd(iconMargin)
img.SetMarginBottom(iconMargin)
}
func updateList(input string) {
listBox.InvalidateSort()
@ -350,3 +354,13 @@ func rowID(row *gtk.ListBoxRow) int {
return id
}
func fallbackIcon(entry *gmenu.ListEntry) string {
if entry.Entry == nil {
return "utilities-terminal"
} else if entry.Type == desktop.Application {
return "application-x-executable"
}
return "text-html"
}

58
cmd/gtkmenu/icon.go Normal file
View File

@ -0,0 +1,58 @@
package main
import (
"crypto/md5"
"fmt"
"log"
"os"
"path"
"github.com/gotk3/gotk3/gdk"
"github.com/gotk3/gotk3/gtk"
)
const CachedIconCompressionLevel = 3
var (
iconTheme *gtk.IconTheme
iconCacheDir string
)
func loadIcon(icon string) (*gdk.Pixbuf, error) {
cachedIcon := path.Join(iconCacheDir, fmt.Sprintf("%x.png", md5.Sum([]byte(icon))))
if _, err := os.Stat(cachedIcon); !os.IsNotExist(err) {
return gdk.PixbufNewFromFileAtSize(cachedIcon, iconSize, iconSize)
}
var (
pbuf *gdk.Pixbuf
err error
)
if path.IsAbs(icon) {
pbuf, err = gdk.PixbufNewFromFileAtSize(icon, iconSize, iconSize)
} else {
pbuf, err = iconTheme.LoadIcon(icon, iconSize, gtk.ICON_LOOKUP_USE_BUILTIN)
}
if err != nil {
return nil, err
}
if pbuf.GetWidth() != iconSize || pbuf.GetHeight() != iconSize {
pbuf, _ = pbuf.ScaleSimple(iconSize, iconSize, gdk.INTERP_BILINEAR)
}
go cacheIcon(cachedIcon, pbuf)
return pbuf, nil
}
func cacheIcon(cachedIcon string, pbuf *gdk.Pixbuf) {
f, err := os.OpenFile(cachedIcon, os.O_CREATE|os.O_RDWR, 0644)
if err != nil {
log.Println(err)
return
}
pbuf.SavePNG(f.Name(), CachedIconCompressionLevel)
}

View File

@ -4,11 +4,12 @@ import (
"flag"
"log"
"os"
"path"
"git.sr.ht/~tslocum/gmenu/pkg/gmenu"
"github.com/gotk3/gotk3/gdk"
"github.com/gotk3/gotk3/glib"
"github.com/tslocum/gotk3/gdk"
"github.com/tslocum/gotk3/gtk"
"github.com/gotk3/gotk3/gtk"
)
const (
@ -20,6 +21,7 @@ type Config struct {
Width, Height int
Resizable bool
Fullscreen bool
HideAppIcons bool
}
@ -28,23 +30,29 @@ var (
config = &Config{}
listBox *gtk.ListBox
inputView *gtk.TextView
loaded = make(chan bool)
)
func init() {
gmenu.SharedInit(&config.Config)
flag.IntVar(&config.Width, "width", 800, "window width")
flag.IntVar(&config.Width, "width", 600, "window width")
flag.IntVar(&config.Height, "height", 200, "window height")
flag.BoolVar(&config.Resizable, "resizable", false, "allow window to be resized")
flag.BoolVar(&config.Fullscreen, "fullscreen", false, "start fullscreen")
flag.BoolVar(&config.HideAppIcons, "no-icons", false, "hide application icons")
}
func main() {
flag.Parse()
go load()
application, err := gtk.ApplicationNew(appID, glib.APPLICATION_HANDLES_COMMAND_LINE)
if err != nil {
log.Fatal("failed to create application:", err)
}
flag.Parse()
_, err = application.Connect("command-line", func() {
flag.Parse()
@ -64,6 +72,23 @@ func main() {
os.Exit(application.Run(os.Args))
}
func load() {
homeDir, err := os.UserHomeDir()
if err != nil {
log.Fatal(err)
}
iconCacheDir = path.Join(homeDir, ".cache", "gmenu", "icons")
if _, err := os.Stat(iconCacheDir); os.IsNotExist(err) {
os.MkdirAll(iconCacheDir, 0744) // TODO: Warn and disable cache on error
}
gmenu.LoadEntries(&config.Config)
gmenu.FilterEntries()
loaded <- true
}
func onActivate(application *gtk.Application) {
w, err := gtk.ApplicationWindowNew(application)
@ -78,8 +103,21 @@ func onActivate(application *gtk.Application) {
}
w.SetTitle("gmenu")
gmenu.LoadEntries(&config.Config)
gmenu.FilterEntries()
w.SetDecorated(false)
w.SetBorderWidth(0)
w.Stick()
w.SetKeepAbove(true)
w.SetTypeHint(gdk.WINDOW_TYPE_HINT_UTILITY)
if !config.Fullscreen {
w.SetResizable(config.Resizable)
w.SetSizeRequest(config.Width, config.Height)
w.SetPosition(gtk.WIN_POS_CENTER)
} else {
w.Fullscreen()
}
<-loaded
go gmenu.HandleInput(func(input string) {
_, err := glib.IdleAdd(updateList, input)
@ -93,19 +131,11 @@ func onActivate(application *gtk.Application) {
gmenu.FilteredEntries = append(gmenu.FilteredEntries, &gmenu.ListEntry{Label: "Shell command", Entry: nil})
container := newBox(gtk.ORIENTATION_VERTICAL)
initList(container)
w.Add(container)
w.SetResizable(config.Resizable)
w.SetSizeRequest(config.Width, config.Height)
w.SetPosition(gtk.WIN_POS_CENTER)
w.SetDecorated(false)
w.SetBorderWidth(0)
w.Stick()
w.SetKeepAbove(true)
_, err = w.Connect("key-press-event", func(win *gtk.ApplicationWindow, ev *gdk.Event) bool {
keyEvent := &gdk.EventKey{ev}
switch keyEvent.KeyVal() {

8
go.mod
View File

@ -4,14 +4,12 @@ go 1.12
require (
git.sr.ht/~tslocum/desktop v0.1.1
github.com/gotk3/gotk3 v0.0.0-20190620081259-6dcdf9e5c51e
github.com/gotk3/gotk3 v0.0.0-20190809225113-dc58eba1cccc
github.com/jroimartin/gocui v0.4.0
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
github.com/lithammer/fuzzysearch v1.0.2
github.com/mattn/go-isatty v0.0.8
github.com/mattn/go-isatty v0.0.9
github.com/mattn/go-runewidth v0.0.4 // indirect
github.com/nsf/termbox-go v0.0.0-20190624072549-eeb6cd0a1762 // indirect
github.com/nsf/termbox-go v0.0.0-20190817171036-93860e161317 // indirect
github.com/pkg/errors v0.8.1
github.com/tslocum/gotk3 v0.0.0-20190727120037-6cb68ea19890
golang.org/x/sys v0.0.0-20190712062909-fae7ac547cb7 // indirect
)

20
go.sum
View File

@ -1,24 +1,20 @@
git.sr.ht/~tslocum/desktop v0.1.1 h1:hS1DgT1Ur0DR42Z4vr+Zsasjjd8M9PVwIEmeAd1xLS4=
git.sr.ht/~tslocum/desktop v0.1.1/go.mod h1:cUn0Q8ALjkAq40qSei795yN3CfO5pkeYKo2gmzaZ2SI=
github.com/gotk3/gotk3 v0.0.0-20190620081259-6dcdf9e5c51e h1:KFy3swDjmbaSAE6b1iExIgsYt0OkfoLP3HjLm4ifSR8=
github.com/gotk3/gotk3 v0.0.0-20190620081259-6dcdf9e5c51e/go.mod h1:Eew3QBwAOBTrfFFDmsDE5wZWbcagBL1NUslj1GhRveo=
github.com/gotk3/gotk3 v0.0.0-20190809225113-dc58eba1cccc h1:QtXtC6AdJ57L/rw/YMF41a+6YmLTy92IvxsCjDp4dYE=
github.com/gotk3/gotk3 v0.0.0-20190809225113-dc58eba1cccc/go.mod h1:Eew3QBwAOBTrfFFDmsDE5wZWbcagBL1NUslj1GhRveo=
github.com/jroimartin/gocui v0.4.0 h1:52jnalstgmc25FmtGcWqa0tcbMEWS6RpFLsOIO+I+E8=
github.com/jroimartin/gocui v0.4.0/go.mod h1:7i7bbj99OgFHzo7kB2zPb8pXLqMBSQegY7azfqXMkyY=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
github.com/lithammer/fuzzysearch v1.0.2 h1:AjCE2iwc5y+8K+h2nXVc0Pmrpjvu+JVqMgiZ0oakXDM=
github.com/lithammer/fuzzysearch v1.0.2/go.mod h1:bvAJyokfCQ7Vknrd4Kgc+izmMrPj5CiBAu2t6rK1Kak=
github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.9 h1:d5US/mDsogSGW37IV293h//ZFaeajb69h+EHFsv2xGg=
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
github.com/mattn/go-runewidth v0.0.4 h1:2BvfKmzob6Bmd4YsL0zygOqfdFnK7GR4QL06Do4/p7Y=
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/nsf/termbox-go v0.0.0-20190624072549-eeb6cd0a1762 h1:44Lv0bNi88GweB54TCjB/lEJgp+2Ze5WFpwNu0nh0ag=
github.com/nsf/termbox-go v0.0.0-20190624072549-eeb6cd0a1762/go.mod h1:IuKpRQcYE1Tfu+oAQqaLisqDeXgjyyltCfsaoYN18NQ=
github.com/nsf/termbox-go v0.0.0-20190817171036-93860e161317 h1:hhGN4SFXgXo61Q4Sjj/X9sBjyeSa2kdpaOzCO+8EVQw=
github.com/nsf/termbox-go v0.0.0-20190817171036-93860e161317/go.mod h1:IuKpRQcYE1Tfu+oAQqaLisqDeXgjyyltCfsaoYN18NQ=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/tslocum/gotk3 v0.0.0-20190727120037-6cb68ea19890 h1:E4ShJr9mkfrOgv2gJfLhuc8s1uFfWsTG24h2JDIjrVs=
github.com/tslocum/gotk3 v0.0.0-20190727120037-6cb68ea19890/go.mod h1:lKW1BMxjgJ9vFqOZ2pNvC6p6ln6en3KqvV4KRt5FLB4=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223 h1:DH4skfRX4EBpamg7iV4ZlCpblAHI6s6TDM39bFZumv8=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190712062909-fae7ac547cb7 h1:LepdCS8Gf/MVejFIt8lsiexZATdoGVyp5bcyS+rYoUI=
golang.org/x/sys v0.0.0-20190712062909-fae7ac547cb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a h1:aYOabOQFp6Vj6W1F80affTUvO9UxmJRx8K0gsfABByQ=
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=