1
0
mirror of https://github.com/Toxblh/MTMR.git synced 2026-01-10 00:58:37 +00:00

Implement multiple actions (double tap, triple tap) (#349)

* Implement double tap and new actions array in config

* Update native widgets to use new actions parameter

* Refactor new actions parameter moving it to main definition
Renamed old action and longAction to legacy

* Fix tests

* Remove unused code

* Readd test for legacyAction

* Implement triple tap

* Add new actions explanation

* Add support for multiple actions and same trigger
This commit is contained in:
Matteo Piccina 2020-08-03 12:53:39 +02:00 committed by GitHub
parent 87141e381b
commit 588e6ae09b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 487 additions and 128 deletions

View File

@ -8,18 +8,32 @@
import Cocoa 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 { class CustomButtonTouchBarItem: NSCustomTouchBarItem, NSGestureRecognizerDelegate {
var tapClosure: (() -> Void)?
var longTapClosure: (() -> Void)? { var actions: [ItemAction] = [] {
didSet { 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: ()->() = {} var finishViewConfiguration: ()->() = {}
private var button: NSButton! private var button: NSButton!
private var singleClick: HapticClickGestureRecognizer!
private var longClick: LongPressGestureRecognizer! private var longClick: LongPressGestureRecognizer!
private var multiClick: MultiClickGestureRecognizer!
init(identifier: NSTouchBarItem.Identifier, title: String) { init(identifier: NSTouchBarItem.Identifier, title: String) {
attributedTitle = title.defaultTouchbarAttributedString attributedTitle = title.defaultTouchbarAttributedString
@ -32,9 +46,16 @@ class CustomButtonTouchBarItem: NSCustomTouchBarItem, NSGestureRecognizerDelegat
longClick.allowedTouchTypes = .direct longClick.allowedTouchTypes = .direct
longClick.delegate = self longClick.delegate = self
singleClick = HapticClickGestureRecognizer(target: self, action: #selector(handleGestureSingle)) multiClick = MultiClickGestureRecognizer(
singleClick.allowedTouchTypes = .direct target: self,
singleClick.delegate = self action: #selector(handleGestureSingleTap),
doubleAction: #selector(handleGestureDoubleTap),
tripleAction: #selector(handleGestureTripleTap)
)
multiClick.allowedTouchTypes = .direct
multiClick.delegate = self
multiClick.isDoubleClickEnabled = false
multiClick.isTripleClickEnabled = false
reinstallButton() reinstallButton()
button.attributedTitle = attributedTitle button.attributedTitle = attributedTitle
@ -100,33 +121,43 @@ class CustomButtonTouchBarItem: NSCustomTouchBarItem, NSGestureRecognizerDelegat
view = button view = button
view.addGestureRecognizer(longClick) view.addGestureRecognizer(longClick)
view.addGestureRecognizer(singleClick) // view.addGestureRecognizer(singleClick)
view.addGestureRecognizer(multiClick)
finishViewConfiguration() finishViewConfiguration()
} }
func gestureRecognizer(_ gestureRecognizer: NSGestureRecognizer, shouldRequireFailureOf otherGestureRecognizer: NSGestureRecognizer) -> Bool { func gestureRecognizer(_ gestureRecognizer: NSGestureRecognizer, shouldRequireFailureOf otherGestureRecognizer: NSGestureRecognizer) -> Bool {
if gestureRecognizer == singleClick && otherGestureRecognizer == longClick if gestureRecognizer == multiClick && otherGestureRecognizer == longClick
|| gestureRecognizer == longClick && otherGestureRecognizer == singleClick // need it || gestureRecognizer == longClick && otherGestureRecognizer == multiClick // need it
{ {
return false return false
} }
return true return true
} }
@objc func handleGestureSingle(gr: NSClickGestureRecognizer) { func callActions(for trigger: Action.Trigger) {
switch gr.state { let itemActions = self.actions.filter { $0.trigger == trigger }
case .ended: for itemAction in itemActions {
tapClosure?() itemAction.closure?()
break
default:
break
} }
} }
@objc func handleGestureSingleTap() {
callActions(for: .singleTap)
}
@objc func handleGestureDoubleTap() {
callActions(for: .doubleTap)
}
@objc func handleGestureTripleTap() {
callActions(for: .tripleTap)
}
@objc func handleGestureLong(gr: NSPressGestureRecognizer) { @objc func handleGestureLong(gr: NSPressGestureRecognizer) {
switch gr.state { switch gr.state {
case .possible: // tiny hack because we're calling action manually case .possible: // tiny hack because we're calling action manually
(self.longTapClosure ?? self.tapClosure)?() callActions(for: .longTap)
break break
default: default:
break break
@ -176,7 +207,38 @@ 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) { override func touchesBegan(with event: NSEvent) {
HapticFeedback.shared?.tap(strong: 2) HapticFeedback.shared?.tap(strong: 2)
super.touchesBegan(with: event) super.touchesBegan(with: event)
@ -185,6 +247,38 @@ class HapticClickGestureRecognizer: NSClickGestureRecognizer {
override func touchesEnded(with event: NSEvent) { override func touchesEnded(with event: NSEvent) {
HapticFeedback.shared?.tap(strong: 1) HapticFeedback.shared?.tap(strong: 1)
super.touchesEnded(with: event) 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
} }
} }

View File

@ -3,47 +3,52 @@ import Foundation
extension Data { extension Data {
func barItemDefinitions() -> [BarItemDefinition]? { 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 { struct BarItemDefinition: Decodable {
let type: ItemType let type: ItemType
let action: ActionType let actions: [Action]
let longAction: LongActionType let legacyAction: LegacyActionType
let legacyLongAction: LegacyLongActionType
let additionalParameters: [GeneralParameters.CodingKeys: GeneralParameter] let additionalParameters: [GeneralParameters.CodingKeys: GeneralParameter]
private enum CodingKeys: String, CodingKey { private enum CodingKeys: String, CodingKey {
case type 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.type = type
self.action = action self.actions = actions
self.longAction = longAction self.legacyAction = action
self.legacyLongAction = legacyLongAction
self.additionalParameters = additionalParameters self.additionalParameters = additionalParameters
} }
init(from decoder: Decoder) throws { init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self) let container = try decoder.container(keyedBy: CodingKeys.self)
let type = try container.decode(String.self, forKey: .type) 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 var additionalParameters = try GeneralParameters(from: decoder).parameters
if let result = try? parametersDecoder(decoder), 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 } 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 { } 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 -> ( typealias ParametersDecoder = (Decoder) throws -> (
item: ItemType, item: ItemType,
action: ActionType, actions: [Action],
longAction: LongActionType, legacyAction: LegacyActionType,
legacyLongAction: LegacyLongActionType,
parameters: [GeneralParameters.CodingKeys: GeneralParameter] parameters: [GeneralParameters.CodingKeys: GeneralParameter]
) )
@ -51,15 +56,21 @@ class SupportedTypesHolder {
private var supportedTypes: [String: ParametersDecoder] = [ private var supportedTypes: [String: ParametersDecoder] = [
"escape": { _ in ( "escape": { _ in (
item: .staticButton(title: "esc"), item: .staticButton(title: "esc"),
action: .keyPress(keycode: 53), actions: [
longAction: .none, Action(trigger: .singleTap, value: .keyPress(keycode: 53))
],
legacyAction: .none,
legacyLongAction: .none,
parameters: [.align: .align(.left)] parameters: [.align: .align(.left)]
) }, ) },
"delete": { _ in ( "delete": { _ in (
item: .staticButton(title: "del"), item: .staticButton(title: "del"),
action: .keyPress(keycode: 117), actions: [
longAction: .none, Action(trigger: .singleTap, value: .keyPress(keycode: 117))
],
legacyAction: .none,
legacyLongAction: .none,
parameters: [:] parameters: [:]
) }, ) },
@ -67,8 +78,11 @@ class SupportedTypesHolder {
let imageParameter = GeneralParameter.image(source: #imageLiteral(resourceName: "brightnessUp")) let imageParameter = GeneralParameter.image(source: #imageLiteral(resourceName: "brightnessUp"))
return ( return (
item: .staticButton(title: ""), item: .staticButton(title: ""),
action: .keyPress(keycode: 144), actions: [
longAction: .none, Action(trigger: .singleTap, value: .keyPress(keycode: 144))
],
legacyAction: .none,
legacyLongAction: .none,
parameters: [.image: imageParameter] parameters: [.image: imageParameter]
) )
}, },
@ -77,8 +91,11 @@ class SupportedTypesHolder {
let imageParameter = GeneralParameter.image(source: #imageLiteral(resourceName: "brightnessDown")) let imageParameter = GeneralParameter.image(source: #imageLiteral(resourceName: "brightnessDown"))
return ( return (
item: .staticButton(title: ""), item: .staticButton(title: ""),
action: .keyPress(keycode: 145), actions: [
longAction: .none, Action(trigger: .singleTap, value: .keyPress(keycode: 145))
],
legacyAction: .none,
legacyLongAction: .none,
parameters: [.image: imageParameter] parameters: [.image: imageParameter]
) )
}, },
@ -87,8 +104,11 @@ class SupportedTypesHolder {
let imageParameter = GeneralParameter.image(source: #imageLiteral(resourceName: "ill_up")) let imageParameter = GeneralParameter.image(source: #imageLiteral(resourceName: "ill_up"))
return ( return (
item: .staticButton(title: ""), item: .staticButton(title: ""),
action: .hidKey(keycode: NX_KEYTYPE_ILLUMINATION_UP), actions: [
longAction: .none, Action(trigger: .singleTap, value: .hidKey(keycode: NX_KEYTYPE_ILLUMINATION_UP))
],
legacyAction: .none,
legacyLongAction: .none,
parameters: [.image: imageParameter] parameters: [.image: imageParameter]
) )
}, },
@ -97,8 +117,11 @@ class SupportedTypesHolder {
let imageParameter = GeneralParameter.image(source: #imageLiteral(resourceName: "ill_down")) let imageParameter = GeneralParameter.image(source: #imageLiteral(resourceName: "ill_down"))
return ( return (
item: .staticButton(title: ""), item: .staticButton(title: ""),
action: .hidKey(keycode: NX_KEYTYPE_ILLUMINATION_DOWN), actions: [
longAction: .none, Action(trigger: .singleTap, value: .hidKey(keycode: NX_KEYTYPE_ILLUMINATION_DOWN))
],
legacyAction: .none,
legacyLongAction: .none,
parameters: [.image: imageParameter] parameters: [.image: imageParameter]
) )
}, },
@ -107,8 +130,11 @@ class SupportedTypesHolder {
let imageParameter = GeneralParameter.image(source: NSImage(named: NSImage.touchBarVolumeDownTemplateName)!) let imageParameter = GeneralParameter.image(source: NSImage(named: NSImage.touchBarVolumeDownTemplateName)!)
return ( return (
item: .staticButton(title: ""), item: .staticButton(title: ""),
action: .hidKey(keycode: NX_KEYTYPE_SOUND_DOWN), actions: [
longAction: .none, Action(trigger: .singleTap, value: .hidKey(keycode: NX_KEYTYPE_SOUND_DOWN))
],
legacyAction: .none,
legacyLongAction: .none,
parameters: [.image: imageParameter] parameters: [.image: imageParameter]
) )
}, },
@ -117,8 +143,11 @@ class SupportedTypesHolder {
let imageParameter = GeneralParameter.image(source: NSImage(named: NSImage.touchBarVolumeUpTemplateName)!) let imageParameter = GeneralParameter.image(source: NSImage(named: NSImage.touchBarVolumeUpTemplateName)!)
return ( return (
item: .staticButton(title: ""), item: .staticButton(title: ""),
action: .hidKey(keycode: NX_KEYTYPE_SOUND_UP), actions: [
longAction: .none, Action(trigger: .singleTap, value: .hidKey(keycode: NX_KEYTYPE_SOUND_UP))
],
legacyAction: .none,
legacyLongAction: .none,
parameters: [.image: imageParameter] parameters: [.image: imageParameter]
) )
}, },
@ -127,8 +156,11 @@ class SupportedTypesHolder {
let imageParameter = GeneralParameter.image(source: NSImage(named: NSImage.touchBarAudioOutputMuteTemplateName)!) let imageParameter = GeneralParameter.image(source: NSImage(named: NSImage.touchBarAudioOutputMuteTemplateName)!)
return ( return (
item: .staticButton(title: ""), item: .staticButton(title: ""),
action: .hidKey(keycode: NX_KEYTYPE_MUTE), actions: [
longAction: .none, Action(trigger: .singleTap, value: .hidKey(keycode: NX_KEYTYPE_MUTE))
],
legacyAction: .none,
legacyLongAction: .none,
parameters: [.image: imageParameter] parameters: [.image: imageParameter]
) )
}, },
@ -137,8 +169,11 @@ class SupportedTypesHolder {
let imageParameter = GeneralParameter.image(source: NSImage(named: NSImage.touchBarRewindTemplateName)!) let imageParameter = GeneralParameter.image(source: NSImage(named: NSImage.touchBarRewindTemplateName)!)
return ( return (
item: .staticButton(title: ""), item: .staticButton(title: ""),
action: .hidKey(keycode: NX_KEYTYPE_PREVIOUS), actions: [
longAction: .none, Action(trigger: .singleTap, value: .hidKey(keycode: NX_KEYTYPE_PREVIOUS))
],
legacyAction: .none,
legacyLongAction: .none,
parameters: [.image: imageParameter] parameters: [.image: imageParameter]
) )
}, },
@ -147,8 +182,11 @@ class SupportedTypesHolder {
let imageParameter = GeneralParameter.image(source: NSImage(named: NSImage.touchBarPlayPauseTemplateName)!) let imageParameter = GeneralParameter.image(source: NSImage(named: NSImage.touchBarPlayPauseTemplateName)!)
return ( return (
item: .staticButton(title: ""), item: .staticButton(title: ""),
action: .hidKey(keycode: NX_KEYTYPE_PLAY), actions: [
longAction: .none, Action(trigger: .singleTap, value: .hidKey(keycode: NX_KEYTYPE_PLAY))
],
legacyAction: .none,
legacyLongAction: .none,
parameters: [.image: imageParameter] parameters: [.image: imageParameter]
) )
}, },
@ -157,23 +195,32 @@ class SupportedTypesHolder {
let imageParameter = GeneralParameter.image(source: NSImage(named: NSImage.touchBarFastForwardTemplateName)!) let imageParameter = GeneralParameter.image(source: NSImage(named: NSImage.touchBarFastForwardTemplateName)!)
return ( return (
item: .staticButton(title: ""), item: .staticButton(title: ""),
action: .hidKey(keycode: NX_KEYTYPE_NEXT), actions: [
longAction: .none, Action(trigger: .singleTap, value: .hidKey(keycode: NX_KEYTYPE_NEXT))
],
legacyAction: .none,
legacyLongAction: .none,
parameters: [.image: imageParameter] parameters: [.image: imageParameter]
) )
}, },
"sleep": { _ in ( "sleep": { _ in (
item: .staticButton(title: "☕️"), item: .staticButton(title: "☕️"),
action: .shellScript(executable: "/usr/bin/pmset", parameters: ["sleepnow"]), actions: [
longAction: .none, Action(trigger: .singleTap, value: .shellScript(executable: "/usr/bin/pmset", parameters: ["sleepnow"]))
],
legacyAction: .none,
legacyLongAction: .none,
parameters: [:] parameters: [:]
) }, ) },
"displaySleep": { _ in ( "displaySleep": { _ in (
item: .staticButton(title: "☕️"), item: .staticButton(title: "☕️"),
action: .shellScript(executable: "/usr/bin/pmset", parameters: ["displaysleepnow"]), actions: [
longAction: .none, Action(trigger: .singleTap, value: .shellScript(executable: "/usr/bin/pmset", parameters: ["displaysleepnow"]))
],
legacyAction: .none,
legacyLongAction: .none,
parameters: [:] parameters: [:]
) }, ) },
@ -181,11 +228,12 @@ class SupportedTypesHolder {
static let sharedInstance = SupportedTypesHolder() static let sharedInstance = SupportedTypesHolder()
func lookup(by type: String) -> ParametersDecoder { func lookup(by type: String, actions: [Action]) -> ParametersDecoder {
return supportedTypes[type] ?? { decoder in ( return supportedTypes[type] ?? { decoder in (
item: try ItemType(from: decoder), item: try ItemType(from: decoder),
action: try ActionType(from: decoder), actions: actions,
longAction: try LongActionType(from: decoder), legacyAction: try LegacyActionType(from: decoder),
legacyLongAction: try LegacyLongActionType(from: decoder),
parameters: [:] parameters: [:]
) } ) }
} }
@ -194,12 +242,13 @@ class SupportedTypesHolder {
supportedTypes[typename] = decoder 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 register(typename: typename) { _ in
( (
item: item, item: item,
action, actions,
longAction, legacyAction,
legacyLongAction,
parameters: [:] parameters: [:]
) )
} }
@ -393,7 +442,94 @@ enum ItemType: Decodable {
} }
} }
enum ActionType: Decodable { struct FailableDecodable<Base : Decodable> : 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 none
case hidKey(keycode: Int32) case hidKey(keycode: Int32)
case keyPress(keycode: Int) case keyPress(keycode: Int)
@ -451,7 +587,7 @@ enum ActionType: Decodable {
} }
} }
enum LongActionType: Decodable { enum LegacyLongActionType: Decodable {
case none case none
case hidKey(keycode: Int32) case hidKey(keycode: Int32)
case keyPress(keycode: Int) case keyPress(keycode: Int)

View File

@ -92,13 +92,28 @@ class TouchBarController: NSObject, NSTouchBarDelegate {
private override init() { private override init() {
super.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 SupportedTypesHolder.sharedInstance.register(typename: "close") { _ in
(item: .staticButton(title: ""), action: .custom(closure: { [weak self] in (
item: .staticButton(title: ""),
actions: [
Action(trigger: .singleTap, value: .custom(closure: { [weak self] in
guard let `self` = self else { return } guard let `self` = self else { return }
self.reloadPreset(path: self.lastPresetPath) self.reloadPreset(path: self.lastPresetPath)
}), longAction: .none, parameters: [.width: .width(30), .image: .image(source: (NSImage(named: NSImage.stopProgressFreestandingTemplateName))!)]) }))
],
legacyAction: .none,
legacyLongAction: .none,
parameters: [.width: .width(30), .image: .image(source: (NSImage(named: NSImage.stopProgressFreestandingTemplateName))!)])
} }
blacklistAppIdentifiers = AppSettings.blacklistedAppIds blacklistAppIdentifiers = AppSettings.blacklistedAppIds
@ -170,7 +185,7 @@ class TouchBarController: NSObject, NSTouchBarDelegate {
func reloadPreset(path: String) { func reloadPreset(path: String) {
lastPresetPath = path 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) createAndUpdatePreset(newJsonItems: items)
} }
@ -308,10 +323,16 @@ class TouchBarController: NSObject, NSTouchBarDelegate {
} }
if let action = self.action(forItem: item), let item = barItem as? CustomButtonTouchBarItem { 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 { 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 { if case let .bordered(bordered)? = item.additionalParameters[.bordered], let item = barItem as? CustomButtonTouchBarItem {
item.isBordered = bordered item.isBordered = bordered
@ -335,8 +356,52 @@ class TouchBarController: NSObject, NSTouchBarDelegate {
return barItem 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)? { func action(forItem item: BarItemDefinition) -> (() -> Void)? {
switch item.action { switch item.legacyAction {
case let .hidKey(keycode: keycode): case let .hidKey(keycode: keycode):
return { HIDPostAuxKey(keycode) } return { HIDPostAuxKey(keycode) }
case let .keyPress(keycode: keycode): case let .keyPress(keycode: keycode):
@ -380,7 +445,7 @@ class TouchBarController: NSObject, NSTouchBarDelegate {
} }
func longAction(forItem item: BarItemDefinition) -> (() -> Void)? { func longAction(forItem item: BarItemDefinition) -> (() -> Void)? {
switch item.longAction { switch item.legacyLongAction {
case let .hidKey(keycode: keycode): case let .hidKey(keycode: keycode):
return { HIDPostAuxKey(keycode) } return { HIDPostAuxKey(keycode) }
case let .keyPress(keycode: keycode): case let .keyPress(keycode: keycode):

View File

@ -82,12 +82,14 @@ class AppScrubberTouchBarItem: NSCustomTouchBarItem {
public func createAppButton(for app: DockItem) -> DockBarItem { public func createAppButton(for app: DockItem) -> DockBarItem {
let item = DockBarItem(app) let item = DockBarItem(app)
item.isBordered = false item.isBordered = false
item.tapClosure = { [weak self] in item.actions.append(contentsOf: [
ItemAction(trigger: .singleTap) { [weak self] in
self?.switchToApp(app: app) self?.switchToApp(app: app)
} },
item.longTapClosure = { [weak self] in ItemAction(trigger: .longTap) { [weak self] in
self?.handleHalfLongPress(item: app) self?.handleHalfLongPress(item: app)
} }
])
item.killAppClosure = {[weak self] in item.killAppClosure = {[weak self] in
self?.handleLongPress(item: app) self?.handleLongPress(item: app)
} }

View File

@ -11,7 +11,7 @@ class DarkModeBarItem: CustomButtonTouchBarItem, Widget {
isBordered = false isBordered = false
setWidth(value: 24) 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) timer = Timer.scheduledTimer(timeInterval: 3, target: self, selector: #selector(refresh), userInfo: nil, repeats: true)

View File

@ -16,7 +16,7 @@ class DnDBarItem: CustomButtonTouchBarItem {
isBordered = false isBordered = false
setWidth(value: 32) 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) timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(refresh), userInfo: nil, repeats: true)

View File

@ -18,9 +18,10 @@ class InputSourceBarItem: CustomButtonTouchBarItem {
observeIputSourceChangedNotification() observeIputSourceChangedNotification()
textInputSourceDidChange() textInputSourceDidChange()
tapClosure = { [weak self] in
actions.append(ItemAction(trigger: .singleTap) { [weak self] in
self?.switchInputSource() self?.switchInputSource()
} })
} }
required init?(coder _: NSCoder) { required init?(coder _: NSCoder) {

View File

@ -42,8 +42,11 @@ class MusicBarItem: CustomButtonTouchBarItem {
super.init(identifier: identifier, title: "") super.init(identifier: identifier, title: "")
isBordered = false isBordered = false
tapClosure = { [weak self] in self?.playPause() } actions = [
longTapClosure = { [weak self] in self?.nextTrack() } 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() refreshAndSchedule()
} }
@ -178,6 +181,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() { func refreshAndSchedule() {
DispatchQueue.main.async { DispatchQueue.main.async {
self.updatePlayer() self.updatePlayer()

View File

@ -31,7 +31,7 @@ class NightShiftBarItem: CustomButtonTouchBarItem {
isBordered = false isBordered = false
setWidth(value: 28) 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) timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(refresh), userInfo: nil, repeats: true)

View File

@ -23,8 +23,9 @@ class PomodoroBarItem: CustomButtonTouchBarItem, Widget {
return ( return (
item: .pomodoro(workTime: workTime ?? 1500.00, restTime: restTime ?? 300), item: .pomodoro(workTime: workTime ?? 1500.00, restTime: restTime ?? 300),
action: .none, actions: [],
longAction: .none, legacyAction: .none,
legacyLongAction: .none,
parameters: [:] parameters: [:]
) )
} }
@ -50,8 +51,10 @@ class PomodoroBarItem: CustomButtonTouchBarItem, Widget {
self.workTime = workTime self.workTime = workTime
self.restTime = restTime self.restTime = restTime
super.init(identifier: identifier, title: defaultTitle) super.init(identifier: identifier, title: defaultTitle)
tapClosure = { [weak self] in self?.startStopWork() } actions.append(contentsOf: [
longTapClosure = { [weak self] in self?.startStopRest() } ItemAction(trigger: .singleTap) { [weak self] in self?.startStopWork() },
ItemAction(trigger: .longTap) { [weak self] in self?.startStopRest() }
])
} }
required init?(coder _: NSCoder) { required init?(coder _: NSCoder) {

View File

@ -75,9 +75,9 @@ class UpNextScrubberTouchBarItem: NSCustomTouchBarItem {
let item = UpNextItem(event: event) let item = UpNextItem(event: event)
item.backgroundColor = self.getBackgroundColor(startDate: event.startDate) item.backgroundColor = self.getBackgroundColor(startDate: event.startDate)
// Bind tap event // Bind tap event
item.tapClosure = { [weak self] in item.actions.append(ItemAction(trigger: .singleTap) { [weak self] in
self?.switchToApp(event: event) self?.switchToApp(event: event)
} })
// Add to view // Add to view
self.items.append(item) self.items.append(item)
// Check if should display any more // Check if should display any more

View File

@ -51,7 +51,12 @@ class YandexWeatherBarItem: CustomButtonTouchBarItem, CLLocationManagerDelegate
manager.desiredAccuracy = kCLLocationAccuracyHundredMeters manager.desiredAccuracy = kCLLocationAccuracyHundredMeters
manager.startUpdatingLocation() manager.startUpdatingLocation()
tapClosure = tapClosure ?? defaultTapAction if actions.filter({ $0.trigger == .singleTap }).isEmpty {
actions.append(ItemAction(
trigger: .singleTap,
defaultTapAction
))
}
} }
required init?(coder _: NSCoder) { required init?(coder _: NSCoder) {

View File

@ -10,13 +10,28 @@ class ParseConfig: XCTestCase {
XCTFail() XCTFail()
return return
} }
guard case .none? = result?.first?.action else { guard result?.first?.actions.count == 0 else {
XCTFail() XCTFail()
return return
} }
} }
func testButtonKeyCodeAction() { func testButtonKeyCodeAction() {
let buttonKeycodeFixture = """
[ { "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?.actions.filter({ $0.trigger == .singleTap }).first?.value else {
XCTFail()
return
}
}
func testButtonKeyCodeLegacyAction() {
let buttonKeycodeFixture = """ let buttonKeycodeFixture = """
[ { "type": "staticButton", "title": "Pew", "action": "hidKey", "keycode": 123 } ] [ { "type": "staticButton", "title": "Pew", "action": "hidKey", "keycode": 123 } ]
""".data(using: .utf8)! """.data(using: .utf8)!
@ -25,7 +40,7 @@ class ParseConfig: XCTestCase {
XCTFail() XCTFail()
return return
} }
guard case .hidKey(keycode: 123)? = result?.first?.action else { guard case .hidKey(keycode: 123)? = result?.first?.legacyAction else {
XCTFail() XCTFail()
return return
} }
@ -40,7 +55,7 @@ class ParseConfig: XCTestCase {
XCTFail() XCTFail()
return 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() XCTFail()
return return
} }
@ -55,7 +70,7 @@ class ParseConfig: XCTestCase {
XCTFail() XCTFail()
return 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() XCTFail()
return return
} }

View File

@ -198,10 +198,15 @@ Example of "CPU load" button which also changes color based on load value.
"source": { "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}'" "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}'"
}, },
"actions": [
{
"trigger": "singleTap",
"action": "appleScript", "action": "appleScript",
"actionAppleScript": { "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" "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", "align": "right",
"image": { "image": {
// Or you can specify a filePath here. // Or you can specify a filePath here.
@ -363,6 +368,27 @@ Displays upcoming events from MacOS Calendar. Does not display current event.
## Actions: ## Actions:
### Example:
```js
"actions": [
{
"trigger": "singleTap",
"action": "hidKey",
"keycode": 53
}
]
```
### Triggers:
- `singleTap`
- `doubleTap`
- `tripleTap`
- `longTap`
### Types
- `hidKey` - `hidKey`
> https://github.com/aosm/IOHIDFamily/blob/master/IOHIDSystem/IOKit/hidsystem/ev_keymap.h use only numbers > 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", "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: ## Additional parameters:
- `width` restrict how much room a particular button will take - `width` restrict how much room a particular button will take