diff --git a/.gitignore b/.gitignore index 9dfafd6..ee99228 100644 --- a/.gitignore +++ b/.gitignore @@ -1,26 +1,4 @@ -# Xcode -# -build/ -*.pbxuser -!default.pbxuser -*.mode1v3 -!default.mode1v3 -*.mode2v3 -!default.mode2v3 -*.perspectivev3 -!default.perspectivev3 -xcuserdata -*.xccheckout -*.moved-aside -DerivedData -*.hmap -*.ipa -*.xcuserstate -/kawa.xcarchive -Kawa.app +Carthage/ .DS_Store - -# Carthage -# -Carthage/Checkouts -Carthage/Build +DerivedData/ +kawa.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved diff --git a/Cartfile b/Cartfile deleted file mode 100644 index 00f8467..0000000 --- a/Cartfile +++ /dev/null @@ -1 +0,0 @@ -github "shpakovski/MASShortcut" ~> 2.4 diff --git a/Cartfile.resolved b/Cartfile.resolved deleted file mode 100644 index c3c24bd..0000000 --- a/Cartfile.resolved +++ /dev/null @@ -1 +0,0 @@ -github "shpakovski/MASShortcut" "2.4.0" diff --git a/README.md b/README.md index 996e793..218e9d8 100644 --- a/README.md +++ b/README.md @@ -33,24 +33,15 @@ sources like [CJKV](https://en.wikipedia.org/wiki/CJK_characters). ## Development -We use [Carthage](https://github.com/Carthage/Carthage) as a dependency manager. -You can find the latest releases of Carthage [here](https://github.com/Carthage/Carthage/releases), -or just install it with [Homebrew](http://brew.sh). +Dependencies are fetched with Swift Package Manager. Open `kawa.xcodeproj` in +Xcode and it will resolve packages automatically, or resolve/build from the +command line: ```bash -$ brew update -$ brew install carthage +xcodebuild -resolvePackageDependencies +xcodebuild -scheme kawa -configuration Debug ``` -To clone the Git repository of Kawa and install dependencies: - -```bash -$ git clone git@github.com:utatti/kawa.git -$ carthage bootstrap -``` - -After dependency installation, open the project with Xcode. - ## License Kawa is released under the [MIT License](LICENSE). diff --git a/kawa.xcodeproj/project.pbxproj b/kawa.xcodeproj/project.pbxproj index 93aed20..e8a608d 100644 --- a/kawa.xcodeproj/project.pbxproj +++ b/kawa.xcodeproj/project.pbxproj @@ -17,25 +17,11 @@ CC6D8ED21B654099005682C0 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = CC6D8ED11B654099005682C0 /* Images.xcassets */; }; CC6D8ED51B654099005682C0 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = CC6D8ED31B654099005682C0 /* Main.storyboard */; }; CC6D8EEB1B654143005682C0 /* InputSourceManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC6D8EEA1B654143005682C0 /* InputSourceManager.swift */; }; - CC9AAD2B1B71AA5400626F65 /* MASShortcut.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CC9AAD2A1B71AA5400626F65 /* MASShortcut.framework */; }; - CC9AAD2C1B71AA5400626F65 /* MASShortcut.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = CC9AAD2A1B71AA5400626F65 /* MASShortcut.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + CCB5F9A52C3A1B3E00D43F0A /* NotificationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = CCB5F9A42C3A1B3E00D43F0A /* NotificationManager.swift */; }; + CCB5F9B62C3A2A8D00D43F0A /* MASShortcut in Frameworks */ = {isa = PBXBuildFile; productRef = CCB5F9B72C3A2A8D00D43F0A /* MASShortcut */; }; EE92E8FD1F6CC98E00559B7C /* TISInputSource+Additions.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE92E8FC1F6CC98E00559B7C /* TISInputSource+Additions.swift */; }; /* End PBXBuildFile section */ -/* Begin PBXCopyFilesBuildPhase section */ - CC9AAD2D1B71AA5400626F65 /* Embed Frameworks */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = ""; - dstSubfolderSpec = 10; - files = ( - CC9AAD2C1B71AA5400626F65 /* MASShortcut.framework in Embed Frameworks */, - ); - name = "Embed Frameworks"; - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXCopyFilesBuildPhase section */ - /* Begin PBXFileReference section */ CC1FEF6D1B685E9A005E9BC8 /* BridgingHeader.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BridgingHeader.h; sourceTree = ""; }; CC1FEF6E1B685FD3005E9BC8 /* ShortcutCellView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ShortcutCellView.swift; sourceTree = ""; }; @@ -49,8 +35,8 @@ CC6D8ECD1B654099005682C0 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; CC6D8ED11B654099005682C0 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; CC6D8EEA1B654143005682C0 /* InputSourceManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InputSourceManager.swift; sourceTree = ""; }; - CC9AAD2A1B71AA5400626F65 /* MASShortcut.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MASShortcut.framework; path = Carthage/Build/Mac/MASShortcut.framework; sourceTree = ""; }; DE8850D624B633EC002964E4 /* en */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = en; path = en.lproj/Main.storyboard; sourceTree = ""; }; + CCB5F9A42C3A1B3E00D43F0A /* NotificationManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotificationManager.swift; sourceTree = ""; }; EE92E8FC1F6CC98E00559B7C /* TISInputSource+Additions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "TISInputSource+Additions.swift"; sourceTree = ""; }; /* End PBXFileReference section */ @@ -59,7 +45,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - CC9AAD2B1B71AA5400626F65 /* MASShortcut.framework in Frameworks */, + CCB5F9B62C3A2A8D00D43F0A /* MASShortcut in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -71,7 +57,6 @@ children = ( CC6D8ECA1B654099005682C0 /* kawa */, CC6D8EC91B654099005682C0 /* Products */, - CC9AAD2A1B71AA5400626F65 /* MASShortcut.framework */, ); sourceTree = ""; }; @@ -93,6 +78,7 @@ CC69E0771B66415300A92F58 /* ShortcutViewController.swift */, CC1FEF6E1B685FD3005E9BC8 /* ShortcutCellView.swift */, CC6D8EEA1B654143005682C0 /* InputSourceManager.swift */, + CCB5F9A42C3A1B3E00D43F0A /* NotificationManager.swift */, CC4181671B6CB3CB007DA794 /* PreferencesViewController.swift */, CC6D8ED11B654099005682C0 /* Images.xcassets */, CC6D8ED31B654099005682C0 /* Main.storyboard */, @@ -121,13 +107,15 @@ CC6D8EC41B654099005682C0 /* Sources */, CC6D8EC51B654099005682C0 /* Frameworks */, CC6D8EC61B654099005682C0 /* Resources */, - CC9AAD2D1B71AA5400626F65 /* Embed Frameworks */, ); buildRules = ( ); dependencies = ( ); name = kawa; + packageProductDependencies = ( + CCB5F9B72C3A2A8D00D43F0A /* MASShortcut */, + ); productName = kawa; productReference = CC6D8EC81B654099005682C0 /* Kawa.app */; productType = "com.apple.product-type.application"; @@ -159,6 +147,9 @@ productRefGroup = CC6D8EC91B654099005682C0 /* Products */; projectDirPath = ""; projectRoot = ""; + packageReferences = ( + CCB5F9B82C3A2A8D00D43F0A /* XCRemoteSwiftPackageReference "MASShortcut" */, + ); targets = ( CC6D8EC71B654099005682C0 /* kawa */, ); @@ -189,6 +180,7 @@ CC6D8ECE1B654099005682C0 /* AppDelegate.swift in Sources */, CC4065BE1B6D334B000E1E87 /* PermanentStorage.swift in Sources */, CC6D8EEB1B654143005682C0 /* InputSourceManager.swift in Sources */, + CCB5F9A52C3A1B3E00D43F0A /* NotificationManager.swift in Sources */, EE92E8FD1F6CC98E00559B7C /* TISInputSource+Additions.swift in Sources */, CC69E0781B66415300A92F58 /* ShortcutViewController.swift in Sources */, ); @@ -207,6 +199,25 @@ }; /* End PBXVariantGroup section */ +/* Begin XCRemoteSwiftPackageReference section */ + CCB5F9B82C3A2A8D00D43F0A /* XCRemoteSwiftPackageReference "MASShortcut" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/shpakovski/MASShortcut.git"; + requirement = { + branch = master; + kind = branch; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + CCB5F9B72C3A2A8D00D43F0A /* MASShortcut */ = { + isa = XCSwiftPackageProductDependency; + package = CCB5F9B82C3A2A8D00D43F0A /* XCRemoteSwiftPackageReference "MASShortcut" */; + productName = MASShortcut; + }; +/* End XCSwiftPackageProductDependency section */ + /* Begin XCBuildConfiguration section */ CC6D8EE21B654099005682C0 /* Debug */ = { isa = XCBuildConfiguration; @@ -321,7 +332,6 @@ COMBINE_HIDPI_IMAGES = YES; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", - "$(PROJECT_DIR)/Carthage/Build/Mac", ); INFOPLIST_FILE = kawa/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -344,7 +354,6 @@ COMBINE_HIDPI_IMAGES = YES; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", - "$(PROJECT_DIR)/Carthage/Build/Mac", ); INFOPLIST_FILE = kawa/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( diff --git a/kawa/AppDelegate.swift b/kawa/AppDelegate.swift index 353d229..1d00ff3 100644 --- a/kawa/AppDelegate.swift +++ b/kawa/AppDelegate.swift @@ -10,6 +10,10 @@ class AppDelegate: NSObject, NSApplicationDelegate { if PermanentStorage.launchedForTheFirstTime { PermanentStorage.launchedForTheFirstTime = false } + + if PermanentStorage.showsNotification { + NotificationManager.requestAuthorizationIfNeeded { _ in } + } } func applicationDidBecomeActive(_ notification: Notification) { diff --git a/kawa/BridgingHeader.h b/kawa/BridgingHeader.h index 316e5b0..d4369a0 100644 --- a/kawa/BridgingHeader.h +++ b/kawa/BridgingHeader.h @@ -11,6 +11,6 @@ #define kawa_BridgingHeader_h #import -#import +@import MASShortcut; #endif diff --git a/kawa/InputSourceManager.swift b/kawa/InputSourceManager.swift index 85ea067..120e435 100644 --- a/kawa/InputSourceManager.swift +++ b/kawa/InputSourceManager.swift @@ -47,8 +47,9 @@ extension InputSource: Equatable { extension InputSource { static var sources: [InputSource] { - let inputSourceNSArray = TISCreateInputSourceList(nil, false).takeRetainedValue() as NSArray - let inputSourceList = inputSourceNSArray as! [TISInputSource] + guard let unmanagedList = TISCreateInputSourceList(nil, false) else { return [] } + let inputSourceNSArray = unmanagedList.takeRetainedValue() as NSArray + guard let inputSourceList = inputSourceNSArray as? [TISInputSource] else { return [] } return inputSourceList .filter { diff --git a/kawa/NotificationManager.swift b/kawa/NotificationManager.swift new file mode 100644 index 0000000..6e231d8 --- /dev/null +++ b/kawa/NotificationManager.swift @@ -0,0 +1,67 @@ +import AppKit +import Foundation +import UserNotifications + +enum NotificationManager { + static func requestAuthorizationIfNeeded(completion: @escaping (Bool) -> Void) { + let center = UNUserNotificationCenter.current() + center.getNotificationSettings { settings in + switch settings.authorizationStatus { + case .authorized, .provisional, .ephemeral: + completion(true) + case .notDetermined: + center.requestAuthorization(options: [.alert, .sound]) { granted, _ in + completion(granted) + } + case .denied: + completion(false) + @unknown default: + completion(false) + } + } + } + + static func deliver(_ message: String, icon: NSImage?) { + requestAuthorizationIfNeeded { granted in + guard granted else { return } + + let content = UNMutableNotificationContent() + content.body = message + + if let attachment = icon.flatMap(createAttachment(from:)) { + content.attachments = [attachment] + } + + let request = UNNotificationRequest( + identifier: UUID().uuidString, + content: content, + trigger: nil + ) + + UNUserNotificationCenter.current().add(request, withCompletionHandler: nil) + } + } + + private static func createAttachment(from image: NSImage) -> UNNotificationAttachment? { + guard let url = writeImageToTemporaryLocation(image) else { return nil } + return try? UNNotificationAttachment(identifier: "kawa-icon", url: url, options: nil) + } + + private static func writeImageToTemporaryLocation(_ image: NSImage) -> URL? { + guard + let tiff = image.tiffRepresentation, + let bitmap = NSBitmapImageRep(data: tiff), + let pngData = bitmap.representation(using: .png, properties: [:]) + else { return nil } + + let url = FileManager.default.temporaryDirectory + .appendingPathComponent("kawa-icon-\(UUID().uuidString).png") + + do { + try pngData.write(to: url) + return url + } catch { + return nil + } + } +} diff --git a/kawa/PermanentStorage.swift b/kawa/PermanentStorage.swift index 49bf01b..30c3a73 100644 --- a/kawa/PermanentStorage.swift +++ b/kawa/PermanentStorage.swift @@ -1,27 +1,24 @@ import Cocoa class PermanentStorage { - private static func object(forKey key: StorageKey, withDefault defaultValue: T) -> T { - if let val = UserDefaults.standard.object(forKey: key.rawValue) as? T { - return val - } else { - return defaultValue - } - } - - private static func set(_ value: T, forKey key: StorageKey) { - UserDefaults.standard.set((value as AnyObject), forKey: key.rawValue) - UserDefaults.standard.synchronize() - } + private static let defaults = UserDefaults.standard private enum StorageKey: String { case showsNotification = "show-notification" case launchedForTheFirstTime = "launched-for-the-first-time" } + private static func bool(forKey key: StorageKey, default defaultValue: Bool) -> Bool { + return defaults.object(forKey: key.rawValue) as? Bool ?? defaultValue + } + + private static func set(_ value: Bool, forKey key: StorageKey) { + defaults.set(value, forKey: key.rawValue) + } + static var showsNotification: Bool { get { - return object(forKey: .showsNotification, withDefault: false) + return bool(forKey: .showsNotification, default: false) } set { set(newValue, forKey: .showsNotification) @@ -30,7 +27,7 @@ class PermanentStorage { static var launchedForTheFirstTime: Bool { get { - return object(forKey: .launchedForTheFirstTime, withDefault: true) + return bool(forKey: .launchedForTheFirstTime, default: true) } set { set(newValue, forKey: .launchedForTheFirstTime) diff --git a/kawa/PreferencesViewController.swift b/kawa/PreferencesViewController.swift index 4fd6796..4d1d9e2 100644 --- a/kawa/PreferencesViewController.swift +++ b/kawa/PreferencesViewController.swift @@ -14,7 +14,12 @@ class PreferencesViewController: NSViewController { } @IBAction func showNotification(_ sender: NSButton) { - PermanentStorage.showsNotification = sender.state.boolValue + let shouldShow = sender.state.boolValue + PermanentStorage.showsNotification = shouldShow + + if shouldShow { + NotificationManager.requestAuthorizationIfNeeded { _ in } + } } } diff --git a/kawa/ShortcutCellView.swift b/kawa/ShortcutCellView.swift index 33c1c78..fa68b72 100644 --- a/kawa/ShortcutCellView.swift +++ b/kawa/ShortcutCellView.swift @@ -9,9 +9,12 @@ class ShortcutCellView: NSTableCellView { func setInputSource(_ inputSource: InputSource) { self.inputSource = inputSource shortcutKey = inputSource.id.replacingOccurrences(of: ".", with: "-") - shortcutView.associatedUserDefaultsKey = shortcutKey! + + guard let shortcutKey = shortcutKey else { return } + + shortcutView.associatedUserDefaultsKey = shortcutKey shortcutView.shortcutValueChange = self.shortcutValueDidChange - MASShortcutBinder.shared().bindShortcut(withDefaultsKey: shortcutKey!, toAction: selectInput) + MASShortcutBinder.shared().bindShortcut(withDefaultsKey: shortcutKey, toAction: selectInput) } func shortcutValueDidChange(_ sender: MASShortcutView?) { @@ -21,8 +24,10 @@ class ShortcutCellView: NSTableCellView { } func resetShortcutBinder() { - MASShortcutBinder.shared().breakBinding(withDefaultsKey: shortcutKey!) - MASShortcutBinder.shared().bindShortcut(withDefaultsKey: shortcutKey!, toAction: selectInput) + guard let shortcutKey = shortcutKey else { return } + + MASShortcutBinder.shared().breakBinding(withDefaultsKey: shortcutKey) + MASShortcutBinder.shared().bindShortcut(withDefaultsKey: shortcutKey, toAction: selectInput) } func selectInput() { @@ -36,10 +41,6 @@ class ShortcutCellView: NSTableCellView { } func showNotification(_ message: String, icon: NSImage?) { - NSUserNotificationCenter.default.removeAllDeliveredNotifications() - let notification = NSUserNotification() - notification.informativeText = message - notification.contentImage = icon - NSUserNotificationCenter.default.deliver(notification) + NotificationManager.deliver(message, icon: icon) } } diff --git a/kawa/ShortcutViewController.swift b/kawa/ShortcutViewController.swift index 448168d..54a7d24 100644 --- a/kawa/ShortcutViewController.swift +++ b/kawa/ShortcutViewController.swift @@ -7,12 +7,13 @@ class ShortcutViewController: NSViewController, NSTableViewDataSource, NSTableVi func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? { let inputSource = InputSource.sources[row] + guard let columnIdentifier = tableColumn?.identifier.rawValue else { return nil } - switch tableColumn!.identifier.rawValue { + switch columnIdentifier { case "Keyboard": return createKeyboardCellView(tableView, inputSource) case "Shortcut": - return createShorcutCellView(tableView, inputSource) + return createShortcutCellView(tableView, inputSource) default: return nil } @@ -25,7 +26,7 @@ class ShortcutViewController: NSViewController, NSTableViewDataSource, NSTableVi return cell } - func createShorcutCellView(_ tableView: NSTableView, _ inputSource: InputSource) -> ShortcutCellView? { + func createShortcutCellView(_ tableView: NSTableView, _ inputSource: InputSource) -> ShortcutCellView? { let cell = tableView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: "ShortcutCellView"), owner: self) as? ShortcutCellView cell?.setInputSource(inputSource) return cell diff --git a/kawa/StatusBar.swift b/kawa/StatusBar.swift index 8d1e7d1..4d29db8 100644 --- a/kawa/StatusBar.swift +++ b/kawa/StatusBar.swift @@ -7,7 +7,7 @@ class StatusBar { let item = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength) init() { - let button = item.button! + guard let button = item.button else { return } let buttonImage = NSImage(named: "StatusItemIcon") buttonImage?.isTemplate = true