Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 45 additions & 2 deletions Sources/Pesty/AppController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ final class AppController: NSObject, NSApplicationDelegate {
let monitor = ClipboardMonitor()

private var barController: BarWindowController?
private var inlinePreviewController: InlinePreviewWindowController?
private var statusItem: NSStatusItem?
private var settingsWindow: NSWindow?
private var keyMonitor: Any?
Expand Down Expand Up @@ -137,6 +138,8 @@ final class AppController: NSObject, NSApplicationDelegate {
store.searchText = ""
store.source = .history
store.selectFirst()
store.inlinePreviewVisible = false
inlinePreviewController?.hide()

if barController == nil {
barController = BarWindowController()
Expand All @@ -147,9 +150,37 @@ final class AppController: NSObject, NSApplicationDelegate {

func hideBar() {
stopKeyMonitor()
store.inlinePreviewVisible = false
inlinePreviewController?.hide()
barController?.hide()
}

func toggleInlinePreview() {
guard Settings.shared.clipPreviewStyle == .inlinePesty,
store.selectedItem != nil else { return }
if store.inlinePreviewVisible {
hideInlinePreview()
} else {
store.inlinePreviewVisible = true
}
}

func hideInlinePreview() {
guard store.inlinePreviewVisible else { return }
store.inlinePreviewVisible = false
inlinePreviewController?.hide()
}

func updateInlinePreview(item: ClipItem, cardFrame: CGRect) {
guard store.inlinePreviewVisible,
let barPanel = barController?.window,
barPanel.isVisible else { return }
if inlinePreviewController == nil {
inlinePreviewController = InlinePreviewWindowController()
}
inlinePreviewController?.show(item: item, anchoredTo: cardFrame, in: barPanel)
}

func pasteSelected() {
guard let item = store.selectedItem else { return }
hideBar()
Expand Down Expand Up @@ -211,16 +242,23 @@ final class AppController: NSObject, NSApplicationDelegate {
}

switch code {
case kVK_Space where store.searchText.isEmpty:
if Settings.shared.clipPreviewStyle == .nativeQuickLook {
QuickLookService.shared.toggle(items: store.visibleItems, selectedID: store.selectedID)
} else {
toggleInlinePreview()
}
return nil
case kVK_Escape:
if !store.searchText.isEmpty { store.searchText = ""; store.selectFirst() }
else { hideBar() }
return nil
case kVK_Return, kVK_ANSI_KeypadEnter:
pasteSelected(); return nil
case kVK_LeftArrow, kVK_UpArrow:
store.moveSelection(by: -1); return nil
moveBarSelection(by: -1); return nil
case kVK_RightArrow, kVK_DownArrow:
store.moveSelection(by: 1); return nil
moveBarSelection(by: 1); return nil
case kVK_Delete:
if cmd, let sel = store.selectedItem { store.delete(sel); return nil }
if !store.searchText.isEmpty {
Expand All @@ -244,6 +282,11 @@ final class AppController: NSObject, NSApplicationDelegate {
}
return event
}

private func moveBarSelection(by delta: Int) {
store.moveSelection(by: delta)
QuickLookService.shared.updateSelection(selectedID: store.selectedID)
}
}

extension Bundle {
Expand Down
20 changes: 20 additions & 0 deletions Sources/Pesty/Models/ClipItem.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import AppKit
import UniformTypeIdentifiers

struct ClipItem: Identifiable, Codable, Equatable {
let id: UUID
Expand Down Expand Up @@ -44,6 +45,25 @@ struct ClipItem: Identifiable, Codable, Equatable {

var charCount: Int { text?.count ?? 0 }

/// Files copied from Finder remain file clips so they paste back as files,
/// but image files should be presented to the person as images.
var imageFileURL: URL? {
guard type == .file,
fileURLs.count == 1,
let value = fileURLs.first,
let url = URL(string: value),
url.isFileURL,
let fileType = UTType(filenameExtension: url.pathExtension),
fileType.conforms(to: .image) else { return nil }
return url
}

var isImageFile: Bool { imageFileURL != nil }

var presentationType: ClipType {
isImageFile ? .image : type
}

var displayTitle: String {
if let t = customTitle, !t.isEmpty { return t }
switch type {
Expand Down
28 changes: 28 additions & 0 deletions Sources/Pesty/Settings/Settings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,27 @@ import AppKit
import Carbon.HIToolbox
import Observation

enum ClipPreviewStyle: Int, CaseIterable, Identifiable {
case nativeQuickLook
case inlinePesty

var id: Int { rawValue }

var title: String {
switch self {
case .nativeQuickLook: "Native Quick Look"
case .inlinePesty: "Inline Pesty preview"
}
}

var detail: String {
switch self {
case .nativeQuickLook: "Open a macOS Quick Look panel with Space."
case .inlinePesty: "Show a rich preview with link titles and favicons inside Pesty."
}
}
}

@Observable
@MainActor
final class Settings {
Expand All @@ -19,6 +40,7 @@ final class Settings {
static let playSound = "playSound"
static let ignoreConcealed = "ignoreConcealed"
static let barHeight = "barHeight"
static let clipPreviewStyle = "clipPreviewStyle"
static let onboarded = "onboarded"
static let iCloudSync = "iCloudSync"
}
Expand Down Expand Up @@ -68,6 +90,10 @@ final class Settings {
}
}

var clipPreviewStyle: ClipPreviewStyle {
didSet { guard isLoaded else { return }; d.set(clipPreviewStyle.rawValue, forKey: Keys.clipPreviewStyle) }
}

var onboarded: Bool {
didSet { guard isLoaded else { return }; d.set(onboarded, forKey: Keys.onboarded) }
}
Expand All @@ -86,6 +112,7 @@ final class Settings {
Keys.playSound: false,
Keys.ignoreConcealed: true,
Keys.barHeight: 430.0,
Keys.clipPreviewStyle: ClipPreviewStyle.nativeQuickLook.rawValue,
Keys.onboarded: false,
Keys.iCloudSync: false
])
Expand All @@ -97,6 +124,7 @@ final class Settings {
playSound = d.bool(forKey: Keys.playSound)
ignoreConcealed = d.bool(forKey: Keys.ignoreConcealed)
barHeight = d.double(forKey: Keys.barHeight)
clipPreviewStyle = ClipPreviewStyle(rawValue: d.integer(forKey: Keys.clipPreviewStyle)) ?? .nativeQuickLook
onboarded = d.bool(forKey: Keys.onboarded)
iCloudSync = d.bool(forKey: Keys.iCloudSync)
isLoaded = true
Expand Down
12 changes: 12 additions & 0 deletions Sources/Pesty/Settings/SettingsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,18 @@ private struct GeneralSettings: View {
#endif
}

Section("Clip previews") {
Picker("Preview style", selection: $settings.clipPreviewStyle) {
ForEach(ClipPreviewStyle.allCases) { style in
Text(style.title).tag(style)
}
}
.pickerStyle(.segmented)
Text(settings.clipPreviewStyle.detail)
.font(.caption)
.foregroundStyle(.secondary)
}

Section("Sync") {
Toggle("Sync clipboard via iCloud Drive", isOn: Binding(
get: { settings.iCloudSync },
Expand Down
1 change: 1 addition & 0 deletions Sources/Pesty/Store/ClipboardStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ final class ClipboardStore {
var source: BarSource = .history
var searchText: String = ""
var selectedID: UUID?
var inlinePreviewVisible = false

var historyLimit: Int {
get { Settings.shared.historyLimit }
Expand Down
52 changes: 50 additions & 2 deletions Sources/Pesty/UI/BarView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,30 @@ import SwiftUI
struct BarView: View {
@Bindable private var store = ClipboardStore.shared
@Bindable private var settings = Settings.shared
@State private var cardFrames: [UUID: CGRect] = [:]

var body: some View {
ZStack {
VisualEffectView(material: .hudWindow)
Theme.panelTint
}
.overlay(alignment: .top) {
VStack(spacing: 0) {
topBar
strip
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
}
.coordinateSpace(name: "PestyBar")
.onPreferenceChange(ClipCardFramePreferenceKey.self) {
cardFrames = $0
updateExternalPreview()
}
.onChange(of: store.inlinePreviewVisible) { _, visible in
guard visible else { return }
DispatchQueue.main.async { updateExternalPreview() }
}
.onChange(of: store.selectedID) { _, _ in
guard store.inlinePreviewVisible else { return }
DispatchQueue.main.async { updateExternalPreview() }
}
.clipShape(RoundedCorners(radius: Theme.cornerRadius, corners: [.topLeft, .topRight]))
.ignoresSafeArea()
Expand All @@ -26,12 +39,24 @@ struct BarView: View {
PinboardTabs()
.layoutPriority(1)
Spacer(minLength: 8)
if settings.clipPreviewStyle == .inlinePesty { previewButton }
moreMenu
}
.padding(.horizontal, 18)
.frame(height: 56)
}

private var previewButton: some View {
Button { AppController.shared.toggleInlinePreview() } label: {
Image(systemName: store.inlinePreviewVisible ? "rectangle.on.rectangle" : "rectangle.on.rectangle.angled")
.font(.system(size: 14, weight: .semibold))
.foregroundStyle(store.inlinePreviewVisible ? Theme.selection : Theme.textSecondary)
.frame(width: 30, height: 30)
}
.buttonStyle(.plain)
.help(store.inlinePreviewVisible ? "Hide clip preview" : "Show clip preview")
}

private var syncButton: some View {
Button {
AppController.shared.toggleICloudSync()
Expand Down Expand Up @@ -95,6 +120,13 @@ struct BarView: View {
index: index,
selected: item.id == store.selectedID)
.id(item.id)
.background {
GeometryReader { proxy in
Color.clear.preference(
key: ClipCardFramePreferenceKey.self,
value: [item.id: proxy.frame(in: .named("PestyBar"))])
}
}
.transition(.asymmetric(
insertion: .scale(scale: 0.92).combined(with: .opacity),
removal: .opacity))
Expand Down Expand Up @@ -128,6 +160,22 @@ struct BarView: View {
.foregroundStyle(Theme.textSecondary)
}
}

private func updateExternalPreview() {
guard settings.clipPreviewStyle == .inlinePesty,
store.inlinePreviewVisible,
let item = store.selectedItem,
let cardFrame = cardFrames[item.id] else { return }
AppController.shared.updateInlinePreview(item: item, cardFrame: cardFrame)
}
}

private struct ClipCardFramePreferenceKey: PreferenceKey {
static var defaultValue: [UUID: CGRect] = [:]

static func reduce(value: inout [UUID: CGRect], nextValue: () -> [UUID: CGRect]) {
value.merge(nextValue(), uniquingKeysWith: { _, next in next })
}
}

struct RoundedCorners: Shape {
Expand Down
10 changes: 9 additions & 1 deletion Sources/Pesty/UI/BarWindowController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,14 @@ final class BarWindowController: NSWindowController, NSWindowDelegate {

func windowDidResignKey(_ notification: Notification) {
guard !isPresenting, !AppController.shared.suppressAutoHide else { return }
AppController.shared.hideBar()
// Quick Look and the separate inline preview can become key while Pesty
// remains open behind them.
// Defer until that transition is visible before deciding whether focus
// actually left Pesty.
DispatchQueue.main.async {
guard !QuickLookService.shared.isVisible,
!ClipboardStore.shared.inlinePreviewVisible else { return }
AppController.shared.hideBar()
}
}
}
Loading