mirror of
https://github.com/Toxblh/MTMR.git
synced 2026-01-10 00:58:37 +00:00
Implement double tap and new actions array in config
This commit is contained in:
parent
87141e381b
commit
420614b7ba
@ -9,17 +9,28 @@
|
||||
import Cocoa
|
||||
|
||||
class CustomButtonTouchBarItem: NSCustomTouchBarItem, NSGestureRecognizerDelegate {
|
||||
var tapClosure: (() -> Void)?
|
||||
var tapClosure: (() -> Void)? {
|
||||
didSet {
|
||||
actions[.singleTap] = tapClosure
|
||||
}
|
||||
}
|
||||
var longTapClosure: (() -> Void)? {
|
||||
didSet {
|
||||
longClick.isEnabled = longTapClosure != nil
|
||||
actions[.longTap] = longTapClosure
|
||||
}
|
||||
}
|
||||
typealias TriggerClosure = (() -> Void)?
|
||||
var actions: [Action.Trigger: TriggerClosure] = [:] {
|
||||
didSet {
|
||||
singleAndDoubleClick.isDoubleClickEnabled = actions[.doubleTap] != nil
|
||||
longClick.isEnabled = actions[.longTap] != nil
|
||||
}
|
||||
}
|
||||
var finishViewConfiguration: ()->() = {}
|
||||
|
||||
private var button: NSButton!
|
||||
private var singleClick: HapticClickGestureRecognizer!
|
||||
private var longClick: LongPressGestureRecognizer!
|
||||
private var singleAndDoubleClick: DoubleClickGestureRecognizer!
|
||||
|
||||
init(identifier: NSTouchBarItem.Identifier, title: String) {
|
||||
attributedTitle = title.defaultTouchbarAttributedString
|
||||
@ -31,10 +42,11 @@ 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
|
||||
|
||||
singleAndDoubleClick = DoubleClickGestureRecognizer(target: self, action: #selector(handleGestureSingleTap), doubleAction: #selector(handleGestureDoubleTap))
|
||||
singleAndDoubleClick.allowedTouchTypes = .direct
|
||||
singleAndDoubleClick.delegate = self
|
||||
singleAndDoubleClick.isDoubleClickEnabled = false
|
||||
|
||||
reinstallButton()
|
||||
button.attributedTitle = attributedTitle
|
||||
@ -100,33 +112,35 @@ class CustomButtonTouchBarItem: NSCustomTouchBarItem, NSGestureRecognizerDelegat
|
||||
view = button
|
||||
|
||||
view.addGestureRecognizer(longClick)
|
||||
view.addGestureRecognizer(singleClick)
|
||||
// view.addGestureRecognizer(singleClick)
|
||||
view.addGestureRecognizer(singleAndDoubleClick)
|
||||
finishViewConfiguration()
|
||||
}
|
||||
|
||||
func gestureRecognizer(_ gestureRecognizer: NSGestureRecognizer, shouldRequireFailureOf otherGestureRecognizer: NSGestureRecognizer) -> Bool {
|
||||
if gestureRecognizer == singleClick && otherGestureRecognizer == longClick
|
||||
|| gestureRecognizer == longClick && otherGestureRecognizer == singleClick // need it
|
||||
if gestureRecognizer == singleAndDoubleClick && otherGestureRecognizer == longClick
|
||||
|| gestureRecognizer == longClick && otherGestureRecognizer == singleAndDoubleClick // need it
|
||||
{
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@objc func handleGestureSingle(gr: NSClickGestureRecognizer) {
|
||||
switch gr.state {
|
||||
case .ended:
|
||||
tapClosure?()
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
@objc func handleGestureSingleTap() {
|
||||
guard let singleTap = self.actions[.singleTap] else { return }
|
||||
singleTap?()
|
||||
}
|
||||
|
||||
@objc func handleGestureDoubleTap() {
|
||||
guard let doubleTap = self.actions[.doubleTap] else { return }
|
||||
doubleTap?()
|
||||
}
|
||||
|
||||
@objc func handleGestureLong(gr: NSPressGestureRecognizer) {
|
||||
switch gr.state {
|
||||
case .possible: // tiny hack because we're calling action manually
|
||||
(self.longTapClosure ?? self.tapClosure)?()
|
||||
guard let longTap = self.actions[.longTap] else { return }
|
||||
longTap?()
|
||||
break
|
||||
default:
|
||||
break
|
||||
@ -176,15 +190,62 @@ class CustomButtonCell: NSButtonCell {
|
||||
}
|
||||
}
|
||||
|
||||
class HapticClickGestureRecognizer: NSClickGestureRecognizer {
|
||||
// Thanks to https://stackoverflow.com/a/49843893
|
||||
final class DoubleClickGestureRecognizer: NSClickGestureRecognizer {
|
||||
|
||||
private let _action: Selector
|
||||
private let _doubleAction: Selector
|
||||
private var _clickCount: Int = 0
|
||||
|
||||
public var isDoubleClickEnabled = 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) {
|
||||
_action = action
|
||||
_doubleAction = doubleAction
|
||||
super.init(target: target, action: nil)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(target:action:doubleAction) 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
|
||||
|
||||
guard isDoubleClickEnabled else {
|
||||
_ = target?.perform(_action)
|
||||
return
|
||||
}
|
||||
|
||||
let delayThreshold = 0.20 // fine tune this as needed
|
||||
perform(#selector(_resetAndPerformActionIfNecessary), with: nil, afterDelay: delayThreshold)
|
||||
if _clickCount == 2 {
|
||||
_ = target?.perform(_doubleAction)
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func _resetAndPerformActionIfNecessary() {
|
||||
if _clickCount == 1 {
|
||||
_ = target?.perform(_action)
|
||||
}
|
||||
_clickCount = 0
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -3,7 +3,7 @@ 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)!)
|
||||
}
|
||||
}
|
||||
|
||||
@ -51,25 +51,31 @@ class SupportedTypesHolder {
|
||||
private var supportedTypes: [String: ParametersDecoder] = [
|
||||
"escape": { _ in (
|
||||
item: .staticButton(title: "esc"),
|
||||
action: .keyPress(keycode: 53),
|
||||
action: .none,
|
||||
longAction: .none,
|
||||
parameters: [.align: .align(.left)]
|
||||
parameters: [.align: .align(.left), .actions: .actions([
|
||||
Action(trigger: .singleTap, value: .keyPress(keycode: 53))
|
||||
])]
|
||||
) },
|
||||
|
||||
"delete": { _ in (
|
||||
item: .staticButton(title: "del"),
|
||||
action: .keyPress(keycode: 117),
|
||||
action: .none,
|
||||
longAction: .none,
|
||||
parameters: [:]
|
||||
parameters: [.actions: .actions([
|
||||
Action(trigger: .singleTap, value: .keyPress(keycode: 117))
|
||||
])]
|
||||
) },
|
||||
|
||||
"brightnessUp": { _ in
|
||||
let imageParameter = GeneralParameter.image(source: #imageLiteral(resourceName: "brightnessUp"))
|
||||
return (
|
||||
item: .staticButton(title: ""),
|
||||
action: .keyPress(keycode: 144),
|
||||
action: .none,
|
||||
longAction: .none,
|
||||
parameters: [.image: imageParameter]
|
||||
parameters: [.image: imageParameter, .actions: .actions([
|
||||
Action(trigger: .singleTap, value: .keyPress(keycode: 144))
|
||||
])]
|
||||
)
|
||||
},
|
||||
|
||||
@ -77,9 +83,11 @@ class SupportedTypesHolder {
|
||||
let imageParameter = GeneralParameter.image(source: #imageLiteral(resourceName: "brightnessDown"))
|
||||
return (
|
||||
item: .staticButton(title: ""),
|
||||
action: .keyPress(keycode: 145),
|
||||
action: .none,
|
||||
longAction: .none,
|
||||
parameters: [.image: imageParameter]
|
||||
parameters: [.image: imageParameter, .actions: .actions([
|
||||
Action(trigger: .singleTap, value: .keyPress(keycode: 145))
|
||||
])]
|
||||
)
|
||||
},
|
||||
|
||||
@ -87,9 +95,11 @@ class SupportedTypesHolder {
|
||||
let imageParameter = GeneralParameter.image(source: #imageLiteral(resourceName: "ill_up"))
|
||||
return (
|
||||
item: .staticButton(title: ""),
|
||||
action: .hidKey(keycode: NX_KEYTYPE_ILLUMINATION_UP),
|
||||
action: .none,
|
||||
longAction: .none,
|
||||
parameters: [.image: imageParameter]
|
||||
parameters: [.image: imageParameter, .actions: .actions([
|
||||
Action(trigger: .singleTap, value: .hidKey(keycode: NX_KEYTYPE_ILLUMINATION_UP))
|
||||
])]
|
||||
)
|
||||
},
|
||||
|
||||
@ -97,9 +107,11 @@ class SupportedTypesHolder {
|
||||
let imageParameter = GeneralParameter.image(source: #imageLiteral(resourceName: "ill_down"))
|
||||
return (
|
||||
item: .staticButton(title: ""),
|
||||
action: .hidKey(keycode: NX_KEYTYPE_ILLUMINATION_DOWN),
|
||||
action: .none,
|
||||
longAction: .none,
|
||||
parameters: [.image: imageParameter]
|
||||
parameters: [.image: imageParameter, .actions: .actions([
|
||||
Action(trigger: .singleTap, value: .hidKey(keycode: NX_KEYTYPE_ILLUMINATION_DOWN))
|
||||
])]
|
||||
)
|
||||
},
|
||||
|
||||
@ -107,9 +119,11 @@ class SupportedTypesHolder {
|
||||
let imageParameter = GeneralParameter.image(source: NSImage(named: NSImage.touchBarVolumeDownTemplateName)!)
|
||||
return (
|
||||
item: .staticButton(title: ""),
|
||||
action: .hidKey(keycode: NX_KEYTYPE_SOUND_DOWN),
|
||||
action: .none,
|
||||
longAction: .none,
|
||||
parameters: [.image: imageParameter]
|
||||
parameters: [.image: imageParameter, .actions: .actions([
|
||||
Action(trigger: .singleTap, value: .hidKey(keycode: NX_KEYTYPE_SOUND_DOWN))
|
||||
])]
|
||||
)
|
||||
},
|
||||
|
||||
@ -117,9 +131,11 @@ class SupportedTypesHolder {
|
||||
let imageParameter = GeneralParameter.image(source: NSImage(named: NSImage.touchBarVolumeUpTemplateName)!)
|
||||
return (
|
||||
item: .staticButton(title: ""),
|
||||
action: .hidKey(keycode: NX_KEYTYPE_SOUND_UP),
|
||||
action: .none,
|
||||
longAction: .none,
|
||||
parameters: [.image: imageParameter]
|
||||
parameters: [.image: imageParameter, .actions: .actions([
|
||||
Action(trigger: .singleTap, value: .hidKey(keycode: NX_KEYTYPE_SOUND_UP))
|
||||
])]
|
||||
)
|
||||
},
|
||||
|
||||
@ -127,9 +143,11 @@ class SupportedTypesHolder {
|
||||
let imageParameter = GeneralParameter.image(source: NSImage(named: NSImage.touchBarAudioOutputMuteTemplateName)!)
|
||||
return (
|
||||
item: .staticButton(title: ""),
|
||||
action: .hidKey(keycode: NX_KEYTYPE_MUTE),
|
||||
action: .none,
|
||||
longAction: .none,
|
||||
parameters: [.image: imageParameter]
|
||||
parameters: [.image: imageParameter, .actions: .actions([
|
||||
Action(trigger: .singleTap, value: .hidKey(keycode: NX_KEYTYPE_MUTE))
|
||||
])]
|
||||
)
|
||||
},
|
||||
|
||||
@ -137,9 +155,11 @@ class SupportedTypesHolder {
|
||||
let imageParameter = GeneralParameter.image(source: NSImage(named: NSImage.touchBarRewindTemplateName)!)
|
||||
return (
|
||||
item: .staticButton(title: ""),
|
||||
action: .hidKey(keycode: NX_KEYTYPE_PREVIOUS),
|
||||
action: .none,
|
||||
longAction: .none,
|
||||
parameters: [.image: imageParameter]
|
||||
parameters: [.image: imageParameter, .actions: .actions([
|
||||
Action(trigger: .singleTap, value: .hidKey(keycode: NX_KEYTYPE_PREVIOUS))
|
||||
])]
|
||||
)
|
||||
},
|
||||
|
||||
@ -147,9 +167,11 @@ class SupportedTypesHolder {
|
||||
let imageParameter = GeneralParameter.image(source: NSImage(named: NSImage.touchBarPlayPauseTemplateName)!)
|
||||
return (
|
||||
item: .staticButton(title: ""),
|
||||
action: .hidKey(keycode: NX_KEYTYPE_PLAY),
|
||||
action: .none,
|
||||
longAction: .none,
|
||||
parameters: [.image: imageParameter]
|
||||
parameters: [.image: imageParameter, .actions: .actions([
|
||||
Action(trigger: .singleTap, value: .hidKey(keycode: NX_KEYTYPE_PLAY))
|
||||
])]
|
||||
)
|
||||
},
|
||||
|
||||
@ -157,24 +179,30 @@ class SupportedTypesHolder {
|
||||
let imageParameter = GeneralParameter.image(source: NSImage(named: NSImage.touchBarFastForwardTemplateName)!)
|
||||
return (
|
||||
item: .staticButton(title: ""),
|
||||
action: .hidKey(keycode: NX_KEYTYPE_NEXT),
|
||||
action: .none,
|
||||
longAction: .none,
|
||||
parameters: [.image: imageParameter]
|
||||
parameters: [.image: imageParameter, .actions: .actions([
|
||||
Action(trigger: .singleTap, value: .hidKey(keycode: NX_KEYTYPE_NEXT))
|
||||
])]
|
||||
)
|
||||
},
|
||||
|
||||
"sleep": { _ in (
|
||||
item: .staticButton(title: "☕️"),
|
||||
action: .shellScript(executable: "/usr/bin/pmset", parameters: ["sleepnow"]),
|
||||
action: .none,
|
||||
longAction: .none,
|
||||
parameters: [:]
|
||||
parameters: [.actions: .actions([
|
||||
Action(trigger: .singleTap, value: .shellScript(executable: "/usr/bin/pmset", parameters: ["sleepnow"]))
|
||||
])]
|
||||
) },
|
||||
|
||||
"displaySleep": { _ in (
|
||||
item: .staticButton(title: "☕️"),
|
||||
action: .shellScript(executable: "/usr/bin/pmset", parameters: ["displaysleepnow"]),
|
||||
action: .none,
|
||||
longAction: .none,
|
||||
parameters: [:]
|
||||
parameters: [.actions: .actions([
|
||||
Action(trigger: .singleTap, value: .shellScript(executable: "/usr/bin/pmset", parameters: ["displaysleepnow"]))
|
||||
])]
|
||||
) },
|
||||
|
||||
]
|
||||
@ -393,6 +421,92 @@ enum ItemType: 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 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 ActionType: Decodable {
|
||||
case none
|
||||
case hidKey(keycode: Int32)
|
||||
@ -516,6 +630,7 @@ enum GeneralParameter {
|
||||
case bordered(_: Bool)
|
||||
case background(_: NSColor)
|
||||
case title(_: String)
|
||||
case actions(_: [Action])
|
||||
}
|
||||
|
||||
struct GeneralParameters: Decodable {
|
||||
@ -528,6 +643,7 @@ struct GeneralParameters: Decodable {
|
||||
case bordered
|
||||
case background
|
||||
case title
|
||||
case actions
|
||||
}
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
@ -556,6 +672,10 @@ struct GeneralParameters: Decodable {
|
||||
if let title = try container.decodeIfPresent(String.self, forKey: .title) {
|
||||
result[.title] = .title(title)
|
||||
}
|
||||
|
||||
if let actions = try container.decodeIfPresent([Action].self, forKey: .actions) {
|
||||
result[.actions] = .actions(actions)//.compactMap { $0.base })
|
||||
}
|
||||
|
||||
parameters = result
|
||||
}
|
||||
|
||||
@ -313,6 +313,11 @@ class TouchBarController: NSObject, NSTouchBarDelegate {
|
||||
if let longAction = self.longAction(forItem: item), let item = barItem as? CustomButtonTouchBarItem {
|
||||
item.longTapClosure = longAction
|
||||
}
|
||||
if case let .actions(actions)? = item.additionalParameters[.actions], let item = barItem as? CustomButtonTouchBarItem {
|
||||
for action in actions {
|
||||
item.actions[action.trigger] = self.action(for: action)
|
||||
}
|
||||
}
|
||||
if case let .bordered(bordered)? = item.additionalParameters[.bordered], let item = barItem as? CustomButtonTouchBarItem {
|
||||
item.isBordered = bordered
|
||||
}
|
||||
@ -334,6 +339,50 @@ class TouchBarController: NSObject, NSTouchBarDelegate {
|
||||
}
|
||||
return barItem
|
||||
}
|
||||
|
||||
func action(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 {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user