diff --git a/Sources/SwiftWin32/CMakeLists.txt b/Sources/SwiftWin32/CMakeLists.txt index 00269655..1128177e 100644 --- a/Sources/SwiftWin32/CMakeLists.txt +++ b/Sources/SwiftWin32/CMakeLists.txt @@ -25,7 +25,10 @@ target_sources(SwiftWin32 PRIVATE UI/ContentSizeCategoryAdjusting.swift UI/ContentSizeCategoryImageAdjusting.swift UI/ContextMenuConfiguration.swift + UI/ContextMenuInteraction.swift UI/ContextMenuInteractionAnimating.swift + UI/ContextMenuInteractionCommitAnimating.swift + UI/ContextMenuInteractionDelegate.swift UI/Control.swift UI/CoordinateSpace.swift UI/DatePicker.swift diff --git a/Sources/SwiftWin32/Support/WindowsHandle+UIExtensions.swift b/Sources/SwiftWin32/Support/WindowsHandle+UIExtensions.swift index 342c5f8b..21d0ee86 100644 --- a/Sources/SwiftWin32/Support/WindowsHandle+UIExtensions.swift +++ b/Sources/SwiftWin32/Support/WindowsHandle+UIExtensions.swift @@ -38,3 +38,14 @@ extension HBITMAP__: HandleValue { } } internal typealias BitmapHandle = ManagedHandle + +extension HMENU: HandleValue { + typealias HandleType = HMENU + internal static func release(_ hMenu: HandleType?) { + if let hMenu = hMenu { + DestroyMenu(hMenu) + } + } +} + +internal typealias MenuHandle = ManagedHandle diff --git a/Sources/SwiftWin32/UI/ContextMenuConfiguration.swift b/Sources/SwiftWin32/UI/ContextMenuConfiguration.swift index 418f9092..be06ff1f 100644 --- a/Sources/SwiftWin32/UI/ContextMenuConfiguration.swift +++ b/Sources/SwiftWin32/UI/ContextMenuConfiguration.swift @@ -44,4 +44,8 @@ public class ContextMenuConfiguration { private var previewProvider: ContextMenuContentPreviewProvider private var actionProvider: ContextMenuActionProvider? + + internal func provideActions() -> Menu? { + actionProvider?([]) // TODO suggest default actions + } } diff --git a/Sources/SwiftWin32/UI/ContextMenuInteraction.swift b/Sources/SwiftWin32/UI/ContextMenuInteraction.swift index 9632af98..7b7d89f5 100644 --- a/Sources/SwiftWin32/UI/ContextMenuInteraction.swift +++ b/Sources/SwiftWin32/UI/ContextMenuInteraction.swift @@ -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 @@ -34,6 +34,14 @@ public class ContextMenuInteraction { /// and responds to interaction-related events. public private(set) weak var delegate: ContextMenuInteractionDelegate? + public private(set) var view: View? = nil + + public func willMove(to view: View?) { } + + public func didMove(to view: View?) { + self.view = view + } + // MARK - Getting the Interaction's Location /// Returns the location of the user interaction in the specified view's diff --git a/Sources/SwiftWin32/UI/Menu.swift b/Sources/SwiftWin32/UI/Menu.swift index 8015ae00..35e5f1be 100644 --- a/Sources/SwiftWin32/UI/Menu.swift +++ b/Sources/SwiftWin32/UI/Menu.swift @@ -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 { @@ -275,12 +277,30 @@ 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 struct Win32Menu { + internal let hMenu: MenuHandle + private let children: [Win32MenuElement] + + internal init(_ hMenu: MenuHandle, children: [MenuElement]) { + self.hMenu = hMenu + self.children = children.map { Win32MenuElement.of($0) } + for (index, child) in self.children.enumerated() { + InsertMenuItemW(hMenu.value, + UINT(index), true, + &(child.info)) + } + } +} diff --git a/Sources/SwiftWin32/UI/MenuElement.swift b/Sources/SwiftWin32/UI/MenuElement.swift index 0b8e5ff8..776a43ee 100644 --- a/Sources/SwiftWin32/UI/MenuElement.swift +++ b/Sources/SwiftWin32/UI/MenuElement.swift @@ -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 { @@ -67,3 +69,49 @@ 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.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(MenuHandle(owning: CreatePopupMenu()), + children: menu.children) + } else { + subMenu = nil + } + return Win32MenuElement(title: element.title, subMenu: subMenu, fType: MFT_STRING) + } + + internal static var separator: Win32MenuElement { + Win32MenuElement(title: "", subMenu: nil, fType: MFT_SEPARATOR) + } + + internal var isSeparator: Bool { + info.fType == MFT_SEPARATOR + } +} diff --git a/Sources/SwiftWin32/UI/View.swift b/Sources/SwiftWin32/UI/View.swift index dc8f2707..43202780 100644 --- a/Sources/SwiftWin32/UI/View.swift +++ b/Sources/SwiftWin32/UI/View.swift @@ -11,8 +11,27 @@ 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(MenuHandle(owning: CreatePopupMenu()), + children: menu.children) + } 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 } @@ -202,6 +221,16 @@ 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) { + interaction.willMove(to: self) + interaction.view?.interactions.removeAll(where: { $0 === interaction }) + interactions.append(interaction) + interaction.didMove(to: self) + } + // MARK - Managing the View Hierarchy public private(set) var superview: View?