Skip to content

Commit 28a2db7

Browse files
committed
Update Menu to support nested submenus, backends now reuse menubar code
1 parent d348bad commit 28a2db7

File tree

15 files changed

+201
-174
lines changed

15 files changed

+201
-174
lines changed

Sources/AppKitBackend/AppKitBackend.swift

Lines changed: 13 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -100,9 +100,8 @@ public final class AppKitBackend: AppBackend {
100100
window.makeKeyAndOrderFront(nil)
101101
}
102102

103-
private static func renderSubmenu(_ submenu: ResolvedMenu.Submenu) -> NSMenuItem {
104-
let renderedMenu = NSMenu()
105-
for item in submenu.content.items {
103+
private static func renderMenuItems(_ items: [ResolvedMenu.Item]) -> [NSMenuItem] {
104+
items.map { item in
106105
switch item {
107106
case .button(let label, let action):
108107
// Custom subclass is used to keep strong reference to action
@@ -118,11 +117,18 @@ public final class AppKitBackend: AppBackend {
118117
renderedItem.action = #selector(wrappedAction.run)
119118
renderedItem.target = wrappedAction
120119
}
121-
renderedMenu.addItem(renderedItem)
120+
return renderedItem
122121
case .submenu(let submenu):
123-
renderedMenu.addItem(renderSubmenu(submenu))
122+
return renderSubmenu(submenu)
124123
}
125124
}
125+
}
126+
127+
private static func renderSubmenu(_ submenu: ResolvedMenu.Submenu) -> NSMenuItem {
128+
let renderedMenu = NSMenu()
129+
for item in renderMenuItems(submenu.content.items) {
130+
renderedMenu.addItem(item)
131+
}
126132

127133
let menuItem = NSMenuItem()
128134
menuItem.title = submenu.label
@@ -841,21 +847,11 @@ public final class AppKitBackend: AppBackend {
841847

842848
public func updatePopoverMenu(
843849
_ menu: Menu,
844-
items: [(String, () -> Void)],
850+
content: ResolvedMenu,
845851
environment: Environment
846852
) {
847853
menu.appearance = environment.colorScheme.nsAppearance
848-
menu.items = items.map { (label, action) in
849-
let wrappedAction = Action(action)
850-
let menuItem = NSCustomMenuItem(
851-
title: label,
852-
action: #selector(wrappedAction.run),
853-
keyEquivalent: ""
854-
)
855-
menuItem.target = wrappedAction
856-
menuItem.actionWrapper = wrappedAction
857-
return menuItem
858-
}
854+
menu.items = Self.renderMenuItems(content.items)
859855
}
860856

861857
public func showPopoverMenu(
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
public protocol GActionGroup {
2+
var actionGroupPointer: OpaquePointer { get }
3+
}

Sources/Gtk/Utility/GMenu.swift

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,9 @@ import CGtk
22

33
public class GMenu {
44
var pointer: OpaquePointer
5-
var actionMap: any GActionMap
65

7-
public init(actionMap: any GActionMap) {
6+
public init() {
87
pointer = g_menu_new()
9-
self.actionMap = actionMap
108
}
119

1210
public func appendItem(label: String, actionName: String) {
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import CGtk
2+
3+
public class GSimpleActionGroup: GActionMap, GActionGroup {
4+
var pointer: UnsafeMutablePointer<CGtk.GSimpleActionGroup>
5+
6+
public var actionMapPointer: OpaquePointer {
7+
OpaquePointer(pointer)
8+
}
9+
10+
public var actionGroupPointer: OpaquePointer {
11+
OpaquePointer(pointer)
12+
}
13+
14+
public init() {
15+
pointer = g_simple_action_group_new()
16+
}
17+
}

Sources/Gtk/Widgets/PopoverMenu.swift

Lines changed: 9 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -167,56 +167,18 @@ public class PopoverMenu: Popover {
167167
)
168168
}
169169

170-
private class Action {
171-
var run: () -> Void
172-
173-
init(_ action: @escaping () -> Void) {
174-
run = action
170+
private var _model: GMenu?
171+
public var model: GMenu? {
172+
get {
173+
_model
175174
}
176-
}
177-
178-
public func populate(items: [(String, () -> Void)]) {
179-
let handler:
180-
@convention(c) (UnsafeMutableRawPointer, OpaquePointer, UnsafeMutableRawPointer) -> Void =
181-
{ _, _, data in
182-
let action = Unmanaged<Action>.fromOpaque(data).takeUnretainedValue()
183-
action.run()
184-
}
185-
186-
let model = g_menu_new()
187-
let actionGroup = g_simple_action_group_new()
188-
for (i, (label, action)) in items.enumerated() {
189-
g_menu_append(model, label, "menu.action\(i)")
190-
191-
let action = Action(action)
192-
let actionName = "action\(i)"
193-
let simpleAction = g_simple_action_new(actionName, nil)
194-
195-
g_simple_action_set_enabled(simpleAction, true.toGBoolean())
196-
197-
g_signal_connect_data(
198-
simpleAction.map(UnsafeMutableRawPointer.init),
199-
"activate",
200-
gCallback(handler),
201-
Unmanaged<Action>.passRetained(action).toOpaque(),
202-
{ data, _ in
203-
Unmanaged<Action>.fromOpaque(data!).release()
204-
},
205-
G_CONNECT_AFTER
206-
)
207-
208-
g_action_map_add_action(
209-
actionGroup.map(OpaquePointer.init),
210-
simpleAction
175+
set {
176+
gtk_popover_menu_set_menu_model(
177+
opaquePointer,
178+
(newValue?.pointer).map(UnsafeMutablePointer.init)
211179
)
180+
_model = newValue
212181
}
213-
214-
gtk_popover_menu_set_menu_model(
215-
opaquePointer,
216-
UnsafeMutablePointer<_GMenuModel>(model)
217-
)
218-
219-
gtk_widget_insert_action_group(widgetPointer, "menu", actionGroup.map(OpaquePointer.init))
220182
}
221183

222184
override func didMoveToParent() {

Sources/Gtk/Widgets/Widget.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,14 @@ open class Widget: GObjectRepresentable {
205205
)
206206
}
207207

208+
public func insertActionGroup(_ name: String, _ actionGroup: any GActionGroup) {
209+
gtk_widget_insert_action_group(
210+
widgetPointer,
211+
name,
212+
actionGroup.actionGroupPointer
213+
)
214+
}
215+
208216
@GObjectProperty(named: "name") public var name: String?
209217

210218
@GObjectProperty(named: "opacity") public var opacity: Double
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
public protocol GActionGroup {
2+
var actionGroupPointer: OpaquePointer { get }
3+
}

Sources/Gtk3/Utility/GMenu.swift

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,9 @@ import CGtk3
22

33
public class GMenu {
44
var pointer: OpaquePointer
5-
var actionMap: any GActionMap
65

7-
public init(actionMap: any GActionMap) {
6+
public init() {
87
pointer = g_menu_new()
9-
self.actionMap = actionMap
108
}
119

1210
public func appendItem(label: String, actionName: String) {
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import CGtk3
2+
3+
public class GSimpleActionGroup: GActionMap, GActionGroup {
4+
var pointer: UnsafeMutablePointer<CGtk3.GSimpleActionGroup>
5+
6+
public var actionMapPointer: OpaquePointer {
7+
OpaquePointer(pointer)
8+
}
9+
10+
public var actionGroupPointer: OpaquePointer {
11+
OpaquePointer(pointer)
12+
}
13+
14+
public init() {
15+
pointer = g_simple_action_group_new()
16+
}
17+
}

Sources/Gtk3/Widgets/Menu.swift

Lines changed: 9 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,15 @@ public class Menu: MenuShell {
9696
widgetPointer = gtk_menu_new_from_model(model)
9797
}
9898

99+
public func bindModel(_ model: GMenu) {
100+
gtk_menu_shell_bind_model(
101+
castedPointer(),
102+
UnsafeMutablePointer(model.pointer),
103+
nil,
104+
false.toGBoolean()
105+
)
106+
}
107+
99108
public func popUpAtWidget(_ widget: Widget, relativePosition: SIMD2<Int>) {
100109
setProperty(named: "rect-anchor-dx", newValue: relativePosition.x)
101110
setProperty(named: "rect-anchor-dy", newValue: relativePosition.y)
@@ -119,60 +128,6 @@ public class Menu: MenuShell {
119128
registerSignalHandlers()
120129
}
121130

122-
private class Action {
123-
var run: () -> Void
124-
125-
init(_ action: @escaping () -> Void) {
126-
run = action
127-
}
128-
}
129-
130-
public func populate(items: [(String, () -> Void)]) {
131-
let handler:
132-
@convention(c) (UnsafeMutableRawPointer, OpaquePointer, UnsafeMutableRawPointer) -> Void =
133-
{ _, _, data in
134-
let action = Unmanaged<Action>.fromOpaque(data).takeUnretainedValue()
135-
action.run()
136-
}
137-
138-
let model = g_menu_new()
139-
let actionGroup = g_simple_action_group_new()
140-
for (i, (label, action)) in items.enumerated() {
141-
g_menu_append(model, label, "menu.action\(i)")
142-
143-
let action = Action(action)
144-
let actionName = "action\(i)"
145-
let simpleAction = g_simple_action_new(actionName, nil)
146-
147-
g_simple_action_set_enabled(simpleAction, true.toGBoolean())
148-
149-
g_signal_connect_data(
150-
simpleAction.map(UnsafeMutableRawPointer.init),
151-
"activate",
152-
gCallback(handler),
153-
Unmanaged<Action>.passRetained(action).toOpaque(),
154-
{ data, _ in
155-
Unmanaged<Action>.fromOpaque(data!).release()
156-
},
157-
G_CONNECT_AFTER
158-
)
159-
160-
g_action_map_add_action(
161-
actionGroup.map(OpaquePointer.init),
162-
simpleAction
163-
)
164-
}
165-
166-
gtk_menu_shell_bind_model(
167-
castedPointer(),
168-
UnsafeMutablePointer<_GMenuModel>(model),
169-
nil,
170-
false.toGBoolean()
171-
)
172-
173-
gtk_widget_insert_action_group(widgetPointer, "menu", actionGroup.map(OpaquePointer.init))
174-
}
175-
176131
private func registerSignalHandlers() {
177132
addSignal(name: "hide") { [weak self] in
178133
guard let self = self else { return }

Sources/Gtk3/Widgets/Widget.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,14 @@ open class Widget: GObjectRepresentable {
212212
)
213213
}
214214

215+
public func insertActionGroup(_ name: String, _ actionGroup: any GActionGroup) {
216+
gtk_widget_insert_action_group(
217+
widgetPointer,
218+
name,
219+
actionGroup.actionGroupPointer
220+
)
221+
}
222+
215223
@GObjectProperty(named: "name") public var name: String?
216224

217225
@GObjectProperty(named: "opacity") public var opacity: Double

Sources/Gtk3Backend/Gtk3Backend.swift

Lines changed: 39 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -132,37 +132,52 @@ public final class Gtk3Backend: AppBackend {
132132
}
133133
}
134134

135-
private func renderSubmenu(
136-
_ submenu: ResolvedMenu.Submenu,
137-
actionPrefix: String
135+
private func renderMenu(
136+
_ menu: ResolvedMenu,
137+
actionMap: any GActionMap,
138+
actionNamespace: String,
139+
actionPrefix: String?
138140
) -> GMenu {
139-
let model = GMenu(actionMap: gtkApp)
140-
for (i, item) in submenu.content.items.enumerated() {
141-
let actionName = "\(actionPrefix)_\(i)"
141+
let model = GMenu()
142+
for (i, item) in menu.items.enumerated() {
143+
let actionName =
144+
if let actionPrefix {
145+
"\(actionPrefix)_\(i)"
146+
} else {
147+
"\(i)"
148+
}
149+
142150
switch item {
143151
case .button(let label, let action):
144152
if let action {
145-
gtkApp.addAction(named: actionName, action: action)
153+
actionMap.addAction(named: actionName, action: action)
146154
}
147155

148-
model.appendItem(label: label, actionName: "app.\(actionName)")
156+
model.appendItem(label: label, actionName: "\(actionNamespace).\(actionName)")
149157
case .submenu(let submenu):
150158
model.appendSubmenu(
151159
label: submenu.label,
152-
content: renderSubmenu(submenu, actionPrefix: actionName)
160+
content: renderMenu(
161+
submenu.content,
162+
actionMap: actionMap,
163+
actionNamespace: actionNamespace,
164+
actionPrefix: actionName
165+
)
153166
)
154167
}
155168
}
156169
return model
157170
}
158171

159172
private func renderMenuBar(_ submenus: [ResolvedMenu.Submenu]) -> GMenu {
160-
let model = GMenu(actionMap: gtkApp)
173+
let model = GMenu()
161174
for (i, submenu) in submenus.enumerated() {
162175
model.appendSubmenu(
163176
label: submenu.label,
164-
content: renderSubmenu(
165-
submenu,
177+
content: renderMenu(
178+
submenu.content,
179+
actionMap: gtkApp,
180+
actionNamespace: "app",
166181
actionPrefix: "\(i)"
167182
)
168183
)
@@ -678,10 +693,20 @@ public final class Gtk3Backend: AppBackend {
678693

679694
public func updatePopoverMenu(
680695
_ menu: Menu,
681-
items: [(String, () -> Void)],
696+
content: ResolvedMenu,
682697
environment: Environment
683698
) {
684-
menu.populate(items: items)
699+
// Update menu model and action handlers
700+
let actionGroup = Gtk3.GSimpleActionGroup()
701+
let model = renderMenu(
702+
content,
703+
actionMap: actionGroup,
704+
actionNamespace: "menu",
705+
actionPrefix: nil
706+
)
707+
menu.bindModel(model)
708+
menu.insertActionGroup("menu", actionGroup)
709+
685710
// menu.cssProvider.loadCss(
686711
// from: """
687712
// menu {

0 commit comments

Comments
 (0)