├── .gitignore ├── JoyKeyMapper.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── xcshareddata │ └── xcschemes │ └── JoyKeyMapper.xcscheme ├── JoyKeyMapper.xcworkspace ├── contents.xcworkspacedata └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── JoyKeyMapper ├── AppDelegate.swift ├── AppNotifications.swift ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── app-icon_1024.png │ │ ├── app-icon_128.png │ │ ├── app-icon_16.png │ │ ├── app-icon_256-1.png │ │ ├── app-icon_256.png │ │ ├── app-icon_32-1.png │ │ ├── app-icon_32.png │ │ ├── app-icon_512-1.png │ │ ├── app-icon_512.png │ │ └── app-icon_64.png │ ├── Contents.json │ ├── GenericApplicationIcon.imageset │ │ ├── Contents.json │ │ └── GenericApplicationIcon.png │ ├── battery_charge.imageset │ │ ├── Contents.json │ │ └── battery_charge.png │ ├── battery_critical.imageset │ │ ├── Contents.json │ │ └── battery_critical.png │ ├── battery_empty.imageset │ │ ├── Contents.json │ │ └── battery_empty.png │ ├── battery_full.imageset │ │ ├── Contents.json │ │ └── battery_full.png │ ├── battery_low.imageset │ │ ├── Contents.json │ │ └── battery_low.png │ ├── battery_medium.imageset │ │ ├── Contents.json │ │ └── battery_medium.png │ ├── famicon_1.imageset │ │ ├── Contents.json │ │ └── famicon_1.png │ ├── famicon_2.imageset │ │ ├── Contents.json │ │ └── famicon_2.png │ ├── joycon_left_base.imageset │ │ ├── Contents.json │ │ └── joycon_left_base.png │ ├── joycon_left_body.imageset │ │ ├── Contents.json │ │ └── joycon_left_body.png │ ├── joycon_left_button.imageset │ │ ├── Contents.json │ │ └── joycon_left_button.png │ ├── joycon_right_base.imageset │ │ ├── Contents.json │ │ └── joycon_right_base.png │ ├── joycon_right_body.imageset │ │ ├── Contents.json │ │ └── joycon_right_body.png │ ├── joycon_right_button.imageset │ │ ├── Contents.json │ │ └── joycon_right_button.png │ ├── menu_icon.imageset │ │ ├── Contents.json │ │ └── menu_icon.png │ ├── procon_base.imageset │ │ ├── Contents.json │ │ └── procon_base.png │ ├── procon_body.imageset │ │ ├── Contents.json │ │ └── procon_body.png │ ├── procon_button.imageset │ │ ├── Contents.json │ │ └── procon_button.png │ ├── procon_left_grip.imageset │ │ ├── Contents.json │ │ └── procon_left_grip.png │ ├── procon_right_grip.imageset │ │ ├── Contents.json │ │ └── procon_right_grip.png │ ├── snescon.imageset │ │ ├── Contents.json │ │ └── snescon.png │ ├── stop.imageset │ │ ├── Contents.json │ │ └── stop.png │ └── unknown_controller.imageset │ │ ├── Contents.json │ │ └── unknown_controller.png ├── DataModels │ ├── DataManager.swift │ ├── GameController.swift │ ├── GameControllerIcon.swift │ ├── JoyKeyMapper.xcdatamodeld │ │ ├── .xccurrentversion │ │ └── JoyKeyMapper.xcdatamodel │ │ │ └── contents │ └── MetaKeyState.swift ├── Info.plist ├── JoyKeyMapper.entitlements ├── Misc │ ├── Notifications.swift │ ├── Utils.swift │ ├── en.lproj │ │ └── Localizable.strings │ └── ja.lproj │ │ └── Localizable.strings └── Views │ ├── AppList │ ├── AppCellView.swift │ └── ViewController+NSTableViewDelegate.swift │ ├── AppSettings │ ├── AppSettings.swift │ └── AppSettingsViewController.swift │ ├── ControllerList │ ├── ControllerView.swift │ ├── ControllerViewItem.swift │ ├── ControllerViewItem.xib │ └── ViewController+NSCollectionViewDelegate.swift │ ├── KeyConfigView │ ├── KeyConfigComboBox.swift │ └── KeyConfigViewController.swift │ ├── KeyMapList │ ├── ButtonNameCellView.swift │ ├── SpecialKeyName.swift │ ├── StickConfigCellView.swift │ └── ViewController+NSOutlineViewDelegate.swift │ ├── ViewController.swift │ ├── en.lproj │ └── Main.storyboard │ └── ja.lproj │ └── Main.storyboard ├── JoyKeyMapperLauncher ├── AppDelegate.swift ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── Contents.json ├── Info.plist ├── JoyKeyMapperLauncher.entitlements ├── en.lproj │ └── Main.storyboard └── ja.lproj │ └── Main.storyboard ├── LICENSE ├── Podfile ├── Podfile.lock ├── README.md ├── lang └── ja │ ├── README.md │ ├── screenshot_1.png │ ├── screenshot_2.png │ ├── screenshot_3.png │ ├── screenshot_4.png │ ├── screenshot_5.png │ ├── screenshot_6.png │ ├── screenshot_7.png │ ├── screenshot_8.png │ └── screenshot_9.png ├── resources ├── app-icon.ai ├── background.png ├── battery.ai ├── famicon.ai ├── joycon_left.ai ├── joycon_right.ai ├── procon.ai ├── screenshot │ ├── screenshot_1.png │ ├── screenshot_2.png │ ├── screenshot_3.png │ ├── screenshot_4.png │ ├── screenshot_5.png │ ├── screenshot_6.png │ ├── screenshot_7.png │ ├── screenshot_8.png │ └── screenshot_9.png └── snescon.ai └── scripts ├── build_dmg.sh ├── dmg_settings.py └── set_build_number.sh /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | *.DS_Store 4 | build/ 5 | *.pbxuser 6 | !default.pbxuser 7 | *.mode1v3 8 | !default.mode1v3 9 | *.mode2v3 10 | !default.mode2v3 11 | *.perspectivev3 12 | !default.perspectivev3 13 | xcuserdata 14 | *.xccheckout 15 | *.moved-aside 16 | DerivedData 17 | *.hmap 18 | *.ipa 19 | *.xcuserstate 20 | 21 | # CocoaPods 22 | # 23 | # We recommend against adding the Pods directory to your .gitignore. However 24 | # you should judge for yourself, the pros and cons are mentioned at: 25 | # http://guides.cocoapods.org/using/using-cocoapods.html#should-i-ignore-the-pods-directory-in-source-control 26 | # 27 | Pods/ 28 | 29 | # Carthage 30 | # 31 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 32 | # Carthage/Checkouts 33 | Carthage/Build 34 | 35 | # Intermediate files 36 | dmg/ 37 | 38 | # etc 39 | Framework/ 40 | *~ 41 | *.swp 42 | 43 | -------------------------------------------------------------------------------- /JoyKeyMapper.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /JoyKeyMapper.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /JoyKeyMapper.xcodeproj/xcshareddata/xcschemes/JoyKeyMapper.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 45 | 51 | 52 | 53 | 54 | 60 | 62 | 68 | 69 | 70 | 71 | 73 | 74 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /JoyKeyMapper.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /JoyKeyMapper.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /JoyKeyMapper/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // JoyKeyMapper 4 | // 5 | // Created by magicien on 2019/07/14. 6 | // Copyright © 2019 DarkHorse. All rights reserved. 7 | // 8 | 9 | import AppKit 10 | import ServiceManagement 11 | import UserNotifications 12 | import JoyConSwift 13 | 14 | let helperAppID: CFString = "jp.0spec.JoyKeyMapperLauncher" as CFString 15 | 16 | @NSApplicationMain 17 | class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate, UNUserNotificationCenterDelegate { 18 | @IBOutlet weak var menu: NSMenu? 19 | @IBOutlet weak var controllersMenu: NSMenuItem? 20 | let statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength) 21 | var windowController: NSWindowController? 22 | 23 | let manager: JoyConManager = JoyConManager() 24 | var dataManager: DataManager? 25 | var controllers: [GameController] = [] 26 | 27 | func applicationDidFinishLaunching(_ aNotification: Notification) { 28 | // Insert code here to initialize your application 29 | 30 | // Window initialization 31 | let storyboard = NSStoryboard(name: "Main", bundle: nil) 32 | self.windowController = storyboard.instantiateController(withIdentifier: "JoyKeyMapperWindowController") as? NSWindowController 33 | 34 | // Menu settings 35 | let icon = NSImage(named: "menu_icon") 36 | icon?.size = NSSize(width: 24, height: 24) 37 | self.statusItem.button?.image = icon 38 | self.statusItem.menu = self.menu 39 | 40 | // Set controller handlers 41 | self.manager.connectHandler = { [weak self] controller in 42 | self?.connectController(controller) 43 | } 44 | self.manager.disconnectHandler = { [weak self] controller in 45 | self?.disconnectController(controller) 46 | } 47 | 48 | self.dataManager = DataManager() { [weak self] manager in 49 | guard let strongSelf = self else { return } 50 | guard let dataManager = manager else { return } 51 | 52 | dataManager.controllers.forEach { data in 53 | let gameController = GameController(data: data) 54 | strongSelf.controllers.append(gameController) 55 | } 56 | _ = strongSelf.manager.runAsync() 57 | 58 | NSWorkspace.shared.notificationCenter.addObserver(strongSelf, selector: #selector(strongSelf.didActivateApp), name: NSWorkspace.didActivateApplicationNotification, object: nil) 59 | 60 | NotificationCenter.default.post(name: .controllerAdded, object: nil) 61 | } 62 | 63 | self.updateControllersMenu() 64 | NotificationCenter.default.addObserver(self, selector: #selector(controllerIconChanged), name: .controllerIconChanged, object: nil) 65 | 66 | // Notification settings 67 | let center = UNUserNotificationCenter.current() 68 | center.delegate = self 69 | } 70 | 71 | // MARK: - Menu 72 | 73 | @IBAction func openAbout(_ sender: Any) { 74 | NSApplication.shared.activate(ignoringOtherApps: true) 75 | NSApplication.shared.orderFrontStandardAboutPanel(NSApplication.shared) 76 | } 77 | 78 | @IBAction func openSettings(_ sender: Any) { 79 | NSApplication.shared.activate(ignoringOtherApps: true) 80 | self.windowController?.showWindow(self) 81 | self.windowController?.window?.orderFrontRegardless() 82 | self.windowController?.window?.delegate = self 83 | } 84 | 85 | @IBAction func quit(_ sender: Any) { 86 | NSApplication.shared.terminate(self) 87 | } 88 | 89 | func updateControllersMenu() { 90 | self.controllersMenu?.submenu?.removeAllItems() 91 | 92 | self.controllers.forEach { controller in 93 | guard controller.controller?.isConnected ?? false else { return } 94 | let item = NSMenuItem() 95 | 96 | item.title = "" 97 | item.image = controller.icon 98 | item.image?.size = NSSize(width: 32, height: 32) 99 | 100 | item.submenu = NSMenu() 101 | 102 | // Enable key mappings menu 103 | let enabled = NSMenuItem() 104 | enabled.title = NSLocalizedString("Enable key mappings", comment: "Enable key mappings") 105 | enabled.action = Selector(("toggleEnableKeyMappings")) 106 | enabled.state = controller.isEnabled ? .on : .off 107 | enabled.target = controller 108 | item.submenu?.addItem(enabled) 109 | 110 | // Disconnect menu 111 | let disconnect = NSMenuItem() 112 | disconnect.title = NSLocalizedString("Disconnect", comment: "Disconnect") 113 | disconnect.action = Selector(("disconnect")) 114 | disconnect.target = controller 115 | item.submenu?.addItem(disconnect) 116 | 117 | // Separator 118 | item.submenu?.addItem(NSMenuItem.separator()) 119 | 120 | // Battery info 121 | let battery = NSMenuItem() 122 | if controller.controller?.battery ?? .unknown != .unknown { 123 | var chargeString = "" 124 | if controller.controller?.isCharging ?? false { 125 | let charging = NSLocalizedString("charging", comment: "charging") 126 | chargeString = " (\(charging))" 127 | } 128 | let batteryString = NSLocalizedString("Battery", comment: "Battery") 129 | battery.title = "\(batteryString): \(controller.localizedBatteryString)\(chargeString)" 130 | } 131 | battery.isEnabled = false 132 | item.submenu?.addItem(battery) 133 | 134 | self.controllersMenu?.submenu?.addItem(item) 135 | } 136 | 137 | if let itemCount = self.controllersMenu?.submenu?.items.count, itemCount <= 0 { 138 | let item = NSMenuItem() 139 | let noControllers = NSLocalizedString("No controllers connected", comment: "No controllers connected") 140 | item.title = "(\(noControllers))" 141 | item.isEnabled = false 142 | self.controllersMenu?.submenu?.addItem(item) 143 | } 144 | } 145 | 146 | // MARK: - Helper app settings 147 | 148 | func setLoginItem(enabled: Bool) { 149 | let succeeded = SMLoginItemSetEnabled(helperAppID, enabled) 150 | if (!succeeded) { 151 | 152 | } 153 | } 154 | 155 | // MARK: - NSWindowDelegate 156 | 157 | func windowWillClose(_ notification: Notification) { 158 | _ = self.dataManager?.save() 159 | } 160 | 161 | // MARK: - Notifications 162 | 163 | @objc func controllerIconChanged(_ notification: NSNotification) { 164 | self.updateControllersMenu() 165 | } 166 | 167 | // MARK: - UNUserNotificationCenterDelegate 168 | 169 | func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { 170 | completionHandler([.alert, .badge, .sound]) 171 | } 172 | 173 | func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) { 174 | completionHandler() 175 | } 176 | 177 | // MARK: - Controller event handlers 178 | 179 | func applicationWillTerminate(_ aNotification: Notification) { 180 | self.controllers.forEach { controller in 181 | controller.controller?.setHCIState(state: .disconnect) 182 | } 183 | } 184 | 185 | func connectController(_ controller: JoyConSwift.Controller) { 186 | if let gameController = self.controllers.first(where: { 187 | $0.data.serialID == controller.serialID 188 | }) { 189 | gameController.controller = controller 190 | gameController.startTimer() 191 | NotificationCenter.default.post(name: .controllerConnected, object: gameController) 192 | 193 | AppNotifications.notifyControllerConnected(gameController) 194 | } else { 195 | self.addController(controller) 196 | } 197 | self.updateControllersMenu() 198 | } 199 | 200 | @objc func disconnectController(sender: Any) { 201 | guard let item = sender as? NSMenuItem else { return } 202 | guard let gameController = item.representedObject as? GameController else { return } 203 | 204 | gameController.disconnect() 205 | self.updateControllersMenu() 206 | } 207 | 208 | func disconnectController(_ controller: JoyConSwift.Controller) { 209 | if let gameController = self.controllers.first(where: { 210 | $0.data.serialID == controller.serialID 211 | }) { 212 | gameController.controller = nil 213 | gameController.updateControllerIcon() 214 | NotificationCenter.default.post(name: .controllerDisconnected, object: gameController) 215 | 216 | AppNotifications.notifyControllerDisconnected(gameController) 217 | } 218 | self.updateControllersMenu() 219 | } 220 | 221 | func addController(_ controller: JoyConSwift.Controller) { 222 | guard let dataManager = self.dataManager else { return } 223 | let controllerData = dataManager.getControllerData(controller: controller) 224 | let gameController = GameController(data: controllerData) 225 | gameController.controller = controller 226 | gameController.startTimer() 227 | self.controllers.append(gameController) 228 | 229 | NotificationCenter.default.post(name: .controllerAdded, object: gameController) 230 | 231 | AppNotifications.notifyControllerConnected(gameController) 232 | } 233 | 234 | func removeController(_ controller: JoyConSwift.Controller) { 235 | guard let gameController = self.controllers.first(where: { 236 | $0.data.serialID == controller.serialID 237 | }) else { return } 238 | self.removeController(gameController: gameController) 239 | } 240 | 241 | func removeController(gameController controller: GameController) { 242 | controller.controller?.setHCIState(state: .disconnect) 243 | 244 | self.dataManager?.delete(controller.data) 245 | self.controllers.removeAll(where: { $0 === controller }) 246 | NotificationCenter.default.post(name: .controllerRemoved, object: controller) 247 | } 248 | 249 | // MARK: - Core Data Saving and Undo support 250 | 251 | @IBAction func saveAction(_ sender: AnyObject?) { 252 | _ = self.dataManager?.save() 253 | } 254 | 255 | func windowWillReturnUndoManager(_ window: NSWindow) -> UndoManager? { 256 | return self.dataManager?.undoManager 257 | } 258 | 259 | func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { 260 | return false 261 | } 262 | 263 | func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply { 264 | let isSucceeded = self.dataManager?.save() ?? false 265 | 266 | if !isSucceeded { 267 | let question = NSLocalizedString("Could not save changes while quitting. Quit anyway?", comment: "Quit without saves error question message") 268 | let info = NSLocalizedString("Quitting now will lose any changes you have made since the last successful save", comment: "Quit without saves error question info"); 269 | let quitButton = NSLocalizedString("Quit anyway", comment: "Quit anyway button title") 270 | let cancelButton = NSLocalizedString("Cancel", comment: "Cancel button title") 271 | let alert = NSAlert() 272 | alert.messageText = question 273 | alert.informativeText = info 274 | alert.addButton(withTitle: quitButton) 275 | alert.addButton(withTitle: cancelButton) 276 | 277 | let answer = alert.runModal() 278 | if answer == .alertSecondButtonReturn { 279 | return .terminateCancel 280 | } 281 | } 282 | 283 | return .terminateNow 284 | } 285 | 286 | // MARK: - Context switch handling 287 | 288 | @objc func didActivateApp(notification: Notification) { 289 | guard let app = notification.userInfo?[NSWorkspace.applicationUserInfoKey] as? NSRunningApplication, 290 | let bundleID = app.bundleIdentifier else { return } 291 | 292 | resetMetaKeyState() 293 | 294 | self.controllers.forEach { controller in 295 | controller.switchApp(bundleID: bundleID) 296 | } 297 | } 298 | } 299 | -------------------------------------------------------------------------------- /JoyKeyMapper/AppNotifications.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppNotifications.swift 3 | // JoyKeyMapper 4 | // 5 | // Created by magicien on 2020/03/08. 6 | // Copyright © 2020 DarkHorse. All rights reserved. 7 | // 8 | 9 | import AppKit 10 | import UserNotifications 11 | 12 | class AppNotifications { 13 | static func createControllerIconAttachment(for controller: GameController) -> UNNotificationAttachment? { 14 | guard let cgImage = controller.icon?.cgImage(forProposedRect: nil, context: nil, hints: nil) else { return nil } 15 | guard let pngData = NSBitmapImageRep(cgImage: cgImage).representation(using: .png, properties: [:]) else { return nil } 16 | 17 | let tmpDirName = ProcessInfo.processInfo.globallyUniqueString 18 | let tmpDirURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(tmpDirName, isDirectory: true) 19 | let fileURL = tmpDirURL.appendingPathComponent("icon.png") 20 | 21 | do { 22 | try FileManager.default.createDirectory(at: tmpDirURL, withIntermediateDirectories: true, attributes: nil) 23 | try pngData.write(to: fileURL, options: .atomicWrite) 24 | return try UNNotificationAttachment(identifier: tmpDirName, url: fileURL, options: [UNNotificationAttachmentOptionsTypeHintKey:kUTTypePNG]) 25 | } catch { 26 | NSLog("Generate notification attachment error: \(error.localizedDescription)") 27 | } 28 | 29 | return nil 30 | } 31 | 32 | static func notifyBatteryFullCharge(_ controller: GameController) { 33 | guard AppSettings.notifyBatteryFull else { return } 34 | 35 | let content = UNMutableNotificationContent() 36 | content.title = NSLocalizedString("Battery fully charged", comment: "Title of a user notification") 37 | content.categoryIdentifier = "info" 38 | if let attachment = self.createControllerIconAttachment(for: controller) { 39 | content.attachments = [attachment] 40 | } 41 | let request = UNNotificationRequest(identifier: "batteryFullCharge", content: content, trigger: nil) 42 | self.notify(request: request) 43 | } 44 | 45 | static func notifyBatteryLevel(_ controller: GameController) { 46 | guard AppSettings.notifyBatteryLevel else { return } 47 | 48 | let content = UNMutableNotificationContent() 49 | let label = NSLocalizedString("Battery level", comment: "Battery level") 50 | let battery = controller.localizedBatteryString 51 | content.title = "\(label): \(battery)" 52 | content.categoryIdentifier = "info" 53 | if let attachment = self.createControllerIconAttachment(for: controller) { 54 | content.attachments = [attachment] 55 | } 56 | let request = UNNotificationRequest(identifier: "batteryLevel", content: content, trigger: nil) 57 | self.notify(request: request) 58 | } 59 | 60 | static func notifyStartCharge(_ controller: GameController) { 61 | guard AppSettings.notifyBatteryCharge else { return } 62 | 63 | let content = UNMutableNotificationContent() 64 | content.title = NSLocalizedString("Charge started", comment: "Charge started") 65 | content.categoryIdentifier = "info" 66 | if let attachment = self.createControllerIconAttachment(for: controller) { 67 | content.attachments = [attachment] 68 | } 69 | let request = UNNotificationRequest(identifier: "startCharge", content: content, trigger: nil) 70 | self.notify(request: request) 71 | } 72 | 73 | static func notifyStopCharge(_ controller: GameController) { 74 | guard AppSettings.notifyBatteryCharge else { return } 75 | 76 | let content = UNMutableNotificationContent() 77 | content.title = NSLocalizedString("Charge stopped", comment: "Charge stopped") 78 | content.categoryIdentifier = "info" 79 | if let attachment = self.createControllerIconAttachment(for: controller) { 80 | content.attachments = [attachment] 81 | } 82 | let request = UNNotificationRequest(identifier: "stopCharge", content: content, trigger: nil) 83 | self.notify(request: request) 84 | 85 | } 86 | 87 | static func notifyControllerConnected(_ controller: GameController) { 88 | guard AppSettings.notifyConnection else { return } 89 | 90 | let content = UNMutableNotificationContent() 91 | content.title = NSLocalizedString("Controller connected", comment: "Controller connected") 92 | content.categoryIdentifier = "info" 93 | if let attachment = self.createControllerIconAttachment(for: controller) { 94 | content.attachments = [attachment] 95 | } 96 | let request = UNNotificationRequest(identifier: "controllerConnected", content: content, trigger: nil) 97 | self.notify(request: request) 98 | } 99 | 100 | static func notifyControllerDisconnected(_ controller: GameController) { 101 | guard AppSettings.notifyConnection else { return } 102 | 103 | let content = UNMutableNotificationContent() 104 | content.title = NSLocalizedString("Controller disconnected", comment: "Controller disconnected") 105 | content.categoryIdentifier = "info" 106 | if let attachment = self.createControllerIconAttachment(for: controller) { 107 | content.attachments = [attachment] 108 | } 109 | let request = UNNotificationRequest(identifier: "test", content: content, trigger: nil) 110 | self.notify(request: request) 111 | } 112 | 113 | static private func notify(request: UNNotificationRequest) { 114 | let center = UNUserNotificationCenter.current() 115 | center.getNotificationSettings { settings in 116 | if settings.authorizationStatus == .authorized { 117 | center.add(request) { error in 118 | NSLog("Notification error: \(error?.localizedDescription ?? "")") 119 | } 120 | } else { 121 | center.requestAuthorization(options: [.alert]) { granted, error in 122 | if error != nil { 123 | NSLog("Notification permission error: \(error?.localizedDescription ?? "")") 124 | } else if granted { 125 | center.add(request) { error in 126 | NSLog("Notification error: \(error?.localizedDescription ?? "")") 127 | } 128 | } 129 | } 130 | } 131 | } 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /JoyKeyMapper/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "16x16", 5 | "idiom" : "mac", 6 | "filename" : "app-icon_16.png", 7 | "scale" : "1x" 8 | }, 9 | { 10 | "size" : "16x16", 11 | "idiom" : "mac", 12 | "filename" : "app-icon_32-1.png", 13 | "scale" : "2x" 14 | }, 15 | { 16 | "size" : "32x32", 17 | "idiom" : "mac", 18 | "filename" : "app-icon_32.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "32x32", 23 | "idiom" : "mac", 24 | "filename" : "app-icon_64.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "128x128", 29 | "idiom" : "mac", 30 | "filename" : "app-icon_128.png", 31 | "scale" : "1x" 32 | }, 33 | { 34 | "size" : "128x128", 35 | "idiom" : "mac", 36 | "filename" : "app-icon_256-1.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "256x256", 41 | "idiom" : "mac", 42 | "filename" : "app-icon_256.png", 43 | "scale" : "1x" 44 | }, 45 | { 46 | "size" : "256x256", 47 | "idiom" : "mac", 48 | "filename" : "app-icon_512-1.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "512x512", 53 | "idiom" : "mac", 54 | "filename" : "app-icon_512.png", 55 | "scale" : "1x" 56 | }, 57 | { 58 | "size" : "512x512", 59 | "idiom" : "mac", 60 | "filename" : "app-icon_1024.png", 61 | "scale" : "2x" 62 | } 63 | ], 64 | "info" : { 65 | "version" : 1, 66 | "author" : "xcode" 67 | } 68 | } -------------------------------------------------------------------------------- /JoyKeyMapper/Assets.xcassets/AppIcon.appiconset/app-icon_1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magicien/JoyKeyMapper/cbfa688c1d00e6ecf563a77c5b5c06959db2fed3/JoyKeyMapper/Assets.xcassets/AppIcon.appiconset/app-icon_1024.png -------------------------------------------------------------------------------- /JoyKeyMapper/Assets.xcassets/AppIcon.appiconset/app-icon_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magicien/JoyKeyMapper/cbfa688c1d00e6ecf563a77c5b5c06959db2fed3/JoyKeyMapper/Assets.xcassets/AppIcon.appiconset/app-icon_128.png -------------------------------------------------------------------------------- /JoyKeyMapper/Assets.xcassets/AppIcon.appiconset/app-icon_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magicien/JoyKeyMapper/cbfa688c1d00e6ecf563a77c5b5c06959db2fed3/JoyKeyMapper/Assets.xcassets/AppIcon.appiconset/app-icon_16.png -------------------------------------------------------------------------------- /JoyKeyMapper/Assets.xcassets/AppIcon.appiconset/app-icon_256-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magicien/JoyKeyMapper/cbfa688c1d00e6ecf563a77c5b5c06959db2fed3/JoyKeyMapper/Assets.xcassets/AppIcon.appiconset/app-icon_256-1.png -------------------------------------------------------------------------------- /JoyKeyMapper/Assets.xcassets/AppIcon.appiconset/app-icon_256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magicien/JoyKeyMapper/cbfa688c1d00e6ecf563a77c5b5c06959db2fed3/JoyKeyMapper/Assets.xcassets/AppIcon.appiconset/app-icon_256.png -------------------------------------------------------------------------------- /JoyKeyMapper/Assets.xcassets/AppIcon.appiconset/app-icon_32-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magicien/JoyKeyMapper/cbfa688c1d00e6ecf563a77c5b5c06959db2fed3/JoyKeyMapper/Assets.xcassets/AppIcon.appiconset/app-icon_32-1.png -------------------------------------------------------------------------------- /JoyKeyMapper/Assets.xcassets/AppIcon.appiconset/app-icon_32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magicien/JoyKeyMapper/cbfa688c1d00e6ecf563a77c5b5c06959db2fed3/JoyKeyMapper/Assets.xcassets/AppIcon.appiconset/app-icon_32.png -------------------------------------------------------------------------------- /JoyKeyMapper/Assets.xcassets/AppIcon.appiconset/app-icon_512-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magicien/JoyKeyMapper/cbfa688c1d00e6ecf563a77c5b5c06959db2fed3/JoyKeyMapper/Assets.xcassets/AppIcon.appiconset/app-icon_512-1.png -------------------------------------------------------------------------------- /JoyKeyMapper/Assets.xcassets/AppIcon.appiconset/app-icon_512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magicien/JoyKeyMapper/cbfa688c1d00e6ecf563a77c5b5c06959db2fed3/JoyKeyMapper/Assets.xcassets/AppIcon.appiconset/app-icon_512.png -------------------------------------------------------------------------------- /JoyKeyMapper/Assets.xcassets/AppIcon.appiconset/app-icon_64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magicien/JoyKeyMapper/cbfa688c1d00e6ecf563a77c5b5c06959db2fed3/JoyKeyMapper/Assets.xcassets/AppIcon.appiconset/app-icon_64.png -------------------------------------------------------------------------------- /JoyKeyMapper/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /JoyKeyMapper/Assets.xcassets/GenericApplicationIcon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "GenericApplicationIcon.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /JoyKeyMapper/Assets.xcassets/GenericApplicationIcon.imageset/GenericApplicationIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magicien/JoyKeyMapper/cbfa688c1d00e6ecf563a77c5b5c06959db2fed3/JoyKeyMapper/Assets.xcassets/GenericApplicationIcon.imageset/GenericApplicationIcon.png -------------------------------------------------------------------------------- /JoyKeyMapper/Assets.xcassets/battery_charge.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "battery_charge.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /JoyKeyMapper/Assets.xcassets/battery_charge.imageset/battery_charge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magicien/JoyKeyMapper/cbfa688c1d00e6ecf563a77c5b5c06959db2fed3/JoyKeyMapper/Assets.xcassets/battery_charge.imageset/battery_charge.png -------------------------------------------------------------------------------- /JoyKeyMapper/Assets.xcassets/battery_critical.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "battery_critical.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /JoyKeyMapper/Assets.xcassets/battery_critical.imageset/battery_critical.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magicien/JoyKeyMapper/cbfa688c1d00e6ecf563a77c5b5c06959db2fed3/JoyKeyMapper/Assets.xcassets/battery_critical.imageset/battery_critical.png -------------------------------------------------------------------------------- /JoyKeyMapper/Assets.xcassets/battery_empty.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "battery_empty.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /JoyKeyMapper/Assets.xcassets/battery_empty.imageset/battery_empty.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magicien/JoyKeyMapper/cbfa688c1d00e6ecf563a77c5b5c06959db2fed3/JoyKeyMapper/Assets.xcassets/battery_empty.imageset/battery_empty.png -------------------------------------------------------------------------------- /JoyKeyMapper/Assets.xcassets/battery_full.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "battery_full.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /JoyKeyMapper/Assets.xcassets/battery_full.imageset/battery_full.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magicien/JoyKeyMapper/cbfa688c1d00e6ecf563a77c5b5c06959db2fed3/JoyKeyMapper/Assets.xcassets/battery_full.imageset/battery_full.png -------------------------------------------------------------------------------- /JoyKeyMapper/Assets.xcassets/battery_low.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "battery_low.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /JoyKeyMapper/Assets.xcassets/battery_low.imageset/battery_low.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magicien/JoyKeyMapper/cbfa688c1d00e6ecf563a77c5b5c06959db2fed3/JoyKeyMapper/Assets.xcassets/battery_low.imageset/battery_low.png -------------------------------------------------------------------------------- /JoyKeyMapper/Assets.xcassets/battery_medium.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "battery_medium.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /JoyKeyMapper/Assets.xcassets/battery_medium.imageset/battery_medium.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magicien/JoyKeyMapper/cbfa688c1d00e6ecf563a77c5b5c06959db2fed3/JoyKeyMapper/Assets.xcassets/battery_medium.imageset/battery_medium.png -------------------------------------------------------------------------------- /JoyKeyMapper/Assets.xcassets/famicon_1.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "famicon_1.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /JoyKeyMapper/Assets.xcassets/famicon_1.imageset/famicon_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magicien/JoyKeyMapper/cbfa688c1d00e6ecf563a77c5b5c06959db2fed3/JoyKeyMapper/Assets.xcassets/famicon_1.imageset/famicon_1.png -------------------------------------------------------------------------------- /JoyKeyMapper/Assets.xcassets/famicon_2.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "famicon_2.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /JoyKeyMapper/Assets.xcassets/famicon_2.imageset/famicon_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magicien/JoyKeyMapper/cbfa688c1d00e6ecf563a77c5b5c06959db2fed3/JoyKeyMapper/Assets.xcassets/famicon_2.imageset/famicon_2.png -------------------------------------------------------------------------------- /JoyKeyMapper/Assets.xcassets/joycon_left_base.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "joycon_left_base.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /JoyKeyMapper/Assets.xcassets/joycon_left_base.imageset/joycon_left_base.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magicien/JoyKeyMapper/cbfa688c1d00e6ecf563a77c5b5c06959db2fed3/JoyKeyMapper/Assets.xcassets/joycon_left_base.imageset/joycon_left_base.png -------------------------------------------------------------------------------- /JoyKeyMapper/Assets.xcassets/joycon_left_body.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "joycon_left_body.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /JoyKeyMapper/Assets.xcassets/joycon_left_body.imageset/joycon_left_body.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magicien/JoyKeyMapper/cbfa688c1d00e6ecf563a77c5b5c06959db2fed3/JoyKeyMapper/Assets.xcassets/joycon_left_body.imageset/joycon_left_body.png -------------------------------------------------------------------------------- /JoyKeyMapper/Assets.xcassets/joycon_left_button.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "joycon_left_button.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /JoyKeyMapper/Assets.xcassets/joycon_left_button.imageset/joycon_left_button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magicien/JoyKeyMapper/cbfa688c1d00e6ecf563a77c5b5c06959db2fed3/JoyKeyMapper/Assets.xcassets/joycon_left_button.imageset/joycon_left_button.png -------------------------------------------------------------------------------- /JoyKeyMapper/Assets.xcassets/joycon_right_base.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "joycon_right_base.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /JoyKeyMapper/Assets.xcassets/joycon_right_base.imageset/joycon_right_base.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magicien/JoyKeyMapper/cbfa688c1d00e6ecf563a77c5b5c06959db2fed3/JoyKeyMapper/Assets.xcassets/joycon_right_base.imageset/joycon_right_base.png -------------------------------------------------------------------------------- /JoyKeyMapper/Assets.xcassets/joycon_right_body.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "joycon_right_body.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /JoyKeyMapper/Assets.xcassets/joycon_right_body.imageset/joycon_right_body.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magicien/JoyKeyMapper/cbfa688c1d00e6ecf563a77c5b5c06959db2fed3/JoyKeyMapper/Assets.xcassets/joycon_right_body.imageset/joycon_right_body.png -------------------------------------------------------------------------------- /JoyKeyMapper/Assets.xcassets/joycon_right_button.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "joycon_right_button.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /JoyKeyMapper/Assets.xcassets/joycon_right_button.imageset/joycon_right_button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magicien/JoyKeyMapper/cbfa688c1d00e6ecf563a77c5b5c06959db2fed3/JoyKeyMapper/Assets.xcassets/joycon_right_button.imageset/joycon_right_button.png -------------------------------------------------------------------------------- /JoyKeyMapper/Assets.xcassets/menu_icon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "menu_icon.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | }, 21 | "properties" : { 22 | "template-rendering-intent" : "template" 23 | } 24 | } -------------------------------------------------------------------------------- /JoyKeyMapper/Assets.xcassets/menu_icon.imageset/menu_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magicien/JoyKeyMapper/cbfa688c1d00e6ecf563a77c5b5c06959db2fed3/JoyKeyMapper/Assets.xcassets/menu_icon.imageset/menu_icon.png -------------------------------------------------------------------------------- /JoyKeyMapper/Assets.xcassets/procon_base.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "procon_base.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /JoyKeyMapper/Assets.xcassets/procon_base.imageset/procon_base.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magicien/JoyKeyMapper/cbfa688c1d00e6ecf563a77c5b5c06959db2fed3/JoyKeyMapper/Assets.xcassets/procon_base.imageset/procon_base.png -------------------------------------------------------------------------------- /JoyKeyMapper/Assets.xcassets/procon_body.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "procon_body.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /JoyKeyMapper/Assets.xcassets/procon_body.imageset/procon_body.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magicien/JoyKeyMapper/cbfa688c1d00e6ecf563a77c5b5c06959db2fed3/JoyKeyMapper/Assets.xcassets/procon_body.imageset/procon_body.png -------------------------------------------------------------------------------- /JoyKeyMapper/Assets.xcassets/procon_button.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "procon_button.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /JoyKeyMapper/Assets.xcassets/procon_button.imageset/procon_button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magicien/JoyKeyMapper/cbfa688c1d00e6ecf563a77c5b5c06959db2fed3/JoyKeyMapper/Assets.xcassets/procon_button.imageset/procon_button.png -------------------------------------------------------------------------------- /JoyKeyMapper/Assets.xcassets/procon_left_grip.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "procon_left_grip.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /JoyKeyMapper/Assets.xcassets/procon_left_grip.imageset/procon_left_grip.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magicien/JoyKeyMapper/cbfa688c1d00e6ecf563a77c5b5c06959db2fed3/JoyKeyMapper/Assets.xcassets/procon_left_grip.imageset/procon_left_grip.png -------------------------------------------------------------------------------- /JoyKeyMapper/Assets.xcassets/procon_right_grip.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "procon_right_grip.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /JoyKeyMapper/Assets.xcassets/procon_right_grip.imageset/procon_right_grip.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magicien/JoyKeyMapper/cbfa688c1d00e6ecf563a77c5b5c06959db2fed3/JoyKeyMapper/Assets.xcassets/procon_right_grip.imageset/procon_right_grip.png -------------------------------------------------------------------------------- /JoyKeyMapper/Assets.xcassets/snescon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "snescon.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /JoyKeyMapper/Assets.xcassets/snescon.imageset/snescon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magicien/JoyKeyMapper/cbfa688c1d00e6ecf563a77c5b5c06959db2fed3/JoyKeyMapper/Assets.xcassets/snescon.imageset/snescon.png -------------------------------------------------------------------------------- /JoyKeyMapper/Assets.xcassets/stop.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "stop.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /JoyKeyMapper/Assets.xcassets/stop.imageset/stop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magicien/JoyKeyMapper/cbfa688c1d00e6ecf563a77c5b5c06959db2fed3/JoyKeyMapper/Assets.xcassets/stop.imageset/stop.png -------------------------------------------------------------------------------- /JoyKeyMapper/Assets.xcassets/unknown_controller.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "unknown_controller.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /JoyKeyMapper/Assets.xcassets/unknown_controller.imageset/unknown_controller.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magicien/JoyKeyMapper/cbfa688c1d00e6ecf563a77c5b5c06959db2fed3/JoyKeyMapper/Assets.xcassets/unknown_controller.imageset/unknown_controller.png -------------------------------------------------------------------------------- /JoyKeyMapper/DataModels/DataManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DataManager.swift 3 | // JoyKeyMapper 4 | // 5 | // Created by magicien on 2019/07/14. 6 | // Copyright © 2019 DarkHorse. All rights reserved. 7 | // 8 | 9 | import CoreData 10 | import JoyConSwift 11 | 12 | enum StickType: String { 13 | case Mouse = "Mouse" 14 | case MouseWheel = "Mouse Wheel" 15 | case Key = "Key" 16 | case None = "None" 17 | } 18 | 19 | enum StickDirection: String { 20 | case Left = "Left" 21 | case Right = "Right" 22 | case Up = "Up" 23 | case Down = "Down" 24 | } 25 | 26 | class DataManager: NSObject { 27 | let container: NSPersistentContainer 28 | 29 | var undoManager: UndoManager? { 30 | return self.container.viewContext.undoManager 31 | } 32 | 33 | var controllers: [ControllerData] { 34 | let context = self.container.viewContext 35 | let request = NSFetchRequest(entityName: "ControllerData") 36 | 37 | do { 38 | let result = try context.fetch(request) as! [ControllerData] 39 | return result 40 | } catch { 41 | fatalError("Failed to fetch ControllerData: \(error)") 42 | } 43 | } 44 | 45 | init(completion: @escaping (DataManager?) -> Void) { 46 | self.container = NSPersistentContainer(name: "JoyKeyMapper") 47 | super.init() 48 | 49 | self.container.loadPersistentStores { [weak self] (storeDescription, error) in 50 | if let error = error { 51 | // Replace this implementation with code to handle the error appropriately. 52 | // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. 53 | 54 | /* 55 | Typical reasons for an error here include: 56 | * The parent directory does not exist, cannot be created, or disallows writing. 57 | * The persistent store is not accessible, due to permissions or data protection when the device is locked. 58 | * The device is out of space. 59 | * The store could not be migrated to the current model version. 60 | Check the error message to determine what the actual problem was. 61 | */ 62 | fatalError("Unresolved error \(error)") 63 | } 64 | self?.container.viewContext.automaticallyMergesChangesFromParent = true 65 | self?.container.viewContext.mergePolicy = NSMergePolicy.mergeByPropertyObjectTrump 66 | 67 | completion(self) 68 | } 69 | } 70 | 71 | func save() -> Bool { 72 | let context = self.container.viewContext 73 | 74 | if !context.commitEditing() { 75 | NSLog("\(NSStringFromClass(type(of: self))) unable to commit editing before saving") 76 | return false 77 | } 78 | 79 | if context.hasChanges { 80 | do { 81 | try context.save() 82 | } catch { 83 | // Customize this code block to include application-specific recovery steps. 84 | let nserror = error as NSError 85 | NSApplication.shared.presentError(nserror) 86 | 87 | return false 88 | } 89 | } 90 | 91 | return true 92 | } 93 | 94 | // MARK: - Import/Export data 95 | 96 | func createContext(for url: URL) -> NSManagedObjectContext? { 97 | let coordinator = NSPersistentStoreCoordinator(managedObjectModel: self.container.managedObjectModel) 98 | do { 99 | // TODO: Set options 100 | try coordinator.addPersistentStore(ofType: NSBinaryStoreType, configurationName: nil, at: url, options: nil) 101 | } catch { 102 | let nserror = error as NSError 103 | NSApplication.shared.presentError(nserror) 104 | 105 | return nil 106 | } 107 | 108 | let context = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType) 109 | context.persistentStoreCoordinator = coordinator 110 | 111 | return context 112 | } 113 | 114 | func saveData(object: NSManagedObject, to url: URL) -> Bool { 115 | guard let context = self.createContext(for: url) else { return false } 116 | 117 | context.insert(object) 118 | if !context.commitEditing() { 119 | return false 120 | } 121 | 122 | do { 123 | try context.save() 124 | } catch { 125 | // Customize this code block to include application-specific recovery steps. 126 | let nserror = error as NSError 127 | NSApplication.shared.presentError(nserror) 128 | 129 | return false 130 | } 131 | 132 | return true 133 | } 134 | 135 | func loadData(from url: URL) -> [T]? { 136 | guard let context = self.createContext(for: url) else { return nil } 137 | guard let entityName = T.entity().name else { return nil } 138 | 139 | let request = NSFetchRequest(entityName: entityName) 140 | do { 141 | return try context.fetch(request) 142 | } catch { 143 | let nserror = error as NSError 144 | NSApplication.shared.presentError(nserror) 145 | } 146 | 147 | return nil 148 | } 149 | 150 | // MARK: - ControllerData 151 | 152 | func createControllerData(type: JoyCon.ControllerType) -> ControllerData { 153 | let controller = ControllerData(context: self.container.viewContext) 154 | controller.appConfigs = [] 155 | controller.defaultConfig = self.createKeyConfig(type: type) 156 | 157 | return controller 158 | } 159 | 160 | func getControllerData(controller: JoyConSwift.Controller) -> ControllerData { 161 | let serialID = controller.serialID 162 | let context = self.container.viewContext 163 | let request = NSFetchRequest(entityName: "ControllerData") 164 | request.predicate = NSPredicate(format: "serialID == %@", serialID) 165 | 166 | do { 167 | let result = try context.fetch(request) as! [ControllerData] 168 | if result.count > 0 { 169 | return result[0] 170 | } 171 | } catch { 172 | fatalError("Failed to fetch ControllerData: \(error)") 173 | } 174 | 175 | let controller = self.createControllerData(type: controller.type) 176 | controller.serialID = serialID 177 | 178 | return controller 179 | } 180 | 181 | // MARK: - AppConfig 182 | 183 | func createAppConfig(type: JoyCon.ControllerType) -> AppConfig { 184 | let appConfig = AppConfig(context: self.container.viewContext) 185 | appConfig.app = self.createAppData() 186 | appConfig.config = self.createKeyConfig(type: type) 187 | 188 | return appConfig 189 | } 190 | 191 | // MARK: - AppData 192 | 193 | func createAppData() -> AppData { 194 | let appData = AppData(context: self.container.viewContext) 195 | 196 | return appData 197 | } 198 | 199 | // MARK: - KeyConfig 200 | 201 | func createKeyConfig(type: JoyCon.ControllerType) -> KeyConfig { 202 | let keyConfig = KeyConfig(context: self.container.viewContext) 203 | 204 | if type == .JoyConL || type == .ProController { 205 | keyConfig.leftStick = self.createStickConfig() 206 | } 207 | if type == .JoyConR || type == .ProController { 208 | keyConfig.rightStick = self.createStickConfig() 209 | } 210 | 211 | keyConfig.keyMaps = [] 212 | 213 | return keyConfig 214 | } 215 | 216 | // MARK: - KeyMap 217 | 218 | func createKeyMap() -> KeyMap { 219 | let keyMap = KeyMap(context: self.container.viewContext) 220 | 221 | return keyMap 222 | } 223 | 224 | // MARK: - StickConfig 225 | 226 | func createStickConfig() -> StickConfig { 227 | let stickConfig = StickConfig(context: self.container.viewContext) 228 | 229 | stickConfig.speed = 10.0 230 | stickConfig.type = StickType.None.rawValue 231 | 232 | let left = self.createKeyMap() 233 | left.button = StickDirection.Left.rawValue 234 | stickConfig.addToKeyMaps(left) 235 | 236 | let right = self.createKeyMap() 237 | right.button = StickDirection.Right.rawValue 238 | stickConfig.addToKeyMaps(right) 239 | 240 | let up = self.createKeyMap() 241 | up.button = StickDirection.Up.rawValue 242 | stickConfig.addToKeyMaps(up) 243 | 244 | let down = self.createKeyMap() 245 | down.button = StickDirection.Down.rawValue 246 | stickConfig.addToKeyMaps(down) 247 | 248 | return stickConfig 249 | } 250 | 251 | // MARK: - Common 252 | 253 | func delete(_ object: NSManagedObject) { 254 | self.container.viewContext.delete(object) 255 | } 256 | } 257 | -------------------------------------------------------------------------------- /JoyKeyMapper/DataModels/GameController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GameController.swift 3 | // JoyKeyMapper 4 | // 5 | // Created by magicien on 2019/07/14. 6 | // Copyright © 2019 DarkHorse. All rights reserved. 7 | // 8 | 9 | import JoyConSwift 10 | import InputMethodKit 11 | 12 | extension JoyCon.BatteryStatus { 13 | static let stringMap: [JoyCon.BatteryStatus: String] = [ 14 | .empty: "Empty", 15 | .critical: "Critical", 16 | .low: "Low", 17 | .medium: "Medium", 18 | .full: "Full", 19 | .unknown: "Unknown" 20 | ] 21 | 22 | var string: String { 23 | return JoyCon.BatteryStatus.stringMap[self] ?? "Unknown" 24 | } 25 | 26 | var localizedString: String { 27 | return NSLocalizedString(self.string, comment: "BatteryStatus localized string") 28 | } 29 | } 30 | 31 | class GameController { 32 | let data: ControllerData 33 | 34 | var type: JoyCon.ControllerType 35 | var bodyColor: NSColor 36 | var buttonColor: NSColor 37 | var leftGripColor: NSColor? 38 | var rightGripColor: NSColor? 39 | 40 | var controller: JoyConSwift.Controller? { 41 | didSet { 42 | self.setControllerHandler() 43 | } 44 | } 45 | var currentConfigData: KeyConfig { 46 | didSet { self.updateKeyMap() } 47 | } 48 | var currentConfig: [JoyCon.Button:KeyMap] = [:] 49 | var currentLStickMode: StickType = .None 50 | var currentLStickConfig: [JoyCon.StickDirection:KeyMap] = [:] 51 | var currentRStickMode: StickType = .None 52 | var currentRStickConfig: [JoyCon.StickDirection:KeyMap] = [:] 53 | 54 | var isEnabled: Bool = true { 55 | didSet { 56 | self.updateControllerIcon() 57 | } 58 | } 59 | var isLeftDragging: Bool = false 60 | var isRightDragging: Bool = false 61 | var isCenterDragging: Bool = false 62 | 63 | var lastAccess: Date? = nil 64 | var timer: Timer? = nil 65 | var icon: NSImage? { 66 | if self._icon == nil { 67 | self.updateControllerIcon() 68 | } 69 | 70 | return self._icon 71 | } 72 | private var _icon: NSImage? 73 | 74 | var localizedBatteryString: String { 75 | return (self.controller?.battery ?? .unknown).localizedString 76 | } 77 | 78 | init(data: ControllerData) { 79 | self.data = data 80 | 81 | guard let defaultConfig = self.data.defaultConfig else { 82 | fatalError("Failed to get defaultConfig") 83 | } 84 | self.currentConfigData = defaultConfig 85 | 86 | let type = JoyCon.ControllerType(rawValue: data.type ?? "") 87 | self.type = type ?? JoyCon.ControllerType(rawValue: "unknown")! 88 | 89 | let defaultColor = NSColor(red: 55.0 / 255, green: 55.0 / 255, blue: 55.0 / 255, alpha: 55.0 / 255) 90 | 91 | self.bodyColor = defaultColor 92 | if let bodyColorData = data.bodyColor { 93 | if let bodyColor = try? NSKeyedUnarchiver.unarchivedObject(ofClass: NSColor.self, from: bodyColorData) { 94 | self.bodyColor = bodyColor 95 | } 96 | } 97 | 98 | self.buttonColor = defaultColor 99 | if let buttonColorData = data.buttonColor { 100 | if let buttonColor = try? NSKeyedUnarchiver.unarchivedObject(ofClass: NSColor.self, from: buttonColorData) { 101 | self.buttonColor = buttonColor 102 | } 103 | } 104 | 105 | self.leftGripColor = nil 106 | if let leftGripColorData = data.leftGripColor { 107 | if let leftGripColor = try? NSKeyedUnarchiver.unarchivedObject(ofClass: NSColor.self, from: leftGripColorData) { 108 | self.leftGripColor = leftGripColor 109 | } 110 | } 111 | 112 | self.rightGripColor = nil 113 | if let rightGripColorData = data.rightGripColor { 114 | if let rightGripColor = try? NSKeyedUnarchiver.unarchivedObject(ofClass: NSColor.self, from: rightGripColorData) { 115 | self.rightGripColor = rightGripColor 116 | } 117 | } 118 | } 119 | 120 | // MARK: - Controller event handlers 121 | 122 | func setControllerHandler() { 123 | guard let controller = self.controller else { return } 124 | 125 | controller.setPlayerLights(l1: .on, l2: .off, l3: .off, l4: .off) 126 | controller.enableIMU(enable: true) 127 | controller.setInputMode(mode: .standardFull) 128 | controller.buttonPressHandler = { [weak self] button in 129 | self?.buttonPressHandler(button: button) 130 | } 131 | controller.buttonReleaseHandler = { [weak self] button in 132 | if !(self?.isEnabled ?? false) { return } 133 | self?.buttonReleaseHandler(button: button) 134 | } 135 | controller.leftStickHandler = { [weak self] (newDir, oldDir) in 136 | if !(self?.isEnabled ?? false) { return } 137 | self?.leftStickHandler(newDirection: newDir, oldDirection: oldDir) 138 | } 139 | controller.rightStickHandler = { [weak self] (newDir, oldDir) in 140 | if !(self?.isEnabled ?? false) { return } 141 | self?.rightStickHandler(newDirection: newDir, oldDirection: oldDir) 142 | } 143 | controller.leftStickPosHandler = { [weak self] pos in 144 | if !(self?.isEnabled ?? false) { return } 145 | self?.leftStickPosHandler(pos: pos) 146 | } 147 | controller.rightStickPosHandler = { [weak self] pos in 148 | if !(self?.isEnabled ?? false) { return } 149 | self?.rightStickPosHandler(pos: pos) 150 | } 151 | 152 | controller.batteryChangeHandler = { [weak self] newState, oldState in 153 | self?.batteryChangeHandler(newState: newState, oldState: oldState) 154 | } 155 | controller.isChargingChangeHandler = { [weak self] isCharging in 156 | self?.isChargingChangeHandler(isCharging: isCharging) 157 | } 158 | 159 | // Update Controller data 160 | 161 | self.data.type = controller.type.rawValue 162 | self.type = controller.type 163 | 164 | let bodyColor = NSColor(cgColor: controller.bodyColor)! 165 | self.data.bodyColor = try! NSKeyedArchiver.archivedData(withRootObject: bodyColor, requiringSecureCoding: false) 166 | self.bodyColor = bodyColor 167 | 168 | let buttonColor = NSColor(cgColor: controller.buttonColor)! 169 | self.data.buttonColor = try! NSKeyedArchiver.archivedData(withRootObject: buttonColor, requiringSecureCoding: false) 170 | self.buttonColor = buttonColor 171 | 172 | self.data.leftGripColor = nil 173 | if let leftGripColor = controller.leftGripColor { 174 | if let nsLeftGripColor = NSColor(cgColor: leftGripColor) { 175 | self.data.leftGripColor = try? NSKeyedArchiver.archivedData(withRootObject: nsLeftGripColor, requiringSecureCoding: false) 176 | self.leftGripColor = nsLeftGripColor 177 | } 178 | } 179 | 180 | self.data.rightGripColor = nil 181 | if let rightGripColor = controller.rightGripColor { 182 | if let nsRightGripColor = NSColor(cgColor: rightGripColor) { 183 | self.data.rightGripColor = try? NSKeyedArchiver.archivedData(withRootObject: nsRightGripColor, requiringSecureCoding: false) 184 | self.rightGripColor = nsRightGripColor 185 | } 186 | } 187 | 188 | self.updateControllerIcon() 189 | } 190 | 191 | func buttonPressHandler(button: JoyCon.Button) { 192 | guard let config = self.currentConfig[button] else { return } 193 | self.buttonPressHandler(config: config) 194 | } 195 | 196 | func buttonPressHandler(config: KeyMap) { 197 | DispatchQueue.main.async { 198 | let source = CGEventSource(stateID: .hidSystemState) 199 | 200 | if config.keyCode >= 0 { 201 | metaKeyEvent(config: config, keyDown: true) 202 | 203 | if let systemKey = systemDefinedKey[Int(config.keyCode)] { 204 | let mousePos = NSEvent.mouseLocation 205 | let flags = NSEvent.ModifierFlags(rawValue: 0x0a00) 206 | let data1 = Int((systemKey << 16) | 0x0a00) 207 | 208 | let ev = NSEvent.otherEvent( 209 | with: .systemDefined, 210 | location: mousePos, 211 | modifierFlags: flags, 212 | timestamp: ProcessInfo().systemUptime, 213 | windowNumber: 0, 214 | context: nil, 215 | subtype: Int16(NX_SUBTYPE_AUX_CONTROL_BUTTONS), 216 | data1: data1, 217 | data2: -1) 218 | ev?.cgEvent?.post(tap: .cghidEventTap) 219 | } else { 220 | let event = CGEvent(keyboardEventSource: source, virtualKey: CGKeyCode(config.keyCode), keyDown: true) 221 | event?.flags = CGEventFlags(rawValue: CGEventFlags.RawValue(config.modifiers)) 222 | event?.post(tap: .cghidEventTap) 223 | } 224 | } 225 | 226 | if config.mouseButton >= 0 { 227 | let mousePos = NSEvent.mouseLocation 228 | let cursorPos = CGPoint(x: mousePos.x, y: NSScreen.main!.frame.maxY - mousePos.y) 229 | 230 | metaKeyEvent(config: config, keyDown: true) 231 | 232 | var event: CGEvent? 233 | if config.mouseButton == 0 { 234 | event = CGEvent(mouseEventSource: source, mouseType: .leftMouseDown, mouseCursorPosition: cursorPos, mouseButton: .left) 235 | self.isLeftDragging = true 236 | } else if config.mouseButton == 1 { 237 | event = CGEvent(mouseEventSource: source, mouseType: .rightMouseDown, mouseCursorPosition: cursorPos, mouseButton: .right) 238 | self.isRightDragging = true 239 | } else if config.mouseButton == 2 { 240 | event = CGEvent(mouseEventSource: source, mouseType: .otherMouseDown, mouseCursorPosition: cursorPos, mouseButton: .center) 241 | self.isCenterDragging = true 242 | } 243 | event?.flags = CGEventFlags(rawValue: CGEventFlags.RawValue(config.modifiers)) 244 | event?.post(tap: .cghidEventTap) 245 | } 246 | } 247 | } 248 | 249 | func buttonReleaseHandler(button: JoyCon.Button) { 250 | guard let config = self.currentConfig[button] else { return } 251 | self.buttonReleaseHandler(config: config) 252 | } 253 | 254 | func buttonReleaseHandler(config: KeyMap) { 255 | DispatchQueue.main.async { 256 | let source = CGEventSource(stateID: .hidSystemState) 257 | 258 | if config.keyCode >= 0 { 259 | if let systemKey = systemDefinedKey[Int(config.keyCode)] { 260 | let mousePos = NSEvent.mouseLocation 261 | let flags = NSEvent.ModifierFlags(rawValue: 0x0b00) 262 | let data1 = Int((systemKey << 16) | 0x0b00) 263 | 264 | let ev = NSEvent.otherEvent( 265 | with: .systemDefined, 266 | location: mousePos, 267 | modifierFlags: flags, 268 | timestamp: ProcessInfo().systemUptime, 269 | windowNumber: 0, 270 | context: nil, 271 | subtype: Int16(NX_SUBTYPE_AUX_CONTROL_BUTTONS), 272 | data1: data1, 273 | data2: -1) 274 | ev?.cgEvent?.post(tap: .cghidEventTap) 275 | } else { 276 | let event = CGEvent(keyboardEventSource: source, virtualKey: CGKeyCode(config.keyCode), keyDown: false) 277 | event?.flags = CGEventFlags(rawValue: CGEventFlags.RawValue(config.modifiers)) 278 | event?.post(tap: .cghidEventTap) 279 | } 280 | 281 | metaKeyEvent(config: config, keyDown: false) 282 | } 283 | 284 | if config.mouseButton >= 0 { 285 | let mousePos = NSEvent.mouseLocation 286 | let cursorPos = CGPoint(x: mousePos.x, y: NSScreen.main!.frame.maxY - mousePos.y) 287 | 288 | var event: CGEvent? 289 | if config.mouseButton == 0 { 290 | event = CGEvent(mouseEventSource: source, mouseType: .leftMouseUp, mouseCursorPosition: cursorPos, mouseButton: .left) 291 | self.isLeftDragging = false 292 | } else if config.mouseButton == 1 { 293 | event = CGEvent(mouseEventSource: source, mouseType: .rightMouseUp, mouseCursorPosition: cursorPos, mouseButton: .right) 294 | self.isRightDragging = false 295 | } else if config.mouseButton == 2 { 296 | event = CGEvent(mouseEventSource: source, mouseType: .otherMouseUp, mouseCursorPosition: cursorPos, mouseButton: .center) 297 | self.isCenterDragging = false 298 | } 299 | event?.post(tap: .cghidEventTap) 300 | } 301 | } 302 | } 303 | 304 | func stickMouseHandler(pos: CGPoint, speed: CGFloat) { 305 | if pos.x == 0 && pos.y == 0 { 306 | return 307 | } 308 | let mousePos = NSEvent.mouseLocation 309 | let newX = mousePos.x + pos.x * speed 310 | let newY = NSScreen.main!.frame.maxY - mousePos.y - pos.y * speed 311 | 312 | let newPos = CGPoint(x: newX, y: newY) 313 | 314 | let source = CGEventSource(stateID: .hidSystemState) 315 | if self.isLeftDragging { 316 | let event = CGEvent(mouseEventSource: source, mouseType: .leftMouseDragged, mouseCursorPosition: newPos, mouseButton: .left) 317 | event?.post(tap: .cghidEventTap) 318 | } else if self.isRightDragging { 319 | let event = CGEvent(mouseEventSource: source, mouseType: .rightMouseDragged, mouseCursorPosition: newPos, mouseButton: .right) 320 | event?.post(tap: .cghidEventTap) 321 | } else if self.isCenterDragging { 322 | let event = CGEvent(mouseEventSource: source, mouseType: .otherMouseDragged, mouseCursorPosition: newPos, mouseButton: .center) 323 | event?.post(tap: .cghidEventTap) 324 | } else { 325 | CGDisplayMoveCursorToPoint(CGMainDisplayID(), newPos) 326 | } 327 | } 328 | 329 | func stickMouseWheelHandler(pos: CGPoint, speed: CGFloat) { 330 | if pos.x == 0 && pos.y == 0 { 331 | return 332 | } 333 | let wheelX = Int32(pos.x * speed) 334 | let wheelY = Int32(pos.y * speed) 335 | 336 | let source = CGEventSource(stateID: .hidSystemState) 337 | let event = CGEvent(scrollWheelEvent2Source: source, units: .pixel, wheelCount: 2, wheel1: wheelY, wheel2: wheelX, wheel3: 0) 338 | event?.post(tap: .cghidEventTap) 339 | } 340 | 341 | func leftStickHandler(newDirection: JoyCon.StickDirection, oldDirection: JoyCon.StickDirection) { 342 | if self.currentLStickMode == .Key { 343 | if let config = self.currentLStickConfig[oldDirection] { 344 | self.buttonReleaseHandler(config: config) 345 | } 346 | if let config = self.currentLStickConfig[newDirection] { 347 | self.buttonPressHandler(config: config) 348 | } 349 | } 350 | } 351 | 352 | func rightStickHandler(newDirection: JoyCon.StickDirection, oldDirection: JoyCon.StickDirection) { 353 | if self.currentRStickMode == .Key { 354 | if let config = self.currentRStickConfig[oldDirection] { 355 | self.buttonReleaseHandler(config: config) 356 | } 357 | if let config = self.currentRStickConfig[newDirection] { 358 | self.buttonPressHandler(config: config) 359 | } 360 | } 361 | } 362 | 363 | func leftStickPosHandler(pos: CGPoint) { 364 | let speed = CGFloat(self.currentConfigData.leftStick?.speed ?? 0) 365 | if self.currentLStickMode == .Mouse { 366 | self.stickMouseHandler(pos: pos, speed: speed) 367 | } else if self.currentLStickMode == .MouseWheel { 368 | self.stickMouseWheelHandler(pos: pos, speed: speed) 369 | } 370 | } 371 | 372 | func rightStickPosHandler(pos: CGPoint) { 373 | let speed = CGFloat(self.currentConfigData.rightStick?.speed ?? 0) 374 | if self.currentRStickMode == .Mouse { 375 | self.stickMouseHandler(pos: pos, speed: speed) 376 | } else if self.currentRStickMode == .MouseWheel { 377 | self.stickMouseWheelHandler(pos: pos, speed: speed) 378 | } 379 | } 380 | 381 | func batteryChangeHandler(newState: JoyCon.BatteryStatus, oldState: JoyCon.BatteryStatus) { 382 | self.updateControllerIcon() 383 | 384 | if newState == .full && oldState != .unknown { 385 | AppNotifications.notifyBatteryFullCharge(self) 386 | } 387 | if newState == .empty { 388 | AppNotifications.notifyBatteryLevel(self) 389 | } 390 | if newState == .critical && oldState != .empty { 391 | AppNotifications.notifyBatteryLevel(self) 392 | } 393 | if newState == .low && oldState != .critical && oldState != .empty { 394 | AppNotifications.notifyBatteryLevel(self) 395 | } 396 | 397 | DispatchQueue.main.async { 398 | guard let delegate = NSApplication.shared.delegate as? AppDelegate else { return } 399 | delegate.updateControllersMenu() 400 | } 401 | } 402 | 403 | func isChargingChangeHandler(isCharging: Bool) { 404 | self.updateControllerIcon() 405 | 406 | if isCharging { 407 | AppNotifications.notifyStartCharge(self) 408 | } else { 409 | AppNotifications.notifyStopCharge(self) 410 | } 411 | 412 | DispatchQueue.main.async { 413 | guard let delegate = NSApplication.shared.delegate as? AppDelegate else { return } 414 | delegate.updateControllersMenu() 415 | } 416 | } 417 | 418 | // MARK: - Controller Icon 419 | 420 | func updateControllerIcon() { 421 | self._icon = GameControllerIcon(for: self) 422 | NotificationCenter.default.post(name: .controllerIconChanged, object: self) 423 | 424 | DispatchQueue.main.async { 425 | guard let delegate = NSApplication.shared.delegate as? AppDelegate else { return } 426 | delegate.updateControllersMenu() 427 | } 428 | } 429 | 430 | // MARK: - 431 | 432 | func switchApp(bundleID: String) { 433 | let appConfig = self.data.appConfigs?.first(where: { 434 | guard let appConfig = $0 as? AppConfig else { return false } 435 | return appConfig.app?.bundleID == bundleID 436 | }) as? AppConfig 437 | 438 | if let keyConfig = appConfig?.config { 439 | self.currentConfigData = keyConfig 440 | return 441 | } 442 | 443 | guard let defaultConfig = self.data.defaultConfig else { 444 | fatalError("Failed to get defaultConfig") 445 | } 446 | self.currentConfigData = defaultConfig 447 | } 448 | 449 | func updateKeyMap() { 450 | var newKeyMap: [JoyCon.Button:KeyMap] = [:] 451 | self.currentConfigData.keyMaps?.enumerateObjects { (map, _) in 452 | guard let keyMap = map as? KeyMap else { return } 453 | guard let buttonStr = keyMap.button else { return } 454 | let buttonName = buttonNames.first { (_, name) in 455 | return name == buttonStr 456 | } 457 | guard let button = buttonName?.key else { return } 458 | 459 | newKeyMap[button] = keyMap 460 | } 461 | self.currentConfig = newKeyMap 462 | 463 | self.currentLStickMode = .None 464 | if let stickTypeStr = self.currentConfigData.leftStick?.type, 465 | let stickType = StickType(rawValue: stickTypeStr) { 466 | self.currentLStickMode = stickType 467 | } 468 | 469 | var newLeftStickMap: [JoyCon.StickDirection:KeyMap] = [:] 470 | self.currentConfigData.leftStick?.keyMaps?.enumerateObjects { (map, _) in 471 | guard let keyMap = map as? KeyMap else { return } 472 | guard let buttonStr = keyMap.button else { return } 473 | let directionName = directionNames.first { (_, name) in 474 | return name == buttonStr 475 | } 476 | guard let direction = directionName?.key else { return } 477 | 478 | newLeftStickMap[direction] = keyMap 479 | } 480 | self.currentLStickConfig = newLeftStickMap 481 | 482 | self.currentRStickMode = .None 483 | if let stickTypeStr = self.currentConfigData.rightStick?.type, 484 | let stickType = StickType(rawValue: stickTypeStr) { 485 | self.currentRStickMode = stickType 486 | } 487 | 488 | var newRightStickMap: [JoyCon.StickDirection:KeyMap] = [:] 489 | self.currentConfigData.rightStick?.keyMaps?.enumerateObjects { (map, _) in 490 | guard let keyMap = map as? KeyMap else { return } 491 | guard let buttonStr = keyMap.button else { return } 492 | let directionName = directionNames.first { (_, name) in 493 | return name == buttonStr 494 | } 495 | guard let direction = directionName?.key else { return } 496 | 497 | newRightStickMap[direction] = keyMap 498 | } 499 | self.currentRStickConfig = newRightStickMap 500 | } 501 | 502 | func addApp(url: URL) { 503 | guard let delegate = NSApplication.shared.delegate as? AppDelegate else { return } 504 | guard let manager = delegate.dataManager else { return } 505 | guard let bundle = Bundle(url: url) else { return } 506 | guard let info = bundle.infoDictionary else { return } 507 | 508 | let bundleID = info["CFBundleIdentifier"] as? String ?? "" 509 | let appIndex = self.data.appConfigs?.index(ofObjectPassingTest: { (obj, index, stop) in 510 | guard let appConfig = obj as? AppConfig else { return false } 511 | return appConfig.app?.bundleID == bundleID 512 | }) 513 | if appIndex != nil && appIndex != NSNotFound { 514 | // The selected app has been already added. 515 | return 516 | } 517 | 518 | let appConfig = manager.createAppConfig(type: self.type) 519 | // appConfig.config = manager.createKeyConfig() 520 | 521 | let displayName = FileManager.default.displayName(atPath: url.absoluteString) 522 | let iconFile = info["CFBundleIconFile"] as? String ?? "" 523 | if let iconURL = bundle.url(forResource: iconFile, withExtension: nil) { 524 | do { 525 | let iconData = try Data(contentsOf: iconURL) 526 | appConfig.app?.icon = iconData 527 | } catch {} 528 | } else if let iconURL = bundle.url(forResource: "\(iconFile).icns", withExtension: nil) { 529 | do { 530 | let iconData = try Data(contentsOf: iconURL) 531 | appConfig.app?.icon = iconData 532 | } catch {} 533 | } 534 | 535 | appConfig.app?.bundleID = bundleID 536 | appConfig.app?.displayName = displayName 537 | 538 | self.data.addToAppConfigs(appConfig) 539 | } 540 | 541 | func removeApp(_ app: AppConfig) { 542 | self.data.removeFromAppConfigs(app) 543 | } 544 | 545 | @objc func toggleEnableKeyMappings() { 546 | self.isEnabled = !self.isEnabled 547 | } 548 | 549 | @objc func disconnect() { 550 | self.stopTimer() 551 | self.controller?.setHCIState(state: .disconnect) 552 | } 553 | 554 | // MARK: - Timer 555 | 556 | func updateAccessTime() { 557 | self.lastAccess = Date(timeIntervalSinceNow: 0) 558 | } 559 | 560 | func startTimer() { 561 | self.stopTimer() 562 | 563 | let checkInterval: TimeInterval = 1 * 60 // 1 min 564 | self.timer = Timer.scheduledTimer(withTimeInterval: checkInterval, repeats: true) { [weak self] _ in 565 | if AppSettings.disconnectTime <= 0 { return } 566 | guard let lastAccess = self?.lastAccess else { return } 567 | let disconnectTime = TimeInterval(AppSettings.disconnectTime * 60) 568 | 569 | let now = Date(timeIntervalSinceNow: 0) 570 | if now.timeIntervalSince(lastAccess) > disconnectTime { 571 | self?.disconnect() 572 | } 573 | } 574 | self.updateAccessTime() 575 | } 576 | 577 | func stopTimer() { 578 | self.timer?.invalidate() 579 | self.timer = nil 580 | } 581 | } 582 | -------------------------------------------------------------------------------- /JoyKeyMapper/DataModels/GameControllerIcon.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GameControllerIcon.swift 3 | // JoyKeyMapper 4 | // 5 | // Created by magicien on 2020/03/07. 6 | // Copyright © 2020 DarkHorse. All rights reserved. 7 | // 8 | 9 | import JoyConSwift 10 | 11 | let proconBase = NSImage(named: "procon_base")! 12 | let proconLeftGrip = NSImage(named: "procon_left_grip")! 13 | let proconRightGrip = NSImage(named: "procon_right_grip")! 14 | let proconBody = NSImage(named: "procon_body")! 15 | let proconButton = NSImage(named: "procon_button")! 16 | 17 | let joyconLBase = NSImage(named: "joycon_left_base")! 18 | let joyconLBody = NSImage(named: "joycon_left_body")! 19 | let joyconLButton = NSImage(named: "joycon_left_button")! 20 | 21 | let joyconRBase = NSImage(named: "joycon_right_base")! 22 | let joyconRBody = NSImage(named: "joycon_right_body")! 23 | let joyconRButton = NSImage(named: "joycon_right_button")! 24 | 25 | let famicon_1 = NSImage(named: "famicon_1")! 26 | let famicon_2 = NSImage(named: "famicon_2")! 27 | let snescon = NSImage(named: "snescon")! 28 | 29 | let unknownController = NSImage(named: "unknown_controller")! 30 | 31 | let batteryFull = NSImage(named: "battery_full")! 32 | let batteryMedium = NSImage(named: "battery_medium")! 33 | let batteryLow = NSImage(named: "battery_low")! 34 | let batteryCritical = NSImage(named: "battery_critical")! 35 | let batteryEmpty = NSImage(named: "battery_empty")! 36 | let batteryCharge = NSImage(named: "battery_charge")! 37 | 38 | let stop = NSImage(named: "stop")! 39 | 40 | func GameControllerIcon(for controller: GameController) -> NSImage { 41 | switch(controller.type) { 42 | case .ProController: 43 | return createProConIcon(for: controller) 44 | case .JoyConL: 45 | return createJoyConLIcon(for: controller) 46 | case .JoyConR: 47 | return createJoyConRIcon(for: controller) 48 | case .FamicomController1: 49 | return famicon_1 50 | case .FamicomController2: 51 | return famicon_2 52 | case .SNESController: 53 | return snescon 54 | default: 55 | return unknownController 56 | } 57 | } 58 | 59 | private func drawBatteryIcon(for controller: GameController) { 60 | guard let controllerData = controller.controller else { return } 61 | let iconRect = NSRect(origin: CGPoint.zero, size: batteryFull.size) 62 | 63 | switch(controllerData.battery) { 64 | case .full: 65 | batteryFull.draw(in: iconRect) 66 | case .medium: 67 | batteryMedium.draw(in: iconRect) 68 | case .low: 69 | batteryLow.draw(in: iconRect) 70 | case .critical: 71 | batteryCritical.draw(in: iconRect) 72 | case .empty: 73 | batteryEmpty.draw(in: iconRect) 74 | default: 75 | break 76 | } 77 | 78 | if controllerData.isCharging { 79 | batteryCharge.draw(in: iconRect) 80 | } 81 | } 82 | 83 | private func drawStopIcon() { 84 | let iconRect = NSRect(origin: CGPoint.zero, size: stop.size) 85 | stop.draw(in: iconRect) 86 | } 87 | 88 | private func createProConIcon(for controller: GameController) -> NSImage { 89 | guard 90 | let leftGripColor = controller.leftGripColor, 91 | let rightGripColor = controller.rightGripColor 92 | else { return unknownController } 93 | 94 | guard let icon = proconBase.copy() as? NSImage else { return unknownController } 95 | let iconRect = NSRect(origin: NSZeroPoint, size: icon.size) 96 | 97 | proconLeftGrip.lockFocus() 98 | leftGripColor.set() 99 | iconRect.fill(using: .sourceAtop) 100 | proconLeftGrip.unlockFocus() 101 | 102 | proconRightGrip.lockFocus() 103 | rightGripColor.set() 104 | iconRect.fill(using: .sourceAtop) 105 | proconRightGrip.unlockFocus() 106 | 107 | proconBody.lockFocus() 108 | controller.bodyColor.set() 109 | iconRect.fill(using: .sourceAtop) 110 | proconBody.unlockFocus() 111 | 112 | proconButton.lockFocus() 113 | controller.buttonColor.set() 114 | iconRect.fill(using: .sourceAtop) 115 | proconButton.unlockFocus() 116 | 117 | icon.lockFocus() 118 | proconLeftGrip.draw(in: iconRect) 119 | proconRightGrip.draw(in: iconRect) 120 | proconBody.draw(in: iconRect) 121 | proconButton.draw(in: iconRect) 122 | drawBatteryIcon(for: controller) 123 | if !controller.isEnabled { 124 | drawStopIcon() 125 | } 126 | icon.unlockFocus() 127 | 128 | return icon 129 | } 130 | 131 | private func createJoyConLIcon(for controller: GameController) -> NSImage { 132 | guard let icon = joyconLBase.copy() as? NSImage else { 133 | return unknownController 134 | } 135 | let iconRect = NSRect(origin: NSZeroPoint, size: icon.size) 136 | 137 | joyconLBody.lockFocus() 138 | controller.bodyColor.set() 139 | iconRect.fill(using: .sourceAtop) 140 | joyconLBody.unlockFocus() 141 | 142 | joyconLButton.lockFocus() 143 | controller.buttonColor.set() 144 | iconRect.fill(using: .sourceAtop) 145 | joyconLButton.unlockFocus() 146 | 147 | icon.lockFocus() 148 | joyconLBody.draw(in: iconRect) 149 | joyconLButton.draw(in: iconRect) 150 | drawBatteryIcon(for: controller) 151 | if !controller.isEnabled { 152 | drawStopIcon() 153 | } 154 | icon.unlockFocus() 155 | 156 | return icon 157 | } 158 | 159 | private func createJoyConRIcon(for controller: GameController) -> NSImage { 160 | guard let icon = joyconRBase.copy() as? NSImage else { 161 | return unknownController 162 | } 163 | let iconRect = NSRect(origin: NSZeroPoint, size: icon.size) 164 | 165 | joyconRBody.lockFocus() 166 | controller.bodyColor.set() 167 | iconRect.fill(using: .sourceAtop) 168 | joyconRBody.unlockFocus() 169 | 170 | joyconRButton.lockFocus() 171 | controller.buttonColor.set() 172 | iconRect.fill(using: .sourceAtop) 173 | joyconRButton.unlockFocus() 174 | 175 | icon.lockFocus() 176 | joyconRBody.draw(in: iconRect) 177 | joyconRButton.draw(in: iconRect) 178 | drawBatteryIcon(for: controller) 179 | if !controller.isEnabled { 180 | drawStopIcon() 181 | } 182 | icon.unlockFocus() 183 | 184 | return icon 185 | } 186 | -------------------------------------------------------------------------------- /JoyKeyMapper/DataModels/JoyKeyMapper.xcdatamodeld/.xccurrentversion: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | _XCCurrentVersionName 6 | JoyKeyMapper.xcdatamodel 7 | 8 | 9 | -------------------------------------------------------------------------------- /JoyKeyMapper/DataModels/JoyKeyMapper.xcdatamodeld/JoyKeyMapper.xcdatamodel/contents: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /JoyKeyMapper/DataModels/MetaKeyState.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MetaKeyState.swift 3 | // JoyKeyMapper 4 | // 5 | // Created by magicien on 2020/06/16. 6 | // Copyright © 2020 DarkHorse. All rights reserved. 7 | // 8 | 9 | import InputMethodKit 10 | 11 | private let shiftKey = Int32(kVK_Shift) 12 | private let optionKey = Int32(kVK_Option) 13 | private let controlKey = Int32(kVK_Control) 14 | private let commandKey = Int32(kVK_Command) 15 | private let metaKeys = [kVK_Shift, kVK_Option, kVK_Control, kVK_Command] 16 | private var pushedKeyConfigs = Set() 17 | 18 | func resetMetaKeyState() { 19 | let source = CGEventSource(stateID: .hidSystemState) 20 | pushedKeyConfigs.removeAll() 21 | 22 | DispatchQueue.main.async { 23 | // Release all meta keys 24 | metaKeys.forEach { 25 | let ev = CGEvent(keyboardEventSource: source, virtualKey: CGKeyCode($0), keyDown: false) 26 | ev?.post(tap: .cghidEventTap) 27 | } 28 | } 29 | } 30 | 31 | func getMetaKeyState() -> (shift: Bool, option: Bool, control: Bool, command: Bool) { 32 | var shift: Bool = false 33 | var option: Bool = false 34 | var control: Bool = false 35 | var command: Bool = false 36 | 37 | pushedKeyConfigs.forEach { 38 | let modifiers = NSEvent.ModifierFlags(rawValue: UInt($0.modifiers)) 39 | shift = shift || modifiers.contains(.shift) 40 | option = option || modifiers.contains(.option) 41 | control = control || modifiers.contains(.control) 42 | command = command || modifiers.contains(.command) 43 | } 44 | 45 | return (shift, option, control, command) 46 | } 47 | 48 | /** 49 | * This command must be called in the main thread 50 | */ 51 | func metaKeyEvent(config: KeyMap, keyDown: Bool) { 52 | var shift: Bool 53 | var option: Bool 54 | var control: Bool 55 | var command: Bool 56 | 57 | if keyDown { 58 | // Check if meta keys are not pressed before pressing keys 59 | (shift, option, control, command) = getMetaKeyState() 60 | pushedKeyConfigs.insert(config) 61 | } else { 62 | pushedKeyConfigs.remove(config) 63 | // Check if meta keys are not pressed after releasing keys 64 | (shift, option, control, command) = getMetaKeyState() 65 | } 66 | 67 | let source = CGEventSource(stateID: .hidSystemState) 68 | let modifiers = NSEvent.ModifierFlags(rawValue: UInt(config.modifiers)) 69 | if !shift && modifiers.contains(.shift) { 70 | let ev = CGEvent(keyboardEventSource: source, virtualKey: CGKeyCode(kVK_Shift), keyDown: keyDown) 71 | ev?.post(tap: .cghidEventTap) 72 | } 73 | 74 | if !option && modifiers.contains(.option) { 75 | let ev = CGEvent(keyboardEventSource: source, virtualKey: CGKeyCode(kVK_Option), keyDown: keyDown) 76 | ev?.post(tap: .cghidEventTap) 77 | } 78 | 79 | if !control && modifiers.contains(.control) { 80 | let ev = CGEvent(keyboardEventSource: source, virtualKey: CGKeyCode(kVK_Control), keyDown: keyDown) 81 | ev?.post(tap: .cghidEventTap) 82 | } 83 | 84 | if !command && modifiers.contains(.command) { 85 | let ev = CGEvent(keyboardEventSource: source, virtualKey: CGKeyCode(kVK_Command), keyDown: keyDown) 86 | ev?.post(tap: .cghidEventTap) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /JoyKeyMapper/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIconFile 10 | 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | $(MARKETING_VERSION) 21 | CFBundleVersion 22 | $(CURRENT_PROJECT_VERSION) 23 | LSApplicationCategoryType 24 | public.app-category.utilities 25 | LSMinimumSystemVersion 26 | $(MACOSX_DEPLOYMENT_TARGET) 27 | LSUIElement 28 | 29 | NSHumanReadableCopyright 30 | Copyright © 2019 DarkHorse. All rights reserved. 31 | NSMainStoryboardFile 32 | Main 33 | NSPrincipalClass 34 | NSApplication 35 | 36 | 37 | -------------------------------------------------------------------------------- /JoyKeyMapper/JoyKeyMapper.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.developer.associated-domains 6 | 7 | applinks:joykeymapper.0spec.jp 8 | 9 | com.apple.security.app-sandbox 10 | 11 | com.apple.security.device.usb 12 | 13 | com.apple.security.files.user-selected.read-only 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /JoyKeyMapper/Misc/Notifications.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Notifications.swift 3 | // JoyKeyMapper 4 | // 5 | // Created by magicien on 2019/07/15. 6 | // Copyright © 2019 DarkHorse. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public extension Notification.Name { 12 | static let controllerAdded = Notification.Name("ControllerAdded") 13 | static let controllerConnected = Notification.Name("ControllerConnected") 14 | static let controllerDisconnected = Notification.Name("ControllerDisconnected") 15 | static let controllerRemoved = Notification.Name("ControllerRemoved") 16 | static let controllerIconChanged = Notification.Name("ControllerIconChanged") 17 | } 18 | -------------------------------------------------------------------------------- /JoyKeyMapper/Misc/Utils.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Utils.swift 3 | // JoyKeyMapper 4 | // 5 | // Created by magicien on 2019/07/29. 6 | // Copyright © 2019 DarkHorse. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import InputMethodKit 11 | 12 | let mouseButtonNames: [String] = [ 13 | "Left Click", 14 | "Right Click", 15 | "Center Click" 16 | ] 17 | let localizedMouseButtonNames = mouseButtonNames.map { 18 | NSLocalizedString($0, comment: $0) 19 | } 20 | let none = NSLocalizedString("none", comment: "none") 21 | 22 | func convertModifierKeys(_ modifiers: NSEvent.ModifierFlags) -> String { 23 | var keyName = "" 24 | if modifiers.contains(.control) { 25 | keyName += NSLocalizedString("⌃", comment: "⌃") 26 | } 27 | if modifiers.contains(.option) { 28 | keyName += NSLocalizedString("⌥", comment: "⌥") 29 | } 30 | if modifiers.contains(.shift) { 31 | keyName += NSLocalizedString("⇧", comment: "⇧") 32 | } 33 | if modifiers.contains(.command) { 34 | keyName += NSLocalizedString("⌘", comment: "⌘") 35 | } 36 | return keyName 37 | } 38 | 39 | func convertKeyName(keyMap: KeyMap?) -> String { 40 | guard let map = keyMap else { return none } 41 | 42 | let modifiers = convertModifierKeys(NSEvent.ModifierFlags(rawValue: UInt(map.modifiers))) 43 | 44 | if map.keyCode >= 0 { 45 | let keyName = getKeyName(keyCode: UInt16(map.keyCode)) 46 | return "\(modifiers)\(keyName)" 47 | } 48 | 49 | if map.mouseButton >= 0 { 50 | let buttonName = localizedMouseButtonNames[Int(map.mouseButton)] 51 | if modifiers != "" { 52 | return "\(modifiers) + \(buttonName)" 53 | } 54 | return buttonName 55 | } 56 | 57 | return none 58 | } 59 | 60 | func getKeyName(keyCode: UInt16) -> String { 61 | if let specialKey = LocalizedSpecialKeyName[Int(keyCode)] { 62 | return specialKey 63 | } 64 | let maxNameLength = 4 65 | var nameBuffer = [UniChar](repeating: 0, count : maxNameLength) 66 | var nameLength = 0 67 | var deadKeys: UInt32 = 0 68 | let keyboardType = UInt32(LMGetKbdType()) 69 | let source = TISCopyCurrentKeyboardLayoutInputSource().takeRetainedValue() 70 | guard let ptr = TISGetInputSourceProperty(source, kTISPropertyUnicodeKeyLayoutData) else { 71 | return none 72 | } 73 | let layoutData = Unmanaged.fromOpaque(ptr).takeUnretainedValue() as Data 74 | layoutData.withUnsafeBytes { 75 | guard let ptr = $0.baseAddress?.assumingMemoryBound(to: UCKeyboardLayout.self) else { return } 76 | UCKeyTranslate(ptr, keyCode, UInt16(kUCKeyActionDown), 77 | 0, keyboardType, UInt32(kUCKeyTranslateNoDeadKeysMask), 78 | &deadKeys, maxNameLength, &nameLength, &nameBuffer) 79 | } 80 | let name = String(utf16CodeUnits: nameBuffer, count: nameLength) 81 | 82 | return name.uppercased() 83 | } 84 | 85 | /** Get the frontmost winodow ID. Currently not used. */ 86 | func getFrontmostWinodowNumber() -> Int? { 87 | let app = NSWorkspace.shared.frontmostApplication 88 | guard let pidInt32 = app?.processIdentifier else { return nil } 89 | let pid = Int64(pidInt32) 90 | guard let windowList = CGWindowListCopyWindowInfo(.optionAll, kCGNullWindowID) as? [NSDictionary] else { return nil } 91 | let window = windowList.first { ($0[kCGWindowOwnerPID] as? Int64 ?? -1) == pid } 92 | 93 | return window?[kCGWindowNumber] as? Int 94 | } 95 | -------------------------------------------------------------------------------- /JoyKeyMapper/Misc/en.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | // Common 2 | "Cancel" = "Cancel"; 3 | "OK" = "OK"; 4 | 5 | // AppDelegate.swift 6 | "Enable key mappings" = "Enable key mappings"; 7 | "Disconnect" = "Disconnect"; 8 | "charging" = "charging"; 9 | "Battery" = "Battery"; 10 | "No controllers connected" = "No controllers connected"; 11 | "Could not save changes while quitting. Quit anyway?" = "Could not save changes while quitting. Quit anyway?"; 12 | "Quitting now will lose any changes you have made since the last successful save" = "Quitting now will lose any changes you have made since the last successful save"; 13 | "Quit anyway" = "Quit anyway"; 14 | 15 | // AppNotifications.swift 16 | "Battery fully charged" = "Battery fully charged"; 17 | "Battery level" = "Battery level"; 18 | "Charge started" = "Charge started"; 19 | "Charge stopped" = "Charge stopped"; 20 | "Controller connected" = "Controller connected"; 21 | "Controller disconnected" = "Controller disconnected"; 22 | 23 | // GameController.swift 24 | "Empty" = "Empty"; 25 | "Critical" = "Critical"; 26 | "Low" = "Low"; 27 | "Medium" = "Medium"; 28 | "Full" = "Full"; 29 | "Unknown" = "Unknown"; 30 | 31 | // ViewController.swift 32 | "Choose an app to add" = "Choose an app to add"; 33 | "Do you really want to delete the settings for %@?" = "Do you really want to delete the settings for %@?"; 34 | 35 | // ViewController+NSCollectionViewDelegate 36 | "Connected" = "Connected"; 37 | 38 | // ControllerViewItem.swift 39 | "Enable key mappings" = "Enable key mappings"; 40 | "Disconnect" = "Disconnect"; 41 | "Remove" = "Remove"; 42 | "Do you really want to remove the controller?" = "Do you really want to remove the controller?"; 43 | 44 | // StickConfigCellView.swift 45 | "Mouse" = "Mouse"; 46 | "Key" = "Key"; 47 | 48 | // SpecialKeyName.swiift 49 | "Section" = "Section"; 50 | "Return" = "Return"; 51 | "Tab" = "Tab"; 52 | "Space" = "Space"; 53 | "Delete" = "Delete"; 54 | "Escape" = "Escape"; 55 | "⌘" = "⌘"; 56 | "⇧" = "⇧"; 57 | "CapsLock" = "CapsLock"; 58 | "⌥" = "⌥"; 59 | "⌃" = "⌃"; 60 | "Right⇧" = "Right⇧"; 61 | "Right⌥" = "Right⌥"; 62 | "Right⌃" = "Right⌃"; 63 | "fn" = "fn"; 64 | "F1" = "F1"; 65 | "F2" = "F2"; 66 | "F3" = "F3"; 67 | "F4" = "F4"; 68 | "F5" = "F5"; 69 | "F6" = "F6"; 70 | "F7" = "F7"; 71 | "F8" = "F8"; 72 | "F9" = "F9"; 73 | "F10" = "F10"; 74 | "F11" = "F11"; 75 | "F12" = "F12"; 76 | "F13" = "F13"; 77 | "F14" = "F14"; 78 | "F15" = "F15"; 79 | "F16" = "F16"; 80 | "F17" = "F17"; 81 | "F18" = "F18"; 82 | "F19" = "F19"; 83 | "F20" = "F20"; 84 | "Keypad 0" = "Keypad 0"; 85 | "Keypad 1" = "Keypad 1"; 86 | "Keypad 2" = "Keypad 2"; 87 | "Keypad 3" = "Keypad 3"; 88 | "Keypad 4" = "Keypad 4"; 89 | "Keypad 5" = "Keypad 5"; 90 | "Keypad 6" = "Keypad 6"; 91 | "Keypad 7" = "Keypad 7"; 92 | "Keypad 8" = "Keypad 8"; 93 | "Keypad 9" = "Keypad 9"; 94 | "Keypad *" = "Keypad *"; 95 | "Keypad +" = "Keypad +"; 96 | "Keypad Clear" = "Keypad Clear"; 97 | "Keypad ," = "Keypad ,"; 98 | "Keypad Enter" = "Keypad Enter"; 99 | "Keypad -" = "Keypad -"; 100 | "Keypad /" = "Keypad /"; 101 | "Keypad =" = "Keypad ="; 102 | "Keypad Decimal" = "Keypad Decimal"; 103 | "VolumeUp" = "VolumeUp"; 104 | "VolumeDown" = "VolumeDown"; 105 | "Mute" = "Mute"; 106 | "¥" = "¥"; 107 | "_" = "_"; 108 | "Eisu" = "Eisu"; 109 | "Kana" = "Kana"; 110 | "Help" = "Help"; 111 | "Home" = "Home"; 112 | "PageUp" = "PageUp"; 113 | "PageDown" = "PageDown"; 114 | "ForwardDelete" = "ForwardDelete"; 115 | "End" = "End"; 116 | "←" = "←"; 117 | "→" = "→"; 118 | "↓" = "↓"; 119 | "↑" = "↑"; 120 | 121 | // ViewController+NSOutlineViewDelegate.swift 122 | "Up" = "Up"; 123 | "Right" = "Right"; 124 | "Down" = "Down"; 125 | "Left" = "Left"; 126 | "A" = "A"; 127 | "B" = "B"; 128 | "X" = "X"; 129 | "Y" = "Y"; 130 | "L" = "L"; 131 | "ZL" = "ZL"; 132 | "R" = "R"; 133 | "ZR" = "ZR"; 134 | "Minus" = "Minus"; 135 | "Plus" = "Plus"; 136 | "Capture" = "Capture"; 137 | "Home" = "Home"; 138 | "LStick Push" = "LStick Push"; 139 | "RStick Push" = "RStick Push"; 140 | "Left SL" = "Left SL"; 141 | "Left SR" = "Left SR"; 142 | "Right SL" = "Right SL"; 143 | "Right SR" = "Right SR"; 144 | "Left Stick" = "Left Stick"; 145 | "Right Stick" = "Right Stick"; 146 | "Mouse Wheel" = "Mouse Wheel"; 147 | "None" = "None"; 148 | "Speed" = "Speed"; 149 | 150 | // KeyConfigViewController.swift 151 | "%@ Button Key Config" = "%@ Button Key Config"; 152 | 153 | // Utils.swift 154 | "none" = "none"; 155 | "Left Click" = "Left Click"; 156 | "Right Click" = "Right Click"; 157 | "Center Click" = "Center Click"; 158 | -------------------------------------------------------------------------------- /JoyKeyMapper/Misc/ja.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | // Common 2 | "Cancel" = "キャンセル"; 3 | "OK" = "OK"; 4 | 5 | // AppDelegate.swift 6 | "Enable key mappings" = "キーマッピング有効"; 7 | "Disconnect" = "接続解除"; 8 | "charging" = "充電中"; 9 | "Battery" = "バッテリー"; 10 | "No controllers connected" = "接続中のコントローラがありません"; 11 | "Could not save changes while quitting. Quit anyway?" = "Could not save changes while quitting. Quit anyway?"; 12 | "Quitting now will lose any changes you have made since the last successful save" = "Quitting now will lose any changes you have made since the last successful save"; 13 | "Quit anyway" = "Quit anyway"; 14 | 15 | // AppNotifications.swift 16 | "Battery fully charged" = "バッテリー充電完了"; 17 | "Battery level" = "バッテリーレベル"; 18 | "Charge started" = "充電を開始しました"; 19 | "Charge stopped" = "充電を中断しました"; 20 | "Controller connected" = "コントローラが接続されました"; 21 | "Controller disconnected" = "コントローラの接続が解除されました"; 22 | 23 | // GameController.swift 24 | "Empty" = "なし"; 25 | "Critical" = "極低"; 26 | "Low" = "低"; 27 | "Medium" = "中"; 28 | "Full" = "最大"; 29 | "Unknown" = "不明"; 30 | 31 | // ViewController.swift 32 | "Choose an app to add" = "追加するアプリケーションを選択してください"; 33 | "Do you really want to delete the settings for %@?" = "本当に「%@」の設定を削除しますか?"; 34 | 35 | // ViewController+NSCollectionViewDelegate 36 | "Connected" = "接続中"; 37 | 38 | // ControllerViewItem.swift 39 | "Enable key mappings" = "キーマッピングを有効化"; 40 | "Disconnect" = "接続解除"; 41 | "Remove" = "削除"; 42 | "Do you really want to remove the controller?" = "本当にコントローラを削除しますか?"; 43 | 44 | // StickConfigCellView.swift 45 | "Mouse" = "マウス"; 46 | "Key" = "キー"; 47 | 48 | // SpecialKeyName.swiift 49 | "Section" = "Section"; 50 | "Return" = "Return"; 51 | "Tab" = "Tab"; 52 | "Space" = "Space"; 53 | "Delete" = "Delete"; 54 | "Escape" = "Escape"; 55 | "⌘" = "⌘"; 56 | "⇧" = "⇧"; 57 | "CapsLock" = "CapsLock"; 58 | "⌥" = "⌥"; 59 | "⌃" = "⌃"; 60 | "Right⇧" = "Right⇧"; 61 | "Right⌥" = "Right⌥"; 62 | "Right⌃" = "Right⌃"; 63 | "fn" = "fn"; 64 | "F1" = "F1"; 65 | "F2" = "F2"; 66 | "F3" = "F3"; 67 | "F4" = "F4"; 68 | "F5" = "F5"; 69 | "F6" = "F6"; 70 | "F7" = "F7"; 71 | "F8" = "F8"; 72 | "F9" = "F9"; 73 | "F10" = "F10"; 74 | "F11" = "F11"; 75 | "F12" = "F12"; 76 | "F13" = "F13"; 77 | "F14" = "F14"; 78 | "F15" = "F15"; 79 | "F16" = "F16"; 80 | "F17" = "F17"; 81 | "F18" = "F18"; 82 | "F19" = "F19"; 83 | "F20" = "F20"; 84 | "Keypad 0" = "Keypad 0"; 85 | "Keypad 1" = "Keypad 1"; 86 | "Keypad 2" = "Keypad 2"; 87 | "Keypad 3" = "Keypad 3"; 88 | "Keypad 4" = "Keypad 4"; 89 | "Keypad 5" = "Keypad 5"; 90 | "Keypad 6" = "Keypad 6"; 91 | "Keypad 7" = "Keypad 7"; 92 | "Keypad 8" = "Keypad 8"; 93 | "Keypad 9" = "Keypad 9"; 94 | "Keypad *" = "Keypad *"; 95 | "Keypad +" = "Keypad +"; 96 | "Keypad Clear" = "Keypad Clear"; 97 | "Keypad ," = "Keypad ,"; 98 | "Keypad Enter" = "Keypad Enter"; 99 | "Keypad -" = "Keypad -"; 100 | "Keypad /" = "Keypad /"; 101 | "Keypad =" = "Keypad ="; 102 | "Keypad Decimal" = "Keypad Decimal"; 103 | "VolumeUp" = "VolumeUp"; 104 | "VolumeDown" = "VolumeDown"; 105 | "Mute" = "Mute"; 106 | "¥" = "¥"; 107 | "_" = "_"; 108 | "Eisu" = "Eisu"; 109 | "Kana" = "Kana"; 110 | "Help" = "Help"; 111 | "Home" = "Home"; 112 | "PageUp" = "PageUp"; 113 | "PageDown" = "PageDown"; 114 | "ForwardDelete" = "ForwardDelete"; 115 | "End" = "End"; 116 | "←" = "←"; 117 | "→" = "→"; 118 | "↓" = "↓"; 119 | "↑" = "↑"; 120 | 121 | // ViewController+NSOutlineViewDelegate.swift 122 | "Up" = "上"; 123 | "Right" = "右"; 124 | "Down" = "下"; 125 | "Left" = "左"; 126 | "A" = "A"; 127 | "B" = "B"; 128 | "X" = "X"; 129 | "Y" = "Y"; 130 | "L" = "L"; 131 | "ZL" = "ZL"; 132 | "R" = "R"; 133 | "ZR" = "ZR"; 134 | "Minus" = "ー"; 135 | "Plus" = "+"; 136 | "Capture" = "Capture"; 137 | "Home" = "Home"; 138 | "LStick Push" = "左スティック押し込み"; 139 | "RStick Push" = "右スティック押し込み"; 140 | "Left SL" = "左SL"; 141 | "Left SR" = "左SR"; 142 | "Right SL" = "右SL"; 143 | "Right SR" = "右SR"; 144 | "Left Stick" = "左スティック"; 145 | "Right Stick" = "右スティック"; 146 | "Mouse Wheel" = "マウスホイール"; 147 | "None" = "なし"; 148 | "Speed" = "速度"; 149 | 150 | // KeyConfigViewController.swift 151 | "%@ Button Key Config" = "%@ボタン設定"; 152 | 153 | // Utils.swift 154 | "none" = "なし"; 155 | "Left Click" = "左クリック"; 156 | "Right Click" = "右クリック"; 157 | "Center Click" = "中クリック"; 158 | -------------------------------------------------------------------------------- /JoyKeyMapper/Views/AppList/AppCellView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppCellView.swift 3 | // JoyKeyMapper 4 | // 5 | // Created by magicien on 2019/07/21. 6 | // Copyright © 2019 DarkHorse. All rights reserved. 7 | // 8 | 9 | import AppKit 10 | 11 | class AppCellView: NSTableCellView { 12 | @IBOutlet weak var appIcon: NSImageView! 13 | @IBOutlet weak var appName: NSTextField! 14 | } 15 | -------------------------------------------------------------------------------- /JoyKeyMapper/Views/AppList/ViewController+NSTableViewDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController+NSTableViewDelegate.swift 3 | // JoyKeyMapper 4 | // 5 | // Created by magicien on 2019/07/21. 6 | // Copyright © 2019 DarkHorse. All rights reserved. 7 | // 8 | 9 | import AppKit 10 | import JoyConSwift 11 | 12 | let appNameColumnID = "appName" 13 | 14 | extension ViewController: NSTableViewDelegate, NSTableViewDataSource { 15 | func numberOfRows(in tableView: NSTableView) -> Int { 16 | if tableView === self.appTableView { 17 | return self.numRowsOfAppTableView() 18 | } 19 | 20 | return 0 21 | } 22 | 23 | func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? { 24 | if tableView === self.appTableView { 25 | return self.viewForAppTable(column: tableColumn, row: row) 26 | } 27 | 28 | return nil 29 | } 30 | 31 | // MARK: - AppTableView 32 | 33 | func convertAppName(_ name: String?) -> String { 34 | guard var appName = name else { return "" } 35 | 36 | if appName.hasSuffix(".app") { 37 | appName.removeLast(4) 38 | } 39 | appName = appName.replacingOccurrences(of: "%20", with: " ") 40 | 41 | return appName 42 | } 43 | 44 | func numRowsOfAppTableView() -> Int { 45 | guard let controller = self.selectedController else { return 0 } 46 | 47 | let numApps = controller.data.appConfigs?.count ?? 0 48 | 49 | return numApps + 1 50 | } 51 | 52 | func viewForAppTable(column: NSTableColumn?, row: Int) -> NSView? { 53 | guard let controller = self.selectedController else { return nil } 54 | guard let col = column else { return nil } 55 | guard let newView = self.appTableView.makeView(withIdentifier: col.identifier, owner: self) as? AppCellView else { return nil } 56 | 57 | if row == 0 { 58 | newView.appIcon.image = NSImage(named: "GenericApplicationIcon") 59 | newView.appName.stringValue = "Default" 60 | } else { 61 | guard let appConfig = controller.data.appConfigs?[row - 1] as? AppConfig else { return nil } 62 | guard let appData = appConfig.app else { return nil } 63 | 64 | if let icon = appData.icon { 65 | newView.appIcon.image = NSImage(data: icon) 66 | } else { 67 | newView.appIcon.image = NSImage(named: "GenericApplicationIcon") 68 | } 69 | 70 | newView.appName.stringValue = self.convertAppName(appData.displayName) 71 | } 72 | 73 | return newView 74 | } 75 | 76 | func tableViewSelectionDidChange(_ notification: Notification) { 77 | self.updateAppAddRemoveButtonState() 78 | self.configTableView.reloadData() 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /JoyKeyMapper/Views/AppSettings/AppSettings.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppSettings.swift 3 | // JoyKeyMapper 4 | // 5 | // Created by magicien on 2020/03/12. 6 | // Copyright © 2020 DarkHorse. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import ServiceManagement 11 | 12 | let helperAppBundleID = "jp.0spec.JoyKeyMapperLauncher" 13 | 14 | class AppSettings { 15 | static var disconnectTime: Int { 16 | get { 17 | return UserDefaults.standard.integer(forKey: "disconnectTime") 18 | } 19 | set { 20 | UserDefaults.standard.set(newValue, forKey: "disconnectTime") 21 | } 22 | } 23 | 24 | static var notifyConnection: Bool { 25 | get { 26 | return UserDefaults.standard.bool(forKey: "notifyConnection") 27 | } 28 | set { 29 | UserDefaults.standard.set(newValue, forKey: "notifyConnection") 30 | } 31 | } 32 | 33 | static var notifyBatteryLevel: Bool { 34 | get { 35 | return UserDefaults.standard.bool(forKey: "notifyBatteryLevel") 36 | } 37 | set { 38 | UserDefaults.standard.set(newValue, forKey: "notifyBatteryLevel") 39 | } 40 | } 41 | 42 | static var notifyBatteryCharge: Bool { 43 | get { 44 | return UserDefaults.standard.bool(forKey: "notifyBatteryCharge") 45 | } 46 | set { 47 | UserDefaults.standard.set(newValue, forKey: "notifyBatteryCharge") 48 | } 49 | } 50 | 51 | static var notifyBatteryFull: Bool { 52 | get { 53 | return UserDefaults.standard.bool(forKey: "notifyBatteryFull") 54 | } 55 | set { 56 | UserDefaults.standard.set(newValue, forKey: "notifyBatteryFull") 57 | } 58 | } 59 | 60 | static var launchOnLogin: Bool { 61 | get { 62 | guard let loginItems = SMCopyAllJobDictionaries(kSMDomainUserLaunchd).takeRetainedValue() as NSArray as? [[String:AnyObject]] else { return false } 63 | return !loginItems.filter { 64 | $0["Label"] as! String == helperAppBundleID 65 | }.isEmpty 66 | } 67 | set { 68 | if (!SMLoginItemSetEnabled(helperAppBundleID as CFString, newValue)) { 69 | Swift.print("Launch on Login setting error") 70 | } 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /JoyKeyMapper/Views/AppSettings/AppSettingsViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppSettingsViewController.swift 3 | // JoyKeyMapper 4 | // 5 | // Created by magicien on 2020/03/11. 6 | // Copyright © 2020 DarkHorse. All rights reserved. 7 | // 8 | 9 | import AppKit 10 | 11 | class AppSettingsViewController: NSViewController { 12 | @IBOutlet weak var disconnectTime: NSPopUpButton! 13 | @IBOutlet weak var notifyConnection: NSButton! 14 | @IBOutlet weak var notifyBatteryLevel: NSButton! 15 | @IBOutlet weak var notifyBatteryCharge: NSButton! 16 | @IBOutlet weak var notifyBatteryFull: NSButton! 17 | @IBOutlet weak var launchOnLogin: NSButton! 18 | 19 | override func viewDidLoad() { 20 | super.viewDidLoad() 21 | 22 | self.disconnectTime.selectItem(withTag: AppSettings.disconnectTime) 23 | self.notifyConnection.state = AppSettings.notifyConnection ? .on : .off 24 | self.notifyBatteryLevel.state = AppSettings.notifyBatteryLevel ? .on : .off 25 | self.notifyBatteryCharge.state = AppSettings.notifyBatteryCharge ? .on : .off 26 | self.notifyBatteryFull.state = AppSettings.notifyBatteryFull ? .on : .off 27 | self.launchOnLogin.state = AppSettings.launchOnLogin ? .on : .off 28 | } 29 | 30 | @IBAction func didChangeDisconnectTime(_ sender: NSPopUpButton) { 31 | AppSettings.disconnectTime = self.disconnectTime.selectedTag() 32 | } 33 | 34 | @IBAction func didChangeNotifyConnection(_ sender: NSButton) { 35 | AppSettings.notifyConnection = self.notifyConnection.state == .on 36 | } 37 | 38 | @IBAction func didChangeNotifyBatteryLevel(_ sender: NSButton) { 39 | AppSettings.notifyBatteryLevel = self.notifyBatteryLevel.state == .on 40 | } 41 | 42 | @IBAction func didChangeNotifyBatteryCharge(_ sender: NSButton) { 43 | AppSettings.notifyBatteryCharge = self.notifyBatteryCharge.state == .on 44 | } 45 | 46 | @IBAction func didChangeNotifyBatteryFull(_ sender: NSButton) { 47 | AppSettings.notifyBatteryFull = self.notifyBatteryFull.state == .on 48 | } 49 | 50 | @IBAction func didChangeLaunchOnLogin(_ sender: NSButton) { 51 | AppSettings.launchOnLogin = self.launchOnLogin.state == .on 52 | } 53 | 54 | @IBAction func didPushOK(_ sender: NSButton) { 55 | guard let window = self.view.window else { return } 56 | window.sheetParent?.endSheet(window, returnCode: .OK) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /JoyKeyMapper/Views/ControllerList/ControllerView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ControllerView.swift 3 | // JoyKeyMapper 4 | // 5 | // Created by magicien on 2019/07/18. 6 | // Copyright © 2019 DarkHorse. All rights reserved. 7 | // 8 | 9 | import AppKit 10 | 11 | class ControllerView: NSView { 12 | var isSelected: Bool = false 13 | 14 | override func draw(_ dirtyRect: NSRect) { 15 | if self.isSelected { 16 | NSColor.alternateSelectedControlColor.setFill() 17 | } else { 18 | NSColor.white.setFill() 19 | } 20 | self.bounds.fill() 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /JoyKeyMapper/Views/ControllerList/ControllerViewItem.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ControllerViewItem.swift 3 | // JoyKeyMapper 4 | // 5 | // Created by Yuki Ohno on 2019/07/15. 6 | // Copyright © 2019 DarkHorse. All rights reserved. 7 | // 8 | 9 | import AppKit 10 | 11 | class ControllerViewItem: NSCollectionViewItem { 12 | @IBOutlet weak var controllerView: ControllerView! 13 | @IBOutlet weak var iconView: NSImageView! 14 | @IBOutlet weak var label: NSTextField! 15 | 16 | var controller: GameController? 17 | 18 | override var isSelected: Bool { 19 | didSet { 20 | self.controllerView.isSelected = self.isSelected 21 | self.controllerView.needsDisplay = true 22 | } 23 | } 24 | 25 | override func viewDidLoad() { 26 | super.viewDidLoad() 27 | // Do view setup here. 28 | self.controllerView.isSelected = self.isSelected 29 | self.controllerView.needsDisplay = true 30 | } 31 | 32 | override func mouseDown(with event: NSEvent) { 33 | super.mouseDown(with: event) 34 | 35 | if event.modifierFlags.contains(.control) { 36 | self.showMenu(event) 37 | } 38 | } 39 | 40 | override func rightMouseDown(with event: NSEvent) { 41 | self.showMenu(event) 42 | } 43 | 44 | func showMenu(_ event: NSEvent) { 45 | let menu = NSMenu(title: "ControllerMenu") 46 | 47 | // Enable key mappings menu 48 | let enableTitle = NSLocalizedString("Enable key mappings", comment: "Enable key mappings") 49 | let enableMenu = NSMenuItem(title: enableTitle, action: Selector(("enableKeyMappings")), keyEquivalent: "") 50 | enableMenu.target = self 51 | enableMenu.state = (self.controller?.isEnabled ?? false) ? .on : .off 52 | menu.addItem(enableMenu) 53 | 54 | // Disconnect menu 55 | let disconnectTitle = NSLocalizedString("Disconnect", comment: "Disconnect") 56 | let disconnectMenu = NSMenuItem(title: disconnectTitle, action: Selector(("disconnect")), keyEquivalent: "") 57 | if self.controller?.controller != nil { 58 | disconnectMenu.target = self 59 | } 60 | menu.addItem(disconnectMenu) 61 | 62 | /* 63 | // Separator 64 | menu.addItem(NSMenuItem.separator()) 65 | 66 | // Import menu 67 | let importTitle = NSLocalizedString("Import key mappings", comment: "Import key mappings") 68 | let importMenu = NSMenuItem(title: importTitle, action: Selector(("importKeyMappings")), keyEquivalent: "") 69 | importMenu.target = self 70 | menu.addItem(importMenu) 71 | 72 | // Export menu 73 | let exportTitle = NSLocalizedString("Export key mappings", comment: "Export key mappings") 74 | let exportMenu = NSMenuItem(title: exportTitle, action: Selector(("exportKeyMappings")), keyEquivalent: "") 75 | exportMenu.target = self 76 | menu.addItem(exportMenu) 77 | */ 78 | 79 | // Separator 80 | menu.addItem(NSMenuItem.separator()) 81 | 82 | // Remove menu 83 | let removeTitle = NSLocalizedString("Remove", comment: "Remove") 84 | let removeMenu = NSMenuItem(title: removeTitle, action: Selector(("remove")), keyEquivalent: "") 85 | removeMenu.target = self 86 | menu.addItem(removeMenu) 87 | 88 | let pos = event.cgEvent?.unflippedLocation ?? CGPoint(x: 0, y: 0) 89 | menu.popUp(positioning: nil, at: pos, in: nil) 90 | } 91 | 92 | @objc func enableKeyMappings() { 93 | guard let controller = self.controller else { return } 94 | controller.isEnabled = !controller.isEnabled 95 | } 96 | 97 | @objc func disconnect() { 98 | self.controller?.disconnect() 99 | } 100 | 101 | @objc func importKeyMappings() { 102 | 103 | } 104 | 105 | @objc func exportKeyMappings() { 106 | 107 | } 108 | 109 | @objc func remove() { 110 | guard let delegate = NSApplication.shared.delegate as? AppDelegate else { return } 111 | guard let controller = self.controller else { return } 112 | 113 | let alert = NSAlert() 114 | alert.icon = controller.icon 115 | alert.messageText = NSLocalizedString("Do you really want to remove the controller?", comment: "Do you really want to remove the controller?") 116 | alert.addButton(withTitle: NSLocalizedString("Cancel", comment: "Cancel")) 117 | alert.addButton(withTitle: NSLocalizedString("OK", comment: "OK")) 118 | let response = alert.runModal() 119 | 120 | if response == .alertFirstButtonReturn { 121 | // Cancel 122 | return 123 | } 124 | 125 | delegate.removeController(gameController: controller) 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /JoyKeyMapper/Views/ControllerList/ControllerViewItem.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /JoyKeyMapper/Views/ControllerList/ViewController+NSCollectionViewDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController+NSCollectionViewDelegate.swift 3 | // JoyKeyMapper 4 | // 5 | // Created by magicien on 2019/07/15. 6 | // Copyright © 2019 DarkHorse. All rights reserved. 7 | // 8 | 9 | import AppKit 10 | 11 | let connected = NSLocalizedString("Connected", comment: "Connected") 12 | 13 | extension ViewController: NSCollectionViewDelegate, NSCollectionViewDataSource { 14 | func collectionView(_ collectionView: NSCollectionView, numberOfItemsInSection section: Int) -> Int { 15 | let controllers = self.appDelegate?.controllers ?? [] 16 | 17 | return controllers.count 18 | } 19 | 20 | func collectionView(_ collectionView: NSCollectionView, itemForRepresentedObjectAt indexPath: IndexPath) -> NSCollectionViewItem { 21 | let item = collectionView.makeItem(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: "ControllerViewItem"), for: indexPath) 22 | 23 | guard let controllerItem = item as? ControllerViewItem else { return item } 24 | let index = indexPath.item 25 | guard let controllers = self.appDelegate?.controllers else { return item } 26 | guard controllers.count > index else { return item } 27 | let controller = controllers[index] 28 | 29 | controllerItem.iconView.image = controller.icon 30 | controllerItem.controller = controller 31 | controllerItem.label.stringValue = controller.controller != nil ? connected : "" 32 | 33 | return controllerItem 34 | } 35 | 36 | func collectionView(_ collectionView: NSCollectionView, didSelectItemsAt indexPaths: Set) { 37 | guard let index = indexPaths.first?.item else { 38 | self.selectedController = nil 39 | return 40 | } 41 | guard let controllers = self.appDelegate?.controllers else { 42 | self.selectedController = nil 43 | return 44 | } 45 | guard controllers.count > index else { 46 | self.selectedController = nil 47 | return 48 | } 49 | self.selectedController = controllers[index] 50 | } 51 | 52 | func collectionView(_ collectionView: NSCollectionView, didDeselectItemsAt indexPaths: Set) { 53 | self.selectedController = nil 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /JoyKeyMapper/Views/KeyConfigView/KeyConfigComboBox.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KeyConfigComboBox.swift 3 | // JoyKeyMapper 4 | // 5 | // Created by magicien on 2019/07/29. 6 | // Copyright © 2019 DarkHorse. All rights reserved. 7 | // 8 | 9 | import AppKit 10 | import InputMethodKit 11 | 12 | protocol KeyConfigComboBoxDelegate { 13 | func setKeyCode(_ keyCode: UInt16) 14 | } 15 | 16 | let keyCodeList: [Int] = [ 17 | kVK_ISO_Section, 18 | kVK_Return, 19 | kVK_Tab, 20 | kVK_Space, 21 | kVK_Delete, 22 | kVK_Escape, 23 | // kVK_CapsLock, 24 | kVK_RightShift, 25 | kVK_RightOption, 26 | kVK_RightControl, 27 | kVK_F1, 28 | kVK_F2, 29 | kVK_F3, 30 | kVK_F4, 31 | kVK_F5, 32 | kVK_F6, 33 | kVK_F7, 34 | kVK_F8, 35 | kVK_F9, 36 | kVK_F10, 37 | kVK_F11, 38 | kVK_F12, 39 | kVK_F13, 40 | kVK_F14, 41 | kVK_F15, 42 | kVK_F16, 43 | kVK_F17, 44 | kVK_F18, 45 | kVK_F19, 46 | kVK_F20, 47 | kVK_ANSI_Keypad0, 48 | kVK_ANSI_Keypad1, 49 | kVK_ANSI_Keypad2, 50 | kVK_ANSI_Keypad3, 51 | kVK_ANSI_Keypad4, 52 | kVK_ANSI_Keypad5, 53 | kVK_ANSI_Keypad6, 54 | kVK_ANSI_Keypad7, 55 | kVK_ANSI_Keypad8, 56 | kVK_ANSI_Keypad9, 57 | kVK_ANSI_KeypadMultiply, 58 | kVK_ANSI_KeypadPlus, 59 | kVK_ANSI_KeypadClear, 60 | kVK_JIS_KeypadComma, 61 | kVK_ANSI_KeypadEnter, 62 | kVK_ANSI_KeypadMinus, 63 | kVK_ANSI_KeypadDivide, 64 | kVK_ANSI_KeypadEquals, 65 | kVK_ANSI_KeypadDecimal, 66 | kVK_VolumeUp, 67 | kVK_VolumeDown, 68 | kVK_Mute, 69 | kVK_JIS_Yen, 70 | kVK_JIS_Underscore, 71 | kVK_JIS_Eisu, 72 | kVK_JIS_Kana, 73 | // kVK_Help, 74 | kVK_Home, 75 | kVK_PageUp, 76 | kVK_PageDown, 77 | kVK_ForwardDelete, 78 | kVK_End, 79 | kVK_LeftArrow, 80 | kVK_RightArrow, 81 | kVK_DownArrow, 82 | kVK_UpArrow, 83 | SpecialKey_BrightnessUp, 84 | SpecialKey_BrightnessDown, 85 | // SpecialKey_NumLock, 86 | SpecialKey_Play, 87 | SpecialKey_Next, 88 | SpecialKey_Previous, 89 | SpecialKey_Fast, 90 | SpecialKey_Rewind 91 | ] 92 | 93 | let keyCells: [NSComboBoxCell] = { 94 | return keyCodeList.map { 95 | let keyName = getKeyName(keyCode: UInt16($0)) 96 | return NSComboBoxCell(textCell: keyName) 97 | } 98 | }() 99 | 100 | class KeyConfigComboBox: NSComboBox { 101 | var configDelegate: KeyConfigComboBoxDelegate? 102 | 103 | var monitor: Any? 104 | 105 | override init(frame frameRect: NSRect) { 106 | super.init(frame: frameRect) 107 | } 108 | 109 | required init?(coder: NSCoder) { 110 | super.init(coder: coder) 111 | self.addItems(withObjectValues: keyCells) 112 | } 113 | 114 | override func becomeFirstResponder() -> Bool { 115 | self.stringValue = "" 116 | self.monitor = NSEvent.addLocalMonitorForEvents(matching: .keyDown, handler: { [weak self] event in 117 | guard let _self = self else { return event } 118 | 119 | _self.window?.makeFirstResponder(nil) 120 | _self.configDelegate?.setKeyCode(event.keyCode) 121 | if let monitor = _self.monitor { 122 | _self.monitor = nil 123 | NSEvent.removeMonitor(monitor) 124 | } 125 | return nil 126 | }) 127 | return true 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /JoyKeyMapper/Views/KeyConfigView/KeyConfigViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KeyConfigViewController.swift 3 | // JoyKeyMapper 4 | // 5 | // Created by magicien on 2019/07/29. 6 | // Copyright © 2019 DarkHorse. All rights reserved. 7 | // 8 | 9 | import AppKit 10 | import InputMethodKit 11 | 12 | protocol KeyConfigSetDelegate { 13 | func setKeyConfig(controller: KeyConfigViewController) 14 | } 15 | 16 | class KeyConfigViewController: NSViewController, NSComboBoxDelegate, KeyConfigComboBoxDelegate { 17 | var delegate: KeyConfigSetDelegate? 18 | var keyMap: KeyMap? 19 | var keyCode: Int16 = -1 20 | 21 | @IBOutlet weak var titleLabel: NSTextField! 22 | 23 | @IBOutlet weak var shiftKey: NSButton! 24 | @IBOutlet weak var optionKey: NSButton! 25 | @IBOutlet weak var controlKey: NSButton! 26 | @IBOutlet weak var commandKey: NSButton! 27 | 28 | @IBOutlet weak var keyRadioButton: NSButton! 29 | @IBOutlet weak var mouseRadioButton: NSButton! 30 | 31 | @IBOutlet weak var keyAction: KeyConfigComboBox! 32 | @IBOutlet weak var mouseAction: NSPopUpButton! 33 | 34 | override func viewDidLoad() { 35 | super.viewDidLoad() 36 | 37 | guard let keyMap = self.keyMap else { return } 38 | 39 | let title = NSLocalizedString("%@ Button Key Config", comment: "%@ Button Key Config") 40 | let buttonName = NSLocalizedString((keyMap.button ?? ""), comment: "Button Name") 41 | self.titleLabel.stringValue = String.localizedStringWithFormat(title, buttonName) 42 | 43 | let modifiers = NSEvent.ModifierFlags(rawValue: UInt(keyMap.modifiers)) 44 | self.shiftKey.state = modifiers.contains(.shift) ? .on : .off 45 | self.optionKey.state = modifiers.contains(.option) ? .on : .off 46 | self.controlKey.state = modifiers.contains(.control) ? .on : .off 47 | self.commandKey.state = modifiers.contains(.command) ? .on : .off 48 | 49 | if keyMap.keyCode >= 0 { 50 | self.keyRadioButton.state = .on 51 | self.keyAction.stringValue = getKeyName(keyCode: UInt16(keyMap.keyCode)) 52 | } else { 53 | self.mouseRadioButton.state = .on 54 | self.mouseAction.selectItem(withTag: Int(keyMap.mouseButton)) 55 | } 56 | self.keyCode = keyMap.keyCode 57 | self.keyAction.configDelegate = self 58 | self.keyAction.delegate = self 59 | } 60 | 61 | func updateKeyMap() { 62 | guard let keyMap = self.keyMap else { return } 63 | 64 | var flags = NSEvent.ModifierFlags(rawValue: 0) 65 | 66 | if self.shiftKey.state == .on { 67 | flags.formUnion(.shift) 68 | } else { 69 | flags.remove(.shift) 70 | } 71 | 72 | if self.optionKey.state == .on { 73 | flags.formUnion(.option) 74 | } else { 75 | flags.remove(.option) 76 | } 77 | 78 | if self.controlKey.state == .on { 79 | flags.formUnion(.control) 80 | } else { 81 | flags.remove(.control) 82 | } 83 | 84 | 85 | if self.commandKey.state == .on { 86 | flags.formUnion(.command) 87 | } else { 88 | flags.remove(.command) 89 | } 90 | 91 | keyMap.modifiers = Int32(flags.rawValue) 92 | 93 | if self.keyRadioButton.state == .on { 94 | keyMap.keyCode = self.keyCode 95 | keyMap.mouseButton = -1 96 | } else { 97 | keyMap.keyCode = -1 98 | keyMap.mouseButton = Int16(self.mouseAction.selectedTag()) 99 | } 100 | 101 | keyMap.isEnabled = true 102 | 103 | self.delegate?.setKeyConfig(controller: self) 104 | } 105 | 106 | func comboBoxSelectionDidChange(_ notification: Notification) { 107 | let index = self.keyAction.indexOfSelectedItem 108 | if index >= 0 { 109 | let keyCode = keyCodeList[index] 110 | self.setKeyCode(UInt16(keyCode)) 111 | } 112 | } 113 | 114 | func setKeyCode(_ keyCode: UInt16) { 115 | self.keyCode = Int16(keyCode) 116 | self.keyAction.stringValue = getKeyName(keyCode: keyCode) 117 | self.keyRadioButton.state = .on 118 | } 119 | 120 | @IBAction func didPushRadioButton(_ sender: NSButton) {} 121 | 122 | @IBAction func didPushOK(_ sender: NSButton) { 123 | guard let window = self.view.window else { return } 124 | self.updateKeyMap() 125 | window.sheetParent?.endSheet(window, returnCode: .OK) 126 | } 127 | 128 | @IBAction func didPushCancel(_ sender: NSButton) { 129 | guard let window = self.view.window else { return } 130 | window.sheetParent?.endSheet(window, returnCode: .cancel) 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /JoyKeyMapper/Views/KeyMapList/ButtonNameCellView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ButtonNameCellView.swift 3 | // JoyKeyMapper 4 | // 5 | // Created by magicien on 2019/07/23. 6 | // Copyright © 2019 DarkHorse. All rights reserved. 7 | // 8 | 9 | import AppKit 10 | 11 | class ButtonNameCellView: NSTableCellView { 12 | @IBOutlet weak var buttonName: NSButton! 13 | } 14 | -------------------------------------------------------------------------------- /JoyKeyMapper/Views/KeyMapList/SpecialKeyName.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SpecialKeyName.swift 3 | // JoyKeyMapper 4 | // 5 | // Created by magicien on 2019/07/25. 6 | // Copyright © 2019 DarkHorse. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import InputMethodKit 11 | 12 | let SpecialKey_BaseValue = 0x7000 13 | let SpecialKey_BrightnessUp = SpecialKey_BaseValue + Int(NX_KEYTYPE_BRIGHTNESS_UP) 14 | let SpecialKey_BrightnessDown = SpecialKey_BaseValue + Int(NX_KEYTYPE_BRIGHTNESS_DOWN) 15 | let SpecialKey_Power = SpecialKey_BaseValue + Int(NX_POWER_KEY) 16 | let SpecialKey_NumLock = SpecialKey_BaseValue + Int(NX_KEYTYPE_NUM_LOCK) 17 | let SpecialKey_Play = SpecialKey_BaseValue + Int(NX_KEYTYPE_PLAY) 18 | let SpecialKey_Next = SpecialKey_BaseValue + Int(NX_KEYTYPE_NEXT) 19 | let SpecialKey_Previous = SpecialKey_BaseValue + Int(NX_KEYTYPE_PREVIOUS) 20 | let SpecialKey_Fast = SpecialKey_BaseValue + Int(NX_KEYTYPE_FAST) 21 | let SpecialKey_Rewind = SpecialKey_BaseValue + Int(NX_KEYTYPE_REWIND) 22 | 23 | let SpecialKeyName: [Int:String] = [ 24 | kVK_ISO_Section: "Section", 25 | kVK_Return: "Return", 26 | kVK_Tab: "Tab", 27 | kVK_Space: "Space", 28 | kVK_Delete: "Delete", 29 | kVK_Escape: "Escape", 30 | kVK_Command: "⌘", 31 | kVK_Shift: "⇧", 32 | kVK_CapsLock: "CapsLock", 33 | kVK_Option: "⌥", 34 | kVK_Control: "⌃", 35 | kVK_RightShift: "Right⇧", 36 | kVK_RightOption: "Right⌥", 37 | kVK_RightControl: "Right⌃", 38 | kVK_Function: "fn", 39 | kVK_F1: "F1", 40 | kVK_F2: "F2", 41 | kVK_F3: "F3", 42 | kVK_F4: "F4", 43 | kVK_F5: "F5", 44 | kVK_F6: "F6", 45 | kVK_F7: "F7", 46 | kVK_F8: "F8", 47 | kVK_F9: "F9", 48 | kVK_F10: "F10", 49 | kVK_F11: "F11", 50 | kVK_F12: "F12", 51 | kVK_F13: "F13", 52 | kVK_F14: "F14", 53 | kVK_F15: "F15", 54 | kVK_F16: "F16", 55 | kVK_F17: "F17", 56 | kVK_F18: "F18", 57 | kVK_F19: "F19", 58 | kVK_F20: "F20", 59 | kVK_ANSI_Keypad0: "Keypad 0", 60 | kVK_ANSI_Keypad1: "Keypad 1", 61 | kVK_ANSI_Keypad2: "Keypad 2", 62 | kVK_ANSI_Keypad3: "Keypad 3", 63 | kVK_ANSI_Keypad4: "Keypad 4", 64 | kVK_ANSI_Keypad5: "Keypad 5", 65 | kVK_ANSI_Keypad6: "Keypad 6", 66 | kVK_ANSI_Keypad7: "Keypad 7", 67 | kVK_ANSI_Keypad8: "Keypad 8", 68 | kVK_ANSI_Keypad9: "Keypad 9", 69 | kVK_ANSI_KeypadMultiply: "Keypad *", 70 | kVK_ANSI_KeypadPlus: "Keypad +", 71 | kVK_ANSI_KeypadClear: "Keypad Clear", 72 | kVK_JIS_KeypadComma: "Keypad ,", 73 | kVK_ANSI_KeypadEnter: "Keypad Enter", 74 | kVK_ANSI_KeypadMinus: "Keypad -", 75 | kVK_ANSI_KeypadDivide: "Keypad /", 76 | kVK_ANSI_KeypadEquals: "Keypad =", 77 | kVK_ANSI_KeypadDecimal: "Keypad Decimal", 78 | kVK_VolumeUp: "VolumeUp", 79 | kVK_VolumeDown: "VolumeDown", 80 | kVK_Mute: "Mute", 81 | kVK_JIS_Yen: "¥", 82 | kVK_JIS_Underscore: "_", 83 | kVK_JIS_Eisu: "Eisu", 84 | kVK_JIS_Kana: "Kana", 85 | kVK_Help: "Help", 86 | kVK_Home: "Home", 87 | kVK_PageUp: "PageUp", 88 | kVK_PageDown: "PageDown", 89 | kVK_ForwardDelete: "ForwardDelete", 90 | kVK_End: "End", 91 | kVK_LeftArrow: "←", 92 | kVK_RightArrow: "→", 93 | kVK_DownArrow: "↓", 94 | kVK_UpArrow: "↑", 95 | SpecialKey_BrightnessUp: "BrightnessUp", 96 | SpecialKey_BrightnessDown: "BrightnessDown", 97 | SpecialKey_NumLock: "NumLock", 98 | SpecialKey_Play: "Play", 99 | SpecialKey_Next: "Next", 100 | SpecialKey_Previous: "Previous", 101 | SpecialKey_Fast: "Fast", 102 | SpecialKey_Rewind: "Rewind" 103 | ] 104 | 105 | let LocalizedSpecialKeyName = SpecialKeyName.mapValues { 106 | NSLocalizedString($0, comment: $0) 107 | } 108 | 109 | let systemDefinedKey: [Int: Int32] = [ 110 | kVK_VolumeUp: NX_KEYTYPE_SOUND_UP, 111 | kVK_VolumeDown: NX_KEYTYPE_SOUND_DOWN, 112 | SpecialKey_BrightnessUp: NX_KEYTYPE_BRIGHTNESS_UP, 113 | SpecialKey_BrightnessDown: NX_KEYTYPE_BRIGHTNESS_DOWN, 114 | // kVK_CapsLock: NX_KEYTYPE_CAPS_LOCK, 115 | // kVK_Help: NX_KEYTYPE_HELP, 116 | // NX_POWER_KEY 117 | kVK_Mute: NX_KEYTYPE_MUTE, 118 | // NX_UP_ARROW_KEY 119 | // NX_DOWN_ARROW_KEY 120 | // NX_KEYTYPE_NUM_LOCK, 121 | // NX_KEYTYPE_CONTRAST_UP 122 | // NX_KEYTYPE_CONTRAST_DOWN 123 | // NX_KEYTYPE_LAUNCH_PANEL 124 | // NX_KEYTYPE_EJECT 125 | // NX_KEYTYPE_VIDMIRROR 126 | SpecialKey_Play: NX_KEYTYPE_PLAY, 127 | SpecialKey_Next: NX_KEYTYPE_NEXT, 128 | SpecialKey_Previous: NX_KEYTYPE_PREVIOUS, 129 | SpecialKey_Fast: NX_KEYTYPE_FAST, 130 | SpecialKey_Rewind: NX_KEYTYPE_REWIND 131 | ] 132 | -------------------------------------------------------------------------------- /JoyKeyMapper/Views/KeyMapList/StickConfigCellView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StickConfigCellView.swift 3 | // JoyKeyMapper 4 | // 5 | // Created by magicien on 2019/08/07. 6 | // Copyright © 2019 DarkHorse. All rights reserved. 7 | // 8 | 9 | import AppKit 10 | 11 | protocol StickConfigCellViewDelegate { 12 | func typeDidChange(_ sender: NSPopUpButton) 13 | } 14 | 15 | class StickConfigCellView: NSTableCellView { 16 | var typeButton: NSPopUpButton 17 | 18 | override init(frame frameRect: NSRect) { 19 | self.typeButton = NSPopUpButton(frame: frameRect) 20 | super.init(frame: frameRect) 21 | 22 | let mouse = NSLocalizedString("Mouse", comment: "Mouse") 23 | let key = NSLocalizedString("Key", comment: "Key") 24 | self.typeButton.addItems(withTitles: [mouse, key]) 25 | 26 | self.addSubview(self.typeButton) 27 | } 28 | 29 | required init?(coder decoder: NSCoder) { 30 | fatalError("init(coder:) has not been implemented") 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /JoyKeyMapper/Views/KeyMapList/ViewController+NSOutlineViewDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController+NSOutlineViewDelegate.swift 3 | // JoyKeyMapper 4 | // 5 | // Created by magicien on 2019/07/23. 6 | // Copyright © 2019 DarkHorse. All rights reserved. 7 | // 8 | 9 | import AppKit 10 | import JoyConSwift 11 | 12 | let buttonNames: [JoyCon.Button: String] = [ 13 | .Up: "Up", 14 | .Right: "Right", 15 | .Down: "Down", 16 | .Left: "Left", 17 | .A: "A", 18 | .B: "B", 19 | .X: "X", 20 | .Y: "Y", 21 | .L: "L", 22 | .ZL: "ZL", 23 | .R: "R", 24 | .ZR: "ZR", 25 | .Minus: "Minus", 26 | .Plus: "Plus", 27 | .Capture: "Capture", 28 | .Home: "Home", 29 | .LStick: "LStick Push", 30 | .RStick: "RStick Push", 31 | .LeftSL: "Left SL", 32 | .LeftSR: "Left SR", 33 | .RightSL: "Right SL", 34 | .RightSR: "Right SR", 35 | .Start: "Start", 36 | .Select: "Select", 37 | ] 38 | let directionNames: [JoyCon.StickDirection: String] = [ 39 | .Up: "Up", 40 | .Right: "Right", 41 | .Down: "Down", 42 | .Left: "Left" 43 | ] 44 | let leftStickName = NSLocalizedString("Left Stick", comment: "Left Stick") 45 | let rightStickName = NSLocalizedString("Right Stick", comment: "Right Stick") 46 | 47 | let controllerButtons: [JoyCon.ControllerType: [JoyCon.Button]] = [ 48 | .JoyConL: [.Up, .Right, .Down, .Left, .LeftSL, .LeftSR, .L, .ZL, .Minus, .Capture, .LStick], 49 | .JoyConR: [.A, .B, .X, .Y, .RightSL, .RightSR, .R, .ZR, .Plus, .Home, .RStick], 50 | .ProController: [.A, .B, .X, .Y, .L, .ZL, .R, .ZR, .Up, .Right, .Down, .Left, .Minus, .Plus, .Capture, .Home, .LStick, .RStick], 51 | .FamicomController1: [.A, .B, .L, .R, .Up, .Right, .Down, .Left, .Start, .Select], 52 | .FamicomController2: [.A, .B, .L, .R, .Up, .Right, .Down, .Left], 53 | .SNESController: [.A, .B, .X, .Y, .L, .ZL, .R, .ZR, .Up, .Right, .Down, .Left, .Start, .Select], 54 | ] 55 | let numSticks: [JoyCon.ControllerType: Int] = [ 56 | .JoyConL: 1, 57 | .JoyConR: 1, 58 | .ProController: 2, 59 | .FamicomController1: 0, 60 | .FamicomController2: 0, 61 | .SNESController: 0 62 | ] 63 | let stickerDirections: [JoyCon.StickDirection] = [ 64 | .Up, .Right, .Down, .Left 65 | ] 66 | let stickTypes: [StickType] = [ 67 | .Key, .Mouse, .MouseWheel, .None 68 | ] 69 | 70 | let buttonNameColumnID = "buttonName" 71 | let buttonKeyColumnID = "buttonKey" 72 | 73 | class StickSpeedField: NSTextField { 74 | var config: KeyConfig 75 | var stick: JoyCon.Button 76 | 77 | init(frame frameRect: NSRect, config: KeyConfig, stick: JoyCon.Button) { 78 | self.config = config 79 | self.stick = stick 80 | super.init(frame: frameRect) 81 | } 82 | 83 | required init?(coder: NSCoder) { 84 | fatalError("init(coder:) has not been implemented") 85 | } 86 | 87 | override func textDidEndEditing(_ notification: Notification) { 88 | if self.stick == .LStick { 89 | self.config.leftStick?.speed = self.floatValue 90 | } else if self.stick == .RStick { 91 | self.config.rightStick?.speed = self.floatValue 92 | } 93 | } 94 | } 95 | 96 | extension ViewController: NSOutlineViewDelegate, NSOutlineViewDataSource, KeyConfigSetDelegate { 97 | func outlineView(_ outlineView: NSOutlineView, numberOfChildrenOfItem item: Any?) -> Int { 98 | guard self.selectedKeyConfig != nil else { return 0 } 99 | guard let controller = self.selectedController else { return 0 } 100 | guard let buttons = controllerButtons[controller.type] else { return 0 } 101 | guard let config = self.selectedKeyConfig else { return 0 } 102 | 103 | if controller.type == .unknown { 104 | return 0 105 | } 106 | 107 | if let indexOfItem = item as? Int { 108 | let stickIndex = indexOfItem - buttons.count 109 | 110 | // Stick settings 111 | if controller.type == .JoyConL { 112 | return self.numberOfChildItemOfStick(for: config.leftStick?.type) 113 | } 114 | 115 | if controller.type == .JoyConR { 116 | return self.numberOfChildItemOfStick(for: config.rightStick?.type) 117 | } 118 | 119 | if controller.type == .ProController { 120 | if stickIndex == 0 { 121 | return self.numberOfChildItemOfStick(for: config.leftStick?.type) 122 | } 123 | if stickIndex == 1 { 124 | return self.numberOfChildItemOfStick(for: config.rightStick?.type) 125 | } 126 | } 127 | 128 | return 0 129 | } 130 | 131 | return buttons.count + (numSticks[controller.type] ?? 0) 132 | } 133 | 134 | func numberOfChildItemOfStick(for type: String?) -> Int { 135 | guard let typeStr = type else { return 0 } 136 | 137 | switch(typeStr) { 138 | case StickType.Key.rawValue: 139 | return 4 140 | case StickType.Mouse.rawValue: 141 | return 1 142 | case StickType.MouseWheel.rawValue: 143 | return 1 144 | default: 145 | return 0 146 | } 147 | } 148 | 149 | func outlineView(_ outlineView: NSOutlineView, isItemExpandable item: Any) -> Bool { 150 | guard let controller = self.selectedController else { return false } 151 | guard let config = self.selectedKeyConfig else { return false } 152 | guard let buttons = controllerButtons[controller.type] else { return false } 153 | guard let itemIndex = item as? Int else { return false } 154 | 155 | let stickIndex = itemIndex - buttons.count 156 | 157 | if stickIndex < 0 { 158 | return false 159 | } 160 | 161 | if controller.type == .JoyConL { 162 | return self.isStickItemExpandable(for: config.leftStick?.type) 163 | } 164 | 165 | if controller.type == .JoyConR { 166 | return self.isStickItemExpandable(for: config.rightStick?.type) 167 | } 168 | 169 | if controller.type == .ProController { 170 | if stickIndex == 0 { 171 | return self.isStickItemExpandable(for: config.leftStick?.type) 172 | } 173 | if stickIndex == 1 { 174 | return self.isStickItemExpandable(for: config.rightStick?.type) 175 | } 176 | } 177 | 178 | return false 179 | } 180 | 181 | func isStickItemExpandable(for type: String?) -> Bool { 182 | guard let typeString = type else { return false } 183 | 184 | if typeString == StickType.None.rawValue { 185 | return false 186 | } 187 | 188 | return true 189 | } 190 | 191 | func outlineView(_ outlineView: NSOutlineView, child index: Int, ofItem item: Any?) -> Any { 192 | guard let parentItem = item as? Int else { return index } 193 | guard let controller = self.selectedController else { return false } 194 | guard let buttons = controllerButtons[controller.type] else { return false } 195 | 196 | let stickIndex = parentItem - buttons.count 197 | if stickIndex < 0 { return false } 198 | 199 | if controller.type == .JoyConL { 200 | return (JoyCon.Button.LStick, index) 201 | } 202 | 203 | if controller.type == .JoyConR { 204 | return (JoyCon.Button.RStick, index) 205 | } 206 | 207 | if controller.type == .ProController { 208 | if stickIndex == 0 { 209 | return (JoyCon.Button.LStick, index) 210 | } 211 | 212 | if stickIndex == 1 { 213 | return (JoyCon.Button.RStick, index) 214 | } 215 | 216 | return "unknown index" 217 | } 218 | 219 | return "unknown controller" 220 | } 221 | 222 | func stickDirectionView(stick: JoyCon.Button, column: NSTableColumn, row: Int) -> NSView? { 223 | guard let keyConfig = self.selectedKeyConfig else { return nil } 224 | 225 | var stickConfig: StickConfig 226 | if stick == .LStick { 227 | guard let conf = keyConfig.leftStick else { return nil } 228 | stickConfig = conf 229 | } else if stick == .RStick { 230 | guard let conf = keyConfig.rightStick else { return nil } 231 | stickConfig = conf 232 | } else { 233 | return nil 234 | } 235 | 236 | if column.identifier.rawValue == buttonNameColumnID { 237 | guard let itemView = self.configTableView.makeView(withIdentifier: column.identifier, owner: self) as? ButtonNameCellView else { 238 | return nil 239 | } 240 | 241 | let view = NSTextView(frame: NSRect(origin: CGPoint.zero, size: itemView.frame.size)) 242 | view.isEditable = false 243 | view.font = itemView.buttonName.font 244 | view.backgroundColor = .clear 245 | 246 | if stick == .LStick { 247 | view.string = leftStickName 248 | } else if stick == .RStick { 249 | view.string = rightStickName 250 | } 251 | 252 | return view 253 | } 254 | 255 | if column.identifier.rawValue == buttonKeyColumnID { 256 | guard let itemView = self.configTableView.makeView(withIdentifier: column.identifier, owner: self) as? NSTableCellView else { 257 | return nil 258 | } 259 | 260 | let selection = NSPopUpButton(frame: NSRect(origin: CGPoint.zero, size: itemView.frame.size)) 261 | stickTypes.forEach { type in 262 | selection.addItem(withTitle: NSLocalizedString(type.rawValue, comment: "")) 263 | selection.lastItem?.representedObject = type 264 | } 265 | 266 | if stickConfig.type == StickType.Mouse.rawValue { 267 | selection.selectItem(at: 1) 268 | } else if stickConfig.type == StickType.MouseWheel.rawValue { 269 | selection.selectItem(at: 2) 270 | } else if stickConfig.type == StickType.None.rawValue { 271 | selection.selectItem(at: 3) 272 | } else { 273 | // Default: .Key 274 | selection.selectItem(at: 0) 275 | } 276 | 277 | if stick == .LStick { 278 | selection.action = Selector(("leftStickTypeDidChange:")) 279 | } else if stick == .RStick { 280 | selection.action = Selector(("rightStickTypeDidChange:")) 281 | } 282 | selection.target = self 283 | 284 | return selection 285 | } 286 | 287 | return nil 288 | } 289 | 290 | func stickChildView(stick: JoyCon.Button, column: NSTableColumn, row: Int) -> NSView? { 291 | guard let config = self.selectedKeyConfig else { return nil } 292 | 293 | var type: String 294 | if stick == .LStick { 295 | guard let typeString = config.leftStick?.type else { return nil } 296 | type = typeString 297 | } else if stick == .RStick { 298 | guard let typeString = config.rightStick?.type else { return nil } 299 | type = typeString 300 | } else { 301 | return nil 302 | } 303 | 304 | if type == StickType.Key.rawValue { 305 | return self.stickDirectionKeyView(stick: stick, column: column, row: row) 306 | } else if type == StickType.Mouse.rawValue || type == StickType.MouseWheel.rawValue { 307 | return self.stickMouseView(stick: stick, column: column, row: row) 308 | } 309 | 310 | return nil 311 | } 312 | 313 | func stickMouseView(stick: JoyCon.Button, column: NSTableColumn, row: Int) -> NSView? { 314 | guard self.selectedController != nil else { return nil } 315 | guard let keyConfig = self.selectedKeyConfig else { return nil } 316 | 317 | var stickConfig: StickConfig 318 | if stick == .LStick { 319 | guard let conf = keyConfig.leftStick else { return nil } 320 | stickConfig = conf 321 | } else if stick == .RStick { 322 | guard let conf = keyConfig.rightStick else { return nil } 323 | stickConfig = conf 324 | } else { 325 | return nil 326 | } 327 | 328 | if column.identifier.rawValue == buttonNameColumnID { 329 | guard let itemView = self.configTableView.makeView(withIdentifier: column.identifier, owner: self) as? ButtonNameCellView else { 330 | return nil 331 | } 332 | 333 | let view = NSTextView(frame: NSRect(origin: CGPoint.zero, size: itemView.frame.size)) 334 | view.isEditable = false 335 | view.font = itemView.buttonName.font 336 | view.string = NSLocalizedString("Speed", comment: "Speed") 337 | view.backgroundColor = .clear 338 | 339 | return view 340 | } 341 | 342 | if column.identifier.rawValue == buttonKeyColumnID { 343 | guard let itemView = self.configTableView.makeView(withIdentifier: column.identifier, owner: self) as? NSTableCellView else { 344 | return nil 345 | } 346 | 347 | let field = StickSpeedField(frame: NSRect(origin: CGPoint.zero, size: itemView.frame.size), config: keyConfig, stick: stick) 348 | field.floatValue = stickConfig.speed 349 | field.isEditable = true 350 | field.formatter = NumberFormatter() 351 | field.alignment = .right 352 | 353 | return field 354 | } 355 | 356 | return nil 357 | } 358 | 359 | func stickDirectionKeyView(stick: JoyCon.Button, column: NSTableColumn, row: Int) -> NSView? { 360 | guard self.selectedController != nil else { return nil } 361 | guard let keyConfig = self.selectedKeyConfig else { return nil } 362 | 363 | var stickConfig: StickConfig 364 | if stick == .LStick { 365 | guard let conf = keyConfig.leftStick else { return nil } 366 | stickConfig = conf 367 | } else if stick == .RStick { 368 | guard let conf = keyConfig.rightStick else { return nil } 369 | stickConfig = conf 370 | } else { 371 | return nil 372 | } 373 | 374 | guard let keyMaps = stickConfig.keyMaps else { return nil } 375 | let direction = stickerDirections[row] 376 | let directionName = directionNames[direction] ?? "" 377 | guard let keyMap = keyMaps.first(where: { map in 378 | guard let keyMap = map as? KeyMap else { return false } 379 | return keyMap.button == directionName 380 | }) as? KeyMap else { return nil } 381 | 382 | if column.identifier.rawValue == buttonNameColumnID { 383 | guard let itemView = self.configTableView.makeView(withIdentifier: column.identifier, owner: self) as? ButtonNameCellView else { 384 | return nil 385 | } 386 | 387 | itemView.buttonName.state = keyMap.isEnabled ? .on : .off 388 | itemView.buttonName.title = NSLocalizedString(directionName, comment: "") 389 | if stick == .LStick { 390 | itemView.buttonName.action = Selector(("leftStickDirectionCheckDidChange:")) 391 | } else if stick == .RStick { 392 | itemView.buttonName.action = Selector(("rightStickDirectionCheckDidChange:")) 393 | } 394 | itemView.buttonName.target = self 395 | 396 | return itemView 397 | } 398 | 399 | if column.identifier.rawValue == buttonKeyColumnID { 400 | guard let itemView = self.configTableView.makeView(withIdentifier: column.identifier, owner: self) as? NSTableCellView else { 401 | return nil 402 | } 403 | 404 | let keyName = convertKeyName(keyMap: keyMap) 405 | itemView.textField?.stringValue = keyName 406 | 407 | return itemView 408 | } 409 | 410 | return nil 411 | } 412 | 413 | func outlineView(_ outlineView: NSOutlineView, viewFor tableColumn: NSTableColumn?, item: Any) -> NSView? { 414 | guard let column = tableColumn else { return nil } 415 | 416 | if let (stickButton, stickIndex) = item as? (JoyCon.Button, Int) { 417 | return self.stickChildView(stick: stickButton, column: column, row: stickIndex) 418 | } 419 | 420 | guard let row = item as? Int else { return nil } 421 | guard let controller = self.selectedController else { return nil } 422 | guard let config = self.selectedKeyConfig else { return nil } 423 | guard let buttons = controllerButtons[controller.type] else { return nil } 424 | if row >= buttons.count { 425 | if controller.type == .JoyConL { 426 | return self.stickDirectionView(stick: .LStick, column: column, row: row) 427 | } 428 | if controller.type == .JoyConR { 429 | return self.stickDirectionView(stick: .RStick, column: column, row: row) 430 | } 431 | if controller.type == .ProController { 432 | if row - buttons.count == 0 { 433 | return self.stickDirectionView(stick: .LStick, column: column, row: row) 434 | } 435 | return self.stickDirectionView(stick: .RStick, column: column, row: row) 436 | } 437 | return nil 438 | } 439 | let button = buttons[row] 440 | 441 | let keyMap = config.keyMaps?.first(where: { map in 442 | guard let keyMap = map as? KeyMap else { return false } 443 | return keyMap.button == buttonNames[button] 444 | }) as? KeyMap 445 | 446 | if column.identifier.rawValue == buttonNameColumnID { 447 | guard let itemView = outlineView.makeView(withIdentifier: column.identifier, owner: self) as? ButtonNameCellView else { 448 | return nil 449 | } 450 | 451 | itemView.buttonName.state = (keyMap?.isEnabled ?? false) ? .on : .off 452 | itemView.buttonName.title = NSLocalizedString(buttonNames[button] ?? "", comment: "") 453 | itemView.buttonName.action = Selector(("checkDidChange:")) 454 | itemView.buttonName.target = self 455 | 456 | return itemView 457 | } 458 | 459 | if column.identifier.rawValue == buttonKeyColumnID { 460 | guard let itemView = outlineView.makeView(withIdentifier: column.identifier, owner: self) as? NSTableCellView else { 461 | return nil 462 | } 463 | 464 | let keyName = convertKeyName(keyMap: keyMap) 465 | itemView.textField?.stringValue = keyName 466 | 467 | return itemView 468 | } 469 | 470 | return nil 471 | } 472 | 473 | @IBAction func didClick(_ sender: AnyObject) { 474 | guard self.keyDownHandler == nil else { return } 475 | guard let type = self.selectedController?.type else { return } 476 | 477 | let selectedRow = self.configTableView.selectedRow 478 | let item = self.configTableView.item(atRow: selectedRow) 479 | 480 | if let rowIndex = item as? Int { 481 | guard let buttons = controllerButtons[type] else { return } 482 | guard rowIndex < buttons.count else { return } 483 | let button = buttons[rowIndex] 484 | self.didClick(button: button) 485 | return 486 | } 487 | 488 | if let (stick, rowIndex) = item as? (JoyCon.Button, Int) { 489 | let type: String 490 | if stick == .LStick { 491 | guard let typeString = self.selectedKeyConfig?.leftStick?.type else { return } 492 | type = typeString 493 | } else if stick == .RStick { 494 | guard let typeString = self.selectedKeyConfig?.rightStick?.type else { return } 495 | type = typeString 496 | } else { 497 | return 498 | } 499 | 500 | if type == StickType.Key.rawValue { 501 | let direction = stickerDirections[rowIndex] 502 | self.didClick(stick: stick, direction: direction) 503 | } 504 | return 505 | } 506 | } 507 | 508 | func didClick(button: JoyCon.Button) { 509 | guard let buttonName = buttonNames[button] else { return } 510 | 511 | var keyMap = self.selectedKeyConfig?.keyMaps?.first(where: { map in 512 | guard let keyMap = map as? KeyMap else { return false } 513 | return keyMap.button == buttonName 514 | }) as? KeyMap 515 | if keyMap == nil { 516 | keyMap = self.appDelegate?.dataManager?.createKeyMap() 517 | keyMap?.button = buttonName 518 | guard let map = keyMap else { return } 519 | self.selectedKeyConfig?.addToKeyMaps(map) 520 | } 521 | guard let map = keyMap else { return } 522 | 523 | guard let controller = self.storyboard?.instantiateController(withIdentifier: "KeyConfigViewController") as? KeyConfigViewController else { return } 524 | controller.keyMap = map 525 | controller.delegate = self 526 | 527 | self.presentAsSheet(controller) 528 | } 529 | 530 | func didClick(stick: JoyCon.Button, direction: JoyCon.StickDirection) { 531 | guard let directionName = directionNames[direction] else { return } 532 | 533 | var stickConfigData: StickConfig? = nil 534 | if stick == .LStick { 535 | stickConfigData = self.selectedKeyConfig?.leftStick 536 | } else if stick == .RStick { 537 | stickConfigData = self.selectedKeyConfig?.rightStick 538 | } 539 | guard let stickConfig = stickConfigData else { return } 540 | 541 | var keyMap: KeyMap? = stickConfig.keyMaps?.first(where: { map in 542 | guard let keyMap = map as? KeyMap else { return false } 543 | return keyMap.button == directionName 544 | }) as? KeyMap 545 | if keyMap == nil { 546 | keyMap = self.appDelegate?.dataManager?.createKeyMap() 547 | keyMap?.button = directionName 548 | guard let map = keyMap else { return } 549 | stickConfig.addToKeyMaps(map) 550 | } 551 | guard let map = keyMap else { return } 552 | 553 | guard let controller = self.storyboard?.instantiateController(withIdentifier: "KeyConfigViewController") as? KeyConfigViewController else { return } 554 | controller.keyMap = map 555 | controller.delegate = self 556 | 557 | self.presentAsSheet(controller) 558 | } 559 | 560 | func setKeyConfig(controller: KeyConfigViewController) { 561 | self.configTableView.reloadData() 562 | } 563 | 564 | @objc func checkDidChange(_ sender: NSButton) { 565 | guard let controller = self.selectedController else { return } 566 | guard let config = self.selectedKeyConfig else { return } 567 | guard let keyMaps = config.keyMaps else { return } 568 | 569 | let result = keyMaps.first(where: { map in 570 | guard let keyMap = map as? KeyMap else { return false } 571 | return keyMap.button == sender.title // TODO: Use consistent value instead of "title" 572 | }) 573 | guard let keyMapData = result as? KeyMap else { 574 | guard let keyMap = self.appDelegate?.dataManager?.createKeyMap() else { return } 575 | keyMap.button = sender.title // TODO: Use consistent value instead of "title" 576 | keyMap.isEnabled = sender.state == .on 577 | config.addToKeyMaps(keyMap) 578 | controller.updateKeyMap() 579 | 580 | return 581 | } 582 | keyMapData.isEnabled = sender.state == .on 583 | 584 | controller.updateKeyMap() 585 | } 586 | 587 | @objc func leftStickTypeDidChange(_ sender: NSPopUpButton) { 588 | guard let config = self.selectedKeyConfig else { return } 589 | let type = sender.selectedItem?.representedObject as? StickType 590 | config.leftStick?.type = type?.rawValue ?? "" 591 | self.configTableView.reloadData() 592 | self.selectedController?.updateKeyMap() 593 | } 594 | 595 | @objc func rightStickTypeDidChange(_ sender: NSPopUpButton) { 596 | guard let config = self.selectedKeyConfig else { return } 597 | let type = sender.selectedItem?.representedObject as? StickType 598 | config.rightStick?.type = type?.rawValue ?? "" 599 | self.configTableView.reloadData() 600 | self.selectedController?.updateKeyMap() 601 | } 602 | 603 | @objc func leftStickDirectionCheckDidChange(_ sender: NSButton) { 604 | guard let controller = self.selectedController else { return } 605 | guard let config = self.selectedKeyConfig else { return } 606 | guard let keyMaps = config.leftStick?.keyMaps else { return } 607 | 608 | let result = keyMaps.first(where: { map in 609 | guard let keyMap = map as? KeyMap else { return false } 610 | return keyMap.button == sender.title // TODO: Use consistent value instead of "title" 611 | }) 612 | guard let keyMapData = result as? KeyMap else { return } 613 | keyMapData.isEnabled = sender.state == .on 614 | 615 | controller.updateKeyMap() 616 | } 617 | 618 | @objc func rightStickDirectionCheckDidChange(_ sender: NSButton) { 619 | guard let controller = self.selectedController else { return } 620 | guard let config = self.selectedKeyConfig else { return } 621 | guard let keyMaps = config.rightStick?.keyMaps else { return } 622 | 623 | let result = keyMaps.first(where: { map in 624 | guard let keyMap = map as? KeyMap else { return false } 625 | return keyMap.button == sender.title // TODO: Use consistent value instead of "title" 626 | }) 627 | guard let keyMapData = result as? KeyMap else { return } 628 | keyMapData.isEnabled = sender.state == .on 629 | 630 | controller.updateKeyMap() 631 | } 632 | 633 | @objc func leftStickSpeedDidChange(_ sender: NSTextField) { 634 | guard let config = self.selectedKeyConfig else { return } 635 | config.leftStick?.speed = sender.floatValue 636 | } 637 | 638 | @objc func rightStickSpeedDidChange(_ sender: NSTextField) { 639 | guard let config = self.selectedKeyConfig else { return } 640 | config.rightStick?.speed = sender.floatValue 641 | } 642 | } 643 | -------------------------------------------------------------------------------- /JoyKeyMapper/Views/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // JoyKeyMapper 4 | // 5 | // Created by magicien on 2019/07/14. 6 | // Copyright © 2019 DarkHorse. All rights reserved. 7 | // 8 | 9 | import AppKit 10 | import InputMethodKit 11 | import JoyConSwift 12 | 13 | class ViewController: NSViewController { 14 | 15 | @IBOutlet weak var controllerCollectionView: NSCollectionView! 16 | @IBOutlet weak var appTableView: NSTableView! 17 | @IBOutlet weak var appAddRemoveButton: NSSegmentedControl! 18 | @IBOutlet weak var configTableView: NSOutlineView! 19 | 20 | var appDelegate: AppDelegate? { 21 | return NSApplication.shared.delegate as? AppDelegate 22 | } 23 | var selectedController: GameController? { 24 | didSet { 25 | self.appTableView.reloadData() 26 | self.configTableView.reloadData() 27 | self.updateAppAddRemoveButtonState() 28 | } 29 | } 30 | var selectedControllerData: ControllerData? { 31 | return self.selectedController?.data 32 | } 33 | var selectedAppConfig: AppConfig? { 34 | guard let data = self.selectedControllerData else { 35 | return nil 36 | } 37 | let row = self.appTableView.selectedRow 38 | if row < 1 { 39 | return nil 40 | } 41 | return data.appConfigs?[row - 1] as? AppConfig 42 | } 43 | var selectedKeyConfig: KeyConfig? { 44 | if self.appTableView.selectedRow < 0 { 45 | return nil 46 | } 47 | return self.selectedAppConfig?.config ?? self.selectedControllerData?.defaultConfig 48 | } 49 | var keyDownHandler: Any? 50 | 51 | override func viewDidLoad() { 52 | super.viewDidLoad() 53 | 54 | if self.controllerCollectionView == nil { return } 55 | 56 | self.controllerCollectionView.delegate = self 57 | self.controllerCollectionView.dataSource = self 58 | 59 | self.appTableView.delegate = self 60 | self.appTableView.dataSource = self 61 | 62 | self.configTableView.delegate = self 63 | self.configTableView.dataSource = self 64 | 65 | self.updateAppAddRemoveButtonState() 66 | 67 | NotificationCenter.default.addObserver(self, selector: #selector(controllerAdded), name: .controllerAdded, object: nil) 68 | NotificationCenter.default.addObserver(self, selector: #selector(controllerRemoved), name: .controllerRemoved, object: nil) 69 | NotificationCenter.default.addObserver(self, selector: #selector(controllerConnected), name: .controllerConnected, object: nil) 70 | NotificationCenter.default.addObserver(self, selector: #selector(controllerDisconnected), name: .controllerDisconnected, object: nil) 71 | NotificationCenter.default.addObserver(self, selector: #selector(controllerIconChanged), name: .controllerIconChanged, object: nil) 72 | } 73 | 74 | override func viewDidDisappear() { 75 | 76 | } 77 | 78 | override var representedObject: Any? { 79 | didSet { 80 | // Update the view, if already loaded. 81 | } 82 | } 83 | 84 | // MARK: - Apps 85 | 86 | @IBAction func clickAppSegmentButton(_ sender: NSSegmentedControl) { 87 | let selectedSegment = sender.selectedSegment 88 | 89 | if selectedSegment == 0 { 90 | self.addApp() 91 | } else if selectedSegment == 1 { 92 | self.removeApp() 93 | } 94 | } 95 | 96 | func updateAppAddRemoveButtonState() { 97 | if self.selectedController == nil { 98 | self.appAddRemoveButton.setEnabled(false, forSegment: 0) 99 | self.appAddRemoveButton.setEnabled(false, forSegment: 1) 100 | } else if self.appTableView.selectedRow < 1 { 101 | self.appAddRemoveButton.setEnabled(true, forSegment: 0) 102 | self.appAddRemoveButton.setEnabled(false, forSegment: 1) 103 | } else { 104 | self.appAddRemoveButton.setEnabled(true, forSegment: 0) 105 | self.appAddRemoveButton.setEnabled(true, forSegment: 1) 106 | } 107 | } 108 | 109 | func addApp() { 110 | guard let controller = self.selectedController else { return } 111 | 112 | let panel = NSOpenPanel() 113 | panel.message = NSLocalizedString("Choose an app to add", comment: "Choosing app message") 114 | panel.allowsMultipleSelection = false 115 | panel.canChooseDirectories = false 116 | panel.canCreateDirectories = false 117 | panel.canChooseFiles = true 118 | panel.allowedFileTypes = ["app"] 119 | panel.directoryURL = URL(fileURLWithPath: "/Applications") 120 | panel.begin { [weak self] response in 121 | if response == .OK { 122 | guard let url = panel.url else { return } 123 | controller.addApp(url: url) 124 | self?.appTableView.reloadData() 125 | } 126 | } 127 | } 128 | 129 | func removeApp() { 130 | guard let controller = self.selectedController else { return } 131 | guard let appConfig = self.selectedAppConfig else { return } 132 | let appName = self.convertAppName(appConfig.app?.displayName) 133 | 134 | let alert = NSAlert() 135 | alert.alertStyle = .warning 136 | alert.messageText = String.localizedStringWithFormat(NSLocalizedString("Do you really want to delete the settings for %@?", comment: "Do you really want to delete the settings for ?"), appName) 137 | alert.addButton(withTitle: NSLocalizedString("Cancel", comment: "Cancel")) 138 | alert.addButton(withTitle: NSLocalizedString("OK", comment: "OK")) 139 | let result = alert.runModal() 140 | 141 | if result == .alertSecondButtonReturn { 142 | controller.removeApp(appConfig) 143 | self.appTableView.reloadData() 144 | self.configTableView.reloadData() 145 | } 146 | } 147 | 148 | // MARK: - Controllers 149 | 150 | @objc func controllerAdded() { 151 | DispatchQueue.main.async { [weak self] in 152 | self?.controllerCollectionView.reloadData() 153 | } 154 | } 155 | 156 | @objc func controllerConnected() { 157 | DispatchQueue.main.async { [weak self] in 158 | self?.controllerCollectionView.reloadData() 159 | } 160 | } 161 | 162 | @objc func controllerDisconnected() { 163 | DispatchQueue.main.async { [weak self] in 164 | self?.controllerCollectionView.reloadData() 165 | } 166 | } 167 | 168 | @objc func controllerRemoved(_ notification: NSNotification) { 169 | guard let gameController = notification.object as? GameController else { return } 170 | 171 | DispatchQueue.main.async { [weak self] in 172 | guard let _self = self else { return } 173 | let numItems = _self.controllerCollectionView.numberOfItems(inSection: 0) 174 | for i in 0.. 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIconFile 10 | 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleVersion 22 | 1 23 | LSApplicationCategoryType 24 | public.app-category.utilities 25 | LSMinimumSystemVersion 26 | $(MACOSX_DEPLOYMENT_TARGET) 27 | LSUIElement 28 | 29 | NSHumanReadableCopyright 30 | Copyright © 2020 DarkHorse. All rights reserved. 31 | NSMainStoryboardFile 32 | Main 33 | NSPrincipalClass 34 | NSApplication 35 | NSSupportsAutomaticTermination 36 | 37 | NSSupportsSuddenTermination 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /JoyKeyMapperLauncher/JoyKeyMapperLauncher.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.files.user-selected.read-only 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 magicien 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment the next line to define a global platform for your project 2 | platform :osx, '10.14' 3 | 4 | target 'JoyKeyMapper' do 5 | # Comment the next line if you don't want to use dynamic frameworks 6 | use_frameworks! 7 | 8 | # Pods for JoyKeyMapper 9 | pod 'JoyConSwift', '0.2.1' 10 | 11 | end 12 | -------------------------------------------------------------------------------- /Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - JoyConSwift (0.2.1) 3 | 4 | DEPENDENCIES: 5 | - JoyConSwift (= 0.2.1) 6 | 7 | SPEC REPOS: 8 | trunk: 9 | - JoyConSwift 10 | 11 | SPEC CHECKSUMS: 12 | JoyConSwift: a8025cc234394cbc38fc1b48cfe1de8de6f66551 13 | 14 | PODFILE CHECKSUM: f077d9a7d63235de745659f9e64c4c8ec6de86af 15 | 16 | COCOAPODS: 1.9.3 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [日本語](https://github.com/magicien/JoyKeyMapper/blob/master/lang/ja/README.md) 2 | 3 | # JoyKeyMapper 4 | Nintendo Joy-Con/ProController Key mapper for macOS 5 | 6 | ![screenshot](https://github.com/magicien/JoyKeyMapper/blob/master/resources/screenshot/screenshot_1.png) 7 | 8 | ## Install from App Store (Recommended) 9 | 10 | [Mac App Store page](https://apps.apple.com/app/joykeymapper/id1511416593) 11 | 12 | ## Install from Github 13 | 14 | 1. Download a dmg file (JoyKeyMapper-vX.X.X.dmg) from [Releases](https://github.com/magicien/JoyKeyMapper/releases) 15 | 16 | 2. Copy JoyKeyMapper.app to Applications 17 | ![screenshot_install](https://github.com/magicien/JoyKeyMapper/blob/master/resources/screenshot/screenshot_2.png) 18 | 19 | ## How to use 20 | 21 | 1. Connect your controller via Bluetooth 22 | 23 | 1.1. Open "System Preferences" > "Bluetooth" on your Mac 24 | 25 | 1.2. Hold down your controller's sync button 26 | 27 | 1.3. Click the "Connect" button 28 | 29 | ![screenshot_usage_1_3](https://github.com/magicien/JoyKeyMapper/blob/master/resources/screenshot/screenshot_3.png) 30 | 31 | 2. Set key mappings 32 | 33 | 2.1 Launch JoyKeyMapper.app 34 | 35 | 2.2 Choose the "Settings..." menu 36 | 37 | ![screenshot_usage_2_2](https://github.com/magicien/JoyKeyMapper/blob/master/resources/screenshot/screenshot_4.png) 38 | 39 | 2.3 Add apps to set key mappings (optional) 40 | 41 | ![screenshot_usage_2_3](https://github.com/magicien/JoyKeyMapper/blob/master/resources/screenshot/screenshot_5.png) 42 | 43 | 2.4 Click a button to set a key 44 | 45 | ![screenshot_usage_2_4_1](https://github.com/magicien/JoyKeyMapper/blob/master/resources/screenshot/screenshot_6.png) 46 | 47 | ![screenshot_usage_2_4_2](https://github.com/magicien/JoyKeyMapper/blob/master/resources/screenshot/screenshot_7.png) 48 | 49 | 3. Allow JoyKeyMapper to control Accessibility 50 | 51 | 3.1 When you start using your controller, you will see this alert. 52 | 53 | ![screenshot_usage_3_1](https://github.com/magicien/JoyKeyMapper/blob/master/resources/screenshot/screenshot_8.png) 54 | 55 | 3.2 Open "System Preferences" > "Security & Privacy" > "Privacy" tab > "Accessibility", and check "JoyKeyMapper.app" 56 | 57 | ![screenshot_usage_3_2](https://github.com/magicien/JoyKeyMapper/blob/master/resources/screenshot/screenshot_9.png) 58 | 59 | ## See also 60 | 61 | [JoyConSwift](https://github.com/magicien/JoyConSwift) - IOKit wrapper for Nintendo Joy-Con and ProController (macOS, Swift) 62 | -------------------------------------------------------------------------------- /lang/ja/README.md: -------------------------------------------------------------------------------- 1 | [English](https://github.com/magicien/JoyKeyMapper/blob/master/README.md) 2 | 3 | # JoyKeyMapper 4 | macOS用Joy-Con/ProControllerキーマッピングツール 5 | 6 | ![screenshot](https://github.com/magicien/JoyKeyMapper/blob/master/lang/ja/screenshot_1.png) 7 | 8 | ## App Store からインストール(推奨) 9 | 10 | [Mac App Store のページ](https://apps.apple.com/app/joykeymapper/id1511416593) 11 | 12 | ## Github からインストール 13 | 14 | 1. [Releases](https://github.com/magicien/JoyKeyMapper/releases) からdmgファイル(JoyKeyMapper-vX.X.X.dmg)をダウンロードする 15 | 16 | 2. JoyKeyMapper.appをApplicationsへコピーする 17 | ![screenshot_install](https://github.com/magicien/JoyKeyMapper/blob/master/lang/ja/screenshot_2.png) 18 | 19 | ## 使い方 20 | 21 | 1. BluetoothでコントローラをMacへ接続する 22 | 23 | 1.1. Macで「システム環境設定」>「Bluetooth」を開く 24 | 25 | 1.2. コントローラのシンクロボタンを長押しする 26 | 27 | 1.3. Macで「接続」ボタンを押す 28 | 29 | ![screenshot_usage_1_3](https://github.com/magicien/JoyKeyMapper/blob/master/lang/ja/screenshot_3.png) 30 | 31 | 2. キー設定 32 | 33 | 2.1 JoyKeyMapper.app を起動する 34 | 35 | 2.2 メニューから「設定...」を選択する 36 | 37 | ![screenshot_usage_2_2](https://github.com/magicien/JoyKeyMapper/blob/master/lang/ja/screenshot_4.png) 38 | 39 | 2.3 キー設定を行うアプリケーションを追加する(任意) 40 | 41 | ![screenshot_usage_2_3](https://github.com/magicien/JoyKeyMapper/blob/master/lang/ja/screenshot_5.png) 42 | 43 | 2.4 キー設定を行うボタンをクリックする 44 | 45 | ![screenshot_usage_2_4_1](https://github.com/magicien/JoyKeyMapper/blob/master/lang/ja/screenshot_6.png) 46 | 47 | ![screenshot_usage_2_4_2](https://github.com/magicien/JoyKeyMapper/blob/master/lang/ja/screenshot_7.png) 48 | 49 | 3. JoyKeyMapper に「アクセシビリティ」の許可をする 50 | 51 | 3.1 コントローラ使用時にアラートが表示される 52 | 53 | ![screenshot_usage_3_1](https://github.com/magicien/JoyKeyMapper/blob/master/lang/ja/screenshot_8.png) 54 | 55 | 3.2 「システム環境設定」>「セキュリティとプライバシー」>「プライバシー」タブ>「アクセシビリティ」を選択し、「JoyKeyMapper.app」にチェックを入れる 56 | 57 | ![screenshot_usage_3_2](https://github.com/magicien/JoyKeyMapper/blob/master/lang/ja/screenshot_9.png) 58 | 59 | ## 参考 60 | 61 | [JoyConSwift](https://github.com/magicien/JoyConSwift) - Joy-Con/ProController用IOKitラッパー (macOS, Swift) 62 | -------------------------------------------------------------------------------- /lang/ja/screenshot_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magicien/JoyKeyMapper/cbfa688c1d00e6ecf563a77c5b5c06959db2fed3/lang/ja/screenshot_1.png -------------------------------------------------------------------------------- /lang/ja/screenshot_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magicien/JoyKeyMapper/cbfa688c1d00e6ecf563a77c5b5c06959db2fed3/lang/ja/screenshot_2.png -------------------------------------------------------------------------------- /lang/ja/screenshot_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magicien/JoyKeyMapper/cbfa688c1d00e6ecf563a77c5b5c06959db2fed3/lang/ja/screenshot_3.png -------------------------------------------------------------------------------- /lang/ja/screenshot_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magicien/JoyKeyMapper/cbfa688c1d00e6ecf563a77c5b5c06959db2fed3/lang/ja/screenshot_4.png -------------------------------------------------------------------------------- /lang/ja/screenshot_5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magicien/JoyKeyMapper/cbfa688c1d00e6ecf563a77c5b5c06959db2fed3/lang/ja/screenshot_5.png -------------------------------------------------------------------------------- /lang/ja/screenshot_6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magicien/JoyKeyMapper/cbfa688c1d00e6ecf563a77c5b5c06959db2fed3/lang/ja/screenshot_6.png -------------------------------------------------------------------------------- /lang/ja/screenshot_7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magicien/JoyKeyMapper/cbfa688c1d00e6ecf563a77c5b5c06959db2fed3/lang/ja/screenshot_7.png -------------------------------------------------------------------------------- /lang/ja/screenshot_8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magicien/JoyKeyMapper/cbfa688c1d00e6ecf563a77c5b5c06959db2fed3/lang/ja/screenshot_8.png -------------------------------------------------------------------------------- /lang/ja/screenshot_9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magicien/JoyKeyMapper/cbfa688c1d00e6ecf563a77c5b5c06959db2fed3/lang/ja/screenshot_9.png -------------------------------------------------------------------------------- /resources/app-icon.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magicien/JoyKeyMapper/cbfa688c1d00e6ecf563a77c5b5c06959db2fed3/resources/app-icon.ai -------------------------------------------------------------------------------- /resources/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magicien/JoyKeyMapper/cbfa688c1d00e6ecf563a77c5b5c06959db2fed3/resources/background.png -------------------------------------------------------------------------------- /resources/battery.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magicien/JoyKeyMapper/cbfa688c1d00e6ecf563a77c5b5c06959db2fed3/resources/battery.ai -------------------------------------------------------------------------------- /resources/famicon.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magicien/JoyKeyMapper/cbfa688c1d00e6ecf563a77c5b5c06959db2fed3/resources/famicon.ai -------------------------------------------------------------------------------- /resources/joycon_left.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magicien/JoyKeyMapper/cbfa688c1d00e6ecf563a77c5b5c06959db2fed3/resources/joycon_left.ai -------------------------------------------------------------------------------- /resources/joycon_right.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magicien/JoyKeyMapper/cbfa688c1d00e6ecf563a77c5b5c06959db2fed3/resources/joycon_right.ai -------------------------------------------------------------------------------- /resources/procon.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magicien/JoyKeyMapper/cbfa688c1d00e6ecf563a77c5b5c06959db2fed3/resources/procon.ai -------------------------------------------------------------------------------- /resources/screenshot/screenshot_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magicien/JoyKeyMapper/cbfa688c1d00e6ecf563a77c5b5c06959db2fed3/resources/screenshot/screenshot_1.png -------------------------------------------------------------------------------- /resources/screenshot/screenshot_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magicien/JoyKeyMapper/cbfa688c1d00e6ecf563a77c5b5c06959db2fed3/resources/screenshot/screenshot_2.png -------------------------------------------------------------------------------- /resources/screenshot/screenshot_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magicien/JoyKeyMapper/cbfa688c1d00e6ecf563a77c5b5c06959db2fed3/resources/screenshot/screenshot_3.png -------------------------------------------------------------------------------- /resources/screenshot/screenshot_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magicien/JoyKeyMapper/cbfa688c1d00e6ecf563a77c5b5c06959db2fed3/resources/screenshot/screenshot_4.png -------------------------------------------------------------------------------- /resources/screenshot/screenshot_5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magicien/JoyKeyMapper/cbfa688c1d00e6ecf563a77c5b5c06959db2fed3/resources/screenshot/screenshot_5.png -------------------------------------------------------------------------------- /resources/screenshot/screenshot_6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magicien/JoyKeyMapper/cbfa688c1d00e6ecf563a77c5b5c06959db2fed3/resources/screenshot/screenshot_6.png -------------------------------------------------------------------------------- /resources/screenshot/screenshot_7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magicien/JoyKeyMapper/cbfa688c1d00e6ecf563a77c5b5c06959db2fed3/resources/screenshot/screenshot_7.png -------------------------------------------------------------------------------- /resources/screenshot/screenshot_8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magicien/JoyKeyMapper/cbfa688c1d00e6ecf563a77c5b5c06959db2fed3/resources/screenshot/screenshot_8.png -------------------------------------------------------------------------------- /resources/screenshot/screenshot_9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magicien/JoyKeyMapper/cbfa688c1d00e6ecf563a77c5b5c06959db2fed3/resources/screenshot/screenshot_9.png -------------------------------------------------------------------------------- /resources/snescon.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magicien/JoyKeyMapper/cbfa688c1d00e6ecf563a77c5b5c06959db2fed3/resources/snescon.ai -------------------------------------------------------------------------------- /scripts/build_dmg.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Codesign and build a dmg file 4 | 5 | if [ $# -lt 1 ]; then 6 | echo "usage: $0 " 7 | exit 1 8 | fi 9 | 10 | SRC_APP_PATH="${1}" 11 | APP_NAME=`basename "${SRC_APP_PATH}"` 12 | if [ "${APP_NAME}" != "JoyKeyMapper.app" ]; then 13 | echo "error: App name must be 'JoyKeyMapper.app'" 14 | exit 2 15 | fi 16 | 17 | VERSION=`git describe --tags --abbrev=0 --match "v*.*.*"` 18 | if [ "${VERSION}" == "" ]; then 19 | echo "error: version tag not found" 20 | exit 3 21 | fi 22 | 23 | echo "Source app path: ${SRC_APP_PATH}" 24 | 25 | PROJECT_ROOT="`dirname $0`/.." 26 | TMP_DIR="${PROJECT_ROOT}/dmg" 27 | APP_PATH="${TMP_DIR}/JoyKeyMapper.app" 28 | LAUNCHER_ENTITLEMENTS="${PROJECT_ROOT}/JoyKeyMapperLauncher/JoyKeyMapperLauncher.entitlements" 29 | APP_ENTITLEMENTS="${PROJECT_ROOT}/JoyKeyMapper/JoyKeyMapper.entitlements" 30 | DMG_PATH="${TMP_DIR}/JoyKeyMapper-${VERSION}.dmg" 31 | BUNDLE_ID="jp.0spec.JoyKeyMapper" 32 | 33 | if [ "${APP_API_USER}" == "" ]; then 34 | read -p "App Connect User: " APP_API_USER 35 | fi 36 | 37 | if [ "${APP_API_ISSUER}" == "" ]; then 38 | read -p "App Connect Issuer: " APP_API_ISSUER 39 | fi 40 | 41 | if [ "${APP_API_KEY_ID}" == "" ]; then 42 | read -p "App Connect Key ID: " APP_API_KEY_ID 43 | fi 44 | 45 | # Copy App 46 | echo "Copying app..." 47 | rm -rf "${TMP_DIR}" 48 | mkdir "${TMP_DIR}" 49 | cp -Rp "${SRC_APP_PATH}" "${APP_PATH}" 50 | 51 | # Verify 52 | echo "Verifying..." 53 | codesign -dv --verbose=4 "${APP_PATH}" 54 | if [ $? -ne 0 ]; then 55 | echo "error: The app is not correctly signed" 56 | exit 4 57 | fi 58 | 59 | # Create a dmg file 60 | echo "Creating a dmg file at ${DMG_PATH}" 61 | dmgbuild -s "${PROJECT_ROOT}/scripts/dmg_settings.py" JoyKeyMapper "${DMG_PATH}" 62 | if [ $? -ne 0 ]; then 63 | echo "error: Failed to build a dmg file" 64 | exit 5 65 | fi 66 | 67 | echo "Code signing to the dmg file..." 68 | codesign -f -o runtime --timestamp -s "Developer ID Application" "${DMG_PATH}" 69 | if [ $? -ne 0 ]; then 70 | echo "error: Failed to sign to the dmg file" 71 | exit 6 72 | fi 73 | 74 | # Notarize the dmg file 75 | echo "Notarizing the dmg file..." 76 | RESULT=`xcrun altool --notarize-app \ 77 | --primary-bundle-id "${BUNDLE_ID}" \ 78 | -u "${APP_API_USER}" \ 79 | --apiKey "${APP_API_KEY_ID}" \ 80 | --apiIssuer "${APP_API_ISSUER}" \ 81 | -t osx -f "${DMG_PATH}"` 82 | 83 | echo "${RESULT}" 84 | REQUEST_UUID=`echo "${RESULT}" | grep "RequestUUID = " | sed "s/RequestUUID = \(.*\)$/\1/"` 85 | if [ "${REQUEST_UUID}" == "" ]; then 86 | echo "error: Failed to notarize the dmg file" 87 | exit 7 88 | fi 89 | 90 | echo "Waiting for the approval..." 91 | echo "It would take few minutes" 92 | RETRY=20 93 | APPROVED=false 94 | for i in `seq ${RETRY}`; do 95 | sleep 30 96 | RESULT=`xcrun altool --notarization-history 0 \ 97 | -u "${APP_API_USER}" \ 98 | --apiKey "${APP_API_KEY_ID}" \ 99 | --apiIssuer "${APP_API_ISSUER}"` 100 | STATUS=`echo "${RESULT}" | grep "${REQUEST_UUID}" | cut -f 5- -d " "` 101 | 102 | if `echo "${STATUS}" | grep "Package Approved" > /dev/null`; then 103 | APPROVED=true 104 | break 105 | elif [ "${STATUS}" == "" ]; then 106 | echo "waiting for updating the notarization history..." 107 | elif `echo "${STATUS}" | grep "in progress" > /dev/null`; then 108 | echo "in progress..." 109 | else 110 | echo "${RESULT}" 111 | echo "error: Invalid notarization status: ${STATUS}" 112 | exit 8 113 | fi 114 | done 115 | 116 | echo "${RESULT}" 117 | if [ ${APPROVED} = false ] ; then 118 | echo "error: Approval timeout" 119 | exit 9 120 | fi 121 | 122 | # Staple a ticket to the dmg file 123 | xcrun stapler staple "${DMG_PATH}" 124 | if [ $? -ne 0 ]; then 125 | echo "error: Failed to staple a ticket" 126 | exit 10 127 | fi 128 | 129 | echo "Done." 130 | -------------------------------------------------------------------------------- /scripts/dmg_settings.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | import biplist 4 | import os.path 5 | 6 | app = defines.get('app', './dmg/JoyKeyMapper.app') 7 | appname = os.path.basename(app) 8 | 9 | # Basics 10 | 11 | format = defines.get('format', 'UDZO') 12 | size = defines.get('size', None) 13 | files = [ app ] 14 | symlinks = { 'Applications': '/Applications' } 15 | 16 | icon_locations = { 17 | appname: (150, 149), 18 | 'Applications': (456, 148) 19 | } 20 | 21 | # Window configuration 22 | 23 | background = './resources/background.png' 24 | 25 | show_status_bar = False 26 | show_tab_view = False 27 | show_toolbar = False 28 | show_pathbar = False 29 | show_sidebar = False 30 | sidebar_width = 180 31 | 32 | window_rect = ((322, 331), (602, 341)) 33 | 34 | defaullt_view = 'icon_view' 35 | 36 | # Icon view configuration 37 | 38 | arrange_by = None 39 | grid_offset = (0, 0) 40 | grid_spacing = 100 41 | scrolll_position = (0, 0) 42 | label_pos = 'bottom' 43 | text_size = 12 44 | icon_size = 164 45 | 46 | -------------------------------------------------------------------------------- /scripts/set_build_number.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Set the number of git commits to Xcode build number 4 | # The source code is based on https://leenarts.net/2020/02/11/git-based-build-number-in-xcode/ 5 | 6 | GIT=`sh /etc/profile; which git` 7 | 8 | LATEST_TAG=`git describe --tags --abbrev=0 --match "v*.*.*"` 9 | if [ "${LATEST_TAG}" == "" ]; then 10 | echo "error: Version tag not found" 11 | exit 1 12 | fi 13 | 14 | VERSION=`echo "${LATEST_TAG}" | cut -c 2-` 15 | 16 | NUM_COMMITS=`"${GIT}" rev-list HEAD --count` 17 | 18 | TARGET_PLIST="${TARGET_BUILD_DIR}/${INFOPLIST_PATH}" 19 | DSYM_PLIST="${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}/Contents/Info.plist" 20 | 21 | for PLIST in "${TARGET_PLIST}" "${DSYM_PLIST}"; do 22 | if [ -f "${PLIST}" ]; then 23 | /usr/libexec/PlistBuddy -c "Set :CFBundleShortVersionString ${VERSION}" "${PLIST}" 24 | /usr/libexec/PlistBuddy -c "Set :CFBundleVersion ${NUM_COMMITS}" "${PLIST}" 25 | fi 26 | done 27 | 28 | ROOT_PLIST="${TARGET_BUILD_DIR}/${PRODUCT_NAME}.app/Settings.bundle/Root.plist" 29 | 30 | if [ -f "${ROOT_PLIST}" ]; then 31 | SETTINGS_VERSION="${APP_MARKETING_VERSION} (${NUM_COMMITS})" 32 | /usr/libexec/PlistBuddy -c "Set :PreferenceSpecifiers:1:DefaultValue ${SETTINGS_VERSION}" "${ROOT_PLIST}" 33 | else 34 | echo "Could not find: ${ROOT_PLIST}" 35 | exit 0 36 | fi 37 | --------------------------------------------------------------------------------