From 44b59d50c96f8c367b61ccf9b3ae5d5e414cac1e Mon Sep 17 00:00:00 2001 From: Gremlin Date: Sat, 21 Mar 2026 17:01:43 +0100 Subject: [PATCH] feat: make tray menu actions work and add functional demo sections --- SwiftTrayDemo/ContentView.swift | 290 ++++++++++++++++++++------- SwiftTrayDemo/SwiftTrayDemoApp.swift | 110 ++++++---- 2 files changed, 293 insertions(+), 107 deletions(-) diff --git a/SwiftTrayDemo/ContentView.swift b/SwiftTrayDemo/ContentView.swift index edd8482..9b6deb3 100644 --- a/SwiftTrayDemo/ContentView.swift +++ b/SwiftTrayDemo/ContentView.swift @@ -1,5 +1,32 @@ import SwiftUI +enum DemoSection: String, CaseIterable, Identifiable, Hashable { + case overview + case windowControls + case secondaryWindow + case dialogs + + var id: String { rawValue } + + var title: String { + switch self { + case .overview: "Overview" + case .windowControls: "Window Controls" + case .secondaryWindow: "Secondary Window" + case .dialogs: "Dialogs" + } + } + + var systemImage: String { + switch self { + case .overview: "macwindow" + case .windowControls: "slider.horizontal.3" + case .secondaryWindow: "uiwindow.split.2x1" + case .dialogs: "square.and.pencil" + } + } +} + struct FeatureCard: View { let title: String let systemImage: String @@ -22,103 +49,227 @@ struct FeatureCard: View { struct ContentView: View { @Environment(\.openWindow) private var openWindow + @State private var selection: DemoSection? = .overview @State private var documentTitle = "Swift Window Demo" @State private var width: Double = 980 @State private var height: Double = 640 + @State private var windowOpacity: Double = 1.0 @State private var showSheet = false @State private var showDialog = false + @State private var showAlert = false + @State private var statusMessage = "Ready" var body: some View { NavigationSplitView { - List { - Label("Overview", systemImage: "macwindow") - Label("Window Controls", systemImage: "slider.horizontal.3") - Label("Secondary Window", systemImage: "uiwindow.split.2x1") - Label("Dialogs", systemImage: "square.and.pencil") + List(DemoSection.allCases, selection: $selection) { section in + Label(section.title, systemImage: section.systemImage) + .tag(section) } .navigationTitle("Capabilities") .listStyle(.sidebar) } detail: { - ScrollView { - VStack(alignment: .leading, spacing: 22) { - VStack(alignment: .leading, spacing: 8) { - Text("Main Window Demo") - .font(.system(size: 34, weight: .bold)) - Text("A current-style SwiftUI macOS app that exercises real desktop window behaviors instead of just rendering a static view.") - .foregroundStyle(.secondary) + Group { + switch selection ?? .overview { + case .overview: + overviewView + case .windowControls: + windowControlsView + case .secondaryWindow: + secondaryWindowView + case .dialogs: + dialogsView + } + } + .navigationTitle((selection ?? .overview).title) + .sheet(isPresented: $showSheet) { + SheetDemoView(statusMessage: $statusMessage) + } + .alert("Demo alert", isPresented: $showAlert) { + Button("OK") { + statusMessage = "Alert acknowledged" + } + } message: { + Text("This is a standard alert attached to the app session.") + } + .confirmationDialog("Test confirmation dialog", isPresented: $showDialog, titleVisibility: .visible) { + Button("Center Main Window") { + WindowCommandCenter.centerMainWindow() + statusMessage = "Centered the main window" + } + Button("Open Utility Window") { + openWindow(id: "inspector") + statusMessage = "Opened the utility window" + } + Button("Cancel", role: .cancel) { + statusMessage = "Confirmation cancelled" + } + } message: { + Text("Choose an action to prove the dialog actually triggers app behavior.") + } + } + } + + private var overviewView: some View { + ScrollView { + VStack(alignment: .leading, spacing: 22) { + VStack(alignment: .leading, spacing: 8) { + Text("Main Window Demo") + .font(.system(size: 34, weight: .bold)) + Text("A functional SwiftUI macOS app that demonstrates real window controls, dialogs, multi-window behavior, and tray integration.") + .foregroundStyle(.secondary) + } + + Grid(horizontalSpacing: 16, verticalSpacing: 16) { + GridRow { + FeatureCard(title: "Resizable Window", systemImage: "arrow.up.left.and.arrow.down.right", detail: "Resize, center, retitle, and fade the main window using live controls.") + FeatureCard(title: "Working Sidebar", systemImage: "sidebar.left", detail: "Each sidebar section now drives a different detail view instead of being static decoration.") } - - Grid(horizontalSpacing: 16, verticalSpacing: 16) { - GridRow { - FeatureCard(title: "Resizable", systemImage: "arrow.up.left.and.arrow.down.right", detail: "Set a target content size and apply it to the frontmost app window.") - FeatureCard(title: "Secondary Window", systemImage: "macwindow.on.rectangle", detail: "Open a separate utility-style scene to prove multi-window support.") - } - GridRow { - FeatureCard(title: "Sheet + Dialog", systemImage: "rectangle.portrait.and.arrow.right", detail: "Trigger standard macOS sheets and confirmation dialogs.") - FeatureCard(title: "Tray Integration", systemImage: "menubar.rectangle", detail: "Use the menu-bar item to reopen the main window at any time.") - } - } - - GroupBox("Window controls") { - VStack(alignment: .leading, spacing: 14) { - TextField("Window title", text: $documentTitle) - .textFieldStyle(.roundedBorder) - - HStack { - VStack(alignment: .leading) { - Text("Width: \(Int(width))") - Slider(value: $width, in: 700...1400, step: 10) - } - VStack(alignment: .leading) { - Text("Height: \(Int(height))") - Slider(value: $height, in: 480...960, step: 10) - } - } - - HStack(spacing: 12) { - Button("Apply Size") { - WindowCommandCenter.resizeMainWindow(width: width, height: height) - } - Button("Center Window") { - WindowCommandCenter.centerMainWindow() - } - Button("Toggle Full Screen") { - WindowCommandCenter.toggleFullScreen() - } - } - } - .padding(.top, 4) + GridRow { + FeatureCard(title: "Dialogs + Alerts", systemImage: "rectangle.portrait.and.arrow.right", detail: "Open a sheet, alert, and confirmation dialog that each perform visible actions.") + FeatureCard(title: "Tray Menu", systemImage: "menubar.rectangle", detail: "Tray items are explicitly enabled and can reopen or center the main window.") } + } + GroupBox("Quick actions") { HStack(spacing: 12) { + Button("Center Window") { + WindowCommandCenter.centerMainWindow() + statusMessage = "Centered main window" + } Button("Open Utility Window") { openWindow(id: "inspector") + statusMessage = "Opened utility window" } Button("Show Sheet") { showSheet = true } - Button("Show Confirmation") { - showDialog = true - } + } + .padding(.top, 4) + } + + Label(statusMessage, systemImage: "checkmark.circle") + .foregroundStyle(.secondary) + } + .padding(24) + } + } + + private var windowControlsView: some View { + Form { + Section("Window identity") { + TextField("Window title", text: $documentTitle) + Button("Apply Title") { + WindowCommandCenter.renameMainWindow(to: documentTitle) + statusMessage = "Renamed main window" + } + } + + Section("Window size") { + VStack(alignment: .leading) { + Text("Width: \(Int(width))") + Slider(value: $width, in: 700...1400, step: 10) + } + VStack(alignment: .leading) { + Text("Height: \(Int(height))") + Slider(value: $height, in: 480...960, step: 10) + } + HStack(spacing: 12) { + Button("Apply Size") { + WindowCommandCenter.resizeMainWindow(width: width, height: height) + statusMessage = "Resized main window" + } + Button("Center") { + WindowCommandCenter.centerMainWindow() + statusMessage = "Centered main window" + } + Button("Toggle Full Screen") { + WindowCommandCenter.toggleFullScreen() + statusMessage = "Toggled full screen" } } - .padding(24) } - .navigationTitle(documentTitle) - .sheet(isPresented: $showSheet) { - SheetDemoView() - } - .confirmationDialog("Test confirmation dialog", isPresented: $showDialog, titleVisibility: .visible) { - Button("Acknowledge") {} - Button("Cancel", role: .cancel) {} - } message: { - Text("This validates standard app-window attached dialogs on macOS.") + + Section("Window appearance") { + VStack(alignment: .leading) { + Text("Opacity: \(windowOpacity.formatted(.number.precision(.fractionLength(2))))") + Slider(value: $windowOpacity, in: 0.35...1.0, step: 0.05) + } + Button("Apply Opacity") { + WindowCommandCenter.setMainWindowAlpha(windowOpacity) + statusMessage = "Adjusted window opacity" + } } } + .formStyle(.grouped) + .padding(24) + } + + private var secondaryWindowView: some View { + VStack(alignment: .leading, spacing: 18) { + Text("Secondary Window Demo") + .font(.largeTitle.bold()) + Text("Use this section to prove the app can open and focus a second real window, which is useful for inspectors, logs, tools, or floating utilities.") + .foregroundStyle(.secondary) + + HStack(spacing: 12) { + Button("Open Utility Window") { + openWindow(id: "inspector") + statusMessage = "Opened utility window" + } + Button("Bring Main Window Forward") { + WindowCommandCenter.showMainWindow() + statusMessage = "Brought main window forward" + } + } + + GroupBox("What this demonstrates") { + VStack(alignment: .leading, spacing: 10) { + Label("Multiple scenes/windows", systemImage: "macwindow.on.rectangle") + Label("Window switching from app UI", systemImage: "arrow.triangle.2.circlepath") + Label("Useful split between content and tools", systemImage: "sidebar.right") + } + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.top, 4) + } + + Spacer() + } + .padding(24) + } + + private var dialogsView: some View { + VStack(alignment: .leading, spacing: 18) { + Text("Dialogs Demo") + .font(.largeTitle.bold()) + Text("These controls trigger different interaction patterns so the app demonstrates more than a static window.") + .foregroundStyle(.secondary) + + HStack(spacing: 12) { + Button("Show Sheet") { + showSheet = true + } + Button("Show Alert") { + showAlert = true + } + Button("Show Confirmation") { + showDialog = true + } + } + + GroupBox("Last action") { + Text(statusMessage) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.top, 4) + } + + Spacer() + } + .padding(24) } } struct SheetDemoView: View { + @Binding var statusMessage: String @Environment(\.dismiss) private var dismiss var body: some View { @@ -129,8 +280,11 @@ struct SheetDemoView: View { .foregroundStyle(.secondary) HStack { Spacer() - Button("Close") { dismiss() } - .keyboardShortcut(.defaultAction) + Button("Close") { + statusMessage = "Closed attached sheet" + dismiss() + } + .keyboardShortcut(.defaultAction) } } .padding(24) diff --git a/SwiftTrayDemo/SwiftTrayDemoApp.swift b/SwiftTrayDemo/SwiftTrayDemoApp.swift index a623925..56f2741 100644 --- a/SwiftTrayDemo/SwiftTrayDemoApp.swift +++ b/SwiftTrayDemo/SwiftTrayDemoApp.swift @@ -12,6 +12,7 @@ enum WindowCommandCenter { var frame = window.frame frame.size = NSSize(width: width, height: height) window.setFrame(frame, display: true, animate: true) + NSApp.activate(ignoringOtherApps: true) window.makeKeyAndOrderFront(nil) } @@ -33,6 +34,18 @@ enum WindowCommandCenter { NSApp.activate(ignoringOtherApps: true) window.makeKeyAndOrderFront(nil) } + + static func renameMainWindow(to title: String) { + guard let window = mainWindow() else { return } + window.title = title + } + + static func setMainWindowAlpha(_ value: Double) { + guard let window = mainWindow() else { return } + window.alphaValue = CGFloat(value) + NSApp.activate(ignoringOtherApps: true) + window.makeKeyAndOrderFront(nil) + } } final class WindowAccessor: NSView { @@ -65,45 +78,6 @@ struct MainWindowConfigurator: NSViewRepresentable { func updateNSView(_ nsView: NSView, context: Context) {} } -final class TrayController: NSObject { - private var statusItem: NSStatusItem? - - func install() { - let item = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength) - item.button?.image = NSImage(systemSymbolName: "menubar.rectangle", accessibilityDescription: "Swift Window Demo") - item.button?.toolTip = "Swift Window Demo" - - let menu = NSMenu() - - let openItem = NSMenuItem(title: "Open Main Window", action: #selector(MenuActionBox.performAction), keyEquivalent: "o") - openItem.target = MenuActionBox { - WindowCommandCenter.showMainWindow() - } - menu.addItem(openItem) - - let centerItem = NSMenuItem(title: "Center Main Window", action: #selector(MenuActionBox.performAction), keyEquivalent: "c") - centerItem.target = MenuActionBox { - WindowCommandCenter.centerMainWindow() - } - menu.addItem(centerItem) - - menu.addItem(.separator()) - - let quitItem = NSMenuItem(title: "Quit", action: #selector(MenuActionBox.performAction), keyEquivalent: "q") - quitItem.target = MenuActionBox { - NSApp.terminate(nil) - } - menu.addItem(quitItem) - - objc_setAssociatedObject(openItem, UnsafeRawPointer(bitPattern: 1)!, openItem.target, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) - objc_setAssociatedObject(centerItem, UnsafeRawPointer(bitPattern: 2)!, centerItem.target, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) - objc_setAssociatedObject(quitItem, UnsafeRawPointer(bitPattern: 3)!, quitItem.target, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) - - item.menu = menu - statusItem = item - } -} - final class MenuActionBox: NSObject { private let action: () -> Void @@ -116,6 +90,58 @@ final class MenuActionBox: NSObject { } } +final class TrayController: NSObject { + private var statusItem: NSStatusItem? + private var retainedTargets: [MenuActionBox] = [] + + func install() { + let item = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength) + item.button?.image = NSImage(systemSymbolName: "menubar.rectangle", accessibilityDescription: "Swift Window Demo") + item.button?.toolTip = "Swift Window Demo" + + let menu = NSMenu() + menu.autoenablesItems = false + + let openTarget = MenuActionBox { + WindowCommandCenter.showMainWindow() + } + let openItem = NSMenuItem(title: "Open Main Window", action: #selector(MenuActionBox.performAction), keyEquivalent: "o") + openItem.target = openTarget + openItem.isEnabled = true + menu.addItem(openItem) + + let centerTarget = MenuActionBox { + WindowCommandCenter.centerMainWindow() + } + let centerItem = NSMenuItem(title: "Center Main Window", action: #selector(MenuActionBox.performAction), keyEquivalent: "c") + centerItem.target = centerTarget + centerItem.isEnabled = true + menu.addItem(centerItem) + + let fullscreenTarget = MenuActionBox { + WindowCommandCenter.toggleFullScreen() + } + let fullscreenItem = NSMenuItem(title: "Toggle Full Screen", action: #selector(MenuActionBox.performAction), keyEquivalent: "f") + fullscreenItem.target = fullscreenTarget + fullscreenItem.isEnabled = true + menu.addItem(fullscreenItem) + + menu.addItem(.separator()) + + let quitTarget = MenuActionBox { + NSApp.terminate(nil) + } + let quitItem = NSMenuItem(title: "Quit", action: #selector(MenuActionBox.performAction), keyEquivalent: "q") + quitItem.target = quitTarget + quitItem.isEnabled = true + menu.addItem(quitItem) + + retainedTargets = [openTarget, centerTarget, fullscreenTarget, quitTarget] + item.menu = menu + statusItem = item + } +} + final class AppDelegate: NSObject, NSApplicationDelegate { private let trayController = TrayController() @@ -134,6 +160,7 @@ struct InspectorView: View { Divider() Label("Independent scene", systemImage: "macwindow.on.rectangle") Label("Useful for inspectors, logs, or tools", systemImage: "sidebar.right") + Label("Opened from both sidebar actions and tray-driven workflows", systemImage: "menubar.rectangle") } .padding(20) .frame(minWidth: 360, minHeight: 220) @@ -156,6 +183,11 @@ struct SwiftTrayDemoApp: App { WindowCommandCenter.centerMainWindow() } .keyboardShortcut("0") + + Button("Toggle Full Screen") { + WindowCommandCenter.toggleFullScreen() + } + .keyboardShortcut("f") } }