mirror of
https://github.com/Toxblh/MTMR.git
synced 2026-11-02 03:18:34 +00:00
Compare commits
11
Commits
v0.26
..
7a1800252c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7a1800252c | ||
|
|
588e6ae09b | ||
|
|
87141e381b | ||
|
|
14282b86a9 | ||
|
|
810cdeed36 | ||
|
|
a65613acaf | ||
|
|
aa69d5f592 | ||
|
|
2f00c9ffb3 | ||
|
|
1def53878d | ||
|
|
3e5fa14494 | ||
|
|
2e2f556daf |
@@ -71,6 +71,7 @@
|
|||||||
B0F8771A207AC1EA00D6E430 /* TouchBarSupport.m in Sources */ = {isa = PBXBuildFile; fileRef = B0F87719207AC1EA00D6E430 /* TouchBarSupport.m */; };
|
B0F8771A207AC1EA00D6E430 /* TouchBarSupport.m in Sources */ = {isa = PBXBuildFile; fileRef = B0F87719207AC1EA00D6E430 /* TouchBarSupport.m */; };
|
||||||
BAF5AB5724317B4300B04904 /* BasicView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAF5AB5624317B4300B04904 /* BasicView.swift */; };
|
BAF5AB5724317B4300B04904 /* BasicView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAF5AB5624317B4300B04904 /* BasicView.swift */; };
|
||||||
BAF5AB5924317CAF00B04904 /* SwipeItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAF5AB5824317CAF00B04904 /* SwipeItem.swift */; };
|
BAF5AB5924317CAF00B04904 /* SwipeItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAF5AB5824317CAF00B04904 /* SwipeItem.swift */; };
|
||||||
|
F29F6A2524BC7148004FF8E4 /* UpNextScrubberTouchBarItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = F29F6A2424BC7148004FF8E4 /* UpNextScrubberTouchBarItem.swift */; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXCopyFilesBuildPhase section */
|
/* Begin PBXCopyFilesBuildPhase section */
|
||||||
@@ -163,6 +164,7 @@
|
|||||||
B0F8771B207AC92700D6E430 /* TouchBarSupport.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TouchBarSupport.h; sourceTree = "<group>"; };
|
B0F8771B207AC92700D6E430 /* TouchBarSupport.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TouchBarSupport.h; sourceTree = "<group>"; };
|
||||||
BAF5AB5624317B4300B04904 /* BasicView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BasicView.swift; sourceTree = "<group>"; };
|
BAF5AB5624317B4300B04904 /* BasicView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BasicView.swift; sourceTree = "<group>"; };
|
||||||
BAF5AB5824317CAF00B04904 /* SwipeItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwipeItem.swift; sourceTree = "<group>"; };
|
BAF5AB5824317CAF00B04904 /* SwipeItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwipeItem.swift; sourceTree = "<group>"; };
|
||||||
|
F29F6A2424BC7148004FF8E4 /* UpNextScrubberTouchBarItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpNextScrubberTouchBarItem.swift; sourceTree = "<group>"; };
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
/* Begin PBXFrameworksBuildPhase section */
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
@@ -322,6 +324,7 @@
|
|||||||
4CFF5E5B22E623DD00BFB1EE /* YandexWeatherBarItem.swift */,
|
4CFF5E5B22E623DD00BFB1EE /* YandexWeatherBarItem.swift */,
|
||||||
B08126F0217BE19000A98970 /* WidgetProtocol.swift */,
|
B08126F0217BE19000A98970 /* WidgetProtocol.swift */,
|
||||||
B0F54A792295AC7D00B4C509 /* DarkModeBarItem.swift */,
|
B0F54A792295AC7D00B4C509 /* DarkModeBarItem.swift */,
|
||||||
|
F29F6A2424BC7148004FF8E4 /* UpNextScrubberTouchBarItem.swift */,
|
||||||
);
|
);
|
||||||
path = Widgets;
|
path = Widgets;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -488,6 +491,7 @@
|
|||||||
6027D1BA2080E52A004FFDC7 /* VolumeViewController.swift in Sources */,
|
6027D1BA2080E52A004FFDC7 /* VolumeViewController.swift in Sources */,
|
||||||
4CDC6E5022FCA93F0069ADD4 /* ShellScriptTouchBarItem.swift in Sources */,
|
4CDC6E5022FCA93F0069ADD4 /* ShellScriptTouchBarItem.swift in Sources */,
|
||||||
607EEA4B2087835F009DA5F0 /* WeatherBarItem.swift in Sources */,
|
607EEA4B2087835F009DA5F0 /* WeatherBarItem.swift in Sources */,
|
||||||
|
F29F6A2524BC7148004FF8E4 /* UpNextScrubberTouchBarItem.swift in Sources */,
|
||||||
B0F3112520C9E35F0076BB88 /* SupportNSTouchBar.swift in Sources */,
|
B0F3112520C9E35F0076BB88 /* SupportNSTouchBar.swift in Sources */,
|
||||||
4CFF5E5C22E623DD00BFB1EE /* YandexWeatherBarItem.swift in Sources */,
|
4CFF5E5C22E623DD00BFB1EE /* YandexWeatherBarItem.swift in Sources */,
|
||||||
6042B6AA2083E27000C525C8 /* DeprecatedCarbonAPI.c in Sources */,
|
6042B6AA2083E27000C525C8 /* DeprecatedCarbonAPI.c in Sources */,
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ class BasicView: NSCustomTouchBarItem, NSGestureRecognizerDelegate {
|
|||||||
self.swipeItems = swipeItems
|
self.swipeItems = swipeItems
|
||||||
let views = items.compactMap { $0.view }
|
let views = items.compactMap { $0.view }
|
||||||
let stackView = NSStackView(views: views)
|
let stackView = NSStackView(views: views)
|
||||||
stackView.spacing = 1
|
stackView.spacing = 8
|
||||||
stackView.orientation = .horizontal
|
stackView.orientation = .horizontal
|
||||||
view = stackView
|
view = stackView
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -17,9 +17,9 @@
|
|||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>APPL</string>
|
<string>APPL</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>0.26</string>
|
<string>0.26.1</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>401</string>
|
<string>425</string>
|
||||||
<key>LSApplicationCategoryType</key>
|
<key>LSApplicationCategoryType</key>
|
||||||
<string>public.app-category.utilities</string>
|
<string>public.app-category.utilities</string>
|
||||||
<key>LSMinimumSystemVersion</key>
|
<key>LSMinimumSystemVersion</key>
|
||||||
|
|||||||
+195
-48
@@ -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: [:]
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -227,6 +276,7 @@ enum ItemType: Decodable {
|
|||||||
case network(flip: Bool)
|
case network(flip: Bool)
|
||||||
case darkMode
|
case darkMode
|
||||||
case swipe(direction: String, fingers: Int, minOffset: Float, sourceApple: SourceProtocol?, sourceBash: SourceProtocol?)
|
case swipe(direction: String, fingers: Int, minOffset: Float, sourceApple: SourceProtocol?, sourceBash: SourceProtocol?)
|
||||||
|
case upnext(from: Double, to: Double, maxToShow: Int, autoResize: Bool)
|
||||||
|
|
||||||
private enum CodingKeys: String, CodingKey {
|
private enum CodingKeys: String, CodingKey {
|
||||||
case type
|
case type
|
||||||
@@ -258,6 +308,7 @@ enum ItemType: Decodable {
|
|||||||
case direction
|
case direction
|
||||||
case fingers
|
case fingers
|
||||||
case minOffset
|
case minOffset
|
||||||
|
case maxToShow
|
||||||
}
|
}
|
||||||
|
|
||||||
enum ItemTypeRaw: String, Decodable {
|
enum ItemTypeRaw: String, Decodable {
|
||||||
@@ -281,6 +332,7 @@ enum ItemType: Decodable {
|
|||||||
case network
|
case network
|
||||||
case darkMode
|
case darkMode
|
||||||
case swipe
|
case swipe
|
||||||
|
case upnext
|
||||||
}
|
}
|
||||||
|
|
||||||
init(from decoder: Decoder) throws {
|
init(from decoder: Decoder) throws {
|
||||||
@@ -378,11 +430,106 @@ enum ItemType: Decodable {
|
|||||||
let fingers = try container.decode(Int.self, forKey: .fingers)
|
let fingers = try container.decode(Int.self, forKey: .fingers)
|
||||||
let minOffset = try container.decodeIfPresent(Float.self, forKey: .minOffset) ?? 0.0
|
let minOffset = try container.decodeIfPresent(Float.self, forKey: .minOffset) ?? 0.0
|
||||||
self = .swipe(direction: direction, fingers: fingers, minOffset: minOffset, sourceApple: sourceApple, sourceBash: sourceBash)
|
self = .swipe(direction: direction, fingers: fingers, minOffset: minOffset, sourceApple: sourceApple, sourceBash: sourceBash)
|
||||||
|
|
||||||
|
case .upnext:
|
||||||
|
let from = try container.decodeIfPresent(Double.self, forKey: .from) ?? 0 // Lower bounds of period of time in hours to search for events
|
||||||
|
let to = try container.decodeIfPresent(Double.self, forKey: .to) ?? 12 // Upper bounds of period of time in hours to search for events
|
||||||
|
let maxToShow = try container.decodeIfPresent(Int.self, forKey: .maxToShow) ?? 3 // 1 indexed array. Get the 1st, 2nd, 3rd event to display multiple notifications
|
||||||
|
let autoResize = try container.decodeIfPresent(Bool.self, forKey: .autoResize) ?? false
|
||||||
|
let interval = try container.decodeIfPresent(Double.self, forKey: .refreshInterval) ?? 60.0
|
||||||
|
self = .upnext(from: from, to: to, maxToShow: maxToShow, autoResize: autoResize)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
||||||
@@ -440,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)
|
||||||
|
|||||||
@@ -59,6 +59,8 @@ extension ItemType {
|
|||||||
return DarkModeBarItem.identifier
|
return DarkModeBarItem.identifier
|
||||||
case .swipe(direction: _, fingers: _, minOffset: _, sourceApple: _, sourceBash: _):
|
case .swipe(direction: _, fingers: _, minOffset: _, sourceApple: _, sourceBash: _):
|
||||||
return "com.toxblh.mtmr.swipe."
|
return "com.toxblh.mtmr.swipe."
|
||||||
|
case .upnext(from: _, to: _, maxToShow: _, autoResize: _):
|
||||||
|
return "com.connorgmeehan.mtmrup.next."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -90,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
|
(
|
||||||
guard let `self` = self else { return }
|
item: .staticButton(title: ""),
|
||||||
self.reloadPreset(path: self.lastPresetPath)
|
actions: [
|
||||||
}), longAction: .none, parameters: [.width: .width(30), .image: .image(source: (NSImage(named: NSImage.stopProgressFreestandingTemplateName))!)])
|
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
|
blacklistAppIdentifiers = AppSettings.blacklistedAppIds
|
||||||
@@ -168,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)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -301,13 +318,21 @@ class TouchBarController: NSObject, NSTouchBarDelegate {
|
|||||||
barItem = DarkModeBarItem(identifier: identifier)
|
barItem = DarkModeBarItem(identifier: identifier)
|
||||||
case let .swipe(direction: direction, fingers: fingers, minOffset: minOffset, sourceApple: sourceApple, sourceBash: sourceBash):
|
case let .swipe(direction: direction, fingers: fingers, minOffset: minOffset, sourceApple: sourceApple, sourceBash: sourceBash):
|
||||||
barItem = SwipeItem(identifier: identifier, direction: direction, fingers: fingers, minOffset: minOffset, sourceApple: sourceApple, sourceBash: sourceBash)
|
barItem = SwipeItem(identifier: identifier, direction: direction, fingers: fingers, minOffset: minOffset, sourceApple: sourceApple, sourceBash: sourceBash)
|
||||||
|
case let .upnext(from: from, to: to, maxToShow: maxToShow, autoResize: autoResize):
|
||||||
|
barItem = UpNextScrubberTouchBarItem(identifier: identifier, interval: 60, from: from, to: to, maxToShow: maxToShow, autoResize: autoResize)
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
||||||
@@ -331,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):
|
||||||
@@ -376,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):
|
||||||
@@ -428,6 +497,12 @@ extension NSCustomTouchBarItem: CanSetWidth {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension NSPopoverTouchBarItem: CanSetWidth {
|
||||||
|
func setWidth(value: CGFloat) {
|
||||||
|
view?.widthAnchor.constraint(equalToConstant: value).isActive = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
extension BarItemDefinition {
|
extension BarItemDefinition {
|
||||||
var align: Align {
|
var align: Align {
|
||||||
if case let .align(result)? = additionalParameters[.align] {
|
if case let .align(result)? = additionalParameters[.align] {
|
||||||
|
|||||||
@@ -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: [
|
||||||
self?.switchToApp(app: app)
|
ItemAction(trigger: .singleTap) { [weak self] in
|
||||||
}
|
self?.switchToApp(app: app)
|
||||||
item.longTapClosure = { [weak self] in
|
},
|
||||||
self?.handleHalfLongPress(item: app)
|
ItemAction(trigger: .longTap) { [weak self] in
|
||||||
}
|
self?.handleHalfLongPress(item: app)
|
||||||
|
}
|
||||||
|
])
|
||||||
item.killAppClosure = {[weak self] in
|
item.killAppClosure = {[weak self] in
|
||||||
self?.handleLongPress(item: app)
|
self?.handleLongPress(item: app)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -0,0 +1,256 @@
|
|||||||
|
//
|
||||||
|
// UpNextScrubberTouchBarItems.swift
|
||||||
|
// MTMR
|
||||||
|
//
|
||||||
|
// Created by Connor Meehan on 13/7/20.
|
||||||
|
// Copyright © 2020 Anton Palgunov. All rights reserved.
|
||||||
|
//
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import EventKit
|
||||||
|
|
||||||
|
class UpNextScrubberTouchBarItem: NSCustomTouchBarItem {
|
||||||
|
// Dependencies
|
||||||
|
private let scrollView = NSScrollView()
|
||||||
|
private let activity: NSBackgroundActivityScheduler // Update scheduler
|
||||||
|
private var eventSources : [IUpNextSource] = []
|
||||||
|
private var items: [UpNextItem] = []
|
||||||
|
|
||||||
|
// Settings
|
||||||
|
private var futureSearchCutoff: Double
|
||||||
|
private var pastSearchCutoff: Double
|
||||||
|
private var maxToShow: Int
|
||||||
|
private var widthConstraint: NSLayoutConstraint?
|
||||||
|
private var autoResize: Bool = false
|
||||||
|
|
||||||
|
/// <#Description#>
|
||||||
|
/// - Parameters:
|
||||||
|
/// - identifier: Unique identifier of widget
|
||||||
|
/// - interval: Update view interval in seconds
|
||||||
|
/// - from: Relative to current time, how far back we search for events in hours
|
||||||
|
/// - to: Relative to current time, how far forward we search for events in hours
|
||||||
|
/// - maxToShow: Which event to show (1 is first, 2 is second, and so on)
|
||||||
|
init(identifier: NSTouchBarItem.Identifier, interval: TimeInterval, from: Double, to: Double, maxToShow: Int, autoResize: Bool) {
|
||||||
|
// Initialise member properties
|
||||||
|
activity = NSBackgroundActivityScheduler(identifier: "\(identifier.rawValue).updateCheck")
|
||||||
|
pastSearchCutoff = from * 3600
|
||||||
|
futureSearchCutoff = to * 3600
|
||||||
|
self.maxToShow = maxToShow
|
||||||
|
self.autoResize = autoResize
|
||||||
|
UpNextItem.df.dateFormat = "HH:mm"
|
||||||
|
// Error handling
|
||||||
|
if (maxToShow <= 0) {
|
||||||
|
fatalError("Error on UpNext bar item. maxToShow property must be greater than 0.")
|
||||||
|
}
|
||||||
|
// Init super
|
||||||
|
super.init(identifier: identifier)
|
||||||
|
view = scrollView
|
||||||
|
// Add event sources
|
||||||
|
// Can optionally pass an update view callback to an event source to redraw element
|
||||||
|
self.eventSources.append(UpNextCalenderSource(updateCallback: self.updateView))
|
||||||
|
// Fallback interactivity via interval
|
||||||
|
activity.interval = interval
|
||||||
|
activity.repeats = true
|
||||||
|
activity.qualityOfService = .utility
|
||||||
|
activity.schedule { (completion: NSBackgroundActivityScheduler.CompletionHandler) in
|
||||||
|
self.updateView()
|
||||||
|
completion(NSBackgroundActivityScheduler.Result.finished)
|
||||||
|
}
|
||||||
|
updateView()
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder _: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateView() -> Void {
|
||||||
|
items = []
|
||||||
|
var upcomingEvents = self.getUpcomingEvents()
|
||||||
|
upcomingEvents.sort(by: {$0.startDate.compare($1.startDate) == .orderedAscending})
|
||||||
|
var index = 1
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
for event in upcomingEvents {
|
||||||
|
// Create UpNextItem
|
||||||
|
let item = UpNextItem(event: event)
|
||||||
|
item.backgroundColor = self.getBackgroundColor(startDate: event.startDate)
|
||||||
|
// Bind tap event
|
||||||
|
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
|
||||||
|
if (index == self.maxToShow) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
index += 1
|
||||||
|
}
|
||||||
|
self.reloadData()
|
||||||
|
self.updateSize()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func reloadData() {
|
||||||
|
let stackView = NSStackView(views: items.compactMap { $0.view })
|
||||||
|
stackView.spacing = 5
|
||||||
|
stackView.orientation = .horizontal
|
||||||
|
let visibleRect = self.scrollView.documentVisibleRect
|
||||||
|
self.scrollView.documentView = stackView
|
||||||
|
stackView.scroll(visibleRect.origin)
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateSize() {
|
||||||
|
if self.autoResize {
|
||||||
|
self.widthConstraint?.isActive = false
|
||||||
|
|
||||||
|
let width = self.scrollView.documentView?.fittingSize.width ?? 0
|
||||||
|
self.widthConstraint = self.scrollView.widthAnchor.constraint(equalToConstant: width)
|
||||||
|
self.widthConstraint!.isActive = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private func getUpcomingEvents() -> [UpNextEventModel] {
|
||||||
|
var upcomingEvents: [UpNextEventModel] = []
|
||||||
|
|
||||||
|
// Calculate the range we're going to search for events in
|
||||||
|
let dateLowerBounds = Date(timeIntervalSinceNow: self.pastSearchCutoff)
|
||||||
|
let dateUpperBounds = Date(timeIntervalSinceNow: self.futureSearchCutoff)
|
||||||
|
|
||||||
|
// Get all events from all sources
|
||||||
|
for eventSource in self.eventSources {
|
||||||
|
if (eventSource.hasPermission) {
|
||||||
|
let events = eventSource.getUpcomingEvents(dateLowerBounds: dateLowerBounds, dateUpperBounds: dateUpperBounds)
|
||||||
|
upcomingEvents.append(contentsOf: events)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return upcomingEvents
|
||||||
|
}
|
||||||
|
|
||||||
|
public func switchToApp(event: UpNextEventModel) {
|
||||||
|
var bundleIdentifier: String
|
||||||
|
switch(event.sourceType) {
|
||||||
|
case .iCalendar:
|
||||||
|
bundleIdentifier = UpNextCalenderSource.bundleIdentifier
|
||||||
|
}
|
||||||
|
|
||||||
|
NSWorkspace.shared.launchApplication(withBundleIdentifier: bundleIdentifier, options: [.default], additionalEventParamDescriptor: nil, launchIdentifier: nil)
|
||||||
|
|
||||||
|
// NB: if you can't open app which on another space, try to check mark
|
||||||
|
// "When switching to an application, switch to a Space with open windows for the application"
|
||||||
|
// in Mission control settings
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func getBackgroundColor(startDate: Date) -> NSColor {
|
||||||
|
let distance = startDate.timeIntervalSinceReferenceDate/60 - Date().timeIntervalSinceReferenceDate/60 // Get time difference in minutes
|
||||||
|
if (distance < 0 as TimeInterval) { // If it's in the past, draw as blue
|
||||||
|
return NSColor.systemBlue
|
||||||
|
} else if (distance < 30 as TimeInterval) { // Less than 30 minutes, backround is red
|
||||||
|
return NSColor.systemRed
|
||||||
|
}
|
||||||
|
return NSColor.clear
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class UpNextItem : CustomButtonTouchBarItem {
|
||||||
|
static public let df = DateFormatter()
|
||||||
|
|
||||||
|
init(event: UpNextEventModel) {
|
||||||
|
let identifier = UpNextItem.getIdentifier(event: event)
|
||||||
|
let title = UpNextItem.getTitle(event: event)
|
||||||
|
super.init(identifier: NSTouchBarItem.Identifier(rawValue: identifier), title: title)
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder _: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func getTitle(event: UpNextEventModel) -> String {
|
||||||
|
var title = ""
|
||||||
|
let startDateString = UpNextItem.df.string(for: event.startDate)
|
||||||
|
switch event.sourceType {
|
||||||
|
case .iCalendar:
|
||||||
|
title = String.init(format: "🗓 %@ - %@ ", event.title, startDateString!)
|
||||||
|
}
|
||||||
|
return title
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func getIdentifier(event: UpNextEventModel) -> String {
|
||||||
|
var identifier : String
|
||||||
|
switch event.sourceType {
|
||||||
|
case .iCalendar:
|
||||||
|
identifier = "com.mtmr.iCalendarEvent"
|
||||||
|
}
|
||||||
|
return identifier + "." + event.title
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum UpNextSourceType {
|
||||||
|
case iCalendar
|
||||||
|
}
|
||||||
|
|
||||||
|
// Model for events to be displayed in dock
|
||||||
|
struct UpNextEventModel {
|
||||||
|
let title: String
|
||||||
|
let startDate: Date
|
||||||
|
let sourceType: UpNextSourceType
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Interface for any event source
|
||||||
|
protocol IUpNextSource {
|
||||||
|
static var bundleIdentifier: String { get }
|
||||||
|
var hasPermission : Bool { get }
|
||||||
|
var updateCallback : () -> Void { get set }
|
||||||
|
|
||||||
|
init(updateCallback: @escaping () -> Void)
|
||||||
|
func getUpcomingEvents(dateLowerBounds: Date, dateUpperBounds: Date) -> [UpNextEventModel]
|
||||||
|
}
|
||||||
|
|
||||||
|
class UpNextCalenderSource : IUpNextSource {
|
||||||
|
static public let bundleIdentifier: String = "com.apple.iCal"
|
||||||
|
|
||||||
|
public var hasPermission: Bool = false
|
||||||
|
private var eventStore : EKEventStore
|
||||||
|
internal var updateCallback: () -> Void
|
||||||
|
|
||||||
|
required init(updateCallback: @escaping () -> Void = {}) {
|
||||||
|
self.updateCallback = updateCallback
|
||||||
|
eventStore = EKEventStore()
|
||||||
|
NotificationCenter.default.addObserver(forName: .EKEventStoreChanged, object: eventStore, queue: nil, using: handleUpdate)
|
||||||
|
let authStatus = EKEventStore.authorizationStatus(for: .event)
|
||||||
|
if (authStatus != .authorized) {
|
||||||
|
eventStore.requestAccess(to: .event){ granted, error in
|
||||||
|
self.hasPermission = granted;
|
||||||
|
self.handleUpdate()
|
||||||
|
if(!granted) {
|
||||||
|
NSLog("Error: MTMR UpNextBarWidget not given calendar access.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.handleUpdate()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
public func handleUpdate() {
|
||||||
|
self.handleUpdate(note: Notification(name: Notification.Name("refresh view")))
|
||||||
|
}
|
||||||
|
public func handleUpdate(note: Notification) {
|
||||||
|
self.updateCallback()
|
||||||
|
}
|
||||||
|
|
||||||
|
public func getUpcomingEvents(dateLowerBounds: Date, dateUpperBounds: Date) -> [UpNextEventModel] {
|
||||||
|
var upcomingEvents: [UpNextEventModel] = []
|
||||||
|
let calendars = self.eventStore.calendars(for: .event)
|
||||||
|
let predicate = self.eventStore.predicateForEvents(withStart: dateLowerBounds, end: dateUpperBounds, calendars: calendars)
|
||||||
|
let events = self.eventStore.events(matching: predicate)
|
||||||
|
for event in events {
|
||||||
|
upcomingEvents.append(UpNextEventModel(title: event.title, startDate: event.startDate, sourceType: UpNextSourceType.iCalendar))
|
||||||
|
}
|
||||||
|
return upcomingEvents
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,12 +13,13 @@ class YandexWeatherBarItem: CustomButtonTouchBarItem, CLLocationManagerDelegate
|
|||||||
private let activity: NSBackgroundActivityScheduler
|
private let activity: NSBackgroundActivityScheduler
|
||||||
private let unitsStr = "°C"
|
private let unitsStr = "°C"
|
||||||
private let iconsSource = [
|
private let iconsSource = [
|
||||||
"Ясно": "☀️", "Малооблачно": "🌤", "Облачно с прояснениями": "⛅️", "Пасмурно": "☁️", "Небольшой дождь": "🌦", "Дождь": "🌧", "Ливень": "⛈", "Гроза": "🌩", "Дождь со снегом": "☔️", "Небольшой снег": "❄️", "Снег": "🌨", "Туман": "🌫",
|
"Ясно": "☀️", "Малооблачно": "🌤", "Облачно с прояснениями": "⛅️", "Пасмурно": "☁️", "Небольшой дождь": "🌦", "Дождь": "🌧", "Ливень": "⛈", "Гроза": "🌩", "Дождь с грозой": "⛈", "Дождь со снегом": "☔️", "Небольшой снег": "❄️", "Снег": "🌨", "Туман": "🌫",
|
||||||
"Clear": "☀️", "Mostly clear": "🌤", "Partly cloudy": "⛅️", "Overcast": "☁️", "Light rain": "🌦", "Rain": "🌧", "Heavy rain": "⛈", "Storm": "🌩", "Sleet": "☔️", "Light snow": "❄️", "Snow": "🌨", "Fog": "🌫"
|
"Clear": "☀️", "Mostly clear": "🌤", "Partly cloudy": "⛅️", "Overcast": "☁️", "Light rain": "🌦", "Rain": "🌧", "Heavy rain": "⛈", "Storm": "🌩", "Thunderstorm with rain": "⛈", "Sleet": "☔️", "Light snow": "❄️", "Snow": "🌨", "Fog": "🌫"
|
||||||
]
|
]
|
||||||
private var location: CLLocation!
|
private var location: CLLocation!
|
||||||
private var prevLocation: CLLocation!
|
private var prevLocation: CLLocation!
|
||||||
private var manager: CLLocationManager!
|
private var manager: CLLocationManager!
|
||||||
|
private var updateWeatherTask: URLSessionDataTask?
|
||||||
|
|
||||||
init(identifier: NSTouchBarItem.Identifier, interval: TimeInterval) {
|
init(identifier: NSTouchBarItem.Identifier, interval: TimeInterval) {
|
||||||
activity = NSBackgroundActivityScheduler(identifier: "\(identifier.rawValue).updatecheck")
|
activity = NSBackgroundActivityScheduler(identifier: "\(identifier.rawValue).updatecheck")
|
||||||
@@ -50,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) {
|
||||||
@@ -61,7 +67,8 @@ class YandexWeatherBarItem: CustomButtonTouchBarItem, CLLocationManagerDelegate
|
|||||||
var urlRequest = URLRequest(url: URL(string: getWeatherUrl())!)
|
var urlRequest = URLRequest(url: URL(string: getWeatherUrl())!)
|
||||||
urlRequest.addValue("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.100 Safari/537.36", forHTTPHeaderField: "user-agent") // important for the right format
|
urlRequest.addValue("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.100 Safari/537.36", forHTTPHeaderField: "user-agent") // important for the right format
|
||||||
|
|
||||||
let task = URLSession.shared.dataTask(with: urlRequest) { data, _, error in
|
updateWeatherTask?.cancel()
|
||||||
|
updateWeatherTask = URLSession.shared.dataTask(with: urlRequest) { data, _, error in
|
||||||
guard error == nil, let response = data?.utf8string else {
|
guard error == nil, let response = data?.utf8string else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -86,12 +93,12 @@ class YandexWeatherBarItem: CustomButtonTouchBarItem, CLLocationManagerDelegate
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
task.resume()
|
updateWeatherTask?.resume()
|
||||||
}
|
}
|
||||||
|
|
||||||
func getWeatherUrl() -> String {
|
func getWeatherUrl() -> String {
|
||||||
if location != nil {
|
if location != nil {
|
||||||
return "https://yandex.ru/pogoda/?lat=\(location.coordinate.latitude)&lon=\(location.coordinate.longitude)?lang=ru"
|
return "https://yandex.ru/pogoda/?lat=\(location.coordinate.latitude)&lon=\(location.coordinate.longitude)&lang=ru"
|
||||||
} else {
|
} else {
|
||||||
return "https://yandex.ru/pogoda/?lang=ru" // Yandex will try to determine your location by default
|
return "https://yandex.ru/pogoda/?lang=ru" // Yandex will try to determine your location by default
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ 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
|
||||||
}
|
}
|
||||||
@@ -18,14 +18,29 @@ class ParseConfig: XCTestCase {
|
|||||||
|
|
||||||
func testButtonKeyCodeAction() {
|
func testButtonKeyCodeAction() {
|
||||||
let buttonKeycodeFixture = """
|
let buttonKeycodeFixture = """
|
||||||
[ { "type": "staticButton", "title": "Pew", "action": "hidKey", "keycode": 123} ]
|
[ { "type": "staticButton", "title": "Pew", "actions": [ { "trigger": "singleTap", "action": "hidKey", "keycode": 123 } ] } ]
|
||||||
""".data(using: .utf8)!
|
""".data(using: .utf8)!
|
||||||
let result = try? JSONDecoder().decode([BarItemDefinition].self, from: buttonKeycodeFixture)
|
let result = try? JSONDecoder().decode([BarItemDefinition].self, from: buttonKeycodeFixture)
|
||||||
guard case .staticButton("Pew")? = result?.first?.type else {
|
guard case .staticButton("Pew")? = result?.first?.type else {
|
||||||
XCTFail()
|
XCTFail()
|
||||||
return
|
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()
|
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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -84,6 +84,7 @@ The pre-installed configuration contains less or more than you'll probably want,
|
|||||||
- darkMode
|
- darkMode
|
||||||
- pomodoro
|
- pomodoro
|
||||||
- network
|
- network
|
||||||
|
- upnext (Calendar events)
|
||||||
|
|
||||||
> Media Keys
|
> Media Keys
|
||||||
|
|
||||||
@@ -116,7 +117,7 @@ You can add custom actions for two/three/four finger swipes. To do it, you need
|
|||||||
"type": "swipe",
|
"type": "swipe",
|
||||||
"fingers": 2, // number of fingers required (2,3 or 4)
|
"fingers": 2, // number of fingers required (2,3 or 4)
|
||||||
"direction": "right", // direction of swipe (right/left)
|
"direction": "right", // direction of swipe (right/left)
|
||||||
"minOffset" 10, // optional: minimal required offset for gesture to emit event
|
"minOffset": 10, // optional: minimal required offset for gesture to emit event
|
||||||
"sourceApple": { // optional: apple script to run
|
"sourceApple": { // optional: apple script to run
|
||||||
"inline": "beep"
|
"inline": "beep"
|
||||||
},
|
},
|
||||||
@@ -197,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}'"
|
||||||
},
|
},
|
||||||
"action": "appleScript",
|
"actions": [
|
||||||
"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"
|
"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",
|
"align": "right",
|
||||||
"image": {
|
"image": {
|
||||||
// Or you can specify a filePath here.
|
// Or you can specify a filePath here.
|
||||||
@@ -343,8 +349,46 @@ To close a group, use the button:
|
|||||||
},
|
},
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### `upnext`
|
||||||
|
|
||||||
|
> Calender next event plugin
|
||||||
|
Displays upcoming events from MacOS Calendar. Does not display current event.
|
||||||
|
|
||||||
|
```js
|
||||||
|
{
|
||||||
|
"type": "upnext",
|
||||||
|
"from": 0, // Lower bound of search range for next event in hours. Default 0 (current time)(can be negative to view events in the past)
|
||||||
|
"to": 12, // Upper bounds of search range for next event in hours. Default 12 (12 hours in the future)
|
||||||
|
"maxToShow": 3 // Limits the maximum number of events displayed. Default 3 (the first 3 upcoming events)
|
||||||
|
"autoResize": false // If true, widget will expand to display all events. Default false (scrollable view within "width")
|
||||||
|
},
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## 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
|
||||||
|
|
||||||
@@ -386,22 +430,6 @@ To close a group, use the button:
|
|||||||
"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
|
||||||
@@ -446,40 +474,21 @@ by using background with color "#000000" and bordered == false you can create bu
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
### Roadmap
|
#### If you can't open preferences:
|
||||||
|
- Opening another program which can't edit text
|
||||||
|
1. Open Terminal.app
|
||||||
|
2. Put `open -a TextEdit ~/Library/Application\ Support/MTMR/items.json` command and press <kbd>Enter</kbd>
|
||||||
|
|
||||||
- [x] Create the first prototype with TouchBar in Storyboard
|
|
||||||
- [x] Put in stripe menu on startup the application
|
|
||||||
- [x] Find how to simulate real buttons like brightness, volume, night shift and etc.
|
|
||||||
- [x] Time in touchbar!
|
|
||||||
- [x] First the weather plugin
|
|
||||||
- [x] Find how to open full-screen TouchBar without the cross and stripe menu
|
|
||||||
- [x] Find how to add haptic feedback
|
|
||||||
- [x] Add icon and menu in StatusBar
|
|
||||||
- [x] Hide from Dock
|
|
||||||
- [x] Status menu: "preferences", "quit"
|
|
||||||
- [x] JSON or another approch for save preset, maybe in `~/Library/Application Support/MTMR/`
|
|
||||||
- [x] Custom buttons size, actions by click
|
|
||||||
- [x] Layout: [always left, NSSliderView for center, always right]
|
|
||||||
- [x] System for autoupdate (https://sparkle-project.org/)
|
|
||||||
- [ ] Overwrite default values from item types (e.g. title for brightness)
|
|
||||||
- [ ] Custom settings for paddings and margins for buttons
|
|
||||||
- [ ] XPC Service for scripts
|
|
||||||
- [ ] UI for settings
|
|
||||||
- [ ] Import config from BTT
|
|
||||||
|
|
||||||
Settings:
|
#### Buttons or gestures doesn't work:
|
||||||
|
- "After the last update my mtmr is not working anymore!"
|
||||||
|
- "Buttons sometimes do not trigger action"
|
||||||
|
- "ESC don't work"
|
||||||
|
- "Gestures don't work"
|
||||||
|
|
||||||
- [ ] Interface for plugins and export like presets
|
Re-tick or check a tick for access 🍏→ System Preferences → Security and Privacy → tab Privacy → Accessibility → MTMR
|
||||||
- [x] Startup at login
|
|
||||||
- [ ] Show on/off in Dock
|
|
||||||
- [ ] Show on/off in StatusBar
|
|
||||||
- [ x] On/off Haptic Feedback
|
|
||||||
|
|
||||||
Maybe:
|
|
||||||
|
|
||||||
- [ ] Refactoring the application into packages (AppleScript, JavaScript? and Swift?)
|
|
||||||
|
|
||||||
## Credits
|
## Credits
|
||||||
|
|
||||||
|
|||||||
@@ -4,3 +4,38 @@
|
|||||||
* try move away from enums when parse preset – enums are hard to extend
|
* try move away from enums when parse preset – enums are hard to extend
|
||||||
* find better way to hide bar items
|
* find better way to hide bar items
|
||||||
* extract bar items creating from TouchBarController to separate class, cover with tests
|
* extract bar items creating from TouchBarController to separate class, cover with tests
|
||||||
|
|
||||||
|
|
||||||
|
### Roadmap
|
||||||
|
|
||||||
|
- [x] Create the first prototype with TouchBar in Storyboard
|
||||||
|
- [x] Put in stripe menu on startup the application
|
||||||
|
- [x] Find how to simulate real buttons like brightness, volume, night shift and etc.
|
||||||
|
- [x] Time in touchbar!
|
||||||
|
- [x] First the weather plugin
|
||||||
|
- [x] Find how to open full-screen TouchBar without the cross and stripe menu
|
||||||
|
- [x] Find how to add haptic feedback
|
||||||
|
- [x] Add icon and menu in StatusBar
|
||||||
|
- [x] Hide from Dock
|
||||||
|
- [x] Status menu: "preferences", "quit"
|
||||||
|
- [x] JSON or another approch for save preset, maybe in `~/Library/Application Support/MTMR/`
|
||||||
|
- [x] Custom buttons size, actions by click
|
||||||
|
- [x] Layout: [always left, NSSliderView for center, always right]
|
||||||
|
- [x] System for autoupdate (https://sparkle-project.org/)
|
||||||
|
- [ ] Overwrite default values from item types (e.g. title for brightness)
|
||||||
|
- [ ] Custom settings for paddings and margins for buttons
|
||||||
|
- [ ] XPC Service for scripts
|
||||||
|
- [ ] UI for settings
|
||||||
|
- [ ] Import config from BTT
|
||||||
|
|
||||||
|
Settings:
|
||||||
|
|
||||||
|
- [ ] Interface for plugins and export like presets
|
||||||
|
- [x] Startup at login
|
||||||
|
- [ ] Show on/off in Dock
|
||||||
|
- [ ] Show on/off in StatusBar
|
||||||
|
- [x] On/off Haptic Feedback
|
||||||
|
|
||||||
|
Maybe:
|
||||||
|
|
||||||
|
- [ ] Refactoring the application into packages (AppleScript, JavaScript? and Swift?)
|
||||||
|
|||||||
Reference in New Issue
Block a user