diff --git a/Ice.xcodeproj/project.pbxproj b/Ice.xcodeproj/project.pbxproj index 50442797..b73e5cea 100644 --- a/Ice.xcodeproj/project.pbxproj +++ b/Ice.xcodeproj/project.pbxproj @@ -28,6 +28,7 @@ 17540BDC2B23BD5700A0F965 /* NSBezierPath+intersects.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17540BDB2B23BD5700A0F965 /* NSBezierPath+intersects.swift */; }; 175584122B541CFC00EDC9D3 /* CustomTabView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 175584112B541CFC00EDC9D3 /* CustomTabView.swift */; }; 175584152B541D6F00EDC9D3 /* MenuBarLayoutTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = 175584142B541D6F00EDC9D3 /* MenuBarLayoutTab.swift */; }; + 175812542B80FBFA00D622DA /* MenuBarHelperPanel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 175812532B80FBFA00D622DA /* MenuBarHelperPanel.swift */; }; 176B23F42ADB76A1008AE86B /* CustomColorPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 176B23F32ADB76A1008AE86B /* CustomColorPicker.swift */; }; 177354662B1B8502001CF731 /* MenuBarShape.swift in Sources */ = {isa = PBXBuildFile; fileRef = 177354652B1B8502001CF731 /* MenuBarShape.swift */; }; 1773546A2B1BBACF001CF731 /* MenuBarAppearanceTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = 177354692B1BBACF001CF731 /* MenuBarAppearanceTab.swift */; }; @@ -109,6 +110,7 @@ 17540BDB2B23BD5700A0F965 /* NSBezierPath+intersects.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSBezierPath+intersects.swift"; sourceTree = ""; }; 175584112B541CFC00EDC9D3 /* CustomTabView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomTabView.swift; sourceTree = ""; }; 175584142B541D6F00EDC9D3 /* MenuBarLayoutTab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuBarLayoutTab.swift; sourceTree = ""; }; + 175812532B80FBFA00D622DA /* MenuBarHelperPanel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuBarHelperPanel.swift; sourceTree = ""; }; 176B23F32ADB76A1008AE86B /* CustomColorPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomColorPicker.swift; sourceTree = ""; }; 177354652B1B8502001CF731 /* MenuBarShape.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuBarShape.swift; sourceTree = ""; }; 177354692B1BBACF001CF731 /* MenuBarAppearanceTab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuBarAppearanceTab.swift; sourceTree = ""; }; @@ -532,6 +534,7 @@ children = ( 17B7F32A2B264C1800CDCF49 /* MenuBarAppearanceManager.swift */, 174AA5D52B71D97100E3FE74 /* MenuBarAppearancePanel.swift */, + 175812532B80FBFA00D622DA /* MenuBarHelperPanel.swift */, 7166834F2A7681AF006ABF84 /* MenuBarManager.swift */, 7162406E2AA0A323003EC671 /* MenuBarSection.swift */, 177354652B1B8502001CF731 /* MenuBarShape.swift */, @@ -725,6 +728,7 @@ 1787C4302B16AE78002F50DF /* PermissionsManager.swift in Sources */, 170CF88C2B0ED4FA0073F982 /* HotkeyRecordingFailure.swift in Sources */, 170CF88E2B0EDD780073F982 /* LocalizedErrorBox.swift in Sources */, + 175812542B80FBFA00D622DA /* MenuBarHelperPanel.swift in Sources */, 7166832E2A767E6A006ABF84 /* IceApp.swift in Sources */, 71FEA2502A8D5D590048341A /* HotkeyRecorder.swift in Sources */, 17C3C5F72B75A36100B9648C /* NSScreen+displayID.swift in Sources */, diff --git a/Ice/MenuBar/MenuBarHelperPanel.swift b/Ice/MenuBar/MenuBarHelperPanel.swift new file mode 100644 index 00000000..f40a3e9c --- /dev/null +++ b/Ice/MenuBar/MenuBarHelperPanel.swift @@ -0,0 +1,138 @@ +// +// MenuBarHelperPanel.swift +// Ice +// + +import SwiftUI + +// MARK: - MenuBarHelperPanel + +/// A panel that manages the menu that appears when the user +/// right-clicks on the menu bar. +class MenuBarHelperPanel: NSPanel { + /// A Boolean value that indicates whether the panel is + /// currently showing the appearance editor popover. + private var isShowingPopover = false + + init(origin: CGPoint) { + super.init( + contentRect: CGRect(origin: origin, size: CGSize(width: 1, height: 1)), + styleMask: [.borderless, .nonactivatingPanel], + backing: .buffered, + defer: false + ) + level = .statusBar + backgroundColor = .clear + } + + /// Shows the right-click menu. + func showMenu() { + let menu = NSMenu(title: Constants.appName) + menu.delegate = self + + let editItem = NSMenuItem( + title: "Edit Menu Bar Appearanceā€¦", + action: #selector(showAppearanceEditorPopover), + keyEquivalent: "" + ) + editItem.target = self + menu.addItem(editItem) + + menu.addItem(.separator()) + + let settingsItem = NSMenuItem( + title: "\(Constants.appName) Settingsā€¦", + action: #selector(AppDelegate.openSettingsWindow), + keyEquivalent: "," + ) + menu.addItem(settingsItem) + + menu.popUp(positioning: nil, at: frame.origin, in: nil) + } + + /// Shows the appearance editor popover, centered under + /// the menu bar. + @objc private func showAppearanceEditorPopover() { + guard + let contentView, + let screen + else { + return + } + setFrameOrigin(CGPoint(x: screen.frame.midX - (frame.width / 2), y: screen.visibleFrame.maxY)) + let popover = MenuBarAppearanceEditorPopover() + popover.delegate = self + popover.show(relativeTo: .zero, of: contentView, preferredEdge: .minY) + isShowingPopover = true + } +} + +// MARK: MenuBarHelperPanel: NSMenuDelegate +extension MenuBarHelperPanel: NSMenuDelegate { + func menuDidClose(_ menu: NSMenu) { + // async so that `isShowingPopover` has a chance to get set + DispatchQueue.main.async { [self] in + guard !isShowingPopover else { + return + } + orderOut(menu) + } + } +} + +// MARK: MenuBarHelperPanel: NSPopoverDelegate +extension MenuBarHelperPanel: NSPopoverDelegate { + func popoverDidClose(_ notification: Notification) { + guard let popover = notification.object as? MenuBarAppearanceEditorPopover else { + return + } + isShowingPopover = false + orderOut(popover) + popover.mouseDownMonitor.stop() + } +} + +// MARK: - MenuBarAppearanceEditorPopover + +/// A popover that displays the menu bar appearance editor at +/// a centered location under the menu bar. +private class MenuBarAppearanceEditorPopover: NSPopover { + private(set) lazy var mouseDownMonitor = GlobalEventMonitor(mask: .leftMouseDown) { [weak self] event in + self?.performClose(self) + } + + @ViewBuilder + private var contentView: some View { + VStack { + Text("Menu Bar Appearance") + .font(.title2) + .padding(.top) + + MenuBarAppearanceTab() + .padding(.top, -14) + .environmentObject(AppState.shared) + + HStack { + Spacer() + Button("Done") { [weak self] in + self?.performClose(self) + } + .controlSize(.large) + } + .padding() + } + } + + override init() { + super.init() + self.contentViewController = NSHostingController(rootView: contentView) + self.contentSize = CGSize(width: 500, height: 500) + self.behavior = .applicationDefined + self.mouseDownMonitor.start() + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/Ice/MenuBar/MenuBarManager.swift b/Ice/MenuBar/MenuBarManager.swift index 67bb2111..bf50b0a0 100644 --- a/Ice/MenuBar/MenuBarManager.swift +++ b/Ice/MenuBar/MenuBarManager.swift @@ -163,6 +163,9 @@ final class MenuBarManager: ObservableObject { case .rightMouseDown: if isMouseInsideMenuBar { showOnHoverPreventedByUserInteraction = true + let helperPanel = MenuBarHelperPanel(origin: NSEvent.mouseLocation) + helperPanel.orderFrontRegardless() + helperPanel.showMenu() } default: break