├── .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 | 
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 | 
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 | 
30 |
31 | 2. Set key mappings
32 |
33 | 2.1 Launch JoyKeyMapper.app
34 |
35 | 2.2 Choose the "Settings..." menu
36 |
37 | 
38 |
39 | 2.3 Add apps to set key mappings (optional)
40 |
41 | 
42 |
43 | 2.4 Click a button to set a key
44 |
45 | 
46 |
47 | 
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 | 
54 |
55 | 3.2 Open "System Preferences" > "Security & Privacy" > "Privacy" tab > "Accessibility", and check "JoyKeyMapper.app"
56 |
57 | 
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 | 
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 | 
18 |
19 | ## 使い方
20 |
21 | 1. BluetoothでコントローラをMacへ接続する
22 |
23 | 1.1. Macで「システム環境設定」>「Bluetooth」を開く
24 |
25 | 1.2. コントローラのシンクロボタンを長押しする
26 |
27 | 1.3. Macで「接続」ボタンを押す
28 |
29 | 
30 |
31 | 2. キー設定
32 |
33 | 2.1 JoyKeyMapper.app を起動する
34 |
35 | 2.2 メニューから「設定...」を選択する
36 |
37 | 
38 |
39 | 2.3 キー設定を行うアプリケーションを追加する(任意)
40 |
41 | 
42 |
43 | 2.4 キー設定を行うボタンをクリックする
44 |
45 | 
46 |
47 | 
48 |
49 | 3. JoyKeyMapper に「アクセシビリティ」の許可をする
50 |
51 | 3.1 コントローラ使用時にアラートが表示される
52 |
53 | 
54 |
55 | 3.2 「システム環境設定」>「セキュリティとプライバシー」>「プライバシー」タブ>「アクセシビリティ」を選択し、「JoyKeyMapper.app」にチェックを入れる
56 |
57 | 
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 |
--------------------------------------------------------------------------------