Skip to content

Commit c8b7345

Browse files
committed
UI: support showing basic context & window menus
This change adds support for basic context menus (attached to a `View` instance) and window title menus.
1 parent c73feb9 commit c8b7345

8 files changed

+159
-5
lines changed

Examples/UICatalog/UICatalog.swift

+25
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,19 @@ final class UICatalog: ApplicationDelegate, SceneDelegate {
110110
window.addSubview(self.tableview)
111111
window.addSubview(self.imageview)
112112

113+
window.rootViewController?.titleMenu = Menu(children: [
114+
Menu(title: "Foo", children: [
115+
Menu(title: "Another Submenu", children: [
116+
MenuElement(title: "Deep Item")
117+
])
118+
]),
119+
Menu(title: "Bar", children: [
120+
MenuElement(title: "Item Without a Submenu")
121+
]),
122+
])
123+
124+
self.progress.addInteraction(ContextMenuInteraction(delegate: self))
125+
113126
self.label.font = Font(name: "Consolas", size: 10)!
114127

115128
self.button.addTarget(self, action: UICatalog.pressMe(_:),
@@ -184,3 +197,15 @@ extension UICatalog: TableViewDataSource {
184197
return cell
185198
}
186199
}
200+
201+
extension UICatalog: ContextMenuInteractionDelegate {
202+
public func contextMenuInteraction(_ interaction: ContextMenuInteraction,
203+
configurationForMenuAtLocation location: Point) -> ContextMenuConfiguration? {
204+
return ContextMenuConfiguration(identifier: nil, previewProvider: nil) { suggestedElements -> Menu? in
205+
Menu(children: [
206+
MenuElement(title: "Popup!"),
207+
MenuElement(title: "2nd Element"),
208+
])
209+
}
210+
}
211+
}

Sources/SwiftWin32/UI/ContextMenuConfiguration.swift

+4
Original file line numberDiff line numberDiff line change
@@ -44,4 +44,8 @@ public class ContextMenuConfiguration {
4444

4545
private var previewProvider: ContextMenuContentPreviewProvider
4646
private var actionProvider: ContextMenuActionProvider?
47+
48+
internal func provideActions() -> Menu? {
49+
actionProvider?([]) // TODO suggest default actions
50+
}
4751
}

Sources/SwiftWin32/UI/ContextMenuInteraction.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ extension ContextMenuInteraction {
1919
}
2020
}
2121

22-
public class ContextMenuInteraction {
22+
public class ContextMenuInteraction: Interaction {
2323
// MARK - Creating a Context Menu Interaction Object
2424

2525
/// Creates a context menu interaction object with the specified delegate
+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
/**
2+
* Copyright © 2020 Saleem Abdulrasool <[email protected]>
3+
* All rights reserved.
4+
*
5+
* SPDX-License-Identifier: BSD-3-Clause
6+
**/
7+
8+
public protocol Interaction {
9+
}

Sources/SwiftWin32/UI/Menu.swift

+32
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
* SPDX-License-Identifier: BSD-3-Clause
66
**/
77

8+
import WinSDK
9+
810
extension Menu {
911
/// Constants for identifying an application's standard menus.
1012
public struct Identifier: Equatable, Hashable, RawRepresentable {
@@ -275,12 +277,42 @@ extension Menu.Options {
275277
/// A container for grouping related menu elements in an application menu or
276278
/// contextual menu.
277279
public class Menu: MenuElement {
280+
internal let children: [MenuElement]
281+
278282
/// Creating a Menu Object
279283

280284
/// Creates a new menu with the specified values.
281285
public init(title: String = "", image: Image? = nil,
282286
identifier: Menu.Identifier? = nil, options: Menu.Options = [],
283287
children: [MenuElement] = []) {
288+
self.children = children
284289
super.init(title: title, image: image)
285290
}
286291
}
292+
293+
internal class WrappedMenu {
294+
internal let hMenu: HMENU
295+
private let children: [WrappedMenuElement]
296+
297+
private init(hMenu: HMENU, children: [MenuElement]) {
298+
self.hMenu = hMenu
299+
self.children = children.map { WrappedMenuElement.of($0) }
300+
for (index, child) in self.children.enumerated() {
301+
InsertMenuItemW(hMenu,
302+
UINT(index), true,
303+
&(child.info))
304+
}
305+
}
306+
307+
internal static func ofContextMenu(_ menu: Menu) -> WrappedMenu {
308+
WrappedMenu(hMenu: CreatePopupMenu(), children: menu.children)
309+
}
310+
311+
internal static func ofWindowMenu(_ menu: Menu) -> WrappedMenu {
312+
WrappedMenu(hMenu: CreateMenu(), children: menu.children)
313+
}
314+
315+
deinit {
316+
DestroyMenu(hMenu)
317+
}
318+
}

Sources/SwiftWin32/UI/MenuElement.swift

+50-3
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
* SPDX-License-Identifier: BSD-3-Clause
66
**/
77

8+
import WinSDK
9+
810
extension MenuElement {
911
/// Attributes that determine the style of the menu element.
1012
public struct Attributes: OptionSet {
@@ -55,15 +57,60 @@ public class MenuElement {
5557
/// Getting the Element Attributes
5658

5759
/// The title of the menu element.
58-
public var title: String
60+
public let title: String
5961

6062
/// The image to display alongside the menu element's title.
61-
public var image: Image?
63+
public let image: Image?
6264

6365
/// Creating a Menu Element
6466

65-
internal init(title: String, image: Image?) {
67+
public init(title: String, image: Image? = nil) {
6668
self.title = title
6769
self.image = image
6870
}
6971
}
72+
73+
internal class WrappedMenuElement {
74+
internal var info: MENUITEMINFOW
75+
private var titleW: [WCHAR]
76+
private let subMenu: WrappedMenu?
77+
78+
private init(title: String, subMenu: WrappedMenu?, fType: Int32) {
79+
self.titleW = title.LPCWSTR
80+
let titleSize = titleW.count
81+
82+
self.subMenu = subMenu
83+
self.info = titleW.withUnsafeMutableBufferPointer {
84+
MENUITEMINFOW(cbSize: UINT(MemoryLayout<MENUITEMINFOW>.size),
85+
fMask: UINT(MIIM_FTYPE | MIIM_STATE | MIIM_ID | MIIM_STRING | MIIM_SUBMENU | MIIM_DATA),
86+
fType: UINT(fType),
87+
fState: UINT(MFS_ENABLED),
88+
wID: UInt32.random(in: .min ... .max),
89+
hSubMenu: subMenu?.hMenu,
90+
hbmpChecked: nil,
91+
hbmpUnchecked: nil,
92+
dwItemData: 0,
93+
dwTypeData: $0.baseAddress,
94+
cch: UINT(titleSize),
95+
hbmpItem: nil)
96+
}
97+
}
98+
99+
internal static func of(_ element: MenuElement) -> WrappedMenuElement {
100+
let subMenu: WrappedMenu?
101+
if let menu = element as? Menu {
102+
subMenu = WrappedMenu.ofContextMenu(menu)
103+
} else {
104+
subMenu = nil
105+
}
106+
return WrappedMenuElement(title: element.title, subMenu: subMenu, fType: MFT_STRING)
107+
}
108+
109+
internal static func ofSeparator() -> WrappedMenuElement {
110+
return WrappedMenuElement(title: "", subMenu: nil, fType: MFT_SEPARATOR)
111+
}
112+
113+
internal var isSeparator: Bool {
114+
info.fType == MFT_SEPARATOR
115+
}
116+
}

Sources/SwiftWin32/UI/View.swift

+25-1
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,25 @@ private let SwiftViewProc: SUBCLASSPROC = { (hWnd, uMsg, wParam, lParam, uIdSubc
1111
let view: View? = unsafeBitCast(dwRefData, to: AnyObject.self) as? View
1212
switch uMsg {
1313
case UINT(WM_CONTEXTMENU):
14-
// TODO handle popup menu events
14+
guard let view = view,
15+
let menuInteraction = view.interactions.first(where: { $0 is ContextMenuInteraction })
16+
as? ContextMenuInteraction else { break }
17+
let x = Int16(truncatingIfNeeded: lParam)
18+
let y = Int16(truncatingIfNeeded: lParam >> 16)
19+
let point = Point(x: Int(x), y: Int(y))
20+
let menuConfiguration = menuInteraction.delegate?.contextMenuInteraction(menuInteraction,
21+
configurationForMenuAtLocation: point)
22+
if let menu = menuConfiguration?.provideActions() {
23+
view.wrappedContextMenu = WrappedMenu.ofContextMenu(menu)
24+
} else {
25+
view.wrappedContextMenu = nil
26+
}
27+
TrackPopupMenu(view.wrappedContextMenu?.hMenu, UINT(TPM_RIGHTBUTTON),
28+
Int32(x), Int32(y), 0, view.hWnd, nil)
1529
return 0
30+
case UINT(WM_COMMAND):
31+
// TODO handle menu actions
32+
break
1633
default:
1734
break
1835
}
@@ -202,6 +219,13 @@ public class View: Responder {
202219
set { _ = EnableWindow(self.hWnd, newValue) }
203220
}
204221

222+
public private(set) var interactions: [Interaction] = []
223+
internal var wrappedContextMenu: WrappedMenu? = nil
224+
225+
public func addInteraction(_ interaction: Interaction) {
226+
interactions.append(interaction)
227+
}
228+
205229
// MARK - Managing the View Hierarchy
206230

207231
public private(set) var superview: View?

Sources/SwiftWin32/UI/ViewController.swift

+13
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,19 @@ public class ViewController: Responder {
5757
set(value) { _ = SetWindowTextW(view.hWnd, value?.LPCWSTR) }
5858
}
5959

60+
/// Menu bar shown below the window title
61+
public var titleMenu: Menu? {
62+
didSet {
63+
if let newValue = titleMenu {
64+
wrappedTitleMenu = WrappedMenu.ofWindowMenu(newValue)
65+
} else {
66+
wrappedTitleMenu = nil
67+
}
68+
SetMenu(view.hWnd, wrappedTitleMenu?.hMenu)
69+
}
70+
}
71+
private var wrappedTitleMenu: WrappedMenu? = nil
72+
6073
/// The preferred size for the view controller's view.
6174
public var preferredContentSize: Size {
6275
fatalError("not yet implemented")

0 commit comments

Comments
 (0)