diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..1c6dc69 Binary files /dev/null and b/.DS_Store differ diff --git a/Jumpcut/Jumpcut.xcodeproj/project.pbxproj b/Jumpcut/Jumpcut.xcodeproj/project.pbxproj index b03e81b..d0fce19 100644 --- a/Jumpcut/Jumpcut.xcodeproj/project.pbxproj +++ b/Jumpcut/Jumpcut.xcodeproj/project.pbxproj @@ -7,6 +7,18 @@ objects = { /* Begin PBXBuildFile section */ + 52FEE61C2B59A9C900D81962 /* pop.wav in Resources */ = {isa = PBXBuildFile; fileRef = 52FEE61B2B59A9C900D81962 /* pop.wav */; }; + 52FEE61D2B59A9C900D81962 /* pop.wav in Resources */ = {isa = PBXBuildFile; fileRef = 52FEE61B2B59A9C900D81962 /* pop.wav */; }; + 52FEE61E2B59A9C900D81962 /* pop.wav in Resources */ = {isa = PBXBuildFile; fileRef = 52FEE61B2B59A9C900D81962 /* pop.wav */; }; + 52FEE61F2B59A9C900D81962 /* pop.wav in Resources */ = {isa = PBXBuildFile; fileRef = 52FEE61B2B59A9C900D81962 /* pop.wav */; }; + 52FEE6222B59AA1A00D81962 /* AudioManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52FEE6212B59AA1A00D81962 /* AudioManager.swift */; }; + 52FEE6232B59AA1A00D81962 /* AudioManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52FEE6212B59AA1A00D81962 /* AudioManager.swift */; }; + 52FEE6242B59AA1A00D81962 /* AudioManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52FEE6212B59AA1A00D81962 /* AudioManager.swift */; }; + 52FEE6252B59AA1A00D81962 /* AudioManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52FEE6212B59AA1A00D81962 /* AudioManager.swift */; }; + 52FEE6272B59AD3200D81962 /* clear.wav in Resources */ = {isa = PBXBuildFile; fileRef = 52FEE6262B59AD3200D81962 /* clear.wav */; }; + 52FEE6282B59AD3200D81962 /* clear.wav in Resources */ = {isa = PBXBuildFile; fileRef = 52FEE6262B59AD3200D81962 /* clear.wav */; }; + 52FEE6292B59AD3200D81962 /* clear.wav in Resources */ = {isa = PBXBuildFile; fileRef = 52FEE6262B59AD3200D81962 /* clear.wav */; }; + 52FEE62A2B59AD3200D81962 /* clear.wav in Resources */ = {isa = PBXBuildFile; fileRef = 52FEE6262B59AD3200D81962 /* clear.wav */; }; AA16DB6E28393DD000F73A2C /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA16DB6D28393DD000F73A2C /* Constants.swift */; }; AA31159A280A60DD00CBAF91 /* JumpcutTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA311599280A60DD00CBAF91 /* JumpcutTests.swift */; }; AA3115A4280A60DD00CBAF91 /* JumpcutUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA3115A3280A60DD00CBAF91 /* JumpcutUITests.swift */; }; @@ -57,6 +69,10 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 52FEE61B2B59A9C900D81962 /* pop.wav */ = {isa = PBXFileReference; lastKnownFileType = audio.wav; path = pop.wav; sourceTree = ""; }; + 52FEE6202B59AA1A00D81962 /* JumpcutHelper-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "JumpcutHelper-Bridging-Header.h"; sourceTree = ""; }; + 52FEE6212B59AA1A00D81962 /* AudioManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioManager.swift; sourceTree = ""; }; + 52FEE6262B59AD3200D81962 /* clear.wav */ = {isa = PBXFileReference; lastKnownFileType = audio.wav; path = clear.wav; sourceTree = ""; }; AA16DB6D28393DD000F73A2C /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = ""; }; AA311586280A60DB00CBAF91 /* Jumpcut.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Jumpcut.app; sourceTree = BUILT_PRODUCTS_DIR; }; AA311595280A60DD00CBAF91 /* JumpcutTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = JumpcutTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -184,10 +200,14 @@ AA3115B7280A618900CBAF91 /* Settings.swift */, AA3115B9280A618900CBAF91 /* StatusItem.swift */, AA3115C2280A618900CBAF91 /* Types.swift */, + 52FEE6212B59AA1A00D81962 /* AudioManager.swift */, AAD0C444280C4DC6003482B4 /* Preferences */, AA3115B5280A618900CBAF91 /* Assets.xcassets */, + 52FEE61B2B59A9C900D81962 /* pop.wav */, + 52FEE6262B59AD3200D81962 /* clear.wav */, AA3115C3280A618900CBAF91 /* Info.plist */, AA3115C5280A618900CBAF91 /* Jumpcut.entitlements */, + 52FEE6202B59AA1A00D81962 /* JumpcutHelper-Bridging-Header.h */, ); path = Jumpcut; sourceTree = ""; @@ -317,7 +337,7 @@ }; AAD0C42C280B779E003482B4 = { CreatedOnToolsVersion = 13.2.1; - LastSwiftMigration = 1320; + LastSwiftMigration = 1520; }; }; }; @@ -355,6 +375,8 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + 52FEE6272B59AD3200D81962 /* clear.wav in Resources */, + 52FEE61C2B59A9C900D81962 /* pop.wav in Resources */, AA3115C9280A618900CBAF91 /* Assets.xcassets in Resources */, AA3115D2280A618900CBAF91 /* Credits.html in Resources */, ); @@ -364,6 +386,8 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + 52FEE6282B59AD3200D81962 /* clear.wav in Resources */, + 52FEE61D2B59A9C900D81962 /* pop.wav in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -371,6 +395,8 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + 52FEE6292B59AD3200D81962 /* clear.wav in Resources */, + 52FEE61E2B59A9C900D81962 /* pop.wav in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -378,6 +404,8 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + 52FEE62A2B59AD3200D81962 /* clear.wav in Resources */, + 52FEE61F2B59A9C900D81962 /* pop.wav in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -411,6 +439,7 @@ files = ( AA3115D9280A618900CBAF91 /* Pasteboard.swift in Sources */, AA16DB6E28393DD000F73A2C /* Constants.swift in Sources */, + 52FEE6222B59AA1A00D81962 /* AudioManager.swift in Sources */, AA3115D4280A618900CBAF91 /* Bezel.swift in Sources */, AA3115D1280A618900CBAF91 /* MenuManager.swift in Sources */, AA3115D5280A618900CBAF91 /* AppearancePreferenceViewController.swift in Sources */, @@ -434,6 +463,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 52FEE6232B59AA1A00D81962 /* AudioManager.swift in Sources */, AA31159A280A60DD00CBAF91 /* JumpcutTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -442,6 +472,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 52FEE6242B59AA1A00D81962 /* AudioManager.swift in Sources */, AA3115A4280A60DD00CBAF91 /* JumpcutUITests.swift in Sources */, AA3115A6280A60DD00CBAF91 /* JumpcutUITestsLaunchTests.swift in Sources */, ); @@ -451,6 +482,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 52FEE6252B59AA1A00D81962 /* AudioManager.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -595,7 +627,7 @@ CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; CURRENT_PROJECT_VERSION = 20231204; - DEVELOPMENT_TEAM = 4987SK7L6Z; + DEVELOPMENT_TEAM = N5M97QAGGY; ENABLE_HARDENED_RUNTIME = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = Jumpcut/Info.plist; @@ -608,6 +640,7 @@ "$(inherited)", "@executable_path/../Frameworks", ); + MACOSX_DEPLOYMENT_TARGET = 14.0; MARKETING_VERSION = 0.84; PRODUCT_BUNDLE_IDENTIFIER = net.sf.Jumpcut; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -626,7 +659,7 @@ CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; CURRENT_PROJECT_VERSION = 20231204; - DEVELOPMENT_TEAM = 4987SK7L6Z; + DEVELOPMENT_TEAM = N5M97QAGGY; ENABLE_HARDENED_RUNTIME = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = Jumpcut/Info.plist; @@ -639,6 +672,7 @@ "$(inherited)", "@executable_path/../Frameworks", ); + MACOSX_DEPLOYMENT_TARGET = 14.0; MARKETING_VERSION = 0.84; PRODUCT_BUNDLE_IDENTIFIER = net.sf.Jumpcut; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -654,9 +688,9 @@ BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = 4987SK7L6Z; + DEVELOPMENT_TEAM = N5M97QAGGY; GENERATE_INFOPLIST_FILE = YES; - MACOSX_DEPLOYMENT_TARGET = 12.2; + MACOSX_DEPLOYMENT_TARGET = 14.0; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = net.sf.Jumpcut.JumpcutTests; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -673,9 +707,9 @@ BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = 4987SK7L6Z; + DEVELOPMENT_TEAM = N5M97QAGGY; GENERATE_INFOPLIST_FILE = YES; - MACOSX_DEPLOYMENT_TARGET = 12.2; + MACOSX_DEPLOYMENT_TARGET = 14.0; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = net.sf.Jumpcut.JumpcutTests; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -691,8 +725,9 @@ ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = 4987SK7L6Z; + DEVELOPMENT_TEAM = N5M97QAGGY; GENERATE_INFOPLIST_FILE = YES; + MACOSX_DEPLOYMENT_TARGET = 14.0; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = net.sf.Jumpcut.JumpcutUITests; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -708,8 +743,9 @@ ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = 4987SK7L6Z; + DEVELOPMENT_TEAM = N5M97QAGGY; GENERATE_INFOPLIST_FILE = YES; + MACOSX_DEPLOYMENT_TARGET = 14.0; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = net.sf.Jumpcut.JumpcutUITests; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -727,10 +763,11 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = JumpcutHelper/JumpcutHelper.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = 4987SK7L6Z; + DEVELOPMENT_TEAM = N5M97QAGGY; ENABLE_HARDENED_RUNTIME = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = JumpcutHelper/Info.plist; @@ -740,12 +777,13 @@ "$(inherited)", "@executable_path/../Frameworks", ); - MACOSX_DEPLOYMENT_TARGET = 10.11; + MACOSX_DEPLOYMENT_TARGET = 14.0; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = net.sf.Jumpcut.JumpcutHelper; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_OBJC_BRIDGING_HEADER = "Jumpcut/JumpcutHelper-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; }; @@ -759,10 +797,11 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = JumpcutHelper/JumpcutHelper.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = 4987SK7L6Z; + DEVELOPMENT_TEAM = N5M97QAGGY; ENABLE_HARDENED_RUNTIME = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = JumpcutHelper/Info.plist; @@ -772,12 +811,13 @@ "$(inherited)", "@executable_path/../Frameworks", ); - MACOSX_DEPLOYMENT_TARGET = 10.11; + MACOSX_DEPLOYMENT_TARGET = 14.0; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = net.sf.Jumpcut.JumpcutHelper; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_OBJC_BRIDGING_HEADER = "Jumpcut/JumpcutHelper-Bridging-Header.h"; SWIFT_VERSION = 5.0; }; name = Release; diff --git a/Jumpcut/Jumpcut/AppDelegate.swift b/Jumpcut/Jumpcut/AppDelegate.swift index 86fe5dc..a71314d 100644 --- a/Jumpcut/Jumpcut/AppDelegate.swift +++ b/Jumpcut/Jumpcut/AppDelegate.swift @@ -15,6 +15,7 @@ import Sparkle class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate, SPUStandardUserDriverDelegate, SPUUpdaterDelegate { + let sp = AudioManager.shared private var pasteboard: Pasteboard! private var stack: ClippingStack! private var menu: MenuManager! @@ -202,6 +203,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate, SPUStandardU func clearHotkey() { hotKey = nil hotKeyBase = nil + } func stackEmpty() -> Bool { diff --git a/Jumpcut/Jumpcut/AudioManager.swift b/Jumpcut/Jumpcut/AudioManager.swift new file mode 100644 index 0000000..fffdf8d --- /dev/null +++ b/Jumpcut/Jumpcut/AudioManager.swift @@ -0,0 +1,72 @@ +// +// AudioManager.swift +// Jumpcut +// +// Created by Essam Salah on 18/01/2024. +// + +import Foundation + +import AVFAudio + +class AudioManager { + + static let shared = AudioManager() + private var player: AVAudioPlayer! + + var isSoundEffectEnabled: Bool = true + + private init() { + if let file = Bundle.main.path(forResource: "pop", ofType: "wav") { + let pathURL = URL(fileURLWithPath: file) + do { + try player = AVAudioPlayer(contentsOf: pathURL) + player.prepareToPlay() + player.volume = 0.1 + } catch { + print(error) + } + } + + } + + func playClear() { + + if let file = Bundle.main.path(forResource: "clear", ofType: "wav") { + let pathURL = URL(fileURLWithPath: file) + + do { + try player = AVAudioPlayer(contentsOf: pathURL) + + play() + } catch { + print("error setting up audio session ") + } + } + } + + func playPop() { + + if let file = Bundle.main.path(forResource: "pop", ofType: "wav") { + let pathURL = URL(fileURLWithPath: file) + + do { + try player = AVAudioPlayer(contentsOf: pathURL) + play() + } catch { + print("error setting up audio session ") + } + } + } + + + func play() { + if let isSoundEffectEnabled = UserDefaults.standard.value(forKey: SettingsPath.soundEffect.rawValue) as? Bool { + self.isSoundEffectEnabled = isSoundEffectEnabled + } + if(isSoundEffectEnabled) { + player.play() + } + } + +} diff --git a/Jumpcut/Jumpcut/Clippings.swift b/Jumpcut/Jumpcut/Clippings.swift index 1a5682c..d887f6c 100644 --- a/Jumpcut/Jumpcut/Clippings.swift +++ b/Jumpcut/Jumpcut/Clippings.swift @@ -8,9 +8,9 @@ /* A clipping is a snippet of text with some simple operators to get it in a form that's useful for display. - + A clipping store is a list of clippings, with a persistance backing. - + A clipping stack is an overlay on such a list with a positional index which may be adjusted. (In practice, this is done via the keyboard-based interface in our bezel. @@ -18,327 +18,330 @@ import Cocoa struct JCEngine: Codable { - // displayLen is a duplicative property from older versions of - // Jumpcut and may safely be ignored--even more so than the other - // values beyond jcList, which aren't being used for anything. - var displayLen: Int? - var displayNum: Int - var jcList: [JCListItem] - var rememberNum: Int - var version: String + // displayLen is a duplicative property from older versions of + // Jumpcut and may safely be ignored--even more so than the other + // values beyond jcList, which aren't being used for anything. + var displayLen: Int? + var displayNum: Int + var jcList: [JCListItem] + var rememberNum: Int + var version: String } // These names are from our original Objective-C implementation. // swiftlint:disable identifier_name struct JCListItem: Codable { - let Contents: String - let Position: Int - let `Type`: String + let Contents: String + let Position: Int + let `Type`: String } // swiftlint:enable identifier_name public class ClippingStack: NSObject { - private var store: ClippingStore - public var position: Int = 0 - public var count: Int { - return store.count + private var store: ClippingStore + public var position: Int = 0 + public var count: Int { + return store.count + } + public var skipSave: Bool { + get { return store.skipSave } + set { store.skipSave = newValue } + } + + override init() { + self.store = ClippingStore() + super.init() + self.store.maxLength = UserDefaults.standard.value( + forKey: SettingsPath.rememberNum.rawValue + ) as? Int ?? 99 + } + + func checkWriteAccess() -> Bool { + return store.checkPlistWriteAccess() + } + + func isEmpty() -> Bool { + return store.count == 0 + } + + // swiftlint:disable:next identifier_name + func firstItems(n: Int) -> ArraySlice { + return store.firstItems(n: n) + } + + func clear() { + store.clear() + AudioManager.shared.playClear() + } + + func delete() { + deleteAt(position: self.position) + } + + func add(item: String) { + AudioManager.shared.playPop() + + store.add(item: item) + } + + func deleteAt(position: Int) { + guard position < store.count else { + return } - public var skipSave: Bool { - get { return store.skipSave } - set { store.skipSave = newValue } + store.removeItem(position: position) + if store.count == 0 { + self.position = 0 + } else if self.position > 0 && self.position >= position { + // If we're deleting at or above the stack position, + // move up. + self.position -= 1 } - - override init() { - self.store = ClippingStore() - super.init() - self.store.maxLength = UserDefaults.standard.value( - forKey: SettingsPath.rememberNum.rawValue - ) as? Int ?? 99 - } - - func checkWriteAccess() -> Bool { - return store.checkPlistWriteAccess() - } - - func isEmpty() -> Bool { - return store.count == 0 - } - - // swiftlint:disable:next identifier_name - func firstItems(n: Int) -> ArraySlice { - return store.firstItems(n: n) - } - - func clear() { - store.clear() - } - - func delete() { - deleteAt(position: self.position) - } - - func add(item: String) { - store.add(item: item) - } - - func deleteAt(position: Int) { - guard position < store.count else { - return - } - store.removeItem(position: position) - if store.count == 0 { - self.position = 0 - } else if self.position > 0 && self.position >= position { - // If we're deleting at or above the stack position, - // move up. - self.position -= 1 - } - } - - func down() { - let newPosition = position + 1 - if newPosition < store.count { - position = newPosition - } else { - if let wraparound = UserDefaults.standard.value(forKey: SettingsPath.wraparoundBezel.rawValue) as? Bool { - if wraparound { - position = 0 - } - } - } - } - - func move(steps: Int) { - // Differs from up() and down() methods, as we don't allow wrapping around. - guard self.count > 0 else { - return - } - if steps > 0 { - let comparable: [Int] = [self.position + steps, self.count - 1] - self.position = comparable.min()! - } else { - let comparable: [Int] = [0, self.position + steps] - self.position = comparable.max()! + } + + func down() { + let newPosition = position + 1 + if newPosition < store.count { + position = newPosition + } else { + if let wraparound = UserDefaults.standard.value(forKey: SettingsPath.wraparoundBezel.rawValue) as? Bool { + if wraparound { + position = 0 } + } } - - func itemAt(position: Int) -> Clipping? { - return store.itemAt(position: position) + } + + func move(steps: Int) { + // Differs from up() and down() methods, as we don't allow wrapping around. + guard self.count > 0 else { + return } - - func moveItemToTop(position: Int) { - store.moveItemToTop(position: position) + if steps > 0 { + let comparable: [Int] = [self.position + steps, self.count - 1] + self.position = comparable.min()! + } else { + let comparable: [Int] = [0, self.position + steps] + self.position = comparable.max()! } - - func up() { - let newPosition = position - 1 - if newPosition >= 0 { - position = newPosition - } else { - if let wraparound = UserDefaults.standard.value(forKey: SettingsPath.wraparoundBezel.rawValue) as? Bool { - if wraparound { - position = store.count - 1 - } - } + } + + func itemAt(position: Int) -> Clipping? { + return store.itemAt(position: position) + } + + func moveItemToTop(position: Int) { + store.moveItemToTop(position: position) + } + + func up() { + let newPosition = position - 1 + if newPosition >= 0 { + position = newPosition + } else { + if let wraparound = UserDefaults.standard.value(forKey: SettingsPath.wraparoundBezel.rawValue) as? Bool { + if wraparound { + position = store.count - 1 } + } } + } } public class Clipping: NSObject { - public let fullText: String - public var shortenedText: String - public var length: Int - private let defaultLength = 40 - - init(string: String) { - fullText = string - length = defaultLength - shortenedText = fullText.trimmingCharacters(in: .whitespacesAndNewlines) - shortenedText = shortenedText.components(separatedBy: .newlines)[0] - if shortenedText.count > length { - shortenedText = String(shortenedText.prefix(length)) + "…" - } + public let fullText: String + public var shortenedText: String + public var length: Int + private let defaultLength = 40 + + init(string: String) { + fullText = string + length = defaultLength + shortenedText = fullText.trimmingCharacters(in: .whitespacesAndNewlines) + shortenedText = shortenedText.components(separatedBy: .newlines)[0] + if shortenedText.count > length { + shortenedText = String(shortenedText.prefix(length)) + "…" } + } } private class ClippingStore: NSObject { - - // TK: Back with sqlite3 for persistence - private var clippings: [Clipping] = [] - private var _maxLength = 99 - private let plistUrl: URL? - fileprivate var skipSave: Bool - - fileprivate var maxLength: Int { - get { return _maxLength } - set { - let newValueWithMin = newValue < 10 ? 10 : newValue - clippings = Array(self.firstItems(n: newValueWithMin)) - _maxLength = newValueWithMin - } + + // TK: Back with sqlite3 for persistence + private var clippings: [Clipping] = [] + private var _maxLength = 99 + private let plistUrl: URL? + fileprivate var skipSave: Bool + + fileprivate var maxLength: Int { + get { return _maxLength } + set { + let newValueWithMin = newValue < 10 ? 10 : newValue + clippings = Array(self.firstItems(n: newValueWithMin)) + _maxLength = newValueWithMin } - - private static func getSupportDir() -> URL? { - // Adapted from Rectangle's preferences loader - let paths = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask) - let supportDir = paths.isEmpty ? nil : paths[0].appendingPathComponent("Jumpcut", isDirectory: true) - guard supportDir != nil else { - return nil - } - if !FileManager.default.fileExists(atPath: supportDir!.path) { - do { - try FileManager.default.createDirectory( - at: supportDir!, - withIntermediateDirectories: false, - attributes: nil - ) - } catch { - print("Unable to create support directory", error) - } - } - return supportDir + } + + private static func getSupportDir() -> URL? { + // Adapted from Rectangle's preferences loader + let paths = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask) + let supportDir = paths.isEmpty ? nil : paths[0].appendingPathComponent("Jumpcut", isDirectory: true) + guard supportDir != nil else { + return nil } - - override init() { - // TK: We will eventually be switching this out to use SQLite3, but for - // now we'll use a hardcoded path to a property list. - skipSave = UserDefaults.standard.value(forKey: SettingsPath.skipSave.rawValue) as? Bool ?? false - if let jumpcutSupportDir = ClippingStore.getSupportDir() { - plistUrl = jumpcutSupportDir.appendingPathComponent("JCEngine.save") - } else { - plistUrl = nil - } - super.init() - if !skipSave { - loadFromPlist(path: plistUrl) - } + if !FileManager.default.fileExists(atPath: supportDir!.path) { + do { + try FileManager.default.createDirectory( + at: supportDir!, + withIntermediateDirectories: false, + attributes: nil + ) + } catch { + print("Unable to create support directory", error) + } } - - func checkPlistWriteAccess() -> Bool { - guard plistUrl != nil else { - return false - } - var resourceValues = URLResourceValues() - resourceValues.contentModificationDate = Date() - let manager = FileManager() - let plistPath = plistUrl!.path - let result: Bool - if !manager.fileExists(atPath: plistPath) { - result = manager.createFile(atPath: plistPath, contents: nil, attributes: nil) - } else { - result = manager.isWritableFile(atPath: plistPath) - } - return result + return supportDir + } + + override init() { + // TK: We will eventually be switching this out to use SQLite3, but for + // now we'll use a hardcoded path to a property list. + skipSave = UserDefaults.standard.value(forKey: SettingsPath.skipSave.rawValue) as? Bool ?? false + if let jumpcutSupportDir = ClippingStore.getSupportDir() { + plistUrl = jumpcutSupportDir.appendingPathComponent("JCEngine.save") + } else { + plistUrl = nil } - - func loadFromPlist(path: URL?) { - guard path != nil else { - return - } - var savedValues: JCEngine - let allowWhitespace = UserDefaults.standard.value( - forKey: SettingsPath.allowWhitespaceClippings.rawValue - ) as? Bool ?? false - if let data = try? Data(contentsOf: path!) { - do { - savedValues = try PropertyListDecoder().decode(JCEngine.self, from: data) - for clipDict in savedValues.jcList.reversed() { - let clipIsEmpty = clipDict.Contents.trimmingCharacters( - in: .whitespacesAndNewlines - ).isEmpty - if !clipIsEmpty || allowWhitespace { - self.add(item: clipDict.Contents) - } - } - } catch { - print("Unable to load clippings store plist") - } - } + super.init() + if !skipSave { + loadFromPlist(path: plistUrl) } - - var count: Int { - return clippings.count + } + + func checkPlistWriteAccess() -> Bool { + guard plistUrl != nil else { + return false } - - private func writeClippings() { - guard !skipSave, plistUrl != nil else { - return - } - var items: [JCListItem] = [] - var counter = 0 - for clip in clippings { - items.append(JCListItem(Contents: clip.fullText, Position: counter, Type: "NSStringPboardType")) - counter += 1 - } - let data = JCEngine( - displayNum: UserDefaults.standard.value(forKey: SettingsPath.displayNum.rawValue) as? Int ?? 10, - jcList: items, - rememberNum: UserDefaults.standard.value(forKey: SettingsPath.displayNum.rawValue) as? Int ?? 99, - version: Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "Unknown" - ) - let encoder = PropertyListEncoder() - encoder.outputFormat = .xml - do { - let newData = try encoder.encode(data) - try newData.write(to: plistUrl!) - } catch { - print("Unable to write clippings store plist file") - } + var resourceValues = URLResourceValues() + resourceValues.contentModificationDate = Date() + let manager = FileManager() + let plistPath = plistUrl!.path + let result: Bool + if !manager.fileExists(atPath: plistPath) { + result = manager.createFile(atPath: plistPath, contents: nil, attributes: nil) + } else { + result = manager.isWritableFile(atPath: plistPath) } - - func add(item: String) { - clippings.insert(Clipping(string: item), at: 0) - if clippings.count > maxLength { - clippings.removeLast() + return result + } + + func loadFromPlist(path: URL?) { + guard path != nil else { + return + } + var savedValues: JCEngine + let allowWhitespace = UserDefaults.standard.value( + forKey: SettingsPath.allowWhitespaceClippings.rawValue + ) as? Bool ?? false + if let data = try? Data(contentsOf: path!) { + do { + savedValues = try PropertyListDecoder().decode(JCEngine.self, from: data) + for clipDict in savedValues.jcList.reversed() { + let clipIsEmpty = clipDict.Contents.trimmingCharacters( + in: .whitespacesAndNewlines + ).isEmpty + if !clipIsEmpty || allowWhitespace { + self.add(item: clipDict.Contents) + } } - // TK: When we have SQLite backing, we'll want to change this. - writeClippings() + } catch { + print("Unable to load clippings store plist") + } } - - func clear() { - clippings = [] - // TK: When we have SQLite backing, we'll want to change this. - writeClippings() + } + + var count: Int { + return clippings.count + } + + private func writeClippings() { + guard !skipSave, plistUrl != nil else { + return } - - func moveItemToTop(position: Int) { - // Don't move from an invalid position; also, position 0 - // is a null operation, because it's already at the top. - guard !clippings.isEmpty, - position <= clippings.count, - position > 0 - else { - return - } - let element = clippings.remove(at: position) - clippings.insert(element, at: 0) - writeClippings() + var items: [JCListItem] = [] + var counter = 0 + for clip in clippings { + items.append(JCListItem(Contents: clip.fullText, Position: counter, Type: "NSStringPboardType")) + counter += 1 } - - func itemAt(position: Int) -> Clipping? { - if clippings.isEmpty || position > clippings.count { - return nil - } - return clippings[position] + let data = JCEngine( + displayNum: UserDefaults.standard.value(forKey: SettingsPath.displayNum.rawValue) as? Int ?? 10, + jcList: items, + rememberNum: UserDefaults.standard.value(forKey: SettingsPath.displayNum.rawValue) as? Int ?? 99, + version: Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "Unknown" + ) + let encoder = PropertyListEncoder() + encoder.outputFormat = .xml + do { + let newData = try encoder.encode(data) + try newData.write(to: plistUrl!) + } catch { + print("Unable to write clippings store plist file") } - - func removeItem(position: Int) { - if clippings.isEmpty || position > clippings.count { - return - } - clippings.remove(at: position) - // TK: When we have SQLite backing, we'll want to change this. - writeClippings() + } + + func add(item: String) { + clippings.insert(Clipping(string: item), at: 0) + if clippings.count > maxLength { + clippings.removeLast() } - - // swiftlint:disable:next identifier_name - func firstItems(n: Int) -> ArraySlice { - var slice: ArraySlice - if n > clippings.count { - slice = clippings[...] - } else { - slice = clippings[0 ..< n] - } - return slice + // TK: When we have SQLite backing, we'll want to change this. + writeClippings() + } + + func clear() { + clippings = [] + // TK: When we have SQLite backing, we'll want to change this. + writeClippings() + } + + func moveItemToTop(position: Int) { + // Don't move from an invalid position; also, position 0 + // is a null operation, because it's already at the top. + guard !clippings.isEmpty, + position <= clippings.count, + position > 0 + else { + return + } + let element = clippings.remove(at: position) + clippings.insert(element, at: 0) + writeClippings() + } + + func itemAt(position: Int) -> Clipping? { + if clippings.isEmpty || position > clippings.count { + return nil + } + return clippings[position] + } + + func removeItem(position: Int) { + if clippings.isEmpty || position > clippings.count { + return + } + clippings.remove(at: position) + // TK: When we have SQLite backing, we'll want to change this. + writeClippings() + } + + // swiftlint:disable:next identifier_name + func firstItems(n: Int) -> ArraySlice { + var slice: ArraySlice + if n > clippings.count { + slice = clippings[...] + } else { + slice = clippings[0 ..< n] } + return slice + } } diff --git a/Jumpcut/Jumpcut/JumpcutHelper-Bridging-Header.h b/Jumpcut/Jumpcut/JumpcutHelper-Bridging-Header.h new file mode 100644 index 0000000..1b2cb5d --- /dev/null +++ b/Jumpcut/Jumpcut/JumpcutHelper-Bridging-Header.h @@ -0,0 +1,4 @@ +// +// Use this file to import your target's public headers that you would like to expose to Swift. +// + diff --git a/Jumpcut/Jumpcut/Preferences/GeneralPreferenceViewController.swift b/Jumpcut/Jumpcut/Preferences/GeneralPreferenceViewController.swift index 8b61d7e..3679702 100644 --- a/Jumpcut/Jumpcut/Preferences/GeneralPreferenceViewController.swift +++ b/Jumpcut/Jumpcut/Preferences/GeneralPreferenceViewController.swift @@ -133,6 +133,7 @@ final class GeneralPreferenceViewController: NSViewController, PreferencePane { let (pasteMenu, pasteBezel) = makePasteOptions(settings: settings) let wrapBezel = settings.checkbox(title: "Wraparound bezel", key: SettingsPath.wraparoundBezel) + let soundEffect = settings.checkbox(title: "Enable sound effect", key: SettingsPath.soundEffect) let stickyBezel = settings.checkbox(title: "Sticky bezel", key: SettingsPath.stickyBezel) let rememberNumView = settings.rangeStepper(title: "Remembering", minValue: 10, maxValue: 99, key: .rememberNum) let displayNumView = settings.rangeStepper(title: "Displaying", minValue: 10, maxValue: 99, key: .displayNum) @@ -148,7 +149,7 @@ final class GeneralPreferenceViewController: NSViewController, PreferencePane { let resetRow = makeResetRow() let grid = NSStackView(views: [ - pasteMenu, pasteBezel, bezelToTopRow, wrapBezel, stickyBezel, + pasteMenu, pasteBezel, bezelToTopRow, wrapBezel, stickyBezel, soundEffect, advancedMenuRow, makeSeparator(), stepperViews, makeSeparator(), launchOnLogin, sparkleRow, makeSeparator(), resetRow ]) diff --git a/Jumpcut/Jumpcut/Settings.swift b/Jumpcut/Jumpcut/Settings.swift index aa36493..049aa26 100644 --- a/Jumpcut/Jumpcut/Settings.swift +++ b/Jumpcut/Jumpcut/Settings.swift @@ -35,6 +35,7 @@ enum SettingsPath: String { case rememberNum case skipSave case stickyBezel + case soundEffect case wraparoundBezel } @@ -79,6 +80,7 @@ private let settingsDefaults: [String: Any] = [ SettingsPath.rememberNum.rawValue: 99, SettingsPath.skipSave.rawValue: false, SettingsPath.stickyBezel.rawValue: false, + SettingsPath.soundEffect.rawValue: false, SettingsPath.wraparoundBezel.rawValue: false ] diff --git a/Jumpcut/Jumpcut/clear.wav b/Jumpcut/Jumpcut/clear.wav new file mode 100644 index 0000000..7cf35ae Binary files /dev/null and b/Jumpcut/Jumpcut/clear.wav differ diff --git a/Jumpcut/Jumpcut/pop.wav b/Jumpcut/Jumpcut/pop.wav new file mode 100644 index 0000000..8650d87 Binary files /dev/null and b/Jumpcut/Jumpcut/pop.wav differ