Skip to content
Open
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
64 changes: 43 additions & 21 deletions Jumpcut/Jumpcut.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -302,7 +302,7 @@
attributes = {
BuildIndependentTargetsInParallel = 1;
LastSwiftUpdateCheck = 1320;
LastUpgradeCheck = 1320;
LastUpgradeCheck = 2610;
TargetAttributes = {
AA311585280A60DB00CBAF91 = {
CreatedOnToolsVersion = 13.2.1;
Expand Down Expand Up @@ -397,6 +397,7 @@
outputFileListPaths = (
);
outputPaths = (
"$(BUILT_PRODUCTS_DIR)/$(CONTENTS_FOLDER_PATH)/Library/LoginItems/JumpcutHelper.app",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
Expand Down Expand Up @@ -504,9 +505,12 @@
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 4987SK7L6Z;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
Expand All @@ -521,11 +525,12 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.12;
MACOSX_DEPLOYMENT_TARGET = 14.6;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = macosx;
STRING_CATALOG_GENERATE_SYMBOLS = YES;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
};
Expand Down Expand Up @@ -565,9 +570,12 @@
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = 4987SK7L6Z;
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
Expand All @@ -576,10 +584,12 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.12;
MACOSX_DEPLOYMENT_TARGET = 14.6;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = macosx;
STRING_CATALOG_GENERATE_SYMBOLS = YES;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
};
Expand All @@ -594,20 +604,24 @@
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 20231204;
DEVELOPMENT_TEAM = 4987SK7L6Z;
CURRENT_PROJECT_VERSION = 20251014;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = D9U7U6P65Q;
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = Jumpcut/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = "";
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
INFOPLIST_KEY_LSUIElement = YES;
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2023 Steve Cook";
INFOPLIST_KEY_NSMainNibFile = MainMenu;
INFOPLIST_KEY_NSPrincipalClass = NSApplication;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 14.6;
MARKETING_VERSION = 0.84;
PRODUCT_BUNDLE_IDENTIFIER = net.sf.Jumpcut;
PRODUCT_NAME = "$(TARGET_NAME)";
Expand All @@ -625,20 +639,24 @@
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 20231204;
DEVELOPMENT_TEAM = 4987SK7L6Z;
CURRENT_PROJECT_VERSION = 20251014;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = D9U7U6P65Q;
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = Jumpcut/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = "";
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
INFOPLIST_KEY_LSUIElement = YES;
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2023 Steve Cook";
INFOPLIST_KEY_NSMainNibFile = MainMenu;
INFOPLIST_KEY_NSPrincipalClass = NSApplication;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 14.6;
MARKETING_VERSION = 0.84;
PRODUCT_BUNDLE_IDENTIFIER = net.sf.Jumpcut;
PRODUCT_NAME = "$(TARGET_NAME)";
Expand All @@ -650,13 +668,13 @@
AA3115AD280A60DD00CBAF91 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 4987SK7L6Z;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = D9U7U6P65Q;
GENERATE_INFOPLIST_FILE = YES;
MACOSX_DEPLOYMENT_TARGET = 12.2;
MACOSX_DEPLOYMENT_TARGET = 14.6;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = net.sf.Jumpcut.JumpcutTests;
PRODUCT_NAME = "$(TARGET_NAME)";
Expand All @@ -669,13 +687,13 @@
AA3115AE280A60DD00CBAF91 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 4987SK7L6Z;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = D9U7U6P65Q;
GENERATE_INFOPLIST_FILE = YES;
MACOSX_DEPLOYMENT_TARGET = 12.2;
MACOSX_DEPLOYMENT_TARGET = 14.6;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = net.sf.Jumpcut.JumpcutTests;
PRODUCT_NAME = "$(TARGET_NAME)";
Expand All @@ -688,11 +706,12 @@
AA3115B0280A60DD00CBAF91 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 4987SK7L6Z;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = D9U7U6P65Q;
GENERATE_INFOPLIST_FILE = YES;
MACOSX_DEPLOYMENT_TARGET = 14.6;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = net.sf.Jumpcut.JumpcutUITests;
PRODUCT_NAME = "$(TARGET_NAME)";
Expand All @@ -705,11 +724,12 @@
AA3115B1280A60DD00CBAF91 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 4987SK7L6Z;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = D9U7U6P65Q;
GENERATE_INFOPLIST_FILE = YES;
MACOSX_DEPLOYMENT_TARGET = 14.6;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = net.sf.Jumpcut.JumpcutUITests;
PRODUCT_NAME = "$(TARGET_NAME)";
Expand All @@ -730,7 +750,8 @@
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 4987SK7L6Z;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = D9U7U6P65Q;
ENABLE_HARDENED_RUNTIME = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = JumpcutHelper/Info.plist;
Expand All @@ -740,7 +761,7 @@
"$(inherited)",
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 10.11;
MACOSX_DEPLOYMENT_TARGET = 14.6;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = net.sf.Jumpcut.JumpcutHelper;
PRODUCT_NAME = "$(TARGET_NAME)";
Expand All @@ -762,7 +783,8 @@
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 4987SK7L6Z;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = D9U7U6P65Q;
ENABLE_HARDENED_RUNTIME = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = JumpcutHelper/Info.plist;
Expand All @@ -772,7 +794,7 @@
"$(inherited)",
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 10.11;
MACOSX_DEPLOYMENT_TARGET = 14.6;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = net.sf.Jumpcut.JumpcutHelper;
PRODUCT_NAME = "$(TARGET_NAME)";
Expand Down
55 changes: 55 additions & 0 deletions Jumpcut/Jumpcut/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate, SPUStandardU
private var hotKey: HotKey?
public var hotKeyBase: SauceKey?
public var mainHotkeyIsRecording = false
// Quick paste hotkeys (positions 1-5)
private var quickPasteHotkeys: [HotKey] = []
// Sparkle
public var sparkleUpdater: SPUUpdater!

Expand Down Expand Up @@ -71,6 +73,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate, SPUStandardU

// Set up hotkey and bezel handlers
setHotkey()
setQuickPasteHotkeys()
interactions.setHotkeyHandlers()

// If we are coming from an earlier version, let's set the new launch-on-login
Expand Down Expand Up @@ -148,6 +151,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate, SPUStandardU

@objc func updateKeyboardCodes(_ notification: Notification) {
setHotkey()
setQuickPasteHotkeys()
}

@objc func updateStateFromSettings(_ notification: Notification) {
Expand All @@ -156,6 +160,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate, SPUStandardU
// of what has changed.
statusItem.setVisibility()
menu.rebuild(stack: stack)
setQuickPasteHotkeys()
}

func checkMenuBehavior(_ event: NSEvent) -> Bool {
Expand Down Expand Up @@ -274,6 +279,56 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate, SPUStandardU
}
}

func setQuickPasteHotkeys() {
// Clear existing quick paste hotkeys
quickPasteHotkeys = []

// Check if quick paste is enabled
guard UserDefaults.standard.bool(forKey: SettingsPath.quickPasteEnabled.rawValue) else {
return
}

// Get the modifiers from the main hotkey
guard let dictionary = UserDefaults.standard.value(forKey: SettingsPath.mainHotkey.rawValue)
as? [AnyHashable: Any] else {
return
}

// Convert to carbon modifier flags for HotKey library
let shortcut = Shortcut.init(dictionary: dictionary)
guard shortcut != nil else {
return
}
let carbonModifiers = shortcut!.carbonModifierFlags

// Key codes for number keys 1-5 (QWERTY layout)
let numberKeyCodes: [UInt32] = [
18, // 1
19, // 2
20, // 3
21, // 4
23 // 5
]

// Register hotkeys for positions 1-5 (array indices 0-4)
for (index, keyCode) in numberKeyCodes.enumerated() {
let position = index // This gives us positions 0, 1, 2, 3, 4 (for clippings 1, 2, 3, 4, 5)
let hotKey = HotKey(
carbonKeyCode: keyCode,
carbonModifiers: carbonModifiers
)
hotKey.keyDownHandler = { [weak self] in
guard let self = self else { return }
guard !self.mainHotkeyIsRecording else {
// We're recording, so don't trigger paste
return
}
self.interactions.pasteAtPosition(position: position)
}
quickPasteHotkeys.append(hotKey)
}
}

func pasteboardChangeClosure() {
guard pasteboard.lastFound != nil else {
return
Expand Down
25 changes: 25 additions & 0 deletions Jumpcut/Jumpcut/Interactions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,31 @@ public class Interactions: NSObject {
}
}

func pasteAtPosition(position: Int) {
// Direct paste from a specific position in the stack (used by quick paste hotkeys)
// Unlike paste() which hides the app, this version is for background use
guard let clipping = stack.itemAt(position: position) else {
return
}

// Place on pasteboard without hiding the app
pasteboard.set(clipping.fullText)

// Send Command-V to paste
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
self.pasteboard.fakeCommandV()
}

// Optionally move used clipping to top of stack
let moveToTop = UserDefaults.standard.value(
forKey: SettingsPath.moveClippingsAfterUse.rawValue
) as? Bool ?? false
if moveToTop {
stack.moveItemToTop(position: position)
menu.rebuild(stack: stack)
}
}

// BEZEL
public func bezelSelection() {
let clipping = stack.itemAt(position: stack.position)
Expand Down
36 changes: 34 additions & 2 deletions Jumpcut/Jumpcut/Preferences/HotkeyPreferenceViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,44 @@ final class HotkeyPreferenceViewController: NSViewController, PreferencePane {
override func viewDidLoad() {
let settings = Settings()
toolbarItemIcon.isTemplate = true
self.preferredContentSize = CGSize(width: 480, height: 180)
self.preferredContentSize = CGSize(width: 480, height: 240)
super.viewDidLoad()

let recorder = settings.shortcutRecorder(title: "Main hotkey", key: .mainHotkey)
let grid = NSStackView(views: [ recorder ])

// Quick paste feature
let quickPasteCheckbox = settings.checkbox(
title: "Enable quick paste shortcuts",
key: SettingsPath.quickPasteEnabled
)

// Get the main hotkey modifiers to show in the description
var modifierDescription = "⌃⌥" // Default to Control-Option
if let dictionary = UserDefaults.standard.value(forKey: SettingsPath.mainHotkey.rawValue) as? [AnyHashable: Any],
let modifierFlags = dictionary["modifierFlags"] as? Int {
var parts: [String] = []
if modifierFlags & 256 != 0 { parts.append("⌘") } // Command
if modifierFlags & 2048 != 0 { parts.append("⌥") } // Option
if modifierFlags & 4096 != 0 { parts.append("⌃") } // Control
if modifierFlags & 512 != 0 { parts.append("⇧") } // Shift
if !parts.isEmpty {
modifierDescription = parts.joined()
}
}

let quickPasteDescription = settings.smallText(
"Pastes clipping 1-5 using \(modifierDescription)1, \(modifierDescription)2, \(modifierDescription)3, \(modifierDescription)4, \(modifierDescription)5"
)

let quickPasteStack = NSStackView(views: [quickPasteCheckbox, quickPasteDescription])
quickPasteStack.orientation = .vertical
quickPasteStack.alignment = .leading
quickPasteStack.spacing = 4

let grid = NSStackView(views: [ recorder, quickPasteStack ])
grid.orientation = .vertical
grid.alignment = .leading
grid.spacing = 16
self.view.addSubview(grid)
self.view.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
NSLayoutConstraint.activate([
Expand Down
Loading