WIP: add Quick Look preview groundwork

This commit is contained in:
Akshay Kolli
2026-06-30 01:43:23 -07:00
parent 3a2078c599
commit 28f50c84a6
4 changed files with 71 additions and 2 deletions

View File

@@ -18,6 +18,7 @@ let package = Package(
.linkedFramework("AppKit"), .linkedFramework("AppKit"),
.linkedFramework("Carbon"), .linkedFramework("Carbon"),
.linkedFramework("LocalAuthentication"), .linkedFramework("LocalAuthentication"),
.linkedFramework("QuickLook"),
.linkedFramework("Security"), .linkedFramework("Security"),
.linkedFramework("Vision"), .linkedFramework("Vision"),
.linkedLibrary("sqlite3") .linkedLibrary("sqlite3")

View File

@@ -182,6 +182,23 @@ final class ClipboardCacheService {
} }
} }
func temporaryPreviewURL(for item: ClipboardItem) -> URL? {
switch item.kind {
case .file:
let urls = FilePayload.urls(from: item.payload)
return urls.first { fileManager.fileExists(atPath: $0.path) }
case .text, .unknown:
let text = item.payload.clipboardTrimmed.isEmpty ? item.displayText : item.payload
guard !text.clipboardTrimmed.isEmpty else { return nil }
return writeTemporaryCopy(data: Data(text.utf8), id: item.id, fileExtension: "txt")
case .url:
guard let data = webLocationData(for: item.payload) else { return nil }
return writeTemporaryCopy(data: data, id: item.id, fileExtension: "webloc")
case .image, .pdf, .audio, .richText:
return temporaryReadableURL(for: item)
}
}
func encryptCachedReferencesIfNeeded(for items: [ClipboardItem]) { func encryptCachedReferencesIfNeeded(for items: [ClipboardItem]) {
queue.async { [weak self] in queue.async { [weak self] in
guard let self else { return } guard let self else { return }
@@ -320,6 +337,16 @@ final class ClipboardCacheService {
} }
} }
private func webLocationData(for value: String) -> Data? {
let trimmed = value.clipboardTrimmed
guard !trimmed.isEmpty else { return nil }
return try? PropertyListSerialization.data(
fromPropertyList: ["URL": trimmed],
format: .xml,
options: 0
)
}
private func removeTemporaryPreviewFiles() { private func removeTemporaryPreviewFiles() {
guard fileManager.fileExists(atPath: temporaryPreviewDirectory.path) else { guard fileManager.fileExists(atPath: temporaryPreviewDirectory.path) else {
return return

View File

@@ -1,4 +1,5 @@
import AppKit import AppKit
import QuickLook
struct ClipboardPanelAnimationProfile { struct ClipboardPanelAnimationProfile {
let showDuration: TimeInterval let showDuration: TimeInterval
@@ -15,10 +16,11 @@ struct ClipboardPanelReflowPlan {
enum ClipboardPanelShortcutAction: Equatable { enum ClipboardPanelShortcutAction: Equatable {
case copy case copy
case open case open
case preview
case reveal case reveal
} }
final class ClipboardPanelController: NSObject, NSWindowDelegate { final class ClipboardPanelController: NSObject, NSWindowDelegate, QLPreviewPanelDataSource, QLPreviewPanelDelegate {
private enum Animation { private enum Animation {
static let showDuration: TimeInterval = 0.16 static let showDuration: TimeInterval = 0.16
static let hideDuration: TimeInterval = 0.12 static let hideDuration: TimeInterval = 0.12
@@ -44,6 +46,7 @@ final class ClipboardPanelController: NSObject, NSWindowDelegate {
private let preferredScreenProvider: () -> NSScreen? private let preferredScreenProvider: () -> NSScreen?
private let openSettings: () -> Void private let openSettings: () -> Void
private var isAnimating = false private var isAnimating = false
private var quickLookURL: URL?
private var screenParametersObserver: NSObjectProtocol? private var screenParametersObserver: NSObjectProtocol?
private static let collectionShortcuts: [UInt16: ClipboardSortMode] = [ private static let collectionShortcuts: [UInt16: ClipboardSortMode] = [
18: .mostRecent, 18: .mostRecent,
@@ -82,7 +85,8 @@ final class ClipboardPanelController: NSObject, NSWindowDelegate {
panelView = ClipboardPanelView( panelView = ClipboardPanelView(
viewModel: viewModel, viewModel: viewModel,
onClose: { [weak self] in self?.hide() }, onClose: { [weak self] in self?.hide() },
onSettings: { [weak self] in self?.openSettings() } onSettings: { [weak self] in self?.openSettings() },
onPreview: { [weak self] in self?.previewSelected() }
) )
let contentSize = NSSize(width: 1200, height: 420) let contentSize = NSSize(width: 1200, height: 420)
@@ -333,6 +337,9 @@ final class ClipboardPanelController: NSObject, NSWindowDelegate {
case 53: case 53:
self.hide() self.hide()
return nil return nil
case 49:
self.previewSelected()
return nil
case 36: case 36:
self.viewModel.pasteSelected() self.viewModel.pasteSelected()
return nil return nil
@@ -360,11 +367,26 @@ final class ClipboardPanelController: NSObject, NSWindowDelegate {
viewModel.copySelected() viewModel.copySelected()
case .open: case .open:
viewModel.openSelected() viewModel.openSelected()
case .preview:
previewSelected()
case .reveal: case .reveal:
viewModel.revealSelected() viewModel.revealSelected()
} }
} }
private func previewSelected() {
guard let url = viewModel.previewURLForSelected() else { return }
quickLookURL = url
guard let previewPanel = QLPreviewPanel.shared() else {
NSWorkspace.shared.open(url)
return
}
previewPanel.dataSource = self
previewPanel.delegate = self
previewPanel.currentPreviewItemIndex = 0
previewPanel.makeKeyAndOrderFront(nil)
}
private func shouldHandlePanelKeyEvent(_ event: NSEvent) -> Bool { private func shouldHandlePanelKeyEvent(_ event: NSEvent) -> Bool {
shouldHandlePanelKeyEvent(event, allowSearchFieldEditing: false) shouldHandlePanelKeyEvent(event, allowSearchFieldEditing: false)
} }
@@ -399,6 +421,8 @@ final class ClipboardPanelController: NSObject, NSWindowDelegate {
return .copy return .copy
case 31: case 31:
return .open return .open
case 16:
return .preview
case 15: case 15:
return .reveal return .reveal
default: default:
@@ -406,6 +430,14 @@ final class ClipboardPanelController: NSObject, NSWindowDelegate {
} }
} }
func numberOfPreviewItems(in panel: QLPreviewPanel!) -> Int {
quickLookURL == nil ? 0 : 1
}
func previewPanel(_ panel: QLPreviewPanel!, previewItemAt index: Int) -> QLPreviewItem! {
quickLookURL as NSURL?
}
#if DEBUG #if DEBUG
var debugPanelFrame: NSRect { var debugPanelFrame: NSRect {
panel.frame panel.frame

View File

@@ -168,6 +168,15 @@ final class ClipboardPanelViewModel {
return pasteService.pasteboardWriters(for: visibleItems[index]) return pasteService.pasteboardWriters(for: visibleItems[index])
} }
func previewURLForSelected() -> URL? {
guard let item = selectedItem else { return nil }
return previewURL(for: item)
}
internal func previewURL(for item: ClipboardItem) -> URL? {
cacheService.temporaryPreviewURL(for: item)
}
func openSelected() { func openSelected() {
guard let item = selectedItem else { return } guard let item = selectedItem else { return }
switch item.kind { switch item.kind {