mirror of
https://gitee.com/energye/energy.git
synced 2024-12-15 01:41:49 +08:00
473 lines
11 KiB
Go
473 lines
11 KiB
Go
//----------------------------------------
|
|
//
|
|
// Copyright © yanghy. All Rights Reserved.
|
|
//
|
|
// Licensed under Apache License Version 2.0, January 2004
|
|
//
|
|
// https://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
//----------------------------------------
|
|
|
|
//go:build linux || freebsd || openbsd || netbsd
|
|
// +build linux freebsd openbsd netbsd
|
|
|
|
//Note that you need to have github.com/knightpp/dbus-codegen-go installed from "custom" branch
|
|
//go:generate dbus-codegen-go -prefix org.kde -package notifier -output internal/generated/notifier/status_notifier_item.go internal/StatusNotifierItem.xml
|
|
//go:generate dbus-codegen-go -prefix com.canonical -package menu -output internal/generated/menu/dbus_menu.go internal/DbusMenu.xml
|
|
|
|
package systray
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"image"
|
|
_ "image/png" // used only here
|
|
"log"
|
|
"os"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/godbus/dbus/v5/introspect"
|
|
"github.com/godbus/dbus/v5/prop"
|
|
|
|
"github.com/energye/energy/v2/pkgs/systray/internal/generated/menu"
|
|
"github.com/energye/energy/v2/pkgs/systray/internal/generated/notifier"
|
|
dbus "github.com/godbus/dbus/v5"
|
|
)
|
|
|
|
const (
|
|
path = "/StatusNotifierItem"
|
|
menuPath = "/StatusNotifierMenu"
|
|
)
|
|
|
|
var (
|
|
// to signal quitting the internal main loop
|
|
quitChan = make(chan struct{})
|
|
|
|
// instance is the current instance of our DBus tray server
|
|
instance = &tray{menu: &menuLayout{}, menuVersion: 1}
|
|
)
|
|
|
|
// SetTemplateIcon sets the systray icon as a template icon (on macOS), falling back
|
|
// to a regular icon on other platforms.
|
|
// templateIconBytes and iconBytes should be the content of .ico for windows and
|
|
// .ico/.jpg/.png for other platforms.
|
|
func SetTemplateIcon(templateIconBytes []byte, regularIconBytes []byte) {
|
|
// TODO handle the templateIconBytes?
|
|
SetIcon(regularIconBytes)
|
|
}
|
|
|
|
// SetIcon sets the systray icon.
|
|
// iconBytes should be the content of .ico for windows and .ico/.jpg/.png
|
|
// for other platforms.
|
|
func SetIcon(iconBytes []byte) {
|
|
instance.lock.Lock()
|
|
instance.iconData = iconBytes
|
|
props := instance.props
|
|
conn := instance.conn
|
|
defer instance.lock.Unlock()
|
|
|
|
if props == nil {
|
|
return
|
|
}
|
|
|
|
props.SetMust("org.kde.StatusNotifierItem", "IconPixmap",
|
|
[]PX{convertToPixels(iconBytes)})
|
|
if conn == nil {
|
|
return
|
|
}
|
|
|
|
err := notifier.Emit(conn, ¬ifier.StatusNotifierItem_NewIconSignal{
|
|
Path: path,
|
|
Body: ¬ifier.StatusNotifierItem_NewIconSignalBody{},
|
|
})
|
|
if err != nil {
|
|
log.Printf("systray error: failed to emit new icon signal: %s\n", err)
|
|
return
|
|
}
|
|
}
|
|
|
|
// SetTitle sets the systray title, only available on Mac and Linux.
|
|
func SetTitle(t string) {
|
|
instance.lock.Lock()
|
|
instance.title = t
|
|
props := instance.props
|
|
conn := instance.conn
|
|
defer instance.lock.Unlock()
|
|
|
|
if props == nil {
|
|
return
|
|
}
|
|
dbusErr := props.Set("org.kde.StatusNotifierItem", "Title",
|
|
dbus.MakeVariant(t))
|
|
if dbusErr != nil {
|
|
log.Printf("systray error: failed to set Title prop: %s\n", dbusErr)
|
|
return
|
|
}
|
|
|
|
if conn == nil {
|
|
return
|
|
}
|
|
|
|
err := notifier.Emit(conn, ¬ifier.StatusNotifierItem_NewTitleSignal{
|
|
Path: path,
|
|
Body: ¬ifier.StatusNotifierItem_NewTitleSignalBody{},
|
|
})
|
|
if err != nil {
|
|
log.Printf("systray error: failed to emit new title signal: %s\n", err)
|
|
return
|
|
}
|
|
}
|
|
|
|
// SetTooltip sets the systray tooltip to display on mouse hover of the tray icon,
|
|
// only available on Mac and Windows.
|
|
func SetTooltip(tooltipTitle string) {
|
|
instance.lock.Lock()
|
|
instance.tooltipTitle = tooltipTitle
|
|
props := instance.props
|
|
defer instance.lock.Unlock()
|
|
|
|
if props == nil {
|
|
return
|
|
}
|
|
dbusErr := props.Set("org.kde.StatusNotifierItem", "ToolTip",
|
|
dbus.MakeVariant(tooltip{V2: tooltipTitle}))
|
|
if dbusErr != nil {
|
|
log.Printf("systray error: failed to set ToolTip prop: %s\n", dbusErr)
|
|
return
|
|
}
|
|
}
|
|
|
|
// SetTemplateIcon sets the icon of a menu item as a template icon (on macOS). On Windows and
|
|
// Linux, it falls back to the regular icon bytes.
|
|
// templateIconBytes and regularIconBytes should be the content of .ico for windows and
|
|
// .ico/.jpg/.png for other platforms.
|
|
func (item *MenuItem) SetTemplateIcon(templateIconBytes []byte, regularIconBytes []byte) {
|
|
item.SetIcon(regularIconBytes)
|
|
}
|
|
|
|
func setInternalLoop(_ bool) {
|
|
// nothing to action on Linux
|
|
}
|
|
|
|
func registerSystray() {
|
|
}
|
|
|
|
func nativeLoop() int {
|
|
nativeStart()
|
|
<-quitChan
|
|
nativeEnd()
|
|
return 0
|
|
}
|
|
|
|
func nativeEnd() {
|
|
systrayExit()
|
|
instance.conn.Close()
|
|
}
|
|
|
|
func quit() {
|
|
close(quitChan)
|
|
}
|
|
|
|
var usni = &UnimplementedStatusNotifierItem{}
|
|
|
|
type UnimplementedStatusNotifierItem struct {
|
|
contextMenu func(x int32, y int32)
|
|
activate func(x int32, y int32)
|
|
dActivate func(x int32, y int32)
|
|
secondaryActivate func(x int32, y int32)
|
|
scroll func(delta int32, orientation string)
|
|
dActivateTime int64
|
|
}
|
|
|
|
func (*UnimplementedStatusNotifierItem) iface() string {
|
|
return notifier.InterfaceStatusNotifierItem
|
|
}
|
|
|
|
func (m *UnimplementedStatusNotifierItem) ContextMenu(x int32, y int32) (err *dbus.Error) {
|
|
if m.contextMenu != nil {
|
|
m.contextMenu(x, y)
|
|
} else {
|
|
err = &dbus.ErrMsgUnknownMethod
|
|
}
|
|
return
|
|
}
|
|
|
|
func (m *UnimplementedStatusNotifierItem) Activate(x int32, y int32) (err *dbus.Error) {
|
|
if m.dActivateTime == 0 {
|
|
m.dActivateTime = time.Now().UnixMilli()
|
|
} else {
|
|
nowMilli := time.Now().UnixMilli()
|
|
if nowMilli-m.dActivateTime < dClickTimeMinInterval {
|
|
m.dActivateTime = dClickTimeMinInterval
|
|
if m.dActivate != nil {
|
|
m.dActivate(x, y)
|
|
return
|
|
}
|
|
} else {
|
|
m.dActivateTime = nowMilli
|
|
}
|
|
}
|
|
|
|
if m.activate != nil {
|
|
m.activate(x, y)
|
|
} else {
|
|
err = &dbus.ErrMsgUnknownMethod
|
|
}
|
|
return
|
|
}
|
|
|
|
func (m *UnimplementedStatusNotifierItem) SecondaryActivate(x int32, y int32) (err *dbus.Error) {
|
|
if m.secondaryActivate != nil {
|
|
m.secondaryActivate(x, y)
|
|
} else {
|
|
err = &dbus.ErrMsgUnknownMethod
|
|
}
|
|
return
|
|
}
|
|
|
|
func (m *UnimplementedStatusNotifierItem) Scroll(delta int32, orientation string) (err *dbus.Error) {
|
|
if m.scroll != nil {
|
|
m.scroll(delta, orientation)
|
|
} else {
|
|
err = &dbus.ErrMsgUnknownMethod
|
|
}
|
|
return
|
|
}
|
|
|
|
func setOnClick(fn func()) {
|
|
usni.activate = func(x int32, y int32) {
|
|
fn()
|
|
}
|
|
}
|
|
|
|
func setOnDClick(fn func()) {
|
|
usni.dActivate = func(x int32, y int32) {
|
|
fn()
|
|
}
|
|
}
|
|
|
|
func setOnRClick(dClick func(IMenu)) {
|
|
}
|
|
|
|
func nativeStart() {
|
|
if systrayReady != nil {
|
|
systrayReady()
|
|
}
|
|
conn, err := dbus.SessionBus()
|
|
if err != nil {
|
|
log.Printf("systray error: failed to connect to DBus: %v\n", err)
|
|
return
|
|
}
|
|
err = notifier.ExportStatusNotifierItem(conn, path, ¬ifier.UnimplementedStatusNotifierItem{})
|
|
if err != nil {
|
|
log.Printf("systray error: failed to export status notifier item: %v\n", err)
|
|
}
|
|
err = menu.ExportDbusmenu(conn, menuPath, instance)
|
|
if err != nil {
|
|
log.Printf("systray error: failed to export status notifier menu: %v\n", err)
|
|
return
|
|
}
|
|
|
|
name := fmt.Sprintf("org.kde.StatusNotifierItem-%d-1", os.Getpid()) // register id 1 for this process
|
|
_, err = conn.RequestName(name, dbus.NameFlagDoNotQueue)
|
|
if err != nil {
|
|
log.Printf("systray error: failed to request name: %s\n", err)
|
|
// it's not critical error: continue
|
|
}
|
|
props, err := prop.Export(conn, path, instance.createPropSpec())
|
|
if err != nil {
|
|
log.Printf("systray error: failed to export notifier item properties to bus: %s\n", err)
|
|
return
|
|
}
|
|
menuProps, err := prop.Export(conn, menuPath, createMenuPropSpec())
|
|
if err != nil {
|
|
log.Printf("systray error: failed to export notifier menu properties to bus: %s\n", err)
|
|
return
|
|
}
|
|
|
|
node := introspect.Node{
|
|
Name: path,
|
|
Interfaces: []introspect.Interface{
|
|
introspect.IntrospectData,
|
|
prop.IntrospectData,
|
|
notifier.IntrospectDataStatusNotifierItem,
|
|
},
|
|
}
|
|
err = conn.Export(introspect.NewIntrospectable(&node), path,
|
|
"org.freedesktop.DBus.Introspectable")
|
|
if err != nil {
|
|
log.Printf("systray error: failed to export node introspection: %s\n", err)
|
|
return
|
|
}
|
|
|
|
menuNode := introspect.Node{
|
|
Name: menuPath,
|
|
Interfaces: []introspect.Interface{
|
|
introspect.IntrospectData,
|
|
prop.IntrospectData,
|
|
menu.IntrospectDataDbusmenu,
|
|
},
|
|
}
|
|
err = conn.Export(introspect.NewIntrospectable(&menuNode), menuPath,
|
|
"org.freedesktop.DBus.Introspectable")
|
|
if err != nil {
|
|
log.Printf("systray error: failed to export menu node introspection: %s\n", err)
|
|
return
|
|
}
|
|
|
|
instance.lock.Lock()
|
|
instance.conn = conn
|
|
instance.props = props
|
|
instance.menuProps = menuProps
|
|
instance.lock.Unlock()
|
|
|
|
var (
|
|
obj dbus.BusObject
|
|
call *dbus.Call
|
|
)
|
|
obj = conn.Object("org.kde.StatusNotifierWatcher", "/StatusNotifierWatcher")
|
|
call = obj.Call("org.kde.StatusNotifierWatcher.RegisterStatusNotifierItem", 0, path)
|
|
if call.Err != nil {
|
|
log.Printf("systray error: failed to register our icon with the notifier watcher (maybe no tray is running?): %s\n", call.Err)
|
|
}
|
|
}
|
|
|
|
// tray is a basic type that handles the dbus functionality
|
|
type tray struct {
|
|
// the DBus connection that we will use
|
|
conn *dbus.Conn
|
|
|
|
// icon data for the main systray icon
|
|
iconData []byte
|
|
// title and tooltip state
|
|
title, tooltipTitle string
|
|
|
|
lock sync.Mutex
|
|
menu *menuLayout
|
|
menuLock sync.RWMutex
|
|
props, menuProps *prop.Properties
|
|
menuVersion uint32
|
|
}
|
|
|
|
func (*tray) iface() string {
|
|
return notifier.InterfaceStatusNotifierItem
|
|
}
|
|
|
|
func (t *tray) createPropSpec() map[string]map[string]*prop.Prop {
|
|
t.lock.Lock()
|
|
t.lock.Unlock()
|
|
return map[string]map[string]*prop.Prop{
|
|
"org.kde.StatusNotifierItem": {
|
|
"Status": {
|
|
Value: "Active", // Passive, Active or NeedsAttention
|
|
Writable: false,
|
|
Emit: prop.EmitTrue,
|
|
Callback: nil,
|
|
},
|
|
"Title": {
|
|
Value: t.title,
|
|
Writable: true,
|
|
Emit: prop.EmitTrue,
|
|
Callback: nil,
|
|
},
|
|
"Id": {
|
|
Value: "1",
|
|
Writable: false,
|
|
Emit: prop.EmitTrue,
|
|
Callback: nil,
|
|
},
|
|
"Category": {
|
|
Value: "ApplicationStatus",
|
|
Writable: false,
|
|
Emit: prop.EmitTrue,
|
|
Callback: nil,
|
|
},
|
|
"IconName": {
|
|
Value: "",
|
|
Writable: false,
|
|
Emit: prop.EmitTrue,
|
|
Callback: nil,
|
|
},
|
|
"IconPixmap": {
|
|
Value: []PX{convertToPixels(t.iconData)},
|
|
Writable: true,
|
|
Emit: prop.EmitTrue,
|
|
Callback: nil,
|
|
},
|
|
"IconThemePath": {
|
|
Value: "",
|
|
Writable: false,
|
|
Emit: prop.EmitTrue,
|
|
Callback: nil,
|
|
},
|
|
"ItemIsMenu": {
|
|
Value: true,
|
|
Writable: false,
|
|
Emit: prop.EmitTrue,
|
|
Callback: nil,
|
|
},
|
|
"Menu": {
|
|
Value: dbus.ObjectPath(menuPath),
|
|
Writable: true,
|
|
Emit: prop.EmitTrue,
|
|
Callback: nil,
|
|
},
|
|
"ToolTip": {
|
|
Value: tooltip{V2: t.tooltipTitle},
|
|
Writable: true,
|
|
Emit: prop.EmitTrue,
|
|
Callback: nil,
|
|
},
|
|
}}
|
|
}
|
|
|
|
// PX is picture pix map structure with width and high
|
|
type PX struct {
|
|
W, H int
|
|
Pix []byte
|
|
}
|
|
|
|
// tooltip is our data for a tooltip property.
|
|
// Param names need to match the generated code...
|
|
type tooltip = struct {
|
|
V0 string // name
|
|
V1 []PX // icons
|
|
V2 string // title
|
|
V3 string // description
|
|
}
|
|
|
|
func convertToPixels(data []byte) PX {
|
|
if len(data) == 0 {
|
|
return PX{}
|
|
}
|
|
|
|
img, _, err := image.Decode(bytes.NewReader(data))
|
|
if err != nil {
|
|
log.Printf("Failed to read icon format %v", err)
|
|
return PX{}
|
|
}
|
|
|
|
return PX{
|
|
img.Bounds().Dx(), img.Bounds().Dy(),
|
|
argbForImage(img),
|
|
}
|
|
}
|
|
|
|
func argbForImage(img image.Image) []byte {
|
|
w, h := img.Bounds().Dx(), img.Bounds().Dy()
|
|
data := make([]byte, w*h*4)
|
|
i := 0
|
|
for y := 0; y < h; y++ {
|
|
for x := 0; x < w; x++ {
|
|
r, g, b, a := img.At(x, y).RGBA()
|
|
data[i] = byte(a)
|
|
data[i+1] = byte(r)
|
|
data[i+2] = byte(g)
|
|
data[i+3] = byte(b)
|
|
i += 4
|
|
}
|
|
}
|
|
return data
|
|
}
|