Skip to content

Commit f4484c4

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 5acc514 commit f4484c4

8 files changed

+153
-2
lines changed

Examples/UICatalog/UICatalog.swift

+20
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,14 @@ 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: "Application", children: [
115+
MenuElement(title: "Quit")
116+
]),
117+
])
118+
119+
self.progress.addInteraction(ContextMenuInteraction(delegate: self))
120+
113121
self.label.font = Font(name: "Consolas", size: 10)!
114122

115123
self.button.addTarget(self, action: UICatalog.pressMe(_:),
@@ -184,3 +192,15 @@ extension UICatalog: TableViewDataSource {
184192
return cell
185193
}
186194
}
195+
196+
extension UICatalog: ContextMenuInteractionDelegate {
197+
public func contextMenuInteraction(_ interaction: ContextMenuInteraction,
198+
configurationForMenuAtLocation location: Point) -> ContextMenuConfiguration? {
199+
return ContextMenuConfiguration(identifier: nil, previewProvider: nil) { suggestedElements -> Menu? in
200+
Menu(children: [
201+
MenuElement(title: "Popup!"),
202+
MenuElement(title: "2nd Element"),
203+
])
204+
}
205+
}
206+
}

Sources/SwiftWin32/Support/WindowsHandle+UIExtensions.swift

+11
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,14 @@ extension HBITMAP__: HandleValue {
3838
}
3939
}
4040
internal typealias BitmapHandle = ManagedHandle<HBITMAP__>
41+
42+
extension HMENU: HandleValue {
43+
typealias HandleType = HMENU
44+
internal static func release(_ hMenu: HandleType?) {
45+
if let hMenu = hMenu {
46+
DestroyMenu(hMenu)
47+
}
48+
}
49+
}
50+
51+
internal typealias MenuHandle = ManagedHandle<HMENU>

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

+9-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
@@ -34,6 +34,14 @@ public class ContextMenuInteraction {
3434
/// and responds to interaction-related events.
3535
public private(set) weak var delegate: ContextMenuInteractionDelegate?
3636

37+
public private(set) var view: View? = nil
38+
39+
public func willMove(to view: View?) { }
40+
41+
public func didMove(to view: View?) {
42+
self.view = view
43+
}
44+
3745
// MARK - Getting the Interaction's Location
3846

3947
/// Returns the location of the user interaction in the specified view's

Sources/SwiftWin32/UI/Menu.swift

+20
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,30 @@ 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 struct Win32Menu {
294+
internal let hMenu: MenuHandle
295+
private let children: [Win32MenuElement]
296+
297+
internal init(_ hMenu: MenuHandle, children: [MenuElement]) {
298+
self.hMenu = hMenu
299+
self.children = children.map { Win32MenuElement.of($0) }
300+
for (index, child) in self.children.enumerated() {
301+
InsertMenuItemW(hMenu.value,
302+
UINT(index), true,
303+
&(child.info))
304+
}
305+
}
306+
}

Sources/SwiftWin32/UI/MenuElement.swift

+47
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 {
@@ -67,3 +69,48 @@ public class MenuElement {
6769
self.image = image
6870
}
6971
}
72+
73+
internal class Win32MenuElement {
74+
internal var info: MENUITEMINFOW
75+
private var titleW: [WCHAR]
76+
private let subMenu: Win32Menu?
77+
78+
private init(title: String, subMenu: Win32Menu?, 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.value,
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) -> Win32MenuElement {
100+
let subMenu: Win32Menu?
101+
if let menu = element as? Menu {
102+
subMenu = Win32Menu(MenuHandle(owning: CreatePopupMenu()), children: menu.children)
103+
} else {
104+
subMenu = nil
105+
}
106+
return Win32MenuElement(title: element.title, subMenu: subMenu, fType: MFT_STRING)
107+
}
108+
109+
internal static var separator: Win32MenuElement {
110+
Win32MenuElement(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

+29-1
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,26 @@ 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.win32ContextMenu = Win32Menu(MenuHandle(owning: CreatePopupMenu()), children: menu.children)
24+
} else {
25+
view.win32ContextMenu = nil
26+
}
27+
let hMenu = view.win32ContextMenu?.hMenu.value
28+
TrackPopupMenu(hMenu, UINT(TPM_RIGHTBUTTON),
29+
Int32(x), Int32(y), 0, view.hWnd, nil)
1530
return 0
31+
case UINT(WM_COMMAND):
32+
// TODO handle menu actions
33+
break
1634
default:
1735
break
1836
}
@@ -202,6 +220,16 @@ public class View: Responder {
202220
set { _ = EnableWindow(self.hWnd, newValue) }
203221
}
204222

223+
public private(set) var interactions: [Interaction] = []
224+
internal var win32ContextMenu: Win32Menu? = nil
225+
226+
public func addInteraction(_ interaction: Interaction) {
227+
interaction.willMove(to: self)
228+
interaction.view?.interactions.removeAll(where: { $0 === interaction })
229+
interactions.append(interaction)
230+
interaction.didMove(to: self)
231+
}
232+
205233
// MARK - Managing the View Hierarchy
206234

207235
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+
win32TitleMenu = Win32Menu(MenuHandle(owning: CreateMenu()), children: newValue.children)
65+
} else {
66+
win32TitleMenu = nil
67+
}
68+
SetMenu(view.hWnd, win32TitleMenu?.hMenu.value)
69+
}
70+
}
71+
private var win32TitleMenu: Win32Menu? = 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)