diff --git a/MTMR/CustomButtonTouchBarItem.swift b/MTMR/CustomButtonTouchBarItem.swift index 6b734d2..a10976d 100644 --- a/MTMR/CustomButtonTouchBarItem.swift +++ b/MTMR/CustomButtonTouchBarItem.swift @@ -8,18 +8,32 @@ import Cocoa +struct ItemAction { + typealias TriggerClosure = (() -> Void)? + + let trigger: Action.Trigger + let closure: TriggerClosure + + init(trigger: Action.Trigger, _ closure: TriggerClosure) { + self.trigger = trigger + self.closure = closure + } +} + class CustomButtonTouchBarItem: NSCustomTouchBarItem, NSGestureRecognizerDelegate { - var tapClosure: (() -> Void)? - var longTapClosure: (() -> Void)? { + + var actions: [ItemAction] = [] { didSet { - longClick.isEnabled = longTapClosure != nil + multiClick.isDoubleClickEnabled = actions.filter({ $0.trigger == .doubleTap }).count > 0 + multiClick.isTripleClickEnabled = actions.filter({ $0.trigger == .tripleTap }).count > 0 + longClick.isEnabled = actions.filter({ $0.trigger == .longTap }).count > 0 } } var finishViewConfiguration: ()->() = {} private var button: NSButton! - private var singleClick: HapticClickGestureRecognizer! private var longClick: LongPressGestureRecognizer! + private var multiClick: MultiClickGestureRecognizer! init(identifier: NSTouchBarItem.Identifier, title: String) { attributedTitle = title.defaultTouchbarAttributedString @@ -31,10 +45,17 @@ class CustomButtonTouchBarItem: NSCustomTouchBarItem, NSGestureRecognizerDelegat longClick.isEnabled = false longClick.allowedTouchTypes = .direct longClick.delegate = self - - singleClick = HapticClickGestureRecognizer(target: self, action: #selector(handleGestureSingle)) - singleClick.allowedTouchTypes = .direct - singleClick.delegate = self + + multiClick = MultiClickGestureRecognizer( + target: self, + action: #selector(handleGestureSingleTap), + doubleAction: #selector(handleGestureDoubleTap), + tripleAction: #selector(handleGestureTripleTap) + ) + multiClick.allowedTouchTypes = .direct + multiClick.delegate = self + multiClick.isDoubleClickEnabled = false + multiClick.isTripleClickEnabled = false reinstallButton() button.attributedTitle = attributedTitle @@ -100,33 +121,43 @@ class CustomButtonTouchBarItem: NSCustomTouchBarItem, NSGestureRecognizerDelegat view = button view.addGestureRecognizer(longClick) - view.addGestureRecognizer(singleClick) + // view.addGestureRecognizer(singleClick) + view.addGestureRecognizer(multiClick) finishViewConfiguration() } func gestureRecognizer(_ gestureRecognizer: NSGestureRecognizer, shouldRequireFailureOf otherGestureRecognizer: NSGestureRecognizer) -> Bool { - if gestureRecognizer == singleClick && otherGestureRecognizer == longClick - || gestureRecognizer == longClick && otherGestureRecognizer == singleClick // need it + if gestureRecognizer == multiClick && otherGestureRecognizer == longClick + || gestureRecognizer == longClick && otherGestureRecognizer == multiClick // need it { return false } return true } - - @objc func handleGestureSingle(gr: NSClickGestureRecognizer) { - switch gr.state { - case .ended: - tapClosure?() - break - default: - break + + func callActions(for trigger: Action.Trigger) { + let itemActions = self.actions.filter { $0.trigger == trigger } + for itemAction in itemActions { + itemAction.closure?() } } + + @objc func handleGestureSingleTap() { + callActions(for: .singleTap) + } + + @objc func handleGestureDoubleTap() { + callActions(for: .doubleTap) + } + + @objc func handleGestureTripleTap() { + callActions(for: .tripleTap) + } @objc func handleGestureLong(gr: NSPressGestureRecognizer) { switch gr.state { case .possible: // tiny hack because we're calling action manually - (self.longTapClosure ?? self.tapClosure)?() + callActions(for: .longTap) break default: break @@ -176,15 +207,78 @@ class CustomButtonCell: NSButtonCell { } } -class HapticClickGestureRecognizer: NSClickGestureRecognizer { +// Thanks to https://stackoverflow.com/a/49843893 +final class MultiClickGestureRecognizer: NSClickGestureRecognizer { + + private let _action: Selector + private let _doubleAction: Selector + private let _tripleAction: Selector + private var _clickCount: Int = 0 + + public var isDoubleClickEnabled = true + public var isTripleClickEnabled = true + + override var action: Selector? { + get { + return nil /// prevent base class from performing any actions + } set { + if newValue != nil { // if they are trying to assign an actual action + fatalError("Only use init(target:action:doubleAction) for assigning actions") + } + } + } + + required init(target: AnyObject, action: Selector, doubleAction: Selector, tripleAction: Selector) { + _action = action + _doubleAction = doubleAction + _tripleAction = tripleAction + super.init(target: target, action: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(target:action:doubleAction:tripleAction) is only support atm") + } + override func touchesBegan(with event: NSEvent) { HapticFeedback.shared?.tap(strong: 2) super.touchesBegan(with: event) } - + override func touchesEnded(with event: NSEvent) { HapticFeedback.shared?.tap(strong: 1) super.touchesEnded(with: event) + _clickCount += 1 + + var delayThreshold: TimeInterval // fine tune this as needed + + guard isDoubleClickEnabled || isTripleClickEnabled else { + _ = target?.perform(_action) + return + } + + if (isTripleClickEnabled) { + delayThreshold = 0.4 + perform(#selector(_resetAndPerformActionIfNecessary), with: nil, afterDelay: delayThreshold) + if _clickCount == 3 { + _ = target?.perform(_tripleAction) + } + } else { + delayThreshold = 0.3 + perform(#selector(_resetAndPerformActionIfNecessary), with: nil, afterDelay: delayThreshold) + if _clickCount == 2 { + _ = target?.perform(_doubleAction) + } + } + } + + @objc private func _resetAndPerformActionIfNecessary() { + if _clickCount == 1 { + _ = target?.perform(_action) + } + if isTripleClickEnabled && _clickCount == 2 { + _ = target?.perform(_doubleAction) + } + _clickCount = 0 } } diff --git a/MTMR/ItemsParsing.swift b/MTMR/ItemsParsing.swift index a16beef..7aa3c6e 100644 --- a/MTMR/ItemsParsing.swift +++ b/MTMR/ItemsParsing.swift @@ -3,47 +3,52 @@ import Foundation extension Data { func barItemDefinitions() -> [BarItemDefinition]? { - return try? JSONDecoder().decode([BarItemDefinition].self, from: utf8string!.stripComments().data(using: .utf8)!) + return try! JSONDecoder().decode([BarItemDefinition].self, from: utf8string!.stripComments().data(using: .utf8)!) } } struct BarItemDefinition: Decodable { let type: ItemType - let action: ActionType - let longAction: LongActionType + let actions: [Action] + let legacyAction: LegacyActionType + let legacyLongAction: LegacyLongActionType let additionalParameters: [GeneralParameters.CodingKeys: GeneralParameter] private enum CodingKeys: String, CodingKey { case type + case actions } - init(type: ItemType, action: ActionType, longAction: LongActionType, additionalParameters: [GeneralParameters.CodingKeys: GeneralParameter]) { + init(type: ItemType, actions: [Action], action: LegacyActionType, legacyLongAction: LegacyLongActionType, additionalParameters: [GeneralParameters.CodingKeys: GeneralParameter]) { self.type = type - self.action = action - self.longAction = longAction + self.actions = actions + self.legacyAction = action + self.legacyLongAction = legacyLongAction self.additionalParameters = additionalParameters } init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) let type = try container.decode(String.self, forKey: .type) - let parametersDecoder = SupportedTypesHolder.sharedInstance.lookup(by: type) + let actions = try container.decodeIfPresent([Action].self, forKey: .actions) + let parametersDecoder = SupportedTypesHolder.sharedInstance.lookup(by: type, actions: actions ?? []) var additionalParameters = try GeneralParameters(from: decoder).parameters if let result = try? parametersDecoder(decoder), - case let (itemType, action, longAction, parameters) = result { + case let (itemType, actions, action, longAction, parameters) = result { parameters.forEach { additionalParameters[$0] = $1 } - self.init(type: itemType, action: action, longAction: longAction, additionalParameters: additionalParameters) + self.init(type: itemType, actions: actions, action: action, legacyLongAction: longAction, additionalParameters: additionalParameters) } else { - self.init(type: .staticButton(title: "unknown"), action: .none, longAction: .none, additionalParameters: additionalParameters) + self.init(type: .staticButton(title: "unknown"), actions: [], action: .none, legacyLongAction: .none, additionalParameters: additionalParameters) } } } typealias ParametersDecoder = (Decoder) throws -> ( item: ItemType, - action: ActionType, - longAction: LongActionType, + actions: [Action], + legacyAction: LegacyActionType, + legacyLongAction: LegacyLongActionType, parameters: [GeneralParameters.CodingKeys: GeneralParameter] ) @@ -51,15 +56,21 @@ class SupportedTypesHolder { private var supportedTypes: [String: ParametersDecoder] = [ "escape": { _ in ( item: .staticButton(title: "esc"), - action: .keyPress(keycode: 53), - longAction: .none, + actions: [ + Action(trigger: .singleTap, value: .keyPress(keycode: 53)) + ], + legacyAction: .none, + legacyLongAction: .none, parameters: [.align: .align(.left)] ) }, "delete": { _ in ( item: .staticButton(title: "del"), - action: .keyPress(keycode: 117), - longAction: .none, + actions: [ + Action(trigger: .singleTap, value: .keyPress(keycode: 117)) + ], + legacyAction: .none, + legacyLongAction: .none, parameters: [:] ) }, @@ -67,8 +78,11 @@ class SupportedTypesHolder { let imageParameter = GeneralParameter.image(source: #imageLiteral(resourceName: "brightnessUp")) return ( item: .staticButton(title: ""), - action: .keyPress(keycode: 144), - longAction: .none, + actions: [ + Action(trigger: .singleTap, value: .keyPress(keycode: 144)) + ], + legacyAction: .none, + legacyLongAction: .none, parameters: [.image: imageParameter] ) }, @@ -77,8 +91,11 @@ class SupportedTypesHolder { let imageParameter = GeneralParameter.image(source: #imageLiteral(resourceName: "brightnessDown")) return ( item: .staticButton(title: ""), - action: .keyPress(keycode: 145), - longAction: .none, + actions: [ + Action(trigger: .singleTap, value: .keyPress(keycode: 145)) + ], + legacyAction: .none, + legacyLongAction: .none, parameters: [.image: imageParameter] ) }, @@ -87,8 +104,11 @@ class SupportedTypesHolder { let imageParameter = GeneralParameter.image(source: #imageLiteral(resourceName: "ill_up")) return ( item: .staticButton(title: ""), - action: .hidKey(keycode: NX_KEYTYPE_ILLUMINATION_UP), - longAction: .none, + actions: [ + Action(trigger: .singleTap, value: .hidKey(keycode: NX_KEYTYPE_ILLUMINATION_UP)) + ], + legacyAction: .none, + legacyLongAction: .none, parameters: [.image: imageParameter] ) }, @@ -97,8 +117,11 @@ class SupportedTypesHolder { let imageParameter = GeneralParameter.image(source: #imageLiteral(resourceName: "ill_down")) return ( item: .staticButton(title: ""), - action: .hidKey(keycode: NX_KEYTYPE_ILLUMINATION_DOWN), - longAction: .none, + actions: [ + Action(trigger: .singleTap, value: .hidKey(keycode: NX_KEYTYPE_ILLUMINATION_DOWN)) + ], + legacyAction: .none, + legacyLongAction: .none, parameters: [.image: imageParameter] ) }, @@ -107,8 +130,11 @@ class SupportedTypesHolder { let imageParameter = GeneralParameter.image(source: NSImage(named: NSImage.touchBarVolumeDownTemplateName)!) return ( item: .staticButton(title: ""), - action: .hidKey(keycode: NX_KEYTYPE_SOUND_DOWN), - longAction: .none, + actions: [ + Action(trigger: .singleTap, value: .hidKey(keycode: NX_KEYTYPE_SOUND_DOWN)) + ], + legacyAction: .none, + legacyLongAction: .none, parameters: [.image: imageParameter] ) }, @@ -117,8 +143,11 @@ class SupportedTypesHolder { let imageParameter = GeneralParameter.image(source: NSImage(named: NSImage.touchBarVolumeUpTemplateName)!) return ( item: .staticButton(title: ""), - action: .hidKey(keycode: NX_KEYTYPE_SOUND_UP), - longAction: .none, + actions: [ + Action(trigger: .singleTap, value: .hidKey(keycode: NX_KEYTYPE_SOUND_UP)) + ], + legacyAction: .none, + legacyLongAction: .none, parameters: [.image: imageParameter] ) }, @@ -127,8 +156,11 @@ class SupportedTypesHolder { let imageParameter = GeneralParameter.image(source: NSImage(named: NSImage.touchBarAudioOutputMuteTemplateName)!) return ( item: .staticButton(title: ""), - action: .hidKey(keycode: NX_KEYTYPE_MUTE), - longAction: .none, + actions: [ + Action(trigger: .singleTap, value: .hidKey(keycode: NX_KEYTYPE_MUTE)) + ], + legacyAction: .none, + legacyLongAction: .none, parameters: [.image: imageParameter] ) }, @@ -137,8 +169,11 @@ class SupportedTypesHolder { let imageParameter = GeneralParameter.image(source: NSImage(named: NSImage.touchBarRewindTemplateName)!) return ( item: .staticButton(title: ""), - action: .hidKey(keycode: NX_KEYTYPE_PREVIOUS), - longAction: .none, + actions: [ + Action(trigger: .singleTap, value: .hidKey(keycode: NX_KEYTYPE_PREVIOUS)) + ], + legacyAction: .none, + legacyLongAction: .none, parameters: [.image: imageParameter] ) }, @@ -147,8 +182,11 @@ class SupportedTypesHolder { let imageParameter = GeneralParameter.image(source: NSImage(named: NSImage.touchBarPlayPauseTemplateName)!) return ( item: .staticButton(title: ""), - action: .hidKey(keycode: NX_KEYTYPE_PLAY), - longAction: .none, + actions: [ + Action(trigger: .singleTap, value: .hidKey(keycode: NX_KEYTYPE_PLAY)) + ], + legacyAction: .none, + legacyLongAction: .none, parameters: [.image: imageParameter] ) }, @@ -157,23 +195,32 @@ class SupportedTypesHolder { let imageParameter = GeneralParameter.image(source: NSImage(named: NSImage.touchBarFastForwardTemplateName)!) return ( item: .staticButton(title: ""), - action: .hidKey(keycode: NX_KEYTYPE_NEXT), - longAction: .none, + actions: [ + Action(trigger: .singleTap, value: .hidKey(keycode: NX_KEYTYPE_NEXT)) + ], + legacyAction: .none, + legacyLongAction: .none, parameters: [.image: imageParameter] ) }, "sleep": { _ in ( item: .staticButton(title: "☕️"), - action: .shellScript(executable: "/usr/bin/pmset", parameters: ["sleepnow"]), - longAction: .none, + actions: [ + Action(trigger: .singleTap, value: .shellScript(executable: "/usr/bin/pmset", parameters: ["sleepnow"])) + ], + legacyAction: .none, + legacyLongAction: .none, parameters: [:] ) }, "displaySleep": { _ in ( item: .staticButton(title: "☕️"), - action: .shellScript(executable: "/usr/bin/pmset", parameters: ["displaysleepnow"]), - longAction: .none, + actions: [ + Action(trigger: .singleTap, value: .shellScript(executable: "/usr/bin/pmset", parameters: ["displaysleepnow"])) + ], + legacyAction: .none, + legacyLongAction: .none, parameters: [:] ) }, @@ -181,11 +228,12 @@ class SupportedTypesHolder { static let sharedInstance = SupportedTypesHolder() - func lookup(by type: String) -> ParametersDecoder { + func lookup(by type: String, actions: [Action]) -> ParametersDecoder { return supportedTypes[type] ?? { decoder in ( item: try ItemType(from: decoder), - action: try ActionType(from: decoder), - longAction: try LongActionType(from: decoder), + actions: actions, + legacyAction: try LegacyActionType(from: decoder), + legacyLongAction: try LegacyLongActionType(from: decoder), parameters: [:] ) } } @@ -194,12 +242,13 @@ class SupportedTypesHolder { supportedTypes[typename] = decoder } - func register(typename: String, item: ItemType, action: ActionType, longAction: LongActionType) { + func register(typename: String, item: ItemType, actions: [Action], legacyAction: LegacyActionType, legacyLongAction: LegacyLongActionType) { register(typename: typename) { _ in ( item: item, - action, - longAction, + actions, + legacyAction, + legacyLongAction, parameters: [:] ) } @@ -393,7 +442,94 @@ enum ItemType: Decodable { } } -enum ActionType: Decodable { +struct FailableDecodable : Decodable { + + let base: Base? + + init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + self.base = try? container.decode(Base.self) + } +} + +struct Action: Decodable { + enum Trigger: String, Decodable { + case singleTap + case doubleTap + case tripleTap + case longTap + } + + enum Value { + case none + case hidKey(keycode: Int32) + case keyPress(keycode: Int) + case appleScript(source: SourceProtocol) + case shellScript(executable: String, parameters: [String]) + case custom(closure: () -> Void) + case openUrl(url: String) + } + + private enum ActionTypeRaw: String, Decodable { + case hidKey + case keyPress + case appleScript + case shellScript + case openUrl + } + + enum CodingKeys: String, CodingKey { + case trigger + case action + case keycode + case actionAppleScript + case executablePath + case shellArguments + case url + } + + let trigger: Trigger + let value: Value + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + trigger = try container.decode(Trigger.self, forKey: .trigger) + let type = try container.decodeIfPresent(ActionTypeRaw.self, forKey: .action) + + switch type { + case .some(.hidKey): + let keycode = try container.decode(Int32.self, forKey: .keycode) + value = .hidKey(keycode: keycode) + + case .some(.keyPress): + let keycode = try container.decode(Int.self, forKey: .keycode) + value = .keyPress(keycode: keycode) + + case .some(.appleScript): + let source = try container.decode(Source.self, forKey: .actionAppleScript) + value = .appleScript(source: source) + + case .some(.shellScript): + let executable = try container.decode(String.self, forKey: .executablePath) + let parameters = try container.decodeIfPresent([String].self, forKey: .shellArguments) ?? [] + value = .shellScript(executable: executable, parameters: parameters) + + case .some(.openUrl): + let url = try container.decode(String.self, forKey: .url) + value = .openUrl(url: url) + case .none: + value = .none + } + } + + init(trigger: Trigger, value: Value) { + self.trigger = trigger + self.value = value + } +} + +enum LegacyActionType: Decodable { case none case hidKey(keycode: Int32) case keyPress(keycode: Int) @@ -451,7 +587,7 @@ enum ActionType: Decodable { } } -enum LongActionType: Decodable { +enum LegacyLongActionType: Decodable { case none case hidKey(keycode: Int32) case keyPress(keycode: Int) diff --git a/MTMR/TouchBarController.swift b/MTMR/TouchBarController.swift index e3a5898..9860ee6 100644 --- a/MTMR/TouchBarController.swift +++ b/MTMR/TouchBarController.swift @@ -92,13 +92,28 @@ class TouchBarController: NSObject, NSTouchBarDelegate { private override init() { super.init() - SupportedTypesHolder.sharedInstance.register(typename: "exitTouchbar", item: .staticButton(title: "exit"), action: .custom(closure: { [weak self] in self?.dismissTouchBar() }), longAction: .none) + SupportedTypesHolder.sharedInstance.register( + typename: "exitTouchbar", + item: .staticButton(title: "exit"), + actions: [ + Action(trigger: .singleTap, value: .custom(closure: { [weak self] in self?.dismissTouchBar() })) + ], + legacyAction: .none, + legacyLongAction: .none + ) SupportedTypesHolder.sharedInstance.register(typename: "close") { _ in - (item: .staticButton(title: ""), action: .custom(closure: { [weak self] in - guard let `self` = self else { return } - self.reloadPreset(path: self.lastPresetPath) - }), longAction: .none, parameters: [.width: .width(30), .image: .image(source: (NSImage(named: NSImage.stopProgressFreestandingTemplateName))!)]) + ( + item: .staticButton(title: ""), + actions: [ + Action(trigger: .singleTap, value: .custom(closure: { [weak self] in + guard let `self` = self else { return } + self.reloadPreset(path: self.lastPresetPath) + })) + ], + legacyAction: .none, + legacyLongAction: .none, + parameters: [.width: .width(30), .image: .image(source: (NSImage(named: NSImage.stopProgressFreestandingTemplateName))!)]) } blacklistAppIdentifiers = AppSettings.blacklistedAppIds @@ -170,7 +185,7 @@ class TouchBarController: NSObject, NSTouchBarDelegate { func reloadPreset(path: String) { lastPresetPath = path - let items = path.fileData?.barItemDefinitions() ?? [BarItemDefinition(type: .staticButton(title: "bad preset"), action: .none, longAction: .none, additionalParameters: [:])] + let items = path.fileData?.barItemDefinitions() ?? [BarItemDefinition(type: .staticButton(title: "bad preset"), actions: [], action: .none, legacyLongAction: .none, additionalParameters: [:])] createAndUpdatePreset(newJsonItems: items) } @@ -308,10 +323,16 @@ class TouchBarController: NSObject, NSTouchBarDelegate { } if let action = self.action(forItem: item), let item = barItem as? CustomButtonTouchBarItem { - item.tapClosure = action + item.actions.append(ItemAction(trigger: .singleTap, action)) } if let longAction = self.longAction(forItem: item), let item = barItem as? CustomButtonTouchBarItem { - item.longTapClosure = longAction + item.actions.append(ItemAction(trigger: .longTap, longAction)) + } + + if let touchBarItem = barItem as? CustomButtonTouchBarItem { + for action in item.actions { + touchBarItem.actions.append(ItemAction(trigger: action.trigger, self.closure(for: action))) + } } if case let .bordered(bordered)? = item.additionalParameters[.bordered], let item = barItem as? CustomButtonTouchBarItem { item.isBordered = bordered @@ -334,9 +355,53 @@ class TouchBarController: NSObject, NSTouchBarDelegate { } return barItem } + + func closure(for action: Action) -> (() -> Void)? { + switch action.value { + case let .hidKey(keycode: keycode): + return { HIDPostAuxKey(keycode) } + case let .keyPress(keycode: keycode): + return { GenericKeyPress(keyCode: CGKeyCode(keycode)).send() } + case let .appleScript(source: source): + guard let appleScript = source.appleScript else { + print("cannot create apple script for item \(action)") + return {} + } + return { + DispatchQueue.appleScriptQueue.async { + var error: NSDictionary? + appleScript.executeAndReturnError(&error) + if let error = error { + print("error \(error) when handling \(action) ") + } + } + } + case let .shellScript(executable: executable, parameters: parameters): + return { + let task = Process() + task.launchPath = executable + task.arguments = parameters + task.launch() + } + case let .openUrl(url: url): + return { + if let url = URL(string: url), NSWorkspace.shared.open(url) { + #if DEBUG + print("URL was successfully opened") + #endif + } else { + print("error", url) + } + } + case let .custom(closure: closure): + return closure + case .none: + return nil + } + } func action(forItem item: BarItemDefinition) -> (() -> Void)? { - switch item.action { + switch item.legacyAction { case let .hidKey(keycode: keycode): return { HIDPostAuxKey(keycode) } case let .keyPress(keycode: keycode): @@ -380,7 +445,7 @@ class TouchBarController: NSObject, NSTouchBarDelegate { } func longAction(forItem item: BarItemDefinition) -> (() -> Void)? { - switch item.longAction { + switch item.legacyLongAction { case let .hidKey(keycode: keycode): return { HIDPostAuxKey(keycode) } case let .keyPress(keycode: keycode): diff --git a/MTMR/Widgets/AppScrubberTouchBarItem.swift b/MTMR/Widgets/AppScrubberTouchBarItem.swift index adc5541..b8a0300 100644 --- a/MTMR/Widgets/AppScrubberTouchBarItem.swift +++ b/MTMR/Widgets/AppScrubberTouchBarItem.swift @@ -82,12 +82,14 @@ class AppScrubberTouchBarItem: NSCustomTouchBarItem { public func createAppButton(for app: DockItem) -> DockBarItem { let item = DockBarItem(app) item.isBordered = false - item.tapClosure = { [weak self] in - self?.switchToApp(app: app) - } - item.longTapClosure = { [weak self] in - self?.handleHalfLongPress(item: app) - } + item.actions.append(contentsOf: [ + ItemAction(trigger: .singleTap) { [weak self] in + self?.switchToApp(app: app) + }, + ItemAction(trigger: .longTap) { [weak self] in + self?.handleHalfLongPress(item: app) + } + ]) item.killAppClosure = {[weak self] in self?.handleLongPress(item: app) } diff --git a/MTMR/Widgets/DarkModeBarItem.swift b/MTMR/Widgets/DarkModeBarItem.swift index ceaba78..ecad30b 100644 --- a/MTMR/Widgets/DarkModeBarItem.swift +++ b/MTMR/Widgets/DarkModeBarItem.swift @@ -11,7 +11,7 @@ class DarkModeBarItem: CustomButtonTouchBarItem, Widget { isBordered = false setWidth(value: 24) - tapClosure = { [weak self] in self?.DarkModeToggle() } + actions.append(ItemAction(trigger: .singleTap) { [weak self] in self?.DarkModeToggle() }) timer = Timer.scheduledTimer(timeInterval: 3, target: self, selector: #selector(refresh), userInfo: nil, repeats: true) diff --git a/MTMR/Widgets/DnDBarItem.swift b/MTMR/Widgets/DnDBarItem.swift index 18344ca..eb15f7a 100644 --- a/MTMR/Widgets/DnDBarItem.swift +++ b/MTMR/Widgets/DnDBarItem.swift @@ -16,7 +16,7 @@ class DnDBarItem: CustomButtonTouchBarItem { isBordered = false setWidth(value: 32) - tapClosure = { [weak self] in self?.DnDToggle() } + actions.append(ItemAction(trigger: .singleTap) { [weak self] in self?.DnDToggle() }) timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(refresh), userInfo: nil, repeats: true) diff --git a/MTMR/Widgets/InputSourceBarItem.swift b/MTMR/Widgets/InputSourceBarItem.swift index 6e8b15b..1d3e367 100644 --- a/MTMR/Widgets/InputSourceBarItem.swift +++ b/MTMR/Widgets/InputSourceBarItem.swift @@ -18,9 +18,10 @@ class InputSourceBarItem: CustomButtonTouchBarItem { observeIputSourceChangedNotification() textInputSourceDidChange() - tapClosure = { [weak self] in + + actions.append(ItemAction(trigger: .singleTap) { [weak self] in self?.switchInputSource() - } + }) } required init?(coder _: NSCoder) { diff --git a/MTMR/Widgets/MusicBarItem.swift b/MTMR/Widgets/MusicBarItem.swift index 23b2029..a1d1e11 100644 --- a/MTMR/Widgets/MusicBarItem.swift +++ b/MTMR/Widgets/MusicBarItem.swift @@ -41,9 +41,12 @@ class MusicBarItem: CustomButtonTouchBarItem { super.init(identifier: identifier, title: "⏳") isBordered = false - - tapClosure = { [weak self] in self?.playPause() } - longTapClosure = { [weak self] in self?.nextTrack() } + + actions = [ + ItemAction(trigger: .singleTap) { [weak self] in self?.playPause() }, + ItemAction(trigger: .doubleTap) { [weak self] in self?.previousTrack() }, + ItemAction(trigger: .longTap) { [weak self] in self?.nextTrack() } + ] refreshAndSchedule() } @@ -177,6 +180,31 @@ class MusicBarItem: CustomButtonTouchBarItem { } } } + + @objc func previousTrack() { + for ident in playerBundleIdentifiers { + if let musicPlayer = SBApplication(bundleIdentifier: ident.rawValue) { + if musicPlayer.isRunning { + if ident == .Spotify { + let mp = (musicPlayer as SpotifyApplication) + mp.previousTrack!() + updatePlayer() + return + } else if ident == .iTunes { + let mp = (musicPlayer as iTunesApplication) + mp.previousTrack!() + updatePlayer() + return + } else if ident == .Music { + let mp = (musicPlayer as MusicApplication) + mp.previousTrack!() + updatePlayer() + return + } + } + } + } + } func refreshAndSchedule() { DispatchQueue.main.async { diff --git a/MTMR/Widgets/NightShiftBarItem.swift b/MTMR/Widgets/NightShiftBarItem.swift index 517a8e7..04715ed 100644 --- a/MTMR/Widgets/NightShiftBarItem.swift +++ b/MTMR/Widgets/NightShiftBarItem.swift @@ -30,8 +30,8 @@ class NightShiftBarItem: CustomButtonTouchBarItem { super.init(identifier: identifier, title: "") isBordered = false setWidth(value: 28) - - tapClosure = { [weak self] in self?.nightShiftAction() } + + actions.append(ItemAction(trigger: .singleTap) { [weak self] in self?.nightShiftAction() }) timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(refresh), userInfo: nil, repeats: true) diff --git a/MTMR/Widgets/PomodoroBarItem.swift b/MTMR/Widgets/PomodoroBarItem.swift index 7e081f7..9f7974f 100644 --- a/MTMR/Widgets/PomodoroBarItem.swift +++ b/MTMR/Widgets/PomodoroBarItem.swift @@ -23,8 +23,9 @@ class PomodoroBarItem: CustomButtonTouchBarItem, Widget { return ( item: .pomodoro(workTime: workTime ?? 1500.00, restTime: restTime ?? 300), - action: .none, - longAction: .none, + actions: [], + legacyAction: .none, + legacyLongAction: .none, parameters: [:] ) } @@ -50,8 +51,10 @@ class PomodoroBarItem: CustomButtonTouchBarItem, Widget { self.workTime = workTime self.restTime = restTime super.init(identifier: identifier, title: defaultTitle) - tapClosure = { [weak self] in self?.startStopWork() } - longTapClosure = { [weak self] in self?.startStopRest() } + actions.append(contentsOf: [ + ItemAction(trigger: .singleTap) { [weak self] in self?.startStopWork() }, + ItemAction(trigger: .longTap) { [weak self] in self?.startStopRest() } + ]) } required init?(coder _: NSCoder) { diff --git a/MTMR/Widgets/UpNextScrubberTouchBarItem.swift b/MTMR/Widgets/UpNextScrubberTouchBarItem.swift index fb0f1ea..6ea7e78 100644 --- a/MTMR/Widgets/UpNextScrubberTouchBarItem.swift +++ b/MTMR/Widgets/UpNextScrubberTouchBarItem.swift @@ -75,9 +75,9 @@ class UpNextScrubberTouchBarItem: NSCustomTouchBarItem { let item = UpNextItem(event: event) item.backgroundColor = self.getBackgroundColor(startDate: event.startDate) // Bind tap event - item.tapClosure = { [weak self] in + item.actions.append(ItemAction(trigger: .singleTap) { [weak self] in self?.switchToApp(event: event) - } + }) // Add to view self.items.append(item) // Check if should display any more diff --git a/MTMR/Widgets/YandexWeatherBarItem.swift b/MTMR/Widgets/YandexWeatherBarItem.swift index ed47009..c59309d 100644 --- a/MTMR/Widgets/YandexWeatherBarItem.swift +++ b/MTMR/Widgets/YandexWeatherBarItem.swift @@ -50,8 +50,13 @@ class YandexWeatherBarItem: CustomButtonTouchBarItem, CLLocationManagerDelegate manager.delegate = self manager.desiredAccuracy = kCLLocationAccuracyHundredMeters manager.startUpdatingLocation() - - tapClosure = tapClosure ?? defaultTapAction + + if actions.filter({ $0.trigger == .singleTap }).isEmpty { + actions.append(ItemAction( + trigger: .singleTap, + defaultTapAction + )) + } } required init?(coder _: NSCoder) { diff --git a/MTMRTests/ParseConfigTests.swift b/MTMRTests/ParseConfigTests.swift index a3321cc..6cec324 100644 --- a/MTMRTests/ParseConfigTests.swift +++ b/MTMRTests/ParseConfigTests.swift @@ -10,7 +10,7 @@ class ParseConfig: XCTestCase { XCTFail() return } - guard case .none? = result?.first?.action else { + guard result?.first?.actions.count == 0 else { XCTFail() return } @@ -18,14 +18,29 @@ class ParseConfig: XCTestCase { func testButtonKeyCodeAction() { let buttonKeycodeFixture = """ - [ { "type": "staticButton", "title": "Pew", "action": "hidKey", "keycode": 123} ] + [ { "type": "staticButton", "title": "Pew", "actions": [ { "trigger": "singleTap", "action": "hidKey", "keycode": 123 } ] } ] """.data(using: .utf8)! let result = try? JSONDecoder().decode([BarItemDefinition].self, from: buttonKeycodeFixture) guard case .staticButton("Pew")? = result?.first?.type else { XCTFail() return } - guard case .hidKey(keycode: 123)? = result?.first?.action else { + guard case .hidKey(keycode: 123)? = result?.first?.actions.filter({ $0.trigger == .singleTap }).first?.value else { + XCTFail() + return + } + } + + func testButtonKeyCodeLegacyAction() { + let buttonKeycodeFixture = """ + [ { "type": "staticButton", "title": "Pew", "action": "hidKey", "keycode": 123 } ] + """.data(using: .utf8)! + let result = try? JSONDecoder().decode([BarItemDefinition].self, from: buttonKeycodeFixture) + guard case .staticButton("Pew")? = result?.first?.type else { + XCTFail() + return + } + guard case .hidKey(keycode: 123)? = result?.first?.legacyAction else { XCTFail() return } @@ -40,7 +55,7 @@ class ParseConfig: XCTestCase { XCTFail() return } - guard case .keyPress(keycode: 53)? = result?.first?.action else { + guard case .keyPress(keycode: 53)? = result?.first?.actions.filter({ $0.trigger == .singleTap }).first?.value else { XCTFail() return } @@ -55,7 +70,7 @@ class ParseConfig: XCTestCase { XCTFail() return } - guard case .keyPress(keycode: 53)? = result?.first?.action else { + guard case .keyPress(keycode: 53)? = result?.first?.actions.filter({ $0.trigger == .singleTap }).first?.value else { XCTFail() return } diff --git a/README.md b/README.md index e9babc9..5daea2a 100644 --- a/README.md +++ b/README.md @@ -198,10 +198,15 @@ Example of "CPU load" button which also changes color based on load value. "source": { "inline": "top -l 2 -n 0 -F | egrep -o ' \\d*\\.\\d+% idle' | tail -1 | awk -F% '{p = 100 - $1; if (p > 30) c = \"\\033[33m\"; if (p > 70) c = \"\\033[30;43m\"; printf \"%s%4.1f%%\\n\", c, p}'" }, - "action": "appleScript", - "actionAppleScript": { - "inline": "activate application \"Activity Monitor\"\rtell application \"System Events\"\r\ttell process \"Activity Monitor\"\r\t\ttell radio button \"CPU\" of radio group 1 of group 2 of toolbar 1 of window 1 to perform action \"AXPress\"\r\tend tell\rend tell" - }, + "actions": [ + { + "trigger": "singleTap", + "action": "appleScript", + "actionAppleScript": { + "inline": "activate application \"Activity Monitor\"\rtell application \"System Events\"\r\ttell process \"Activity Monitor\"\r\t\ttell radio button \"CPU\" of radio group 1 of group 2 of toolbar 1 of window 1 to perform action \"AXPress\"\r\tend tell\rend tell" + } + } + ], "align": "right", "image": { // Or you can specify a filePath here. @@ -363,6 +368,27 @@ Displays upcoming events from MacOS Calendar. Does not display current event. ## Actions: +### Example: + +```js +"actions": [ + { + "trigger": "singleTap", + "action": "hidKey", + "keycode": 53 + } +] +``` + +### Triggers: + +- `singleTap` +- `doubleTap` +- `tripleTap` +- `longTap` + +### Types + - `hidKey` > https://github.com/aosm/IOHIDFamily/blob/master/IOHIDSystem/IOKit/hidsystem/ev_keymap.h use only numbers @@ -404,22 +430,6 @@ Displays upcoming events from MacOS Calendar. Does not display current event. "url": "https://google.com", ``` -## LongActions - -If you want to longPress for some operations, it is similar to the configuration for Actions but with additional parameters, for example: - -```js - "longAction": "hidKey", - "longKeycode": 53, -``` - -- longAction -- longKeycode -- longActionAppleScript -- longExecutablePath -- longShellArguments -- longUrl - ## Additional parameters: - `width` restrict how much room a particular button will take