Skip to content

Commit

Permalink
UI: support showing basic context & window menus
Browse files Browse the repository at this point in the history
This change adds support for basic context menus (attached to a `View` instance) and window title menus.
  • Loading branch information
egorzhdan committed Nov 29, 2020
1 parent 3c85b57 commit af570e6
Show file tree
Hide file tree
Showing 9 changed files with 159 additions and 2 deletions.
20 changes: 20 additions & 0 deletions Examples/UICatalog/UICatalog.swift
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,14 @@ final class UICatalog: ApplicationDelegate, SceneDelegate {
window.addSubview(self.tableview)
window.addSubview(self.imageview)

window.rootViewController?.titleMenu = Menu(children: [
Menu(title: "Application", children: [
MenuElement(title: "Quit")
]),
])

self.progress.addInteraction(ContextMenuInteraction(delegate: self))

self.label.font = Font(name: "Consolas", size: 10)!

self.button.addTarget(self, action: UICatalog.pressMe(_:),
Expand Down Expand Up @@ -184,3 +192,15 @@ extension UICatalog: TableViewDataSource {
return cell
}
}

extension UICatalog: ContextMenuInteractionDelegate {
public func contextMenuInteraction(_ interaction: ContextMenuInteraction,
configurationForMenuAtLocation location: Point) -> ContextMenuConfiguration? {
return ContextMenuConfiguration(identifier: nil, previewProvider: nil) { suggestedElements -> Menu? in
Menu(children: [
MenuElement(title: "Popup!"),
MenuElement(title: "2nd Element"),
])
}
}
}
11 changes: 11 additions & 0 deletions Sources/SwiftWin32/Support/WindowsHandle+UIExtensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,14 @@ extension HBITMAP__: HandleValue {
}
}
internal typealias BitmapHandle = ManagedHandle<HBITMAP__>

extension HMENU: HandleValue {
typealias HandleType = HMENU
internal static func release(_ hMenu: HandleType?) {
if let hMenu = hMenu {
DestroyMenu(hMenu)
}
}
}

internal typealias MenuHandle = ManagedHandle<HMENU>
4 changes: 4 additions & 0 deletions Sources/SwiftWin32/UI/ContextMenuConfiguration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,8 @@ public class ContextMenuConfiguration {

private var previewProvider: ContextMenuContentPreviewProvider
private var actionProvider: ContextMenuActionProvider?

internal func provideActions() -> Menu? {
actionProvider?([]) // TODO suggest default actions
}
}
2 changes: 1 addition & 1 deletion Sources/SwiftWin32/UI/ContextMenuInteraction.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ extension ContextMenuInteraction {
}
}

public class ContextMenuInteraction {
public class ContextMenuInteraction: Interaction {
// MARK - Creating a Context Menu Interaction Object

/// Creates a context menu interaction object with the specified delegate
Expand Down
9 changes: 9 additions & 0 deletions Sources/SwiftWin32/UI/Interaction.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/**
* Copyright © 2020 Saleem Abdulrasool <[email protected]>
* All rights reserved.
*
* SPDX-License-Identifier: BSD-3-Clause
**/

public protocol Interaction {
}
28 changes: 28 additions & 0 deletions Sources/SwiftWin32/UI/Menu.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
* SPDX-License-Identifier: BSD-3-Clause
**/

import WinSDK

extension Menu {
/// Constants for identifying an application's standard menus.
public struct Identifier: Equatable, Hashable, RawRepresentable {
Expand Down Expand Up @@ -275,12 +277,38 @@ extension Menu.Options {
/// A container for grouping related menu elements in an application menu or
/// contextual menu.
public class Menu: MenuElement {
internal let children: [MenuElement]

/// Creating a Menu Object

/// Creates a new menu with the specified values.
public init(title: String = "", image: Image? = nil,
identifier: Menu.Identifier? = nil, options: Menu.Options = [],
children: [MenuElement] = []) {
self.children = children
super.init(title: title, image: image)
}
}

internal class Win32Menu {
internal let hMenu: MenuHandle
private let children: [Win32MenuElement]

private init(hMenu: HMENU, children: [MenuElement]) {
self.hMenu = MenuHandle(owning: hMenu)
self.children = children.map { Win32MenuElement.of($0) }
for (index, child) in self.children.enumerated() {
InsertMenuItemW(hMenu,
UINT(index), true,
&(child.info))
}
}

internal static func ofContextMenu(_ menu: Menu) -> Win32Menu {
Win32Menu(hMenu: CreatePopupMenu(), children: menu.children)
}

internal static func ofWindowMenu(_ menu: Menu) -> Win32Menu {
Win32Menu(hMenu: CreateMenu(), children: menu.children)
}
}
47 changes: 47 additions & 0 deletions Sources/SwiftWin32/UI/MenuElement.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
* SPDX-License-Identifier: BSD-3-Clause
**/

import WinSDK

extension MenuElement {
/// Attributes that determine the style of the menu element.
public struct Attributes: OptionSet {
Expand Down Expand Up @@ -67,3 +69,48 @@ public class MenuElement {
self.image = image
}
}

internal class Win32MenuElement {
internal var info: MENUITEMINFOW
private var titleW: [WCHAR]
private let subMenu: Win32Menu?

private init(title: String, subMenu: Win32Menu?, fType: Int32) {
self.titleW = title.LPCWSTR
let titleSize = titleW.count

self.subMenu = subMenu
self.info = titleW.withUnsafeMutableBufferPointer {
MENUITEMINFOW(cbSize: UINT(MemoryLayout<MENUITEMINFOW>.size),
fMask: UINT(MIIM_FTYPE | MIIM_STATE | MIIM_ID | MIIM_STRING | MIIM_SUBMENU | MIIM_DATA),
fType: UINT(fType),
fState: UINT(MFS_ENABLED),
wID: UInt32.random(in: .min ... .max),
hSubMenu: subMenu?.hMenu.value,
hbmpChecked: nil,
hbmpUnchecked: nil,
dwItemData: 0,
dwTypeData: $0.baseAddress,
cch: UINT(titleSize),
hbmpItem: nil)
}
}

internal static func of(_ element: MenuElement) -> Win32MenuElement {
let subMenu: Win32Menu?
if let menu = element as? Menu {
subMenu = Win32Menu.ofContextMenu(menu)
} else {
subMenu = nil
}
return Win32MenuElement(title: element.title, subMenu: subMenu, fType: MFT_STRING)
}

internal static func ofSeparator() -> Win32MenuElement {
return Win32MenuElement(title: "", subMenu: nil, fType: MFT_SEPARATOR)
}

internal var isSeparator: Bool {
info.fType == MFT_SEPARATOR
}
}
27 changes: 26 additions & 1 deletion Sources/SwiftWin32/UI/View.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,26 @@ private let SwiftViewProc: SUBCLASSPROC = { (hWnd, uMsg, wParam, lParam, uIdSubc
let view: View? = unsafeBitCast(dwRefData, to: AnyObject.self) as? View
switch uMsg {
case UINT(WM_CONTEXTMENU):
// TODO handle popup menu events
guard let view = view,
let menuInteraction = view.interactions.first(where: { $0 is ContextMenuInteraction })
as? ContextMenuInteraction else { break }
let x = Int16(truncatingIfNeeded: lParam)
let y = Int16(truncatingIfNeeded: lParam >> 16)
let point = Point(x: Int(x), y: Int(y))
let menuConfiguration = menuInteraction.delegate?.contextMenuInteraction(menuInteraction,
configurationForMenuAtLocation: point)
if let menu = menuConfiguration?.provideActions() {
view.win32ContextMenu = Win32Menu.ofContextMenu(menu)
} else {
view.win32ContextMenu = nil
}
let hMenu = view.win32ContextMenu?.hMenu.value
TrackPopupMenu(hMenu, UINT(TPM_RIGHTBUTTON),
Int32(x), Int32(y), 0, view.hWnd, nil)
return 0
case UINT(WM_COMMAND):
// TODO handle menu actions
break
default:
break
}
Expand Down Expand Up @@ -202,6 +220,13 @@ public class View: Responder {
set { _ = EnableWindow(self.hWnd, newValue) }
}

public private(set) var interactions: [Interaction] = []
internal var win32ContextMenu: Win32Menu? = nil

public func addInteraction(_ interaction: Interaction) {
interactions.append(interaction)
}

// MARK - Managing the View Hierarchy

public private(set) var superview: View?
Expand Down
13 changes: 13 additions & 0 deletions Sources/SwiftWin32/UI/ViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,19 @@ public class ViewController: Responder {
set(value) { _ = SetWindowTextW(view.hWnd, value?.LPCWSTR) }
}

/// Menu bar shown below the window title
public var titleMenu: Menu? {
didSet {
if let newValue = titleMenu {
win32TitleMenu = Win32Menu.ofWindowMenu(newValue)
} else {
win32TitleMenu = nil
}
SetMenu(view.hWnd, win32TitleMenu?.hMenu.value)
}
}
private var win32TitleMenu: Win32Menu? = nil

/// The preferred size for the view controller's view.
public var preferredContentSize: Size {
fatalError("not yet implemented")
Expand Down

0 comments on commit af570e6

Please sign in to comment.