feat: copy and paste selected emoji
This commit is contained in:
@@ -10,7 +10,8 @@ final class ApplicationActionsController: NSViewController, NSSearchFieldDelegat
|
||||
ActionEntry(title: "Show in Finder", symbol: "finder", color: SunsetPalette.text),
|
||||
ActionEntry(title: "Reset ranking", symbol: "arrow.counterclockwise", color: SunsetPalette.text),
|
||||
ActionEntry(title: "Quit application", symbol: "stop.circle", color: SunsetPalette.text),
|
||||
ActionEntry(title: "Uninstall application", symbol: "trash", color: .systemRed)
|
||||
ActionEntry(title: "Uninstall application", symbol: "trash", color: .systemRed),
|
||||
ActionEntry(title: "Copy Emoji", symbol: "doc.on.doc", color: SunsetPalette.text)
|
||||
]
|
||||
private var visibleEntries: [Int] = []
|
||||
private var availableEntries = Array(0..<6)
|
||||
@@ -26,6 +27,7 @@ final class ApplicationActionsController: NSViewController, NSSearchFieldDelegat
|
||||
private var transitionGeneration = 0
|
||||
private var panel: ActionsPanelView { view as! ActionsPanelView }
|
||||
var open: (() -> Void)?
|
||||
var copyEmoji: (() -> Void)?
|
||||
var toggleFavorite: (() -> Void)?
|
||||
var isFavorite = false
|
||||
var showInFinder: (() -> Void)?
|
||||
@@ -126,11 +128,11 @@ final class ApplicationActionsController: NSViewController, NSSearchFieldDelegat
|
||||
|
||||
func show(for application: Application, relativeTo anchor: NSView) {
|
||||
show(title: application.name, relativeTo: anchor,
|
||||
available: application.command == nil ? Array(entries.indices) : [0, 1, 3])
|
||||
available: application.command == nil ? Array(0..<6) : [0, 1, 3])
|
||||
}
|
||||
|
||||
func showEmoji(named name: String, relativeTo anchor: NSView) {
|
||||
show(title: name, relativeTo: anchor, available: [0, 1], emoji: true)
|
||||
show(title: name, relativeTo: anchor, available: [0, 6, 1], emoji: true)
|
||||
}
|
||||
|
||||
private func show(title: String, relativeTo anchor: NSView, available: [Int], emoji: Bool = false) {
|
||||
@@ -140,8 +142,10 @@ final class ApplicationActionsController: NSViewController, NSSearchFieldDelegat
|
||||
_ = view
|
||||
applicationLabel.stringValue = title
|
||||
availableEntries = available
|
||||
entries[0].button.title = emoji ? "Copy Emoji" : "Open"
|
||||
entries[0].icon.image = NSImage(systemSymbolName: emoji ? "doc.on.doc" : "arrow.up.forward.app", accessibilityDescription: nil)
|
||||
let openTitle = emoji ? "Copy and Paste" : "Open"
|
||||
entries[0].button.title = openTitle
|
||||
entries[0].button.setAccessibilityLabel(openTitle)
|
||||
entries[0].icon.image = NSImage(systemSymbolName: emoji ? "doc.on.clipboard" : "arrow.up.forward.app", accessibilityDescription: nil)
|
||||
search.setAccessibilityLabel(emoji ? "Search emoji actions" : "Search application actions")
|
||||
let favorite = entries[1]
|
||||
let favoriteTitle = isFavorite ? "Remove Favorite" : "Favorite"
|
||||
@@ -215,9 +219,8 @@ final class ApplicationActionsController: NSViewController, NSSearchFieldDelegat
|
||||
|
||||
func controlTextDidChange(_ notification: Notification) {
|
||||
let query = search.stringValue.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
visibleEntries = entries.indices.filter {
|
||||
availableEntries.contains($0)
|
||||
&& (query.isEmpty || entries[$0].button.title.range(of: query, options: [.caseInsensitive, .diacriticInsensitive]) != nil)
|
||||
visibleEntries = availableEntries.filter {
|
||||
query.isEmpty || entries[$0].button.title.range(of: query, options: [.caseInsensitive, .diacriticInsensitive]) != nil
|
||||
}
|
||||
selectedEntry = visibleEntries.first
|
||||
for index in entries.indices {
|
||||
@@ -278,6 +281,7 @@ final class ApplicationActionsController: NSViewController, NSSearchFieldDelegat
|
||||
case 3: resetRanking?()
|
||||
case 4: quit?()
|
||||
case 5: uninstall?()
|
||||
case 6: copyEmoji?()
|
||||
default: break
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ final class EmojiPickerController: NSViewController, NSCollectionViewDataSource,
|
||||
private let category = NSPopUpButton()
|
||||
private let status = NSTextField(labelWithString: "")
|
||||
private let empty = NSTextField(labelWithString: "No matching emoji")
|
||||
private let copyButton = NSButton(title: "Copy Emoji", target: nil, action: nil)
|
||||
private let copyButton = NSButton(title: "Copy and Paste", target: nil, action: nil)
|
||||
private let actionsButton = NSButton(title: "Actions", target: nil, action: nil)
|
||||
private let actions = ApplicationActionsController()
|
||||
private var favoriteCount = 0
|
||||
@@ -19,6 +19,7 @@ final class EmojiPickerController: NSViewController, NSCollectionViewDataSource,
|
||||
var input: (() -> String)?
|
||||
var closed: (() -> Void)?
|
||||
var copied: (() -> Void)?
|
||||
var pasteRequested: (() -> Void)?
|
||||
var focusSearch: (() -> Void)?
|
||||
|
||||
init(defaults: UserDefaults = .standard, pasteboard: NSPasteboard = .general) {
|
||||
@@ -59,7 +60,7 @@ final class EmojiPickerController: NSViewController, NSCollectionViewDataSource,
|
||||
status.lineBreakMode = .byTruncatingTail
|
||||
status.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
||||
copyButton.target = self
|
||||
copyButton.action = #selector(copySelection)
|
||||
copyButton.action = #selector(copyAndPasteSelection)
|
||||
copyButton.keyEquivalent = "\r"
|
||||
copyButton.isBordered = false
|
||||
copyButton.font = .systemFont(ofSize: 12, weight: .medium)
|
||||
@@ -198,7 +199,8 @@ final class EmojiPickerController: NSViewController, NSCollectionViewDataSource,
|
||||
guard results.indices.contains(selected) else { return }
|
||||
let emoji = results[selected]
|
||||
actions.isFavorite = (defaults.stringArray(forKey: "favoriteEmoji") ?? []).contains(emoji.value)
|
||||
actions.open = { [weak self] in self?.copySelection() }
|
||||
actions.open = { [weak self] in self?.copyAndPasteSelection() }
|
||||
actions.copyEmoji = { [weak self] in self?.copySelection() }
|
||||
actions.toggleFavorite = { [weak self] in self?.toggleFavorite() }
|
||||
actions.showEmoji(named: emoji.name, relativeTo: actionsButton)
|
||||
copyButton.keyEquivalent = actions.isShown ? "" : "\r"
|
||||
@@ -220,7 +222,7 @@ final class EmojiPickerController: NSViewController, NSCollectionViewDataSource,
|
||||
case #selector(NSResponder.moveRight(_:)): moveSelection(by: 1)
|
||||
case #selector(NSResponder.moveUp(_:)): moveVertically(-1)
|
||||
case #selector(NSResponder.moveDown(_:)): moveVertically(1)
|
||||
case #selector(NSResponder.insertNewline(_:)): copySelection()
|
||||
case #selector(NSResponder.insertNewline(_:)): copyAndPasteSelection()
|
||||
case #selector(NSResponder.cancelOperation(_:)): goBack()
|
||||
default: return false
|
||||
}
|
||||
@@ -238,7 +240,10 @@ final class EmojiPickerController: NSViewController, NSCollectionViewDataSource,
|
||||
(collection.item(at: indexPath(for: selected)) as? EmojiItem)?.highlight(true)
|
||||
}
|
||||
|
||||
@objc func copySelection() {
|
||||
@objc func copySelection() { copySelection(pasting: false) }
|
||||
@objc func copyAndPasteSelection() { copySelection(pasting: true) }
|
||||
|
||||
private func copySelection(pasting: Bool) {
|
||||
guard results.indices.contains(selected) else { return }
|
||||
let value = results[selected].value
|
||||
pasteboard.clearContents()
|
||||
@@ -250,7 +255,7 @@ final class EmojiPickerController: NSViewController, NSCollectionViewDataSource,
|
||||
recent.removeAll { $0 == value }
|
||||
defaults.set(Array(([value] + recent).prefix(30)), forKey: "recentEmoji")
|
||||
closed?()
|
||||
copied?()
|
||||
if pasting { pasteRequested?() } else { copied?() }
|
||||
}
|
||||
|
||||
func numberOfSections(in collectionView: NSCollectionView) -> Int { splitFavorites ? 2 : 1 }
|
||||
@@ -273,7 +278,7 @@ final class EmojiPickerController: NSViewController, NSCollectionViewDataSource,
|
||||
item.highlight(index == selected)
|
||||
item.clicked = { [weak self] in
|
||||
self?.selected = index
|
||||
self?.copySelection()
|
||||
self?.copyAndPasteSelection()
|
||||
}
|
||||
return item
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import AppKit
|
||||
import ConjureKit
|
||||
import CoreText
|
||||
import ApplicationServices
|
||||
|
||||
@MainActor
|
||||
final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSearchFieldDelegate, NSTableViewDataSource, NSTableViewDelegate {
|
||||
@@ -760,6 +761,32 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
|
||||
}
|
||||
}
|
||||
|
||||
private func pasteEmoji() {
|
||||
let target = previousApplication
|
||||
let changeCount = NSPasteboard.general.changeCount
|
||||
dismiss()
|
||||
guard let target, !target.isTerminated,
|
||||
target.processIdentifier != ProcessInfo.processInfo.processIdentifier,
|
||||
AXIsProcessTrustedWithOptions(["AXTrustedCheckOptionPrompt": true] as CFDictionary)
|
||||
else { return }
|
||||
Task { [weak self] in
|
||||
// Wait for focus restoration; never paste into a different app or a reopened launcher.
|
||||
for _ in 0..<20 {
|
||||
try? await Task.sleep(for: .milliseconds(25))
|
||||
guard let self, self.window?.isVisible == false, !target.isTerminated,
|
||||
NSPasteboard.general.changeCount == changeCount else { return }
|
||||
guard NSWorkspace.shared.frontmostApplication?.processIdentifier == target.processIdentifier else { continue }
|
||||
guard let down = CGEvent(keyboardEventSource: nil, virtualKey: 9, keyDown: true),
|
||||
let up = CGEvent(keyboardEventSource: nil, virtualKey: 9, keyDown: false) else { return }
|
||||
down.flags = .maskCommand
|
||||
up.flags = .maskCommand
|
||||
down.postToPid(target.processIdentifier)
|
||||
up.postToPid(target.processIdentifier)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func goBackCommand() {
|
||||
translation?.goBack()
|
||||
emojiPicker?.goBack()
|
||||
@@ -790,6 +817,7 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
|
||||
tool.input = { [weak self] in self?.searchField.stringValue ?? "" }
|
||||
tool.focusSearch = { [weak self] in self?.window?.makeFirstResponder(self?.searchField) }
|
||||
tool.copied = { [weak self] in self?.dismiss() }
|
||||
tool.pasteRequested = { [weak self] in self?.pasteEmoji() }
|
||||
controller = tool
|
||||
placeholder = "Search emoji by name or :shortcode:…"
|
||||
accessibilityLabel = "Search emoji"
|
||||
|
||||
Reference in New Issue
Block a user