Skip to content

Commit 5b7beed

Browse files
committed
Implement Menu view (a button which triggers a pop-up menu containing actions)
The pop-up changes to fit in with the current Environment.colorScheme when using GtkBackend and AppKitBackend, but not Gtk3Backend. Hopefully I can figure out how to implement menu styling for Gtk3Backend eventually (got everything working except the hover state).
1 parent 6228bd5 commit 5b7beed

File tree

11 files changed

+1387
-11
lines changed

11 files changed

+1387
-11
lines changed

Sources/AppKitBackend/AppKitBackend.swift

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ public final class AppKitBackend: AppBackend {
1010

1111
public typealias Widget = NSView
1212

13+
public typealias Menu = NSMenu
14+
1315
public let defaultTableRowContentHeight = 20
1416
public let defaultTableCellVerticalPadding = 4
1517
public let defaultPaddingAmount = 10
@@ -766,6 +768,68 @@ public final class AppKitBackend: AppBackend {
766768
progressBar.stopAnimation(nil)
767769
}
768770
}
771+
772+
public func createPopoverMenu() -> Menu {
773+
return NSMenu()
774+
}
775+
776+
public func updatePopoverMenu(
777+
_ menu: Menu,
778+
items: [(String, () -> Void)],
779+
environment: Environment
780+
) {
781+
menu.appearance = environment.colorScheme.nsAppearance
782+
menu.items = items.map { (label, action) in
783+
let wrappedAction = Action(action)
784+
let menuItem = NSCustomMenuItem(
785+
title: label,
786+
action: #selector(wrappedAction.run),
787+
keyEquivalent: ""
788+
)
789+
menuItem.target = wrappedAction
790+
menuItem.actionWrapper = wrappedAction
791+
return menuItem
792+
}
793+
}
794+
795+
public func showPopoverMenu(
796+
_ menu: Menu, at position: SIMD2<Int>, relativeTo widget: Widget,
797+
closeHandler handleClose: @escaping () -> Void
798+
) {
799+
// NSMenu.popUp(position:at:in:) blocks until the pop up is closed, and has to
800+
// run on the main thread, so I'm not exactly sure how it doesn't break things,
801+
// but it hasn't broken anything yet.
802+
menu.popUp(
803+
positioning: nil,
804+
at: NSPoint(x: CGFloat(position.x + 2), y: CGFloat(position.y + 8)),
805+
in: widget
806+
)
807+
handleClose()
808+
}
809+
}
810+
811+
final class NSCustomMenuItem: NSMenuItem {
812+
/// This property's only purpose is to keep a strong reference to the wrapped
813+
/// action so that it sticks around for long enough to be useful.
814+
var actionWrapper: Action?
815+
}
816+
817+
// TODO: Update all controls to use this style of action passing, seems way nicer
818+
// than the existing associated keys based approach. And probably more efficient too.
819+
// Source: https://stackoverflow.com/a/36983811
820+
final class Action: NSObject {
821+
private let action: () -> Void
822+
823+
init(_ action: @escaping () -> Void) {
824+
self.action = action
825+
super.init()
826+
}
827+
828+
@objc func run() {
829+
print("Running action")
830+
action()
831+
}
832+
769833
}
770834

771835
class NSCustomTableView: NSTableView {

Sources/Gtk/Generated/Popover.swift

Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
import CGtk
2+
3+
/// `GtkPopover` is a bubble-like context popup.
4+
///
5+
/// ![An example GtkPopover](popover.png)
6+
///
7+
/// It is primarily meant to provide context-dependent information
8+
/// or options. Popovers are attached to a parent widget. By default,
9+
/// they point to the whole widget area, although this behavior can be
10+
/// changed with [method@Gtk.Popover.set_pointing_to].
11+
///
12+
/// The position of a popover relative to the widget it is attached to
13+
/// can also be changed with [method@Gtk.Popover.set_position]
14+
///
15+
/// By default, `GtkPopover` performs a grab, in order to ensure input
16+
/// events get redirected to it while it is shown, and also so the popover
17+
/// is dismissed in the expected situations (clicks outside the popover,
18+
/// or the Escape key being pressed). If no such modal behavior is desired
19+
/// on a popover, [method@Gtk.Popover.set_autohide] may be called on it to
20+
/// tweak its behavior.
21+
///
22+
/// ## GtkPopover as menu replacement
23+
///
24+
/// `GtkPopover` is often used to replace menus. The best way to do this
25+
/// is to use the [class@Gtk.PopoverMenu] subclass which supports being
26+
/// populated from a `GMenuModel` with [ctor@Gtk.PopoverMenu.new_from_model].
27+
///
28+
/// ```xml
29+
/// <section><attribute name="display-hint">horizontal-buttons</attribute><item><attribute name="label">Cut</attribute><attribute name="action">app.cut</attribute><attribute name="verb-icon">edit-cut-symbolic</attribute></item><item><attribute name="label">Copy</attribute><attribute name="action">app.copy</attribute><attribute name="verb-icon">edit-copy-symbolic</attribute></item><item><attribute name="label">Paste</attribute><attribute name="action">app.paste</attribute><attribute name="verb-icon">edit-paste-symbolic</attribute></item></section>
30+
/// ```
31+
///
32+
/// # CSS nodes
33+
///
34+
/// ```
35+
/// popover.background[.menu]
36+
/// ├── arrow
37+
/// ╰── contents
38+
/// ╰── <child>
39+
/// ```
40+
///
41+
/// `GtkPopover` has a main node with name `popover`, an arrow with name `arrow`,
42+
/// and another node for the content named `contents`. The `popover` node always
43+
/// gets the `.background` style class. It also gets the `.menu` style class
44+
/// if the popover is menu-like, e.g. is a [class@Gtk.PopoverMenu].
45+
///
46+
/// Particular uses of `GtkPopover`, such as touch selection popups or
47+
/// magnifiers in `GtkEntry` or `GtkTextView` get style classes like
48+
/// `.touch-selection` or `.magnifier` to differentiate from plain popovers.
49+
///
50+
/// When styling a popover directly, the `popover` node should usually
51+
/// not have any background. The visible part of the popover can have
52+
/// a shadow. To specify it in CSS, set the box-shadow of the `contents` node.
53+
///
54+
/// Note that, in order to accomplish appropriate arrow visuals, `GtkPopover`
55+
/// uses custom drawing for the `arrow` node. This makes it possible for the
56+
/// arrow to change its shape dynamically, but it also limits the possibilities
57+
/// of styling it using CSS. In particular, the `arrow` gets drawn over the
58+
/// `content` node's border and shadow, so they look like one shape, which
59+
/// means that the border width of the `content` node and the `arrow` node should
60+
/// be the same. The arrow also does not support any border shape other than
61+
/// solid, no border-radius, only one border width (border-bottom-width is
62+
/// used) and no box-shadow.
63+
public class Popover: Widget, Native, ShortcutManager {
64+
/// Creates a new `GtkPopover`.
65+
override public init() {
66+
super.init()
67+
widgetPointer = gtk_popover_new()
68+
}
69+
70+
override func didMoveToParent() {
71+
removeSignals()
72+
73+
super.didMoveToParent()
74+
75+
addSignal(name: "activate-default") { [weak self] () in
76+
guard let self = self else { return }
77+
self.activateDefault?(self)
78+
}
79+
80+
addSignal(name: "closed") { [weak self] () in
81+
guard let self = self else { return }
82+
self.closed?(self)
83+
}
84+
85+
let handler2:
86+
@convention(c) (UnsafeMutableRawPointer, OpaquePointer, UnsafeMutableRawPointer) -> Void =
87+
{ _, value1, data in
88+
SignalBox1<OpaquePointer>.run(data, value1)
89+
}
90+
91+
addSignal(name: "notify::autohide", handler: gCallback(handler2)) {
92+
[weak self] (_: OpaquePointer) in
93+
guard let self = self else { return }
94+
self.notifyAutohide?(self)
95+
}
96+
97+
let handler3:
98+
@convention(c) (UnsafeMutableRawPointer, OpaquePointer, UnsafeMutableRawPointer) -> Void =
99+
{ _, value1, data in
100+
SignalBox1<OpaquePointer>.run(data, value1)
101+
}
102+
103+
addSignal(name: "notify::cascade-popdown", handler: gCallback(handler3)) {
104+
[weak self] (_: OpaquePointer) in
105+
guard let self = self else { return }
106+
self.notifyCascadePopdown?(self)
107+
}
108+
109+
let handler4:
110+
@convention(c) (UnsafeMutableRawPointer, OpaquePointer, UnsafeMutableRawPointer) -> Void =
111+
{ _, value1, data in
112+
SignalBox1<OpaquePointer>.run(data, value1)
113+
}
114+
115+
addSignal(name: "notify::child", handler: gCallback(handler4)) {
116+
[weak self] (_: OpaquePointer) in
117+
guard let self = self else { return }
118+
self.notifyChild?(self)
119+
}
120+
121+
let handler5:
122+
@convention(c) (UnsafeMutableRawPointer, OpaquePointer, UnsafeMutableRawPointer) -> Void =
123+
{ _, value1, data in
124+
SignalBox1<OpaquePointer>.run(data, value1)
125+
}
126+
127+
addSignal(name: "notify::default-widget", handler: gCallback(handler5)) {
128+
[weak self] (_: OpaquePointer) in
129+
guard let self = self else { return }
130+
self.notifyDefaultWidget?(self)
131+
}
132+
133+
let handler6:
134+
@convention(c) (UnsafeMutableRawPointer, OpaquePointer, UnsafeMutableRawPointer) -> Void =
135+
{ _, value1, data in
136+
SignalBox1<OpaquePointer>.run(data, value1)
137+
}
138+
139+
addSignal(name: "notify::has-arrow", handler: gCallback(handler6)) {
140+
[weak self] (_: OpaquePointer) in
141+
guard let self = self else { return }
142+
self.notifyHasArrow?(self)
143+
}
144+
145+
let handler7:
146+
@convention(c) (UnsafeMutableRawPointer, OpaquePointer, UnsafeMutableRawPointer) -> Void =
147+
{ _, value1, data in
148+
SignalBox1<OpaquePointer>.run(data, value1)
149+
}
150+
151+
addSignal(name: "notify::mnemonics-visible", handler: gCallback(handler7)) {
152+
[weak self] (_: OpaquePointer) in
153+
guard let self = self else { return }
154+
self.notifyMnemonicsVisible?(self)
155+
}
156+
157+
let handler8:
158+
@convention(c) (UnsafeMutableRawPointer, OpaquePointer, UnsafeMutableRawPointer) -> Void =
159+
{ _, value1, data in
160+
SignalBox1<OpaquePointer>.run(data, value1)
161+
}
162+
163+
addSignal(name: "notify::pointing-to", handler: gCallback(handler8)) {
164+
[weak self] (_: OpaquePointer) in
165+
guard let self = self else { return }
166+
self.notifyPointingTo?(self)
167+
}
168+
169+
let handler9:
170+
@convention(c) (UnsafeMutableRawPointer, OpaquePointer, UnsafeMutableRawPointer) -> Void =
171+
{ _, value1, data in
172+
SignalBox1<OpaquePointer>.run(data, value1)
173+
}
174+
175+
addSignal(name: "notify::position", handler: gCallback(handler9)) {
176+
[weak self] (_: OpaquePointer) in
177+
guard let self = self else { return }
178+
self.notifyPosition?(self)
179+
}
180+
}
181+
182+
/// Whether to dismiss the popover on outside clicks.
183+
@GObjectProperty(named: "autohide") public var autohide: Bool
184+
185+
/// Whether the popover pops down after a child popover.
186+
///
187+
/// This is used to implement the expected behavior of submenus.
188+
@GObjectProperty(named: "cascade-popdown") public var cascadePopdown: Bool
189+
190+
/// Whether to draw an arrow.
191+
@GObjectProperty(named: "has-arrow") public var hasArrow: Bool
192+
193+
/// Whether mnemonics are currently visible in this popover.
194+
@GObjectProperty(named: "mnemonics-visible") public var mnemonicsVisible: Bool
195+
196+
/// How to place the popover, relative to its parent.
197+
@GObjectProperty(named: "position") public var position: PositionType
198+
199+
/// Emitted whend the user activates the default widget.
200+
///
201+
/// This is a [keybinding signal](class.SignalAction.html).
202+
public var activateDefault: ((Popover) -> Void)?
203+
204+
/// Emitted when the popover is closed.
205+
public var closed: ((Popover) -> Void)?
206+
207+
public var notifyAutohide: ((Popover) -> Void)?
208+
209+
public var notifyCascadePopdown: ((Popover) -> Void)?
210+
211+
public var notifyChild: ((Popover) -> Void)?
212+
213+
public var notifyDefaultWidget: ((Popover) -> Void)?
214+
215+
public var notifyHasArrow: ((Popover) -> Void)?
216+
217+
public var notifyMnemonicsVisible: ((Popover) -> Void)?
218+
219+
public var notifyPointingTo: ((Popover) -> Void)?
220+
221+
public var notifyPosition: ((Popover) -> Void)?
222+
}

0 commit comments

Comments
 (0)