From a0fc0b33c5759a24778627d3b8db6745d7b16d8b Mon Sep 17 00:00:00 2001 From: Fedor Zaytsev Date: Wed, 1 Apr 2020 14:11:35 -0700 Subject: [PATCH] Custom gestures support (#288) * Updated README Added explanation for missing parameters (background, title and image) * Implemented changable icons for AppleScriptTouchBarItem AppleScriptTouchBarItem now allow to specify any number of icons which can be changed from the script. You cannot change icon from touch event. To change icon, you need to return array from your script with 2 values - title and icn name. More info in readme * Implemented custom swipe actions Co-authored-by: Fedor Zaitsev --- MTMR.xcodeproj/project.pbxproj | 8 +++ MTMR/AppDelegate.swift | 2 +- MTMR/BasicView.swift | 107 +++++++++++++++++++++++++++++++++ MTMR/Info.plist | 2 +- MTMR/ItemsParsing.swift | 15 +++++ MTMR/ScrollViewItem.swift | 66 +------------------- MTMR/SwipeItem.swift | 66 ++++++++++++++++++++ MTMR/TouchBarController.swift | 53 +++++++++------- README.md | 24 +++++++- 9 files changed, 252 insertions(+), 91 deletions(-) create mode 100644 MTMR/BasicView.swift create mode 100644 MTMR/SwipeItem.swift diff --git a/MTMR.xcodeproj/project.pbxproj b/MTMR.xcodeproj/project.pbxproj index 55233c8..855eff5 100644 --- a/MTMR.xcodeproj/project.pbxproj +++ b/MTMR.xcodeproj/project.pbxproj @@ -69,6 +69,8 @@ B0F3112520C9E35F0076BB88 /* SupportNSTouchBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0F3112420C9E35F0076BB88 /* SupportNSTouchBar.swift */; }; B0F54A7A2295AC7D00B4C509 /* DarkModeBarItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0F54A792295AC7D00B4C509 /* DarkModeBarItem.swift */; }; B0F8771A207AC1EA00D6E430 /* TouchBarSupport.m in Sources */ = {isa = PBXBuildFile; fileRef = B0F87719207AC1EA00D6E430 /* TouchBarSupport.m */; }; + BAF5AB5724317B4300B04904 /* BasicView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAF5AB5624317B4300B04904 /* BasicView.swift */; }; + BAF5AB5924317CAF00B04904 /* SwipeItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAF5AB5824317CAF00B04904 /* SwipeItem.swift */; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -159,6 +161,8 @@ B0F54A792295AC7D00B4C509 /* DarkModeBarItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DarkModeBarItem.swift; sourceTree = ""; }; B0F87719207AC1EA00D6E430 /* TouchBarSupport.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TouchBarSupport.m; sourceTree = ""; }; B0F8771B207AC92700D6E430 /* TouchBarSupport.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TouchBarSupport.h; sourceTree = ""; }; + BAF5AB5624317B4300B04904 /* BasicView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BasicView.swift; sourceTree = ""; }; + BAF5AB5824317CAF00B04904 /* SwipeItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwipeItem.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -234,8 +238,10 @@ 36FEF871235A1CFC00A0ABCE /* AppSettings.swift */, B059D623205E04F3006E6B86 /* CustomButtonTouchBarItem.swift */, 36C2ECD8207B74B4003CDA33 /* AppleScriptTouchBarItem.swift */, + BAF5AB5824317CAF00B04904 /* SwipeItem.swift */, 4CDC6E4F22FCA93F0069ADD4 /* ShellScriptTouchBarItem.swift */, B059D621205E03F5006E6B86 /* TouchBarController.swift */, + BAF5AB5624317B4300B04904 /* BasicView.swift */, 36C2ECDA207C3FE7003CDA33 /* ItemsParsing.swift */, B0008E542080286C003AD4DD /* SupportHelpers.swift */, B09EB1E3207C082000D5C1E0 /* HapticFeedback.swift */, @@ -465,11 +471,13 @@ B059D622205E03F5006E6B86 /* TouchBarController.swift in Sources */, B05600D32083E9BB00EB218D /* CustomSlider.swift in Sources */, 36C2ECD9207B74B4003CDA33 /* AppleScriptTouchBarItem.swift in Sources */, + BAF5AB5724317B4300B04904 /* BasicView.swift in Sources */, B0846A752220C968000288A7 /* NetworkBarItem.swift in Sources */, B0F8771A207AC1EA00D6E430 /* TouchBarSupport.m in Sources */, B08126F1217BE19000A98970 /* WidgetProtocol.swift in Sources */, B0008E552080286C003AD4DD /* SupportHelpers.swift in Sources */, 6042B6A72083E03A00C525C8 /* AppScrubberTouchBarItem.swift in Sources */, + BAF5AB5924317CAF00B04904 /* SwipeItem.swift in Sources */, B082B253205C7D8000BC04DC /* AppDelegate.swift in Sources */, B0F54A7A2295AC7D00B4C509 /* DarkModeBarItem.swift in Sources */, B08126EF217BD0B900A98970 /* PomodoroBarItem.swift in Sources */, diff --git a/MTMR/AppDelegate.swift b/MTMR/AppDelegate.swift index ab27818..f9c5084 100644 --- a/MTMR/AppDelegate.swift +++ b/MTMR/AppDelegate.swift @@ -92,7 +92,7 @@ class AppDelegate: NSObject, NSApplicationDelegate { @objc func toggleMultitouch(_ item: NSMenuItem) { item.state = item.state == .on ? .off : .on AppSettings.multitouchGestures = item.state == .on - TouchBarController.shared.scrollArea?.gesturesEnabled = item.state == .on + TouchBarController.shared.basicView?.legacyGesturesEnabled = item.state == .on } @objc func openPreset(_: Any?) { diff --git a/MTMR/BasicView.swift b/MTMR/BasicView.swift new file mode 100644 index 0000000..ce30af8 --- /dev/null +++ b/MTMR/BasicView.swift @@ -0,0 +1,107 @@ +// +// BasicView.swift +// MTMR +// +// Created by Fedor Zaitsev on 3/29/20. +// Copyright © 2020 Anton Palgunov. All rights reserved. +// + +import Foundation + + +class BasicView: NSCustomTouchBarItem, NSGestureRecognizerDelegate { + var twofingers: NSPanGestureRecognizer! + var threefingers: NSPanGestureRecognizer! + var fourfingers: NSPanGestureRecognizer! + var swipeItems: [SwipeItem] = [] + var prevPositions: [Int: CGFloat] = [2:0, 3:0, 4:0] + + // legacy gesture positions + // by legacy I mean gestures to increse/decrease volume/brigtness which can be checked from app menu + var legacyPrevPositions: [Int: CGFloat] = [2:0, 3:0, 4:0] + var legacyGesturesEnabled = false + + init(identifier: NSTouchBarItem.Identifier, items: [NSTouchBarItem], swipeItems: [SwipeItem]) { + super.init(identifier: identifier) + self.swipeItems = swipeItems + let views = items.compactMap { $0.view } + let stackView = NSStackView(views: views) + stackView.spacing = 1 + stackView.orientation = .horizontal + view = stackView + + twofingers = NSPanGestureRecognizer(target: self, action: #selector(twofingersHandler(_:))) + twofingers.numberOfTouchesRequired = 2 + twofingers.allowedTouchTypes = .direct + view.addGestureRecognizer(twofingers) + + threefingers = NSPanGestureRecognizer(target: self, action: #selector(threefingersHandler(_:))) + threefingers.numberOfTouchesRequired = 3 + threefingers.allowedTouchTypes = .direct + view.addGestureRecognizer(threefingers) + + fourfingers = NSPanGestureRecognizer(target: self, action: #selector(fourfingersHandler(_:))) + fourfingers.numberOfTouchesRequired = 4 + fourfingers.allowedTouchTypes = .direct + view.addGestureRecognizer(fourfingers) + } + + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func gestureHandler(position: CGFloat, fingers: Int, state: NSGestureRecognizer.State) { + switch state { + case .began: + prevPositions[fingers] = position + legacyPrevPositions[fingers] = position + case .changed: + if self.legacyGesturesEnabled { + if fingers == 2 { + let prevPos = legacyPrevPositions[fingers]! + if ((position - prevPos) > 10) || ((prevPos - position) > 10) { + if position > prevPos { + HIDPostAuxKey(NX_KEYTYPE_SOUND_UP) + } else if position < prevPos { + HIDPostAuxKey(NX_KEYTYPE_SOUND_DOWN) + } + legacyPrevPositions[fingers] = position + } + } + if fingers == 3 { + let prevPos = legacyPrevPositions[fingers]! + if ((position - prevPos) > 15) || ((prevPos - position) > 15) { + if position > prevPos { + GenericKeyPress(keyCode: CGKeyCode(144)).send() + } else if position < prevPos { + GenericKeyPress(keyCode: CGKeyCode(145)).send() + } + legacyPrevPositions[fingers] = position + } + } + } + case .ended: + print("gesture ended \(position - prevPositions[fingers]!) \(fingers)") + for item in swipeItems { + item.processEvent(offset: position - prevPositions[fingers]!, fingers: fingers) + } + default: + break + } + } + + @objc func twofingersHandler(_ sender: NSGestureRecognizer?) { + let position = (sender?.location(in: sender?.view).x)! + self.gestureHandler(position: position, fingers: 2, state: sender!.state) + } + + @objc func threefingersHandler(_ sender: NSGestureRecognizer?) { + let position = (sender?.location(in: sender?.view).x)! + self.gestureHandler(position: position, fingers: 3, state: sender!.state) + } + + @objc func fourfingersHandler(_ sender: NSGestureRecognizer?) { + let position = (sender?.location(in: sender?.view).x)! + self.gestureHandler(position: position, fingers: 4, state: sender!.state) + } +} diff --git a/MTMR/Info.plist b/MTMR/Info.plist index 8c7e7e0..8ee6ef2 100644 --- a/MTMR/Info.plist +++ b/MTMR/Info.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString 0.25 CFBundleVersion - 297 + 385 LSApplicationCategoryType public.app-category.utilities LSMinimumSystemVersion diff --git a/MTMR/ItemsParsing.swift b/MTMR/ItemsParsing.swift index 5248a2c..851cf15 100644 --- a/MTMR/ItemsParsing.swift +++ b/MTMR/ItemsParsing.swift @@ -226,6 +226,7 @@ enum ItemType: Decodable { case pomodoro(workTime: Double, restTime: Double) case network(flip: Bool) case darkMode + case swipe(direction: String, fingers: Int, minOffset: Float, sourceApple: SourceProtocol?, sourceBash: SourceProtocol?) private enum CodingKeys: String, CodingKey { case type @@ -252,6 +253,11 @@ enum ItemType: Decodable { case filter case disableMarquee case alternativeImages + case sourceApple + case sourceBash + case direction + case fingers + case minOffset } enum ItemTypeRaw: String, Decodable { @@ -274,6 +280,7 @@ enum ItemType: Decodable { case pomodoro case network case darkMode + case swipe } init(from decoder: Decoder) throws { @@ -363,6 +370,14 @@ enum ItemType: Decodable { case .darkMode: self = .darkMode + + case .swipe: + let sourceApple = try container.decodeIfPresent(Source.self, forKey: .sourceApple) + let sourceBash = try container.decodeIfPresent(Source.self, forKey: .sourceBash) + let direction = try container.decode(String.self, forKey: .direction) + let fingers = try container.decode(Int.self, forKey: .fingers) + let minOffset = try container.decodeIfPresent(Float.self, forKey: .minOffset) ?? 0.0 + self = .swipe(direction: direction, fingers: fingers, minOffset: minOffset, sourceApple: sourceApple, sourceBash: sourceBash) } } } diff --git a/MTMR/ScrollViewItem.swift b/MTMR/ScrollViewItem.swift index 96b0691..8a39af1 100644 --- a/MTMR/ScrollViewItem.swift +++ b/MTMR/ScrollViewItem.swift @@ -1,10 +1,6 @@ import Foundation -class ScrollViewItem: NSCustomTouchBarItem, NSGestureRecognizerDelegate { - var twofingersPrev: CGFloat = 0.0 - var threefingersPrev: CGFloat = 0.0 - var twofingers: NSPanGestureRecognizer! - var threefingers: NSPanGestureRecognizer! +class ScrollViewItem: NSCustomTouchBarItem/*, NSGestureRecognizerDelegate*/ { init(identifier: NSTouchBarItem.Identifier, items: [NSTouchBarItem]) { super.init(identifier: identifier) @@ -15,70 +11,10 @@ class ScrollViewItem: NSCustomTouchBarItem, NSGestureRecognizerDelegate { let scrollView = NSScrollView(frame: CGRect(origin: .zero, size: stackView.fittingSize)) scrollView.documentView = stackView view = scrollView - - twofingers = NSPanGestureRecognizer(target: self, action: #selector(twofingersHandler(_:))) - twofingers.allowedTouchTypes = .direct - twofingers.numberOfTouchesRequired = 2 - view.addGestureRecognizer(twofingers) - - threefingers = NSPanGestureRecognizer(target: self, action: #selector(threefingersHandler(_:))) - threefingers.allowedTouchTypes = .direct - threefingers.numberOfTouchesRequired = 3 - view.addGestureRecognizer(threefingers) - } - - var gesturesEnabled = true { - didSet { - twofingers.isEnabled = gesturesEnabled - threefingers.isEnabled = gesturesEnabled - } } required init?(coder _: NSCoder) { fatalError("init(coder:) has not been implemented") } - @objc func twofingersHandler(_ sender: NSGestureRecognizer?) { // Volume - let position = (sender?.location(in: sender?.view).x)! - - switch sender!.state { - case .began: - twofingersPrev = position - case .changed: - if ((position - twofingersPrev) > 10) || ((twofingersPrev - position) > 10) { - if position > twofingersPrev { - HIDPostAuxKey(NX_KEYTYPE_SOUND_UP) - } else if position < twofingersPrev { - HIDPostAuxKey(NX_KEYTYPE_SOUND_DOWN) - } - twofingersPrev = position - } - case .ended: - twofingersPrev = 0.0 - default: - break - } - } - - @objc func threefingersHandler(_ sender: NSGestureRecognizer?) { // Brightness - let position = (sender?.location(in: sender?.view).x)! - - switch sender!.state { - case .began: - threefingersPrev = position - case .changed: - if ((position - threefingersPrev) > 15) || ((threefingersPrev - position) > 15) { - if position > threefingersPrev { - GenericKeyPress(keyCode: CGKeyCode(144)).send() - } else if position < threefingersPrev { - GenericKeyPress(keyCode: CGKeyCode(145)).send() - } - threefingersPrev = position - } - case .ended: - threefingersPrev = 0.0 - default: - break - } - } } diff --git a/MTMR/SwipeItem.swift b/MTMR/SwipeItem.swift new file mode 100644 index 0000000..baf7949 --- /dev/null +++ b/MTMR/SwipeItem.swift @@ -0,0 +1,66 @@ +// +// SwipeItem.swift +// MTMR +// +// Created by Fedor Zaitsev on 3/29/20. +// Copyright © 2020 Anton Palgunov. All rights reserved. +// + +import Foundation +import Foundation + +class SwipeItem: NSCustomTouchBarItem { + private var scriptApple: NSAppleScript? + private var scriptBash: String? + private var direction: String + private var fingers: Int + private var minOffset: Float + init?(identifier: NSTouchBarItem.Identifier, direction: String, fingers: Int, minOffset: Float, sourceApple: SourceProtocol?, sourceBash: SourceProtocol?) { + self.direction = direction + self.fingers = fingers + self.scriptBash = sourceBash?.string + self.scriptApple = sourceApple?.appleScript + self.minOffset = minOffset + super.init(identifier: identifier) + } + + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func processEvent(offset: CGFloat, fingers: Int) { + if direction == "right" && Float(offset) > self.minOffset && self.fingers == fingers { + self.execute() + } + if direction == "left" && Float(offset) < -self.minOffset && self.fingers == fingers { + self.execute() + } + } + + func execute() { + if scriptApple != nil { + DispatchQueue.appleScriptQueue.async { + var error: NSDictionary? + self.scriptApple?.executeAndReturnError(&error) + if let error = error { + print("SwipeItem apple script error: \(error)") + return + } + } + } + if scriptBash != nil { + DispatchQueue.shellScriptQueue.async { + let task = Process() + task.launchPath = "/bin/bash" + task.arguments = ["-c", self.scriptBash!] + task.launch() + task.waitUntilExit() + + + if (task.terminationStatus != 0) { + print("SwipeItem bash script error. Status: \(task.terminationStatus)") + } + } + } + } +} diff --git a/MTMR/TouchBarController.swift b/MTMR/TouchBarController.swift index e28598c..9bf1a78 100644 --- a/MTMR/TouchBarController.swift +++ b/MTMR/TouchBarController.swift @@ -57,6 +57,8 @@ extension ItemType { return NetworkBarItem.identifier case .darkMode: return DarkModeBarItem.identifier + case .swipe(direction: _, fingers: _, minOffset: _, sourceApple: _, sourceBash: _): + return "com.toxblh.mtmr.swipe." } } } @@ -76,10 +78,10 @@ class TouchBarController: NSObject, NSTouchBarDelegate { var items: [NSTouchBarItem.Identifier: NSTouchBarItem] = [:] var leftIdentifiers: [NSTouchBarItem.Identifier] = [] var centerIdentifiers: [NSTouchBarItem.Identifier] = [] - var centerItems: [NSTouchBarItem] = [] var rightIdentifiers: [NSTouchBarItem.Identifier] = [] - var scrollArea: ScrollViewItem? - var centerScrollArea = NSTouchBarItem.Identifier("com.toxblh.mtmr.scrollArea.".appending(UUID().uuidString)) + var basicViewIdentifier = NSTouchBarItem.Identifier("com.toxblh.mtmr.scrollView.".appending(UUID().uuidString)) + var basicView: BasicView? + var swipeItems: [SwipeItem] = [] var blacklistAppIdentifiers: [String] = [] var frontmostApplicationIdentifier: String? { @@ -114,24 +116,29 @@ class TouchBarController: NSObject, NSTouchBarDelegate { jsonItems = newJsonItems itemDefinitions = [:] items = [:] - leftIdentifiers = [] - centerItems = [] - rightIdentifiers = [] loadItemDefinitions(jsonItems: jsonItems) createItems() - centerItems = centerIdentifiers.compactMap({ (identifier) -> NSTouchBarItem? in + let centerItems = centerIdentifiers.compactMap({ (identifier) -> NSTouchBarItem? in items[identifier] }) - - centerScrollArea = NSTouchBarItem.Identifier("com.toxblh.mtmr.scrollArea.".appending(UUID().uuidString)) - scrollArea = ScrollViewItem(identifier: centerScrollArea, items: centerItems) - scrollArea?.gesturesEnabled = AppSettings.multitouchGestures + + let centerScrollArea = NSTouchBarItem.Identifier("com.toxblh.mtmr.scrollArea.".appending(UUID().uuidString)) + let scrollArea = ScrollViewItem(identifier: centerScrollArea, items: centerItems) touchBar.delegate = self - touchBar.defaultItemIdentifiers = [] - touchBar.defaultItemIdentifiers = leftIdentifiers + [centerScrollArea] + rightIdentifiers + touchBar.defaultItemIdentifiers = [basicViewIdentifier] + + let leftItems = leftIdentifiers.compactMap({ (identifier) -> NSTouchBarItem? in + items[identifier] + }) + let rightItems = rightIdentifiers.compactMap({ (identifier) -> NSTouchBarItem? in + items[identifier] + }) + + basicView = BasicView(identifier: basicViewIdentifier, items:leftItems + [scrollArea] + rightItems, swipeItems: swipeItems) + basicView?.legacyGesturesEnabled = AppSettings.multitouchGestures updateActiveApp() } @@ -187,7 +194,12 @@ class TouchBarController: NSObject, NSTouchBarDelegate { func createItems() { for (identifier, definition) in itemDefinitions { - items[identifier] = createItem(forIdentifier: identifier, definition: definition) + let item = createItem(forIdentifier: identifier, definition: definition) + if item is SwipeItem { + swipeItems.append(item as! SwipeItem) + } else { + items[identifier] = item + } } } @@ -223,16 +235,11 @@ class TouchBarController: NSObject, NSTouchBarDelegate { } func touchBar(_: NSTouchBar, makeItemForIdentifier identifier: NSTouchBarItem.Identifier) -> NSTouchBarItem? { - if identifier == centerScrollArea { - return scrollArea + if identifier == basicViewIdentifier { + return basicView } - guard let item = self.items[identifier], - let definition = self.itemDefinitions[identifier], - definition.align != .center else { - return nil - } - return item + return nil } func createItem(forIdentifier identifier: NSTouchBarItem.Identifier, definition item: BarItemDefinition) -> NSTouchBarItem? { @@ -292,6 +299,8 @@ class TouchBarController: NSObject, NSTouchBarDelegate { barItem = NetworkBarItem(identifier: identifier, flip: flip) case .darkMode: barItem = DarkModeBarItem(identifier: identifier) + case let .swipe(direction: direction, fingers: fingers, minOffset: minOffset, sourceApple: sourceApple, sourceBash: sourceBash): + barItem = SwipeItem(identifier: identifier, direction: direction, fingers: fingers, minOffset: minOffset, sourceApple: sourceApple, sourceBash: sourceBash) } if let action = self.action(forItem: item), let item = barItem as? CustomButtonTouchBarItem { diff --git a/README.md b/README.md index 93d21e8..1e51a50 100644 --- a/README.md +++ b/README.md @@ -102,11 +102,31 @@ The pre-installed configuration contains less or more than you'll probably want, - appleScriptTitledButton - shellScriptTitledButton -## Gestures on central part: +## Gestures +By default you can enable basic gestures from application menu (status bar -> MTMR icon -> Volume/Brightness gestures): - two finger slide: change you Volume - three finger slide: change you Brightness +### Custom gestures + +You can add custom actions for two/three/four finger swipes. To do it, you need to use `swipe` type: + +```json + "type": "swipe", + "fingers": 2, // number of fingers required (2,3 or 4) + "direction": "right", // direction of swipe (right/left) + "minOffset" 10, // optional: minimal required offset for gesture to emit event + "sourceApple": { // optional: apple script to run + "inline": "beep" + }, + "sourceBash": { // optional: bash script to run + "inline": "touch /Users/lobster/test" + } +``` + +You may create as many `swipe` objects in the preset as you want. + ## Built-in slider types: - brightness @@ -455,7 +475,7 @@ Settings: - [x] Startup at login - [ ] Show on/off in Dock - [ ] Show on/off in StatusBar -- [ ] On/off Haptic Feedback +- [ x] On/off Haptic Feedback Maybe: