feat(systray): wire tray mutations and submenus
Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
a0cad39fbb
commit
573eb5216a
11 changed files with 126 additions and 14 deletions
|
|
@ -667,6 +667,18 @@ func (s *Service) handleWSMessage(msg WSMessage) (any, bool, error) {
|
|||
Title: title,
|
||||
Message: message,
|
||||
})
|
||||
case "tray:set-tooltip":
|
||||
tooltip, e := wsRequire(msg.Data, "tooltip")
|
||||
if e != nil {
|
||||
return nil, false, e
|
||||
}
|
||||
result, handled, err = s.Core().PERFORM(systray.TaskSetTooltip{Tooltip: tooltip})
|
||||
case "tray:set-label":
|
||||
label, e := wsRequire(msg.Data, "label")
|
||||
if e != nil {
|
||||
return nil, false, e
|
||||
}
|
||||
result, handled, err = s.Core().PERFORM(systray.TaskSetLabel{Label: label})
|
||||
case "dialog:prompt":
|
||||
title, e := wsRequire(msg.Data, "title")
|
||||
if e != nil {
|
||||
|
|
|
|||
|
|
@ -786,6 +786,24 @@ func TestHandleWSMessage_Extended_Good(t *testing.T) {
|
|||
assert.True(t, handled)
|
||||
})
|
||||
|
||||
t.Run("tray tooltip", func(t *testing.T) {
|
||||
_, handled, err := svc.handleWSMessage(WSMessage{
|
||||
Action: "tray:set-tooltip",
|
||||
Data: map[string]any{"tooltip": "Updated"},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.True(t, handled)
|
||||
})
|
||||
|
||||
t.Run("tray label", func(t *testing.T) {
|
||||
_, handled, err := svc.handleWSMessage(WSMessage{
|
||||
Action: "tray:set-label",
|
||||
Data: map[string]any{"label": "Updated"},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.True(t, handled)
|
||||
})
|
||||
|
||||
t.Run("prompt dialog", func(t *testing.T) {
|
||||
result, handled, err := svc.handleWSMessage(WSMessage{
|
||||
Action: "dialog:prompt",
|
||||
|
|
|
|||
|
|
@ -36,8 +36,10 @@ type TraySetTooltipOutput struct {
|
|||
}
|
||||
|
||||
func (s *Subsystem) traySetTooltip(_ context.Context, _ *mcp.CallToolRequest, input TraySetTooltipInput) (*mcp.CallToolResult, TraySetTooltipOutput, error) {
|
||||
// Tooltip is set via the tray menu items; for now this is a no-op placeholder
|
||||
_ = input.Tooltip
|
||||
_, _, err := s.core.PERFORM(systray.TaskSetTooltip{Tooltip: input.Tooltip})
|
||||
if err != nil {
|
||||
return nil, TraySetTooltipOutput{}, err
|
||||
}
|
||||
return nil, TraySetTooltipOutput{Success: true}, nil
|
||||
}
|
||||
|
||||
|
|
@ -51,8 +53,10 @@ type TraySetLabelOutput struct {
|
|||
}
|
||||
|
||||
func (s *Subsystem) traySetLabel(_ context.Context, _ *mcp.CallToolRequest, input TraySetLabelInput) (*mcp.CallToolResult, TraySetLabelOutput, error) {
|
||||
// Label is part of the tray configuration; placeholder for now
|
||||
_ = input.Label
|
||||
_, _, err := s.core.PERFORM(systray.TaskSetLabel{Label: input.Label})
|
||||
if err != nil {
|
||||
return nil, TraySetLabelOutput{}, err
|
||||
}
|
||||
return nil, TraySetLabelOutput{Success: true}, nil
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -16,16 +16,19 @@ func (m *Manager) SetMenu(items []TrayMenuItem) error {
|
|||
// buildMenu recursively builds a PlatformMenu from TrayMenuItem descriptors.
|
||||
func (m *Manager) buildMenu(items []TrayMenuItem) PlatformMenu {
|
||||
menu := m.platform.NewMenu()
|
||||
m.buildMenuInto(menu, items)
|
||||
return menu
|
||||
}
|
||||
|
||||
func (m *Manager) buildMenuInto(menu PlatformMenu, items []TrayMenuItem) {
|
||||
for _, item := range items {
|
||||
if item.Type == "separator" {
|
||||
menu.AddSeparator()
|
||||
continue
|
||||
}
|
||||
if len(item.Submenu) > 0 {
|
||||
sub := m.buildMenu(item.Submenu)
|
||||
mi := menu.Add(item.Label)
|
||||
_ = mi.AddSubmenu()
|
||||
_ = sub // TODO: wire sub into parent via platform
|
||||
sub := menu.AddSubmenu(item.Label)
|
||||
m.buildMenuInto(sub, item.Submenu)
|
||||
continue
|
||||
}
|
||||
mi := menu.Add(item.Label)
|
||||
|
|
@ -47,7 +50,6 @@ func (m *Manager) buildMenu(items []TrayMenuItem) PlatformMenu {
|
|||
})
|
||||
}
|
||||
}
|
||||
return menu
|
||||
}
|
||||
|
||||
// RegisterCallback registers a callback for a menu action ID.
|
||||
|
|
|
|||
|
|
@ -9,6 +9,12 @@ type QueryConfig struct{}
|
|||
// TaskSetTrayIcon sets the tray icon.
|
||||
type TaskSetTrayIcon struct{ Data []byte }
|
||||
|
||||
// TaskSetTooltip updates the tray tooltip text.
|
||||
type TaskSetTooltip struct{ Tooltip string }
|
||||
|
||||
// TaskSetLabel updates the tray label text.
|
||||
type TaskSetLabel struct{ Label string }
|
||||
|
||||
// TaskSetTrayMenu sets the tray menu items.
|
||||
type TaskSetTrayMenu struct{ Items []TrayMenuItem }
|
||||
|
||||
|
|
|
|||
|
|
@ -20,7 +20,10 @@ func (t *exportedMockTray) SetLabel(text string) { t.label = text }
|
|||
func (t *exportedMockTray) SetMenu(menu PlatformMenu) {}
|
||||
func (t *exportedMockTray) AttachWindow(w WindowHandle) {}
|
||||
|
||||
type exportedMockMenu struct{ items []exportedMockMenuItem }
|
||||
type exportedMockMenu struct {
|
||||
items []exportedMockMenuItem
|
||||
submenus []*exportedMockMenu
|
||||
}
|
||||
|
||||
func (m *exportedMockMenu) Add(label string) PlatformMenuItem {
|
||||
mi := &exportedMockMenuItem{label: label}
|
||||
|
|
@ -28,6 +31,12 @@ func (m *exportedMockMenu) Add(label string) PlatformMenuItem {
|
|||
return mi
|
||||
}
|
||||
func (m *exportedMockMenu) AddSeparator() {}
|
||||
func (m *exportedMockMenu) AddSubmenu(label string) PlatformMenu {
|
||||
sub := &exportedMockMenu{}
|
||||
m.items = append(m.items, exportedMockMenuItem{label: label})
|
||||
m.submenus = append(m.submenus, sub)
|
||||
return sub
|
||||
}
|
||||
|
||||
type exportedMockMenuItem struct {
|
||||
label, tooltip string
|
||||
|
|
|
|||
|
|
@ -21,7 +21,8 @@ func (p *mockPlatform) NewMenu() PlatformMenu {
|
|||
}
|
||||
|
||||
type mockTrayMenu struct {
|
||||
items []string
|
||||
items []string
|
||||
submenus []*mockTrayMenu
|
||||
}
|
||||
|
||||
func (m *mockTrayMenu) Add(label string) PlatformMenuItem {
|
||||
|
|
@ -29,10 +30,16 @@ func (m *mockTrayMenu) Add(label string) PlatformMenuItem {
|
|||
return &mockTrayMenuItem{}
|
||||
}
|
||||
func (m *mockTrayMenu) AddSeparator() { m.items = append(m.items, "---") }
|
||||
func (m *mockTrayMenu) AddSubmenu(label string) PlatformMenu {
|
||||
m.items = append(m.items, label)
|
||||
sub := &mockTrayMenu{}
|
||||
m.submenus = append(m.submenus, sub)
|
||||
return sub
|
||||
}
|
||||
|
||||
type mockTrayMenuItem struct{}
|
||||
|
||||
func (mi *mockTrayMenuItem) SetTooltip(text string) {}
|
||||
func (mi *mockTrayMenuItem) SetTooltip(text string) {}
|
||||
func (mi *mockTrayMenuItem) SetChecked(checked bool) {}
|
||||
func (mi *mockTrayMenuItem) SetEnabled(enabled bool) {}
|
||||
func (mi *mockTrayMenuItem) OnClick(fn func()) {}
|
||||
|
|
@ -45,9 +52,9 @@ type mockTray struct {
|
|||
attachedWindow WindowHandle
|
||||
}
|
||||
|
||||
func (t *mockTray) SetIcon(data []byte) { t.icon = data }
|
||||
func (t *mockTray) SetIcon(data []byte) { t.icon = data }
|
||||
func (t *mockTray) SetTemplateIcon(data []byte) { t.templateIcon = data }
|
||||
func (t *mockTray) SetTooltip(text string) { t.tooltip = text }
|
||||
func (t *mockTray) SetLabel(text string) { t.label = text }
|
||||
func (t *mockTray) SetMenu(menu PlatformMenu) { t.menu = menu }
|
||||
func (t *mockTray) AttachWindow(w WindowHandle) { t.attachedWindow = w }
|
||||
func (t *mockTray) AttachWindow(w WindowHandle) { t.attachedWindow = w }
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ type PlatformTray interface {
|
|||
type PlatformMenu interface {
|
||||
Add(label string) PlatformMenuItem
|
||||
AddSeparator()
|
||||
AddSubmenu(label string) PlatformMenu
|
||||
}
|
||||
|
||||
// PlatformMenuItem is a single item in a tray menu.
|
||||
|
|
|
|||
|
|
@ -54,6 +54,10 @@ func (s *Service) handleTask(c *core.Core, t core.Task) (any, bool, error) {
|
|||
switch t := t.(type) {
|
||||
case TaskSetTrayIcon:
|
||||
return nil, true, s.manager.SetIcon(t.Data)
|
||||
case TaskSetTooltip:
|
||||
return nil, true, s.manager.SetTooltip(t.Tooltip)
|
||||
case TaskSetLabel:
|
||||
return nil, true, s.manager.SetLabel(t.Label)
|
||||
case TaskSetTrayMenu:
|
||||
return nil, true, s.taskSetTrayMenu(t)
|
||||
case TaskShowPanel:
|
||||
|
|
|
|||
|
|
@ -39,6 +39,24 @@ func TestTaskSetTrayIcon_Good(t *testing.T) {
|
|||
assert.True(t, handled)
|
||||
}
|
||||
|
||||
func TestTaskSetTooltip_Good(t *testing.T) {
|
||||
svc, c := newTestSystrayService(t)
|
||||
require.NoError(t, svc.manager.Setup("Test", "Test"))
|
||||
|
||||
_, handled, err := c.PERFORM(TaskSetTooltip{Tooltip: "Updated"})
|
||||
require.NoError(t, err)
|
||||
assert.True(t, handled)
|
||||
}
|
||||
|
||||
func TestTaskSetLabel_Good(t *testing.T) {
|
||||
svc, c := newTestSystrayService(t)
|
||||
require.NoError(t, svc.manager.Setup("Test", "Test"))
|
||||
|
||||
_, handled, err := c.PERFORM(TaskSetLabel{Label: "Updated"})
|
||||
require.NoError(t, err)
|
||||
assert.True(t, handled)
|
||||
}
|
||||
|
||||
func TestTaskSetTrayMenu_Good(t *testing.T) {
|
||||
svc, c := newTestSystrayService(t)
|
||||
|
||||
|
|
@ -54,6 +72,33 @@ func TestTaskSetTrayMenu_Good(t *testing.T) {
|
|||
assert.True(t, handled)
|
||||
}
|
||||
|
||||
func TestTaskSetTrayMenu_Submenu_Good(t *testing.T) {
|
||||
p := newMockPlatform()
|
||||
c, err := core.New(
|
||||
core.WithService(Register(p)),
|
||||
core.WithServiceLock(),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, c.ServiceStartup(context.Background(), nil))
|
||||
|
||||
svc := core.MustServiceFor[*Service](c, "systray")
|
||||
require.NoError(t, svc.manager.Setup("Test", "Test"))
|
||||
|
||||
_, handled, err := c.PERFORM(TaskSetTrayMenu{Items: []TrayMenuItem{
|
||||
{
|
||||
Label: "File",
|
||||
Submenu: []TrayMenuItem{
|
||||
{Label: "Open", ActionID: "open"},
|
||||
},
|
||||
},
|
||||
}})
|
||||
require.NoError(t, err)
|
||||
assert.True(t, handled)
|
||||
require.Len(t, p.trays, 1)
|
||||
require.NotEmpty(t, p.menus)
|
||||
require.Len(t, p.menus[0].submenus, 1)
|
||||
}
|
||||
|
||||
func TestTaskSetTrayIcon_Bad(t *testing.T) {
|
||||
// No systray service — PERFORM returns handled=false
|
||||
c, err := core.New(core.WithServiceLock())
|
||||
|
|
|
|||
|
|
@ -56,6 +56,10 @@ func (m *wailsTrayMenu) AddSeparator() {
|
|||
m.menu.AddSeparator()
|
||||
}
|
||||
|
||||
func (m *wailsTrayMenu) AddSubmenu(label string) PlatformMenu {
|
||||
return &wailsTrayMenu{menu: m.menu.AddSubmenu(label)}
|
||||
}
|
||||
|
||||
// wailsTrayMenuItem wraps *application.MenuItem for the PlatformMenuItem interface.
|
||||
type wailsTrayMenuItem struct {
|
||||
item *application.MenuItem
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue