From f01d087dbc99e00fca9d18bc73c902920ea3ed58 Mon Sep 17 00:00:00 2001 From: mobrava <82764703+mobrava@users.noreply.github.com> Date: Sat, 22 Aug 2026 17:37:59 +0900 Subject: [PATCH] Add paste as plain text (Shift modifier, context menu, setting) - PasteService.paste(item:asPlainText:): nil resolves from the setting XOR the Shift modifier - Split card context menu into Paste with Formatting / Paste as Plain Text for RTF and HTML clips - Add an Always Paste as Plain Text toggle in Settings > General Version, appcast, and release steps are intentionally left untouched while 1.3.0 (23) is in App Store review. Refs #14 --- Clipbara/AppState.swift | 8 +++++ Clipbara/Services/PasteService.swift | 29 ++++++++++++++++++- Clipbara/Views/ClipboardCardView.swift | 7 ++++- .../Views/Settings/GeneralSettingsTab.swift | 18 ++++++++++++ 4 files changed, 60 insertions(+), 2 deletions(-) diff --git a/Clipbara/AppState.swift b/Clipbara/AppState.swift index e1f702b..7645df6 100644 --- a/Clipbara/AppState.swift +++ b/Clipbara/AppState.swift @@ -65,6 +65,14 @@ final class AppState { } } + /// Shared paste path for panel and pinboard cards. + /// - Parameter asPlainText: `nil` resolves from the setting combined with the Shift modifier. + func paste(_ item: ClipboardItem, asPlainText: Bool? = nil) { + clipboardMonitor.skipNextChange() + pasteService.paste(item: item, asPlainText: asPlainText) + hidePanel() + } + func hidePanel() { previewItem = nil panelToast = nil diff --git a/Clipbara/Services/PasteService.swift b/Clipbara/Services/PasteService.swift index 7f2c26e..1df0381 100644 --- a/Clipbara/Services/PasteService.swift +++ b/Clipbara/Services/PasteService.swift @@ -5,7 +5,34 @@ struct PasteService { private static let tempDir = NSTemporaryDirectory() + "Clipbara/" - func paste(item: ClipboardItem) { + /// Backing key for the "Always Paste as Plain Text" setting (Settings > General). + nonisolated static let alwaysPlainTextDefaultsKey = "alwaysPastePlainText" + + /// Whether stripping formatting is meaningful for this item (RTF/HTML only). + static func supportsPlainText(_ item: ClipboardItem) -> Bool { + switch item.contentType { + case .richText, .html: + return !(item.textContent?.isEmpty ?? true) + default: + return false + } + } + + /// Combines the setting with the Shift modifier using XOR. + /// Setting off: Shift strips formatting. Setting on: Shift keeps formatting. + static func resolvePlainText() -> Bool { + let always = UserDefaults.standard.bool(forKey: alwaysPlainTextDefaultsKey) + let shiftHeld = NSEvent.modifierFlags.contains(.shift) + return always != shiftHeld + } + + /// - Parameter asPlainText: `nil` resolves from the setting combined with the Shift modifier. + func paste(item: ClipboardItem, asPlainText: Bool? = nil) { + if asPlainText ?? Self.resolvePlainText(), Self.supportsPlainText(item) { + pastePlainText(item: item) + return + } + let pasteboard = NSPasteboard.general pasteboard.clearContents() diff --git a/Clipbara/Views/ClipboardCardView.swift b/Clipbara/Views/ClipboardCardView.swift index 43167a4..f960637 100644 --- a/Clipbara/Views/ClipboardCardView.swift +++ b/Clipbara/Views/ClipboardCardView.swift @@ -62,7 +62,12 @@ struct ClipboardCardView: View { @ViewBuilder private var menuItems: some View { - Button("Paste") { onPaste(item) } + if PasteService.supportsPlainText(item) { + Button("Paste with Formatting") { appState.paste(item, asPlainText: false) } + Button("Paste as Plain Text") { appState.paste(item, asPlainText: true) } + } else { + Button("Paste") { onPaste(item) } + } if showsManagementMenu { Divider() Button("Rename") { diff --git a/Clipbara/Views/Settings/GeneralSettingsTab.swift b/Clipbara/Views/Settings/GeneralSettingsTab.swift index 798320a..9a6dda0 100644 --- a/Clipbara/Views/Settings/GeneralSettingsTab.swift +++ b/Clipbara/Views/Settings/GeneralSettingsTab.swift @@ -7,6 +7,7 @@ struct GeneralSettingsTab: View { @Environment(\.modelContext) private var modelContext @Environment(AppState.self) private var appState @AppStorage("historyLimit") private var historyLimit: Int = 500 + @AppStorage(PasteService.alwaysPlainTextDefaultsKey) private var alwaysPastePlainText: Bool = false @State private var launchAtLogin: Bool = SMAppService.mainApp.status == .enabled @State private var transferMessage: String? @State private var showTransferAlert = false @@ -35,6 +36,23 @@ struct GeneralSettingsTab: View { } } + Section("Pasting") { + HStack { + VStack(alignment: .leading, spacing: 2) { + HStack(spacing: 5) { + Text("Always Paste as Plain Text") + InfoHoverButton(text: "Removes fonts, colors, and links from rich text and HTML clips, pasting only the text.") + } + Text("Hold \u{21e7} while pasting to switch for a single paste.") + .font(.system(size: 11)) + .foregroundStyle(.secondary) + } + Spacer() + Toggle("", isOn: $alwaysPastePlainText) + .labelsHidden() + } + } + Section("Backup") { LabeledContent("Export history, pinboards, and settings to a JSON file.") { Button("Export\u{2026}") { exportHistory() }