diff --git a/DynamicIsland/DynamicIslandApp.swift b/DynamicIsland/DynamicIslandApp.swift index 5e09975..b07593f 100644 --- a/DynamicIsland/DynamicIslandApp.swift +++ b/DynamicIsland/DynamicIslandApp.swift @@ -450,8 +450,10 @@ class AppDelegate: NSObject, NSApplicationDelegate { ClipboardManager.shared.startMonitoring() } - // Check if window mode is enabled - if Defaults[.clipboardWindowMode] { + // Determine display mode based on user preference and context + let shouldUseWindow = self.shouldUseWindowMode() + + if shouldUseWindow { // Use window interface ClipboardWindowManager.shared.toggleClipboardWindow() } else { @@ -622,6 +624,60 @@ class AppDelegate: NSObject, NSApplicationDelegate { NSApplication.shared.terminate(nil) } + + // MARK: - Clipboard Display Mode Logic + + private func shouldUseWindowMode() -> Bool { + let displayMode = Defaults[.clipboardDisplayMode] + + switch displayMode { + case .window: + return true + case .popover: + return false + case .auto: + // Auto mode: use window if any app is in fullscreen, otherwise use popover + return isAnyAppInFullscreen() + } + } + + private func isAnyAppInFullscreen() -> Bool { + // Check if any running application is in fullscreen mode + let workspace = NSWorkspace.shared + + // Get all visible applications + let apps = workspace.runningApplications.filter { app in + app.activationPolicy == .regular && !app.isTerminated + } + + // Check each application's windows for fullscreen state + for app in apps { + if app.isActive { + // Check if the frontmost application has fullscreen windows + let windows = CGWindowListCopyWindowInfo(.optionOnScreenOnly, kCGNullWindowID) as? [[String: Any]] ?? [] + + for window in windows { + if let ownerPID = window[kCGWindowOwnerPID as String] as? Int32, + ownerPID == app.processIdentifier, + let bounds = window[kCGWindowBounds as String] as? [String: Any], + let width = bounds["Width"] as? CGFloat, + let height = bounds["Height"] as? CGFloat { + + // Check if window dimensions match screen dimensions (indicating fullscreen) + for screen in NSScreen.screens { + let screenFrame = screen.frame + if abs(width - screenFrame.width) < 10 && abs(height - screenFrame.height) < 10 { + return true + } + } + } + } + } + } + + return false + } + private func showOnboardingWindow() { if onboardingWindowController == nil { let window = NSWindow( diff --git a/DynamicIsland/Localizable.xcstrings b/DynamicIsland/Localizable.xcstrings index 4563a06..cb41d1a 100644 --- a/DynamicIsland/Localizable.xcstrings +++ b/DynamicIsland/Localizable.xcstrings @@ -136,6 +136,9 @@ }, "Appearance" : { + }, + "Auto mode intelligently chooses between popover (desktop) and window (fullscreen apps) for the best experience. You can override this with manual selection." : { + }, "Automatically check for updates" : { "localizations" : { @@ -388,6 +391,9 @@ }, "Disk Write" : { + }, + "Display Mode" : { + }, "Download" : { @@ -1196,9 +1202,6 @@ }, "When enabled, the Stats tab will display real-time system performance graphs. This feature requires system permissions and may use additional battery." : { - }, - "Window Mode shows clipboard in a moveable window like Raycast with tabs for history and favorites. When disabled, clipboard appears in a popover attached to the Dynamic Island." : { - }, "With %lld graphs enabled, the Dynamic Island will expand horizontally to accommodate all graphs in a single row." : { diff --git a/DynamicIsland/components/Notch/DynamicIslandHeader.swift b/DynamicIsland/components/Notch/DynamicIslandHeader.swift index cbdd9cb..e6c35d8 100644 --- a/DynamicIsland/components/Notch/DynamicIslandHeader.swift +++ b/DynamicIsland/components/Notch/DynamicIslandHeader.swift @@ -63,7 +63,25 @@ struct DynamicIslandHeader: View { if Defaults[.enableClipboardManager] && Defaults[.showClipboardIcon] { Button(action: { - showClipboardPopover.toggle() + // Use the same smart logic as the keyboard shortcut + let displayMode = Defaults[.clipboardDisplayMode] + let shouldUseWindow: Bool + + switch displayMode { + case .window: + shouldUseWindow = true + case .popover: + shouldUseWindow = false + case .auto: + // For header button clicks, prefer popover since user is already interacting with notch + shouldUseWindow = false + } + + if shouldUseWindow { + ClipboardWindowManager.shared.toggleClipboardWindow() + } else { + showClipboardPopover.toggle() + } }) { Capsule() .fill(.black) diff --git a/DynamicIsland/components/Settings/SettingsView.swift b/DynamicIsland/components/Settings/SettingsView.swift index dde6ea5..a03972b 100644 --- a/DynamicIsland/components/Settings/SettingsView.swift +++ b/DynamicIsland/components/Settings/SettingsView.swift @@ -1589,7 +1589,7 @@ struct ClipboardSettings: View { @Default(.enableClipboardManager) var enableClipboardManager @Default(.clipboardHistorySize) var clipboardHistorySize @Default(.showClipboardIcon) var showClipboardIcon - @Default(.clipboardWindowMode) var clipboardWindowMode + @Default(.clipboardDisplayMode) var clipboardDisplayMode var body: some View { Form { @@ -1612,7 +1612,24 @@ struct ClipboardSettings: View { Section { Defaults.Toggle("Show Clipboard Icon", key: .showClipboardIcon) - Defaults.Toggle("Window Mode", key: .clipboardWindowMode) + VStack(alignment: .leading, spacing: 8) { + HStack { + Text("Display Mode") + Spacer() + Picker("Display Mode", selection: $clipboardDisplayMode) { + ForEach(ClipboardDisplayMode.allCases, id: \.self) { mode in + Text(mode.displayName).tag(mode) + } + } + .pickerStyle(.menu) + .frame(width: 140) + } + + Text(clipboardDisplayMode.description) + .font(.caption) + .foregroundColor(.secondary) + .padding(.leading, 0) + } HStack { Text("History Size") @@ -1650,7 +1667,7 @@ struct ClipboardSettings: View { } header: { Text("Settings") } footer: { - Text("Window Mode shows clipboard in a moveable window like Raycast with tabs for history and favorites. When disabled, clipboard appears in a popover attached to the Dynamic Island.") + Text("Auto mode intelligently chooses between popover (desktop) and window (fullscreen apps) for the best experience. You can override this with manual selection.") } Section { diff --git a/DynamicIsland/managers/ClipboardWindowManager.swift b/DynamicIsland/managers/ClipboardWindowManager.swift index bb7834a..a2a0c5c 100644 --- a/DynamicIsland/managers/ClipboardWindowManager.swift +++ b/DynamicIsland/managers/ClipboardWindowManager.swift @@ -17,7 +17,10 @@ class ClipboardWindowManager: ObservableObject { func showClipboardWindow() { if let existingWindow = clipboardWindow { + // Ensure window appears above fullscreen apps + existingWindow.level = .screenSaver existingWindow.makeKeyAndOrderFront(nil) + existingWindow.orderFrontRegardless() // Force window to front even above fullscreen NSApp.activate(ignoringOtherApps: true) return } @@ -32,24 +35,24 @@ class ClipboardWindowManager: ObservableObject { window.title = "Clipboard Manager" window.isMovableByWindowBackground = true - window.level = .floating + window.level = .screenSaver // Use screenSaver level to appear above fullscreen apps window.isReleasedWhenClosed = false window.hasShadow = true window.titlebarAppearsTransparent = true window.styleMask.insert(.fullSizeContentView) + window.collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary] // Allow on all spaces and above fullscreen // Set minimum and maximum sizes window.minSize = NSSize(width: 350, height: 250) window.maxSize = NSSize(width: 600, height: 500) - // Center the window - if let screen = NSScreen.main { - let screenFrame = screen.visibleFrame - let windowFrame = window.frame - let x = (screenFrame.width - windowFrame.width) / 2 + screenFrame.minX - let y = (screenFrame.height - windowFrame.height) / 2 + screenFrame.minY - window.setFrameOrigin(NSPoint(x: x, y: y)) - } + // Center the window on the current screen (important for fullscreen apps) + let currentScreen = NSScreen.main ?? NSScreen.screens.first! + let screenFrame = currentScreen.frame // Use full frame instead of visibleFrame for fullscreen compatibility + let windowFrame = window.frame + let x = (screenFrame.width - windowFrame.width) / 2 + screenFrame.minX + let y = (screenFrame.height - windowFrame.height) / 2 + screenFrame.minY + window.setFrameOrigin(NSPoint(x: x, y: y)) // Set the content view let contentView = ClipboardWindow() @@ -63,6 +66,7 @@ class ClipboardWindowManager: ObservableObject { self.clipboardWindow = window window.makeKeyAndOrderFront(nil) + window.orderFrontRegardless() // Force window to front even above fullscreen apps NSApp.activate(ignoringOtherApps: true) } diff --git a/DynamicIsland/models/Constants.swift b/DynamicIsland/models/Constants.swift index 0f98e94..7d0e2ae 100644 --- a/DynamicIsland/models/Constants.swift +++ b/DynamicIsland/models/Constants.swift @@ -30,6 +30,28 @@ enum CalendarSelectionState: Codable, Defaults.Serializable { case selected(Set) } +enum ClipboardDisplayMode: String, CaseIterable, Codable, Defaults.Serializable { + case auto = "auto" // Automatically choose: popover on desktop, window in fullscreen + case popover = "popover" // Always use popover (attached to notch) + case window = "window" // Always use standalone window + + var displayName: String { + switch self { + case .auto: return "Auto (Smart)" + case .popover: return "Popover" + case .window: return "Window" + } + } + + var description: String { + switch self { + case .auto: return "Automatically chooses the best display mode based on context" + case .popover: return "Always shows clipboard attached to the Dynamic Island" + case .window: return "Always shows clipboard in a standalone moveable window" + } + } +} + enum HideNotchOption: String, Defaults.Serializable { case always case nowPlayingOnly @@ -170,7 +192,7 @@ extension Defaults.Keys { static let enableClipboardManager = Key("enableClipboardManager", default: true) static let clipboardHistorySize = Key("clipboardHistorySize", default: 3) static let showClipboardIcon = Key("showClipboardIcon", default: true) - static let clipboardWindowMode = Key("clipboardWindowMode", default: true) + static let clipboardDisplayMode = Key("clipboardDisplayMode", default: .auto) // MARK: Keyboard Shortcuts static let enableShortcuts = Key("enableShortcuts", default: true)