From e583069870dc10c0b87bbc225b464a21d1b029d6 Mon Sep 17 00:00:00 2001 From: Hariharan Mudaliar Date: Tue, 12 Aug 2025 23:38:10 +0530 Subject: [PATCH] Clipboard Floating Panel --- DynamicIsland/ContentView.swift | 11 +- DynamicIsland/DynamicIslandApp.swift | 90 +--- DynamicIsland/Localizable.xcstrings | 6 +- .../components/Clipboard/ClipboardPanel.swift | 410 ++++++++++++++++++ .../Clipboard/ClipboardShared.swift | 68 +++ .../Clipboard/ClipboardWindow.swift | 60 --- .../Notch/DynamicIslandHeader.swift | 36 +- .../Notch/DynamicIslandWindow.swift | 4 +- .../components/Settings/SettingsView.swift | 20 - .../managers/ClipboardPanelManager.swift | 55 +++ DynamicIsland/models/Constants.swift | 14 +- .../models/DynamicIslandViewModel.swift | 1 - 12 files changed, 549 insertions(+), 226 deletions(-) create mode 100644 DynamicIsland/components/Clipboard/ClipboardPanel.swift create mode 100644 DynamicIsland/components/Clipboard/ClipboardShared.swift create mode 100644 DynamicIsland/managers/ClipboardPanelManager.swift diff --git a/DynamicIsland/ContentView.swift b/DynamicIsland/ContentView.swift index 1291285..d194707 100644 --- a/DynamicIsland/ContentView.swift +++ b/DynamicIsland/ContentView.swift @@ -124,7 +124,7 @@ struct ContentView: View { } // Only close if mouse leaves and the notch is open and no popovers are active - if !hovering && vm.notchState == .open && !vm.isBatteryPopoverActive && !vm.isClipboardPopoverActive { + if !hovering && vm.notchState == .open && !vm.isBatteryPopoverActive { vm.close() } } @@ -177,13 +177,6 @@ struct ContentView: View { } } } - .onChange(of: vm.isClipboardPopoverActive) { _, newPopoverState in - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { - if !newPopoverState && !isHovering && vm.notchState == .open && !vm.isBatteryPopoverActive { - vm.close() - } - } - } .sensoryFeedback(.alignment, trigger: haptics) .contextMenu { SettingsLink(label: { @@ -524,7 +517,7 @@ struct ContentView: View { } // Close the notch if it's open and no popovers are active - if vm.notchState == .open && !vm.isBatteryPopoverActive && !vm.isClipboardPopoverActive { + if vm.notchState == .open && !vm.isBatteryPopoverActive { vm.close() } } diff --git a/DynamicIsland/DynamicIslandApp.swift b/DynamicIsland/DynamicIslandApp.swift index b07593f..42e9ed4 100644 --- a/DynamicIsland/DynamicIslandApp.swift +++ b/DynamicIsland/DynamicIslandApp.swift @@ -450,42 +450,8 @@ class AppDelegate: NSObject, NSApplicationDelegate { ClipboardManager.shared.startMonitoring() } - // Determine display mode based on user preference and context - let shouldUseWindow = self.shouldUseWindowMode() - - if shouldUseWindow { - // Use window interface - ClipboardWindowManager.shared.toggleClipboardWindow() - } else { - // Use traditional popover interface - // Find the appropriate view model based on mouse location - let mouseLocation = NSEvent.mouseLocation - var viewModel = self.vm - - if Defaults[.showOnAllDisplays] { - for screen in NSScreen.screens { - if screen.frame.contains(mouseLocation) { - if let screenViewModel = self.viewModels[screen] { - viewModel = screenViewModel - break - } - } - } - } - - // Open the notch if it's closed - if viewModel.notchState == .closed { - viewModel.open() - - // Wait a bit for the notch to open before toggling clipboard - DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { - self.coordinator.toggleClipboardPopover() - } - } else { - // If notch is already open, toggle immediately - self.coordinator.toggleClipboardPopover() - } - } + // Always use panel mode + ClipboardPanelManager.shared.toggleClipboardPanel() } if !Defaults[.showOnAllDisplays] { @@ -625,58 +591,6 @@ class AppDelegate: NSObject, NSApplicationDelegate { } - // 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 { diff --git a/DynamicIsland/Localizable.xcstrings b/DynamicIsland/Localizable.xcstrings index cb41d1a..7213468 100644 --- a/DynamicIsland/Localizable.xcstrings +++ b/DynamicIsland/Localizable.xcstrings @@ -391,9 +391,6 @@ }, "Disk Write" : { - }, - "Display Mode" : { - }, "Download" : { @@ -873,6 +870,9 @@ }, "Pin items from history to save them here" : { + }, + "Pin items to add them to favorites" : { + }, "Pinned Items" : { diff --git a/DynamicIsland/components/Clipboard/ClipboardPanel.swift b/DynamicIsland/components/Clipboard/ClipboardPanel.swift new file mode 100644 index 0000000..2576e0a --- /dev/null +++ b/DynamicIsland/components/Clipboard/ClipboardPanel.swift @@ -0,0 +1,410 @@ +// +// ClipboardPanel.swift +// DynamicIsland +// +// Created by GitHub Copilot on 12/08/25. +// + +import AppKit +import SwiftUI + +class ClipboardPanel: NSPanel { + + init() { + super.init( + contentRect: .zero, + styleMask: [.borderless, .nonactivatingPanel], + backing: .buffered, + defer: true + ) + + setupWindow() + setupContentView() + } + + // Override to allow the panel to become key window (required for TextField focus) + override var canBecomeKey: Bool { + return true + } + + // Override to allow the panel to become main window (required for text input) + override var canBecomeMain: Bool { + return true + } + + private func setupWindow() { + backgroundColor = .clear + isOpaque = false + hasShadow = true + level = .floating + isMovableByWindowBackground = true // Enable dragging + titlebarAppearsTransparent = true + titleVisibility = .hidden + isFloatingPanel = true // Mark as floating panel for proper behavior + + // Allow dragging from any part of the window + styleMask.insert(.fullSizeContentView) + + collectionBehavior = [ + .canJoinAllSpaces, + .stationary, + .fullScreenAuxiliary // Float above full-screen apps + ] + + // Accept mouse moved events for proper hover behavior + acceptsMouseMovedEvents = true + } + + private func setupContentView() { + let contentView = ClipboardPanelView { + self.close() + } + + let hostingView = NSHostingView(rootView: contentView) + self.contentView = hostingView + + // Set initial size + let preferredSize = CGSize(width: 320, height: 400) + hostingView.setFrameSize(preferredSize) + setContentSize(preferredSize) + } + + func positionNearNotch() { + guard let screen = NSScreen.main else { return } + + let screenFrame = screen.visibleFrame + let panelFrame = frame + + // Position at top center of screen (near where the notch would be) + let xPosition = (screenFrame.width - panelFrame.width) / 2 + screenFrame.minX + let yPosition = screenFrame.maxY - panelFrame.height - 10 // 10px from top + + setFrameOrigin(NSPoint(x: xPosition, y: yPosition)) + } + + func positionNearMouse() { + let mouseLocation = NSEvent.mouseLocation + let panelFrame = frame + + // Position near mouse but ensure it stays on screen + guard let screen = NSScreen.main else { return } + let screenFrame = screen.visibleFrame + + var xPosition = mouseLocation.x - panelFrame.width / 2 + var yPosition = mouseLocation.y - panelFrame.height - 20 + + // Keep within screen bounds + xPosition = max(screenFrame.minX + 10, min(xPosition, screenFrame.maxX - panelFrame.width - 10)) + yPosition = max(screenFrame.minY + 10, min(yPosition, screenFrame.maxY - panelFrame.height - 10)) + + setFrameOrigin(NSPoint(x: xPosition, y: yPosition)) + } +} + +struct ClipboardPanelView: View { + let onClose: () -> Void + @ObservedObject var clipboardManager = ClipboardManager.shared + @State private var selectedTab: ClipboardTab = .history + @State private var searchText = "" + @State private var hoveredItemId: UUID? + + var filteredItems: [ClipboardItem] { + let allItems = selectedTab == .history ? clipboardManager.regularHistory : clipboardManager.pinnedItems + + if searchText.isEmpty { + return allItems + } else { + return allItems.filter { item in + item.preview.localizedCaseInsensitiveContains(searchText) || + item.type.displayName.localizedCaseInsensitiveContains(searchText) + } + } + } + + var body: some View { + VStack(spacing: 0) { + // Header with tabs + ClipboardPanelHeader( + selectedTab: $selectedTab, + searchText: $searchText, + onClose: onClose + ) + + Divider() + .background(Color.gray.opacity(0.3)) + + // Content + if filteredItems.isEmpty { + ClipboardPanelEmptyState( + hasSearch: !searchText.isEmpty, + isHistoryTab: selectedTab == .history + ) + } else { + ScrollView { + LazyVStack(spacing: 1) { + ForEach(filteredItems) { item in + ClipboardPanelItemRow( + item: item, + isHovered: hoveredItemId == item.id, + isPinned: clipboardManager.pinnedItems.contains(where: { $0.id == item.id }) + ) { hoverId in + hoveredItemId = hoverId + } + } + } + .padding(.vertical, 8) + } + } + } + .frame(width: 320, height: 400) + .background(VisualEffectView(material: .hudWindow, blendingMode: .behindWindow)) + .cornerRadius(12) + .shadow(color: .black.opacity(0.4), radius: 20, x: 0, y: 10) + } +} + +struct ClipboardPanelHeader: View { + @Binding var selectedTab: ClipboardTab + @Binding var searchText: String + let onClose: () -> Void + @ObservedObject var clipboardManager = ClipboardManager.shared + @FocusState private var isSearchFieldFocused: Bool + + var body: some View { + VStack(spacing: 8) { + // Title and close button + HStack { + Image(systemName: "doc.on.clipboard") + .foregroundColor(.primary) + .font(.system(size: 16, weight: .medium)) + + Text("Clipboard Manager") + .font(.system(size: 14, weight: .semibold)) + .foregroundColor(.primary) + + Spacer() + + // Clear button + Button(action: { + if selectedTab == .history { + clipboardManager.clearHistory() + } else { + clipboardManager.pinnedItems.removeAll() + clipboardManager.savePinnedItemsToDefaults() + } + }) { + Image(systemName: "trash") + .foregroundColor(.red) + .font(.system(size: 12)) + } + .buttonStyle(PlainButtonStyle()) + .disabled(selectedTab == .history ? clipboardManager.clipboardHistory.isEmpty : clipboardManager.pinnedItems.isEmpty) + + Button(action: onClose) { + Image(systemName: "xmark") + .foregroundColor(.secondary) + .font(.system(size: 12, weight: .medium)) + } + .buttonStyle(PlainButtonStyle()) + } + .padding(.horizontal, 16) + .padding(.top, 12) + + // Tab selector + HStack(spacing: 0) { + ForEach(ClipboardTab.allCases, id: \.self) { tab in + ClipboardTabButton(tab: tab, isSelected: selectedTab == tab) { + withAnimation(.easeInOut(duration: 0.2)) { + selectedTab = tab + } + } + } + + Spacer() + } + .padding(.horizontal, 16) + + // Search bar (always visible) + HStack { + Image(systemName: "magnifyingglass") + .foregroundColor(.secondary) + .font(.system(size: 12)) + + TextField("Search clipboard...", text: $searchText) + .textFieldStyle(PlainTextFieldStyle()) + .font(.system(size: 12)) + .focused($isSearchFieldFocused) + + if !searchText.isEmpty { + Button(action: { searchText = "" }) { + Image(systemName: "xmark.circle.fill") + .foregroundColor(.secondary) + .font(.system(size: 10)) + } + .buttonStyle(PlainButtonStyle()) + } + } + .padding(.horizontal, 12) + .padding(.vertical, 6) + .background( + RoundedRectangle(cornerRadius: 6) + .fill(Color.gray.opacity(0.1)) + ) + .padding(.horizontal, 16) + } + .padding(.bottom, 8) + } +} + +struct ClipboardPanelEmptyState: View { + let hasSearch: Bool + let isHistoryTab: Bool + + var body: some View { + VStack(spacing: 12) { + Image(systemName: hasSearch ? "magnifyingglass" : (isHistoryTab ? "doc.on.clipboard" : "heart.fill")) + .font(.system(size: 32)) + .foregroundColor(.secondary) + + if hasSearch { + Text("No results found") + .font(.system(size: 14, weight: .medium)) + .foregroundColor(.primary) + + Text("Try adjusting your search terms") + .font(.system(size: 12)) + .foregroundColor(.secondary) + } else { + Text(isHistoryTab ? "No clipboard history" : "No favorites") + .font(.system(size: 14, weight: .medium)) + .foregroundColor(.primary) + + Text(isHistoryTab ? "Copy something to get started" : "Pin items to add them to favorites") + .font(.system(size: 12)) + .foregroundColor(.secondary) + } + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + } +} + +struct ClipboardPanelItemRow: View { + let item: ClipboardItem + let isHovered: Bool + let isPinned: Bool + let onHover: (UUID?) -> Void + @ObservedObject var clipboardManager = ClipboardManager.shared + + var body: some View { + HStack(alignment: .top, spacing: 12) { + // Type icon + Image(systemName: item.type.icon) + .font(.system(size: 14)) + .foregroundColor(.blue) + .frame(width: 20) + + // Content + VStack(alignment: .leading, spacing: 4) { + Text(item.preview) + .font(.system(size: 12)) + .foregroundColor(.primary) + .lineLimit(2) + .multilineTextAlignment(.leading) + + HStack { + Text(item.type.displayName) + .font(.system(size: 10)) + .foregroundColor(.secondary) + + Spacer() + + Text(timeAgoString(from: item.timestamp)) + .font(.system(size: 10)) + .foregroundColor(.secondary) + } + } + + Spacer() + + // Action buttons (shown on hover) + if isHovered { + HStack(spacing: 6) { + // Pin/Unpin button + Button(action: { + if isPinned { + clipboardManager.unpinItem(item) + } else { + clipboardManager.pinItem(item) + } + }) { + Image(systemName: isPinned ? "heart.fill" : "heart") + .font(.system(size: 11)) + .foregroundColor(isPinned ? .red : .gray) + } + .buttonStyle(PlainButtonStyle()) + + // Copy button + Button(action: { + clipboardManager.copyToClipboard(item) + }) { + Image(systemName: "doc.on.doc") + .font(.system(size: 11)) + .foregroundColor(.green) + } + .buttonStyle(PlainButtonStyle()) + + // Delete button + Button(action: { + if isPinned { + clipboardManager.unpinItem(item) + } else { + clipboardManager.deleteItem(item) + } + }) { + Image(systemName: "trash") + .font(.system(size: 11)) + .foregroundColor(.red) + } + .buttonStyle(PlainButtonStyle()) + } + } + } + .padding(.horizontal, 16) + .padding(.vertical, 8) + .background( + RoundedRectangle(cornerRadius: 6) + .fill(isHovered ? Color.gray.opacity(0.1) : Color.clear) + ) + .onHover { hovering in + withAnimation(.easeInOut(duration: 0.2)) { + onHover(hovering ? item.id : nil) + } + } + .onTapGesture { + clipboardManager.copyToClipboard(item) + } + } + + private func timeAgoString(from date: Date) -> String { + let interval = Date().timeIntervalSince(date) + + if interval < 60 { + return "Just now" + } else if interval < 3600 { + let minutes = Int(interval / 60) + return "\(minutes)m ago" + } else if interval < 86400 { + let hours = Int(interval / 3600) + return "\(hours)h ago" + } else { + let days = Int(interval / 86400) + return "\(days)d ago" + } + } +} + +#Preview { + ClipboardPanelView { + print("Close panel") + } +} diff --git a/DynamicIsland/components/Clipboard/ClipboardShared.swift b/DynamicIsland/components/Clipboard/ClipboardShared.swift new file mode 100644 index 0000000..33af1b6 --- /dev/null +++ b/DynamicIsland/components/Clipboard/ClipboardShared.swift @@ -0,0 +1,68 @@ +// +// ClipboardShared.swift +// DynamicIsland +// +// Created by GitHub Copilot on 12/08/25. +// + +import SwiftUI + +enum ClipboardTab: String, CaseIterable { + case history = "History" + case favorites = "Favorites" + + var icon: String { + switch self { + case .history: return "clock" + case .favorites: return "heart.fill" + } + } +} + +struct ClipboardTabButton: View { + let tab: ClipboardTab + let isSelected: Bool + let action: () -> Void + @ObservedObject var clipboardManager = ClipboardManager.shared + + var itemCount: Int { + switch tab { + case .history: + return clipboardManager.regularHistory.count + case .favorites: + return clipboardManager.pinnedItems.count + } + } + + var body: some View { + Button(action: action) { + HStack(spacing: 6) { + Image(systemName: tab.icon) + .font(.system(size: 11)) + + Text(tab.rawValue) + .font(.system(size: 11, weight: .medium)) + + if itemCount > 0 { + Text("\(itemCount)") + .font(.system(size: 9, weight: .bold)) + .foregroundColor(isSelected ? .white : .secondary) + .padding(.horizontal, 4) + .padding(.vertical, 1) + .background( + Capsule() + .fill(isSelected ? Color.white.opacity(0.3) : Color.gray.opacity(0.2)) + ) + } + } + .foregroundColor(isSelected ? .white : .secondary) + .padding(.horizontal, 12) + .padding(.vertical, 6) + .background( + RoundedRectangle(cornerRadius: 6) + .fill(isSelected ? Color.blue : Color.clear) + ) + } + .buttonStyle(PlainButtonStyle()) + } +} diff --git a/DynamicIsland/components/Clipboard/ClipboardWindow.swift b/DynamicIsland/components/Clipboard/ClipboardWindow.swift index b727067..bb8ac72 100644 --- a/DynamicIsland/components/Clipboard/ClipboardWindow.swift +++ b/DynamicIsland/components/Clipboard/ClipboardWindow.swift @@ -8,18 +8,6 @@ import SwiftUI import Defaults -enum ClipboardTab: String, CaseIterable { - case history = "History" - case favorites = "Favorites" - - var icon: String { - switch self { - case .history: return "clock" - case .favorites: return "heart.fill" - } - } -} - struct ClipboardWindow: View { @ObservedObject var clipboardManager = ClipboardManager.shared @State private var selectedTab: ClipboardTab = .history @@ -126,54 +114,6 @@ struct ClipboardWindowHeader: View { } } -struct ClipboardTabButton: View { - let tab: ClipboardTab - let isSelected: Bool - let action: () -> Void - @ObservedObject var clipboardManager = ClipboardManager.shared - - var itemCount: Int { - switch tab { - case .history: - return clipboardManager.regularHistory.count - case .favorites: - return clipboardManager.pinnedItems.count - } - } - - var body: some View { - Button(action: action) { - HStack(spacing: 6) { - Image(systemName: tab.icon) - .font(.system(size: 11)) - - Text(tab.rawValue) - .font(.system(size: 11, weight: .medium)) - - if itemCount > 0 { - Text("\(itemCount)") - .font(.system(size: 9, weight: .bold)) - .foregroundColor(isSelected ? .white : .secondary) - .padding(.horizontal, 4) - .padding(.vertical, 1) - .background( - Capsule() - .fill(isSelected ? Color.white.opacity(0.3) : Color.gray.opacity(0.2)) - ) - } - } - .foregroundColor(isSelected ? .white : .secondary) - .padding(.horizontal, 12) - .padding(.vertical, 6) - .background( - RoundedRectangle(cornerRadius: 6) - .fill(isSelected ? Color.blue : Color.clear) - ) - } - .buttonStyle(PlainButtonStyle()) - } -} - struct ClipboardWindowContent: View { let selectedTab: ClipboardTab let searchText: String diff --git a/DynamicIsland/components/Notch/DynamicIslandHeader.swift b/DynamicIsland/components/Notch/DynamicIslandHeader.swift index e6c35d8..0f005d6 100644 --- a/DynamicIsland/components/Notch/DynamicIslandHeader.swift +++ b/DynamicIsland/components/Notch/DynamicIslandHeader.swift @@ -15,7 +15,6 @@ struct DynamicIslandHeader: View { @ObservedObject var coordinator = DynamicIslandViewCoordinator.shared @ObservedObject var clipboardManager = ClipboardManager.shared @StateObject var tvm = TrayDrop.shared - @State private var showClipboardPopover = false var body: some View { HStack(spacing: 0) { @@ -63,25 +62,8 @@ struct DynamicIslandHeader: View { if Defaults[.enableClipboardManager] && Defaults[.showClipboardIcon] { Button(action: { - // 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() - } + // Always use panel mode + ClipboardPanelManager.shared.toggleClipboardPanel() }) { Capsule() .fill(.black) @@ -94,18 +76,6 @@ struct DynamicIslandHeader: View { } } .buttonStyle(PlainButtonStyle()) - .popover(isPresented: $showClipboardPopover, arrowEdge: .bottom) { - ClipboardHistoryPopover(isPresented: $showClipboardPopover) - .onAppear { - vm.isClipboardPopoverActive = true - } - .onDisappear { - vm.isClipboardPopoverActive = false - } - } - .onChange(of: showClipboardPopover) { isOpen in - vm.isClipboardPopoverActive = isOpen - } .onAppear { if Defaults[.enableClipboardManager] && !clipboardManager.isMonitoring { clipboardManager.startMonitoring() @@ -155,7 +125,7 @@ struct DynamicIslandHeader: View { .onChange(of: coordinator.shouldToggleClipboardPopover) { _ in // Only toggle if clipboard is enabled if Defaults[.enableClipboardManager] { - showClipboardPopover.toggle() + ClipboardPanelManager.shared.toggleClipboardPanel() } } } diff --git a/DynamicIsland/components/Notch/DynamicIslandWindow.swift b/DynamicIsland/components/Notch/DynamicIslandWindow.swift index df55f02..47e9bc9 100644 --- a/DynamicIsland/components/Notch/DynamicIslandWindow.swift +++ b/DynamicIsland/components/Notch/DynamicIslandWindow.swift @@ -41,10 +41,10 @@ class DynamicIslandWindow: NSPanel { } override var canBecomeKey: Bool { - false + true } override var canBecomeMain: Bool { - false + true } } diff --git a/DynamicIsland/components/Settings/SettingsView.swift b/DynamicIsland/components/Settings/SettingsView.swift index a03972b..96a02d3 100644 --- a/DynamicIsland/components/Settings/SettingsView.swift +++ b/DynamicIsland/components/Settings/SettingsView.swift @@ -1589,7 +1589,6 @@ struct ClipboardSettings: View { @Default(.enableClipboardManager) var enableClipboardManager @Default(.clipboardHistorySize) var clipboardHistorySize @Default(.showClipboardIcon) var showClipboardIcon - @Default(.clipboardDisplayMode) var clipboardDisplayMode var body: some View { Form { @@ -1612,25 +1611,6 @@ struct ClipboardSettings: View { Section { Defaults.Toggle("Show Clipboard Icon", key: .showClipboardIcon) - 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") Spacer() diff --git a/DynamicIsland/managers/ClipboardPanelManager.swift b/DynamicIsland/managers/ClipboardPanelManager.swift new file mode 100644 index 0000000..ec21c5c --- /dev/null +++ b/DynamicIsland/managers/ClipboardPanelManager.swift @@ -0,0 +1,55 @@ +// +// ClipboardPanelManager.swift +// DynamicIsland +// +// Created by GitHub Copilot on 12/08/25. +// + +import AppKit +import SwiftUI + +class ClipboardPanelManager: ObservableObject { + static let shared = ClipboardPanelManager() + + private var clipboardPanel: ClipboardPanel? + + private init() {} + + func showClipboardPanel() { + hideClipboardPanel() // Close any existing panel + + let panel = ClipboardPanel() + panel.positionNearNotch() + + self.clipboardPanel = panel + + // Make the panel key and order front to ensure it can receive focus + panel.makeKeyAndOrderFront(nil) + panel.orderFrontRegardless() + + // Activate the app to ensure proper focus handling + NSApp.activate(ignoringOtherApps: true) + + // Ensure the panel becomes the key window for text input + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + panel.makeKey() + } + } + + func hideClipboardPanel() { + clipboardPanel?.close() + clipboardPanel = nil + } + + func toggleClipboardPanel() { + if let panel = clipboardPanel, panel.isVisible { + hideClipboardPanel() + } else { + showClipboardPanel() + } + } + + var isPanelVisible: Bool { + return clipboardPanel?.isVisible ?? false + } +} diff --git a/DynamicIsland/models/Constants.swift b/DynamicIsland/models/Constants.swift index 7d0e2ae..ee8d6b4 100644 --- a/DynamicIsland/models/Constants.swift +++ b/DynamicIsland/models/Constants.swift @@ -31,23 +31,17 @@ enum CalendarSelectionState: Codable, Defaults.Serializable { } 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 + case popover = "popover" // Always use floating panel var displayName: String { switch self { - case .auto: return "Auto (Smart)" - case .popover: return "Popover" - case .window: return "Window" + case .popover: return "Panel" } } 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" + case .popover: return "Shows clipboard in a floating panel near the notch" } } } @@ -192,7 +186,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 clipboardDisplayMode = Key("clipboardDisplayMode", default: .auto) + static let clipboardDisplayMode = Key("clipboardDisplayMode", default: .popover) // MARK: Keyboard Shortcuts static let enableShortcuts = Key("enableShortcuts", default: true) diff --git a/DynamicIsland/models/DynamicIslandViewModel.swift b/DynamicIsland/models/DynamicIslandViewModel.swift index 54c64f4..e29d114 100644 --- a/DynamicIsland/models/DynamicIslandViewModel.swift +++ b/DynamicIsland/models/DynamicIslandViewModel.swift @@ -29,7 +29,6 @@ class DynamicIslandViewModel: NSObject, ObservableObject { @Published var hideOnClosed: Bool = true @Published var isHoveringCalendar: Bool = false @Published var isBatteryPopoverActive: Bool = false - @Published var isClipboardPopoverActive: Bool = false let webcamManager = WebcamManager.shared @Published var isCameraExpanded: Bool = false