Compare commits

..
Author SHA1 Message Date
toxblh e5bb90249f Merge remote-tracking branch 'origin/master' into notification-widget 2020-11-20 02:38:17 +00:00
toxblh 3e82676008 upd build.sh 2020-11-20 02:24:31 +00:00
toxblh a2ad47c7ba build counter 2020-11-20 02:21:05 +00:00
toxblh bbe901a572 version 0.27 2020-11-20 01:42:50 +00:00
toxblh 54eaa3fd9f Change brightness for 3 finger gesture. Close #370, #372 2020-11-19 21:32:35 +00:00
Matteo Piccina 6660bb2d8f Fix brightness keys (#367) 2020-11-19 21:27:30 +00:00
Kaibin Yang 7a1800252c Fix error. (#325)
Colon expected.
2020-08-13 12:32:55 +01:00
Matteo Piccina 588e6ae09b Implement multiple actions (double tap, triple tap) (#349)
* Implement double tap and new actions array in config

* Update native widgets to use new actions parameter

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

* Fix tests

* Remove unused code

* Readd test for legacyAction

* Implement triple tap

* Add new actions explanation

* Add support for multiple actions and same trigger
2020-08-03 11:53:39 +01:00
Connor G Meehan 87141e381b Up next calendar widget (#348)
* WIP Implementation of up next widget

* Seperated button view and event source logic

* Adjusted default parameters

* Added the ability to view multiple events

* Added ability to click touchbar item and go to calendar

* renamed nthEvent to maxToShow and changed default

* Updated CFBundleVersion

* Renamed UpNext class and fix ups

* Added "autoResize" property (same functionality as dock)

* Added EKEventStore listener to reduce perfomance impact

* Log cleanup

* Made button blue for current/past events

* Added handling of unauthorised access to calendar
2020-07-27 00:41:05 +01:00
Vladimir Tolstikov 14282b86a9 YandexWeather: fix race condition which randomly leads to displaying wrong data (#345) 2020-07-16 19:11:37 +01:00
Vladimir Tolstikov 810cdeed36 YandexWeather: fix issue with wrong URL which leads to random incorrect location detection and wrong weather display (#344) 2020-07-15 21:04:11 +01:00
Giuseppe Petrosino a65613acaf Allow change of of a group button width (#336) 2020-06-17 22:40:58 +01:00
Anton Palgunov aa69d5f592 Update TECHNICAL_DEBT.md 2020-05-26 17:46:15 +01:00
Anton Palgunov 2f00c9ffb3 Update README.md 2020-05-26 17:45:49 +01:00
Vladimir Tolstikov 1def53878d YandexWeather: update matching array with missing forecast ("Thunderstorm with rain") (#316) 2020-05-21 11:27:50 +01:00
toxblh 3e5fa14494 v0.26.1 2020-05-17 13:42:02 +01:00
Fedor ZaitsevandFedor Zaitsev 2e2f556daf Fixed lost margin (#313)
* Updated README

Added explanation for missing parameters (background, title and image)

* Implemented changable icons for AppleScriptTouchBarItem

AppleScriptTouchBarItem now allow to specify any number of icons which can be changed from the script. You cannot change icon from touch event. To change icon, you need to return array from your script with 2 values - title and icn name. More info in readme

* Fixed #312

Fixed bug that margin is lost on all elements

Co-authored-by: Fedor Zaitsev <lobster@Fedors-MacBook-Pro.local>
2020-05-17 13:39:21 +01:00
toxblh 221cc8a176 del macos target 2019-11-28 21:50:04 +03:00
toxblh a6b52ebe95 Merge remote-tracking branch 'origin/master' into notification-widget 2019-11-28 21:48:48 +03:00
toxblh a4fee54ae0 merge external code 2019-11-28 21:48:41 +03:00
26 changed files with 1528 additions and 192 deletions
+1 -1
View File
@@ -3,4 +3,4 @@
issuehunt: Toxblh
patreon: toxblh
ko_fi: toxblh
custom: https://www.paypal.me/toxblh
custom: https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=WUAAG2HH58WE4
+1 -1
View File
@@ -54,7 +54,7 @@ playground.xcworkspace
# Carthage
#
# Add this line if you want to avoid checking in source code from Carthage dependencies.
# Carthage/Checkouts
Carthage/Checkouts
Carthage/Build
+1
View File
@@ -0,0 +1 @@
github "stephencelis/SQLite.swift" ~> 0.12.0
+1
View File
@@ -0,0 +1 @@
github "stephencelis/SQLite.swift" "0.12.2"
+37 -4
View File
@@ -34,10 +34,14 @@
607EEA4D2087A8DA009DA5F0 /* CurrencyBarItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 607EEA4C2087A8DA009DA5F0 /* CurrencyBarItem.swift */; };
60C44AFD20A373A100C0EC91 /* MusicBarItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 60C44AFC20A373A100C0EC91 /* MusicBarItem.swift */; };
60F7D454208CC31400ABF5D2 /* InputSourceBarItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 60F7D453208CC31400ABF5D2 /* InputSourceBarItem.swift */; };
8338290C2360CA31008981D7 /* SQLite.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8338290B2360CA31008981D7 /* SQLite.framework */; };
8338290D2360CA3F008981D7 /* SQLite.framework in Copy Files */ = {isa = PBXBuildFile; fileRef = 8338290B2360CA31008981D7 /* SQLite.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
839ECD8523600BF500BE2DA5 /* NotificationTouchBarItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 839ECD8423600BF500BE2DA5 /* NotificationTouchBarItem.swift */; };
839ECD882360160100BE2DA5 /* Mail.scpt in Sources */ = {isa = PBXBuildFile; fileRef = 839ECD872360160100BE2DA5 /* Mail.scpt */; };
B0008E552080286C003AD4DD /* SupportHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0008E542080286C003AD4DD /* SupportHelpers.swift */; };
B002E642216C0E38002774BA /* CoreDisplay.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B002E641216C0E38002774BA /* CoreDisplay.framework */; };
B00D181D2152F4A5000806F4 /* Sparkle.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B00D181C2152F4A5000806F4 /* Sparkle.framework */; };
B00D181F2152F521000806F4 /* Sparkle.framework in CopyFiles */ = {isa = PBXBuildFile; fileRef = B00D181C2152F4A5000806F4 /* Sparkle.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
B00D181F2152F521000806F4 /* Sparkle.framework in Copy Files */ = {isa = PBXBuildFile; fileRef = B00D181C2152F4A5000806F4 /* Sparkle.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
B04B7BB72087398C00C835D0 /* BatteryBarItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = B04B7BB62087398C00C835D0 /* BatteryBarItem.swift */; };
B05600D32083E9BB00EB218D /* CustomSlider.swift in Sources */ = {isa = PBXBuildFile; fileRef = B05600D22083E9BB00EB218D /* CustomSlider.swift */; };
B059D622205E03F5006E6B86 /* TouchBarController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B059D621205E03F5006E6B86 /* TouchBarController.swift */; };
@@ -71,17 +75,20 @@
B0F8771A207AC1EA00D6E430 /* TouchBarSupport.m in Sources */ = {isa = PBXBuildFile; fileRef = B0F87719207AC1EA00D6E430 /* TouchBarSupport.m */; };
BAF5AB5724317B4300B04904 /* BasicView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAF5AB5624317B4300B04904 /* BasicView.swift */; };
BAF5AB5924317CAF00B04904 /* SwipeItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAF5AB5824317CAF00B04904 /* SwipeItem.swift */; };
F29F6A2524BC7148004FF8E4 /* UpNextScrubberTouchBarItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = F29F6A2424BC7148004FF8E4 /* UpNextScrubberTouchBarItem.swift */; };
/* End PBXBuildFile section */
/* Begin PBXCopyFilesBuildPhase section */
B00D181E2152F507000806F4 /* CopyFiles */ = {
B00D181E2152F507000806F4 /* Copy Files */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 12;
dstPath = "";
dstSubfolderSpec = 10;
files = (
B00D181F2152F521000806F4 /* Sparkle.framework in CopyFiles */,
B00D181F2152F521000806F4 /* Sparkle.framework in Copy Files */,
8338290D2360CA3F008981D7 /* SQLite.framework in Copy Files */,
);
name = "Copy Files";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
@@ -118,6 +125,10 @@
607EEA4C2087A8DA009DA5F0 /* CurrencyBarItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrencyBarItem.swift; sourceTree = "<group>"; };
60C44AFC20A373A100C0EC91 /* MusicBarItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MusicBarItem.swift; sourceTree = "<group>"; };
60F7D453208CC31400ABF5D2 /* InputSourceBarItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputSourceBarItem.swift; sourceTree = "<group>"; };
8338290B2360CA31008981D7 /* SQLite.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SQLite.framework; path = Carthage/Build/Mac/SQLite.framework; sourceTree = "<group>"; };
839ECD8423600BF500BE2DA5 /* NotificationTouchBarItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationTouchBarItem.swift; sourceTree = "<group>"; };
839ECD86236015E300BE2DA5 /* Messages.scpt */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; path = Messages.scpt; sourceTree = "<group>"; };
839ECD872360160100BE2DA5 /* Mail.scpt */ = {isa = PBXFileReference; lastKnownFileType = text; path = Mail.scpt; sourceTree = "<group>"; };
B0008E542080286C003AD4DD /* SupportHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SupportHelpers.swift; sourceTree = "<group>"; };
B002E641216C0E38002774BA /* CoreDisplay.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreDisplay.framework; path = ../../../../../System/Library/Frameworks/CoreDisplay.framework; sourceTree = "<group>"; };
B00D181C2152F4A5000806F4 /* Sparkle.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = Sparkle.framework; sourceTree = "<group>"; };
@@ -163,6 +174,7 @@
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>"; };
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 */
/* Begin PBXFrameworksBuildPhase section */
@@ -172,6 +184,7 @@
files = (
B002E642216C0E38002774BA /* CoreDisplay.framework in Frameworks */,
B081732A2135F354005D4908 /* CoreBrightness.framework in Frameworks */,
8338290C2360CA31008981D7 /* SQLite.framework in Frameworks */,
B059D62D205F11E8006E6B86 /* DFRFoundation.framework in Frameworks */,
B09EB1E6207C0F8E00D5C1E0 /* MultitouchSupport.framework in Frameworks */,
B00D181D2152F4A5000806F4 /* Sparkle.framework in Frameworks */,
@@ -191,6 +204,7 @@
B059D62B205F11E8006E6B86 /* Frameworks */ = {
isa = PBXGroup;
children = (
8338290B2360CA31008981D7 /* SQLite.framework */,
B002E641216C0E38002774BA /* CoreDisplay.framework */,
B00D181C2152F4A5000806F4 /* Sparkle.framework */,
B08173292135F354005D4908 /* CoreBrightness.framework */,
@@ -280,6 +294,8 @@
B0B1742E207D6B590004B740 /* Vox.next.scpt */,
B0B1742F207D6B590004B740 /* Vox.nowPlaying.scpt */,
B0B17428207D6B580004B740 /* Weather.scpt */,
839ECD86236015E300BE2DA5 /* Messages.scpt */,
839ECD872360160100BE2DA5 /* Mail.scpt */,
);
path = AppleScripts;
sourceTree = "<group>";
@@ -322,6 +338,8 @@
4CFF5E5B22E623DD00BFB1EE /* YandexWeatherBarItem.swift */,
B08126F0217BE19000A98970 /* WidgetProtocol.swift */,
B0F54A792295AC7D00B4C509 /* DarkModeBarItem.swift */,
839ECD8423600BF500BE2DA5 /* NotificationTouchBarItem.swift */,
F29F6A2424BC7148004FF8E4 /* UpNextScrubberTouchBarItem.swift */,
);
path = Widgets;
sourceTree = "<group>";
@@ -336,7 +354,7 @@
B082B24B205C7D8000BC04DC /* Sources */,
B082B24C205C7D8000BC04DC /* Frameworks */,
B082B24D205C7D8000BC04DC /* Resources */,
B00D181E2152F507000806F4 /* CopyFiles */,
B00D181E2152F507000806F4 /* Copy Files */,
B0679BBF215AE085000FC6B4 /* ShellScript */,
);
buildRules = (
@@ -488,6 +506,7 @@
6027D1BA2080E52A004FFDC7 /* VolumeViewController.swift in Sources */,
4CDC6E5022FCA93F0069ADD4 /* ShellScriptTouchBarItem.swift in Sources */,
607EEA4B2087835F009DA5F0 /* WeatherBarItem.swift in Sources */,
F29F6A2524BC7148004FF8E4 /* UpNextScrubberTouchBarItem.swift in Sources */,
B0F3112520C9E35F0076BB88 /* SupportNSTouchBar.swift in Sources */,
4CFF5E5C22E623DD00BFB1EE /* YandexWeatherBarItem.swift in Sources */,
6042B6AA2083E27000C525C8 /* DeprecatedCarbonAPI.c in Sources */,
@@ -495,8 +514,10 @@
B08173272135F02B005D4908 /* NightShiftBarItem.swift in Sources */,
36FEF872235A1CFC00A0ABCE /* AppSettings.swift in Sources */,
60F7D454208CC31400ABF5D2 /* InputSourceBarItem.swift in Sources */,
839ECD882360160100BE2DA5 /* Mail.scpt in Sources */,
36A778BE20A6C27100B38714 /* GeneralExtensions.swift in Sources */,
60669B4320AD8FA80074E817 /* GroupBarItem.swift in Sources */,
839ECD8523600BF500BE2DA5 /* NotificationTouchBarItem.swift in Sources */,
36C2ECDB207C3FE7003CDA33 /* ItemsParsing.swift in Sources */,
B0A7E9AA205D6AA400EEF070 /* KeyPress.swift in Sources */,
36C2ECD7207B6DAE003CDA33 /* TimeTouchBarItem.swift in Sources */,
@@ -653,9 +674,15 @@
"$(inherited)",
"$(SYSTEM_LIBRARY_DIR)/PrivateFrameworks",
"$(PROJECT_DIR)",
"$(PROJECT_DIR)/Carthage/Build/Mac",
);
INFOPLIST_FILE = MTMR/Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks";
OTHER_LDFLAGS = (
"$(inherited)",
"-framework",
"\"SQLite\"",
);
PRODUCT_BUNDLE_IDENTIFIER = Toxblh.MTMR;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@@ -676,9 +703,15 @@
"$(inherited)",
"$(SYSTEM_LIBRARY_DIR)/PrivateFrameworks",
"$(PROJECT_DIR)",
"$(PROJECT_DIR)/Carthage/Build/Mac",
);
INFOPLIST_FILE = MTMR/Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks";
OTHER_LDFLAGS = (
"$(inherited)",
"-framework",
"\"SQLite\"",
);
PRODUCT_BUNDLE_IDENTIFIER = Toxblh.MTMR;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
+10
View File
@@ -0,0 +1,10 @@
if application "Mail" is running then
tell application "Mail"
if player state is playing then
return (get artist of current track) & " – " & (get name of current track)
else
return ""
end if
end tell
end if
return ""
+10
View File
@@ -0,0 +1,10 @@
if application "Messages" is running then
tell application "Messages"
if player state is playing then
return (get artist of current track) & " – " & (get name of current track)
else
return ""
end if
end tell
end if
return ""
+11 -11
View File
@@ -15,41 +15,41 @@ class BasicView: NSCustomTouchBarItem, NSGestureRecognizerDelegate {
var fourfingers: NSPanGestureRecognizer!
var swipeItems: [SwipeItem] = []
var prevPositions: [Int: CGFloat] = [2:0, 3:0, 4:0]
// legacy gesture positions
// by legacy I mean gestures to increse/decrease volume/brigtness which can be checked from app menu
var legacyPrevPositions: [Int: CGFloat] = [2:0, 3:0, 4:0]
var legacyGesturesEnabled = false
init(identifier: NSTouchBarItem.Identifier, items: [NSTouchBarItem], swipeItems: [SwipeItem]) {
super.init(identifier: identifier)
self.swipeItems = swipeItems
let views = items.compactMap { $0.view }
let stackView = NSStackView(views: views)
stackView.spacing = 1
stackView.spacing = 8
stackView.orientation = .horizontal
view = stackView
twofingers = NSPanGestureRecognizer(target: self, action: #selector(twofingersHandler(_:)))
twofingers.numberOfTouchesRequired = 2
twofingers.allowedTouchTypes = .direct
view.addGestureRecognizer(twofingers)
threefingers = NSPanGestureRecognizer(target: self, action: #selector(threefingersHandler(_:)))
threefingers.numberOfTouchesRequired = 3
threefingers.allowedTouchTypes = .direct
view.addGestureRecognizer(threefingers)
fourfingers = NSPanGestureRecognizer(target: self, action: #selector(fourfingersHandler(_:)))
fourfingers.numberOfTouchesRequired = 4
fourfingers.allowedTouchTypes = .direct
view.addGestureRecognizer(fourfingers)
}
required init?(coder _: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func gestureHandler(position: CGFloat, fingers: Int, state: NSGestureRecognizer.State) {
switch state {
case .began:
@@ -72,9 +72,9 @@ class BasicView: NSCustomTouchBarItem, NSGestureRecognizerDelegate {
let prevPos = legacyPrevPositions[fingers]!
if ((position - prevPos) > 15) || ((prevPos - position) > 15) {
if position > prevPos {
GenericKeyPress(keyCode: CGKeyCode(144)).send()
HIDPostAuxKey(NX_KEYTYPE_BRIGHTNESS_UP)
} else if position < prevPos {
GenericKeyPress(keyCode: CGKeyCode(145)).send()
HIDPostAuxKey(NX_KEYTYPE_BRIGHTNESS_DOWN)
}
legacyPrevPositions[fingers] = position
}
@@ -89,7 +89,7 @@ class BasicView: NSCustomTouchBarItem, NSGestureRecognizerDelegate {
break
}
}
@objc func twofingersHandler(_ sender: NSGestureRecognizer?) {
let position = (sender?.location(in: sender?.view).x)!
self.gestureHandler(position: position, fingers: 2, state: sender!.state)
+116 -22
View File
@@ -8,18 +8,32 @@
import Cocoa
struct ItemAction {
typealias TriggerClosure = (() -> Void)?
let trigger: Action.Trigger
let closure: TriggerClosure
init(trigger: Action.Trigger, _ closure: TriggerClosure) {
self.trigger = trigger
self.closure = closure
}
}
class CustomButtonTouchBarItem: NSCustomTouchBarItem, NSGestureRecognizerDelegate {
var tapClosure: (() -> Void)?
var longTapClosure: (() -> Void)? {
var actions: [ItemAction] = [] {
didSet {
longClick.isEnabled = longTapClosure != nil
multiClick.isDoubleClickEnabled = actions.filter({ $0.trigger == .doubleTap }).count > 0
multiClick.isTripleClickEnabled = actions.filter({ $0.trigger == .tripleTap }).count > 0
longClick.isEnabled = actions.filter({ $0.trigger == .longTap }).count > 0
}
}
var finishViewConfiguration: ()->() = {}
private var button: NSButton!
private var singleClick: HapticClickGestureRecognizer!
private var longClick: LongPressGestureRecognizer!
private var multiClick: MultiClickGestureRecognizer!
init(identifier: NSTouchBarItem.Identifier, title: String) {
attributedTitle = title.defaultTouchbarAttributedString
@@ -31,10 +45,17 @@ class CustomButtonTouchBarItem: NSCustomTouchBarItem, NSGestureRecognizerDelegat
longClick.isEnabled = false
longClick.allowedTouchTypes = .direct
longClick.delegate = self
singleClick = HapticClickGestureRecognizer(target: self, action: #selector(handleGestureSingle))
singleClick.allowedTouchTypes = .direct
singleClick.delegate = self
multiClick = MultiClickGestureRecognizer(
target: self,
action: #selector(handleGestureSingleTap),
doubleAction: #selector(handleGestureDoubleTap),
tripleAction: #selector(handleGestureTripleTap)
)
multiClick.allowedTouchTypes = .direct
multiClick.delegate = self
multiClick.isDoubleClickEnabled = false
multiClick.isTripleClickEnabled = false
reinstallButton()
button.attributedTitle = attributedTitle
@@ -100,33 +121,43 @@ class CustomButtonTouchBarItem: NSCustomTouchBarItem, NSGestureRecognizerDelegat
view = button
view.addGestureRecognizer(longClick)
view.addGestureRecognizer(singleClick)
// view.addGestureRecognizer(singleClick)
view.addGestureRecognizer(multiClick)
finishViewConfiguration()
}
func gestureRecognizer(_ gestureRecognizer: NSGestureRecognizer, shouldRequireFailureOf otherGestureRecognizer: NSGestureRecognizer) -> Bool {
if gestureRecognizer == singleClick && otherGestureRecognizer == longClick
|| gestureRecognizer == longClick && otherGestureRecognizer == singleClick // need it
if gestureRecognizer == multiClick && otherGestureRecognizer == longClick
|| gestureRecognizer == longClick && otherGestureRecognizer == multiClick // need it
{
return false
}
return true
}
@objc func handleGestureSingle(gr: NSClickGestureRecognizer) {
switch gr.state {
case .ended:
tapClosure?()
break
default:
break
func callActions(for trigger: Action.Trigger) {
let itemActions = self.actions.filter { $0.trigger == trigger }
for itemAction in itemActions {
itemAction.closure?()
}
}
@objc func handleGestureSingleTap() {
callActions(for: .singleTap)
}
@objc func handleGestureDoubleTap() {
callActions(for: .doubleTap)
}
@objc func handleGestureTripleTap() {
callActions(for: .tripleTap)
}
@objc func handleGestureLong(gr: NSPressGestureRecognizer) {
switch gr.state {
case .possible: // tiny hack because we're calling action manually
(self.longTapClosure ?? self.tapClosure)?()
callActions(for: .longTap)
break
default:
break
@@ -176,15 +207,78 @@ class CustomButtonCell: NSButtonCell {
}
}
class HapticClickGestureRecognizer: NSClickGestureRecognizer {
// Thanks to https://stackoverflow.com/a/49843893
final class MultiClickGestureRecognizer: NSClickGestureRecognizer {
private let _action: Selector
private let _doubleAction: Selector
private let _tripleAction: Selector
private var _clickCount: Int = 0
public var isDoubleClickEnabled = true
public var isTripleClickEnabled = true
override var action: Selector? {
get {
return nil /// prevent base class from performing any actions
} set {
if newValue != nil { // if they are trying to assign an actual action
fatalError("Only use init(target:action:doubleAction) for assigning actions")
}
}
}
required init(target: AnyObject, action: Selector, doubleAction: Selector, tripleAction: Selector) {
_action = action
_doubleAction = doubleAction
_tripleAction = tripleAction
super.init(target: target, action: nil)
}
required init?(coder: NSCoder) {
fatalError("init(target:action:doubleAction:tripleAction) is only support atm")
}
override func touchesBegan(with event: NSEvent) {
HapticFeedback.shared?.tap(strong: 2)
super.touchesBegan(with: event)
}
override func touchesEnded(with event: NSEvent) {
HapticFeedback.shared?.tap(strong: 1)
super.touchesEnded(with: event)
_clickCount += 1
var delayThreshold: TimeInterval // fine tune this as needed
guard isDoubleClickEnabled || isTripleClickEnabled else {
_ = target?.perform(_action)
return
}
if (isTripleClickEnabled) {
delayThreshold = 0.4
perform(#selector(_resetAndPerformActionIfNecessary), with: nil, afterDelay: delayThreshold)
if _clickCount == 3 {
_ = target?.perform(_tripleAction)
}
} else {
delayThreshold = 0.3
perform(#selector(_resetAndPerformActionIfNecessary), with: nil, afterDelay: delayThreshold)
if _clickCount == 2 {
_ = target?.perform(_doubleAction)
}
}
}
@objc private func _resetAndPerformActionIfNecessary() {
if _clickCount == 1 {
_ = target?.perform(_action)
}
if isTripleClickEnabled && _clickCount == 2 {
_ = target?.perform(_doubleAction)
}
_clickCount = 0
}
}
+2 -2
View File
@@ -17,9 +17,9 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>0.26</string>
<string>0.27</string>
<key>CFBundleVersion</key>
<string>401</string>
<string>434</string>
<key>LSApplicationCategoryType</key>
<string>public.app-category.utilities</string>
<key>LSMinimumSystemVersion</key>
+233 -51
View File
@@ -3,47 +3,52 @@ import Foundation
extension Data {
func barItemDefinitions() -> [BarItemDefinition]? {
return try? JSONDecoder().decode([BarItemDefinition].self, from: utf8string!.stripComments().data(using: .utf8)!)
return try! JSONDecoder().decode([BarItemDefinition].self, from: utf8string!.stripComments().data(using: .utf8)!)
}
}
struct BarItemDefinition: Decodable {
let type: ItemType
let action: ActionType
let longAction: LongActionType
let actions: [Action]
let legacyAction: LegacyActionType
let legacyLongAction: LegacyLongActionType
let additionalParameters: [GeneralParameters.CodingKeys: GeneralParameter]
private enum CodingKeys: String, CodingKey {
case type
case actions
}
init(type: ItemType, action: ActionType, longAction: LongActionType, additionalParameters: [GeneralParameters.CodingKeys: GeneralParameter]) {
init(type: ItemType, actions: [Action], action: LegacyActionType, legacyLongAction: LegacyLongActionType, additionalParameters: [GeneralParameters.CodingKeys: GeneralParameter]) {
self.type = type
self.action = action
self.longAction = longAction
self.actions = actions
self.legacyAction = action
self.legacyLongAction = legacyLongAction
self.additionalParameters = additionalParameters
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let type = try container.decode(String.self, forKey: .type)
let parametersDecoder = SupportedTypesHolder.sharedInstance.lookup(by: type)
let actions = try container.decodeIfPresent([Action].self, forKey: .actions)
let parametersDecoder = SupportedTypesHolder.sharedInstance.lookup(by: type, actions: actions ?? [])
var additionalParameters = try GeneralParameters(from: decoder).parameters
if let result = try? parametersDecoder(decoder),
case let (itemType, action, longAction, parameters) = result {
case let (itemType, actions, action, longAction, parameters) = result {
parameters.forEach { additionalParameters[$0] = $1 }
self.init(type: itemType, action: action, longAction: longAction, additionalParameters: additionalParameters)
self.init(type: itemType, actions: actions, action: action, legacyLongAction: longAction, additionalParameters: additionalParameters)
} else {
self.init(type: .staticButton(title: "unknown"), action: .none, longAction: .none, additionalParameters: additionalParameters)
self.init(type: .staticButton(title: "unknown"), actions: [], action: .none, legacyLongAction: .none, additionalParameters: additionalParameters)
}
}
}
typealias ParametersDecoder = (Decoder) throws -> (
item: ItemType,
action: ActionType,
longAction: LongActionType,
actions: [Action],
legacyAction: LegacyActionType,
legacyLongAction: LegacyLongActionType,
parameters: [GeneralParameters.CodingKeys: GeneralParameter]
)
@@ -51,15 +56,28 @@ class SupportedTypesHolder {
private var supportedTypes: [String: ParametersDecoder] = [
"escape": { _ in (
item: .staticButton(title: "esc"),
action: .keyPress(keycode: 53),
longAction: .none,
actions: [
Action(trigger: .singleTap, value: .keyPress(keycode: 53))
],
legacyAction: .none,
legacyLongAction: .none,
parameters: [.align: .align(.left)]
) },
"reply": { _ in (
item: .staticButton(title: "Reply"),
action: .shellScript(executable: "/usr/bin/open", parameters: ["sms://"]),
longAction: .none,
parameters: [.align: .align(.right)]
) },
"delete": { _ in (
item: .staticButton(title: "del"),
action: .keyPress(keycode: 117),
longAction: .none,
actions: [
Action(trigger: .singleTap, value: .keyPress(keycode: 117))
],
legacyAction: .none,
legacyLongAction: .none,
parameters: [:]
) },
@@ -67,8 +85,11 @@ class SupportedTypesHolder {
let imageParameter = GeneralParameter.image(source: #imageLiteral(resourceName: "brightnessUp"))
return (
item: .staticButton(title: ""),
action: .keyPress(keycode: 144),
longAction: .none,
actions: [
Action(trigger: .singleTap, value: .hidKey(keycode: NX_KEYTYPE_BRIGHTNESS_UP))
],
legacyAction: .none,
legacyLongAction: .none,
parameters: [.image: imageParameter]
)
},
@@ -77,8 +98,11 @@ class SupportedTypesHolder {
let imageParameter = GeneralParameter.image(source: #imageLiteral(resourceName: "brightnessDown"))
return (
item: .staticButton(title: ""),
action: .keyPress(keycode: 145),
longAction: .none,
actions: [
Action(trigger: .singleTap, value: .hidKey(keycode: NX_KEYTYPE_BRIGHTNESS_DOWN))
],
legacyAction: .none,
legacyLongAction: .none,
parameters: [.image: imageParameter]
)
},
@@ -87,8 +111,11 @@ class SupportedTypesHolder {
let imageParameter = GeneralParameter.image(source: #imageLiteral(resourceName: "ill_up"))
return (
item: .staticButton(title: ""),
action: .hidKey(keycode: NX_KEYTYPE_ILLUMINATION_UP),
longAction: .none,
actions: [
Action(trigger: .singleTap, value: .hidKey(keycode: NX_KEYTYPE_ILLUMINATION_UP))
],
legacyAction: .none,
legacyLongAction: .none,
parameters: [.image: imageParameter]
)
},
@@ -97,8 +124,11 @@ class SupportedTypesHolder {
let imageParameter = GeneralParameter.image(source: #imageLiteral(resourceName: "ill_down"))
return (
item: .staticButton(title: ""),
action: .hidKey(keycode: NX_KEYTYPE_ILLUMINATION_DOWN),
longAction: .none,
actions: [
Action(trigger: .singleTap, value: .hidKey(keycode: NX_KEYTYPE_ILLUMINATION_DOWN))
],
legacyAction: .none,
legacyLongAction: .none,
parameters: [.image: imageParameter]
)
},
@@ -107,8 +137,11 @@ class SupportedTypesHolder {
let imageParameter = GeneralParameter.image(source: NSImage(named: NSImage.touchBarVolumeDownTemplateName)!)
return (
item: .staticButton(title: ""),
action: .hidKey(keycode: NX_KEYTYPE_SOUND_DOWN),
longAction: .none,
actions: [
Action(trigger: .singleTap, value: .hidKey(keycode: NX_KEYTYPE_SOUND_DOWN))
],
legacyAction: .none,
legacyLongAction: .none,
parameters: [.image: imageParameter]
)
},
@@ -117,8 +150,11 @@ class SupportedTypesHolder {
let imageParameter = GeneralParameter.image(source: NSImage(named: NSImage.touchBarVolumeUpTemplateName)!)
return (
item: .staticButton(title: ""),
action: .hidKey(keycode: NX_KEYTYPE_SOUND_UP),
longAction: .none,
actions: [
Action(trigger: .singleTap, value: .hidKey(keycode: NX_KEYTYPE_SOUND_UP))
],
legacyAction: .none,
legacyLongAction: .none,
parameters: [.image: imageParameter]
)
},
@@ -127,8 +163,11 @@ class SupportedTypesHolder {
let imageParameter = GeneralParameter.image(source: NSImage(named: NSImage.touchBarAudioOutputMuteTemplateName)!)
return (
item: .staticButton(title: ""),
action: .hidKey(keycode: NX_KEYTYPE_MUTE),
longAction: .none,
actions: [
Action(trigger: .singleTap, value: .hidKey(keycode: NX_KEYTYPE_MUTE))
],
legacyAction: .none,
legacyLongAction: .none,
parameters: [.image: imageParameter]
)
},
@@ -137,8 +176,11 @@ class SupportedTypesHolder {
let imageParameter = GeneralParameter.image(source: NSImage(named: NSImage.touchBarRewindTemplateName)!)
return (
item: .staticButton(title: ""),
action: .hidKey(keycode: NX_KEYTYPE_PREVIOUS),
longAction: .none,
actions: [
Action(trigger: .singleTap, value: .hidKey(keycode: NX_KEYTYPE_PREVIOUS))
],
legacyAction: .none,
legacyLongAction: .none,
parameters: [.image: imageParameter]
)
},
@@ -147,8 +189,11 @@ class SupportedTypesHolder {
let imageParameter = GeneralParameter.image(source: NSImage(named: NSImage.touchBarPlayPauseTemplateName)!)
return (
item: .staticButton(title: ""),
action: .hidKey(keycode: NX_KEYTYPE_PLAY),
longAction: .none,
actions: [
Action(trigger: .singleTap, value: .hidKey(keycode: NX_KEYTYPE_PLAY))
],
legacyAction: .none,
legacyLongAction: .none,
parameters: [.image: imageParameter]
)
},
@@ -157,35 +202,66 @@ class SupportedTypesHolder {
let imageParameter = GeneralParameter.image(source: NSImage(named: NSImage.touchBarFastForwardTemplateName)!)
return (
item: .staticButton(title: ""),
action: .hidKey(keycode: NX_KEYTYPE_NEXT),
longAction: .none,
actions: [
Action(trigger: .singleTap, value: .hidKey(keycode: NX_KEYTYPE_NEXT))
],
legacyAction: .none,
legacyLongAction: .none,
parameters: [.image: imageParameter]
)
},
"sleep": { _ in (
item: .staticButton(title: "☕️"),
action: .shellScript(executable: "/usr/bin/pmset", parameters: ["sleepnow"]),
longAction: .none,
actions: [
Action(trigger: .singleTap, value: .shellScript(executable: "/usr/bin/pmset", parameters: ["sleepnow"]))
],
legacyAction: .none,
legacyLongAction: .none,
parameters: [:]
) },
"displaySleep": { _ in (
item: .staticButton(title: "☕️"),
action: .shellScript(executable: "/usr/bin/pmset", parameters: ["displaysleepnow"]),
longAction: .none,
actions: [
Action(trigger: .singleTap, value: .shellScript(executable: "/usr/bin/pmset", parameters: ["displaysleepnow"]))
],
legacyAction: .none,
legacyLongAction: .none,
parameters: [:]
) },
"notification": { decoder in
enum CodingKeys: String, CodingKey { case refreshInterval; case disableMarquee; case image }
let container = try decoder.container(keyedBy: CodingKeys.self)
let interval = try container.decodeIfPresent(Double.self, forKey: .refreshInterval)
let disableMarquee = try container.decodeIfPresent(Bool.self, forKey: .disableMarquee)
if var img = try container.decodeIfPresent(Source.self, forKey: .image) {
return (
item: .notification(interval: interval ?? 5.0, disableMarquee: disableMarquee ?? false),
action: .shellScript(executable: "/usr/bin/open", parameters: ["sms://"]),
longAction: .none,
parameters: [.image: .image(source: img)]
)
} else {
return (
item: .notification(interval: interval ?? 5.0, disableMarquee: disableMarquee ?? false),
action: .shellScript(executable: "/usr/bin/open", parameters: ["sms://"]),
longAction: .none,
parameters: [:]
)
}
},
]
static let sharedInstance = SupportedTypesHolder()
func lookup(by type: String) -> ParametersDecoder {
func lookup(by type: String, actions: [Action]) -> ParametersDecoder {
return supportedTypes[type] ?? { decoder in (
item: try ItemType(from: decoder),
action: try ActionType(from: decoder),
longAction: try LongActionType(from: decoder),
actions: actions,
legacyAction: try LegacyActionType(from: decoder),
legacyLongAction: try LegacyLongActionType(from: decoder),
parameters: [:]
) }
}
@@ -194,12 +270,13 @@ class SupportedTypesHolder {
supportedTypes[typename] = decoder
}
func register(typename: String, item: ItemType, action: ActionType, longAction: LongActionType) {
func register(typename: String, item: ItemType, actions: [Action], legacyAction: LegacyActionType, legacyLongAction: LegacyLongActionType) {
register(typename: typename) { _ in
(
item: item,
action,
longAction,
actions,
legacyAction,
legacyLongAction,
parameters: [:]
)
}
@@ -220,6 +297,7 @@ enum ItemType: Decodable {
case currency(interval: Double, from: String, to: String, full: Bool)
case inputsource
case music(interval: Double, disableMarquee: Bool)
case notification(interval: Double, disableMarquee: Bool)
case group(items: [BarItemDefinition])
case nightShift
case dnd
@@ -227,6 +305,7 @@ enum ItemType: Decodable {
case network(flip: Bool)
case darkMode
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 {
case type
@@ -258,6 +337,7 @@ enum ItemType: Decodable {
case direction
case fingers
case minOffset
case maxToShow
}
enum ItemTypeRaw: String, Decodable {
@@ -274,6 +354,7 @@ enum ItemType: Decodable {
case currency
case inputsource
case music
case notification
case group
case nightShift
case dnd
@@ -281,6 +362,7 @@ enum ItemType: Decodable {
case network
case darkMode
case swipe
case upnext
}
init(from decoder: Decoder) throws {
@@ -292,7 +374,7 @@ enum ItemType: Decodable {
let interval = try container.decodeIfPresent(Double.self, forKey: .refreshInterval) ?? 1800.0
let alternativeImages = try container.decodeIfPresent([String: Source].self, forKey: .alternativeImages) ?? [:]
self = .appleScriptTitledButton(source: source, refreshInterval: interval, alternativeImages: alternativeImages)
case .shellScriptTitledButton:
let source = try container.decode(Source.self, forKey: .source)
let interval = try container.decodeIfPresent(Double.self, forKey: .refreshInterval) ?? 1800.0
@@ -329,7 +411,7 @@ enum ItemType: Decodable {
let api_key = try container.decodeIfPresent(String.self, forKey: .api_key) ?? "32c4256d09a4c52b38aecddba7a078f6"
let icon_type = try container.decodeIfPresent(String.self, forKey: .icon_type) ?? "text"
self = .weather(interval: interval, units: units, api_key: api_key, icon_type: icon_type)
case .yandexWeather:
let interval = try container.decodeIfPresent(Double.self, forKey: .refreshInterval) ?? 1800.0
self = .yandexWeather(interval: interval)
@@ -349,6 +431,11 @@ enum ItemType: Decodable {
let disableMarquee = try container.decodeIfPresent(Bool.self, forKey: .disableMarquee) ?? false
self = .music(interval: interval, disableMarquee: disableMarquee)
case .notification:
let interval = try container.decodeIfPresent(Double.self, forKey: .refreshInterval) ?? 5.0
let disableMarquee = try container.decodeIfPresent(Bool.self, forKey: .disableMarquee) ?? false
self = .notification(interval: interval, disableMarquee: disableMarquee)
case .group:
let items = try container.decode([BarItemDefinition].self, forKey: .items)
self = .group(items: items)
@@ -370,7 +457,7 @@ enum ItemType: Decodable {
case .darkMode:
self = .darkMode
case .swipe:
let sourceApple = try container.decodeIfPresent(Source.self, forKey: .sourceApple)
let sourceBash = try container.decodeIfPresent(Source.self, forKey: .sourceBash)
@@ -378,11 +465,106 @@ enum ItemType: Decodable {
let fingers = try container.decode(Int.self, forKey: .fingers)
let minOffset = try container.decodeIfPresent(Float.self, forKey: .minOffset) ?? 0.0
self = .swipe(direction: direction, fingers: fingers, minOffset: minOffset, sourceApple: sourceApple, sourceBash: sourceBash)
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 hidKey(keycode: Int32)
case keyPress(keycode: Int)
@@ -440,7 +622,7 @@ enum ActionType: Decodable {
}
}
enum LongActionType: Decodable {
enum LegacyLongActionType: Decodable {
case none
case hidKey(keycode: Int32)
case keyPress(keycode: Int)
+98 -15
View File
@@ -45,6 +45,8 @@ extension ItemType {
return "com.toxblh.mtmr.inputsource."
case .music(interval: _):
return "com.toxblh.mtmr.music."
case .notification(interval: _):
return "com.toxblh.mtmr.notification."
case .group(items: _):
return "com.toxblh.mtmr.groupBar."
case .nightShift:
@@ -59,6 +61,8 @@ extension ItemType {
return DarkModeBarItem.identifier
case .swipe(direction: _, fingers: _, minOffset: _, sourceApple: _, sourceBash: _):
return "com.toxblh.mtmr.swipe."
case .upnext(from: _, to: _, maxToShow: _, autoResize: _):
return "com.connorgmeehan.mtmrup.next."
}
}
}
@@ -90,17 +94,32 @@ class TouchBarController: NSObject, NSTouchBarDelegate {
private override init() {
super.init()
SupportedTypesHolder.sharedInstance.register(typename: "exitTouchbar", item: .staticButton(title: "exit"), action: .custom(closure: { [weak self] in self?.dismissTouchBar() }), longAction: .none)
SupportedTypesHolder.sharedInstance.register(
typename: "exitTouchbar",
item: .staticButton(title: "exit"),
actions: [
Action(trigger: .singleTap, value: .custom(closure: { [weak self] in self?.dismissTouchBar() }))
],
legacyAction: .none,
legacyLongAction: .none
)
SupportedTypesHolder.sharedInstance.register(typename: "close") { _ in
(item: .staticButton(title: ""), action: .custom(closure: { [weak self] in
guard let `self` = self else { return }
self.reloadPreset(path: self.lastPresetPath)
}), longAction: .none, parameters: [.width: .width(30), .image: .image(source: (NSImage(named: NSImage.stopProgressFreestandingTemplateName))!)])
(
item: .staticButton(title: ""),
actions: [
Action(trigger: .singleTap, value: .custom(closure: { [weak self] in
guard let `self` = self else { return }
self.reloadPreset(path: self.lastPresetPath)
}))
],
legacyAction: .none,
legacyLongAction: .none,
parameters: [.width: .width(30), .image: .image(source: (NSImage(named: NSImage.stopProgressFreestandingTemplateName))!)])
}
blacklistAppIdentifiers = AppSettings.blacklistedAppIds
NSWorkspace.shared.notificationCenter.addObserver(self, selector: #selector(activeApplicationChanged), name: NSWorkspace.didLaunchApplicationNotification, object: nil)
NSWorkspace.shared.notificationCenter.addObserver(self, selector: #selector(activeApplicationChanged), name: NSWorkspace.didTerminateApplicationNotification, object: nil)
NSWorkspace.shared.notificationCenter.addObserver(self, selector: #selector(activeApplicationChanged), name: NSWorkspace.didActivateApplicationNotification, object: nil)
@@ -123,20 +142,20 @@ class TouchBarController: NSObject, NSTouchBarDelegate {
let centerItems = centerIdentifiers.compactMap({ (identifier) -> NSTouchBarItem? in
items[identifier]
})
let centerScrollArea = NSTouchBarItem.Identifier("com.toxblh.mtmr.scrollArea.".appending(UUID().uuidString))
let scrollArea = ScrollViewItem(identifier: centerScrollArea, items: centerItems)
touchBar.delegate = self
touchBar.defaultItemIdentifiers = [basicViewIdentifier]
let leftItems = leftIdentifiers.compactMap({ (identifier) -> NSTouchBarItem? in
items[identifier]
})
let rightItems = rightIdentifiers.compactMap({ (identifier) -> NSTouchBarItem? in
items[identifier]
})
basicView = BasicView(identifier: basicViewIdentifier, items:leftItems + [scrollArea] + rightItems, swipeItems: swipeItems)
basicView?.legacyGesturesEnabled = AppSettings.multitouchGestures
@@ -168,7 +187,7 @@ class TouchBarController: NSObject, NSTouchBarDelegate {
func reloadPreset(path: String) {
lastPresetPath = path
let items = path.fileData?.barItemDefinitions() ?? [BarItemDefinition(type: .staticButton(title: "bad preset"), action: .none, longAction: .none, additionalParameters: [:])]
let items = path.fileData?.barItemDefinitions() ?? [BarItemDefinition(type: .staticButton(title: "bad preset"), actions: [], action: .none, legacyLongAction: .none, additionalParameters: [:])]
createAndUpdatePreset(newJsonItems: items)
}
@@ -265,7 +284,7 @@ class TouchBarController: NSObject, NSTouchBarDelegate {
} else {
barItem = AppScrubberTouchBarItem(identifier: identifier, autoResize: autoResize)
}
case .volume:
case .volume:
if case let .image(source)? = item.additionalParameters[.image] {
barItem = VolumeViewController(identifier: identifier, image: source.image)
} else {
@@ -287,6 +306,12 @@ class TouchBarController: NSObject, NSTouchBarDelegate {
barItem = InputSourceBarItem(identifier: identifier)
case let .music(interval: interval, disableMarquee: disableMarquee):
barItem = MusicBarItem(identifier: identifier, interval: interval, disableMarquee: disableMarquee)
case let .notification(interval: interval, disableMarquee: disableMarquee):
if case let .image(source)? = item.additionalParameters[.image] {
barItem = NotificationBarItem(identifier: identifier, interval: interval, disableMarquee: disableMarquee, image: source.image)
} else {
barItem = NotificationBarItem(identifier: identifier, interval: interval, disableMarquee: disableMarquee)
}
case let .group(items: items):
barItem = GroupBarItem(identifier: identifier, items: items)
case .nightShift:
@@ -301,13 +326,21 @@ class TouchBarController: NSObject, NSTouchBarDelegate {
barItem = DarkModeBarItem(identifier: identifier)
case let .swipe(direction: direction, fingers: fingers, minOffset: minOffset, sourceApple: sourceApple, sourceBash: sourceBash):
barItem = SwipeItem(identifier: identifier, direction: direction, fingers: fingers, minOffset: minOffset, sourceApple: sourceApple, sourceBash: sourceBash)
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 {
item.tapClosure = action
item.actions.append(ItemAction(trigger: .singleTap, action))
}
if let longAction = self.longAction(forItem: item), let item = barItem as? CustomButtonTouchBarItem {
item.longTapClosure = longAction
item.actions.append(ItemAction(trigger: .longTap, longAction))
}
if let touchBarItem = barItem as? CustomButtonTouchBarItem {
for action in item.actions {
touchBarItem.actions.append(ItemAction(trigger: action.trigger, self.closure(for: action)))
}
}
if case let .bordered(bordered)? = item.additionalParameters[.bordered], let item = barItem as? CustomButtonTouchBarItem {
item.isBordered = bordered
@@ -330,9 +363,53 @@ class TouchBarController: NSObject, NSTouchBarDelegate {
}
return barItem
}
func closure(for action: Action) -> (() -> Void)? {
switch action.value {
case let .hidKey(keycode: keycode):
return { HIDPostAuxKey(keycode) }
case let .keyPress(keycode: keycode):
return { GenericKeyPress(keyCode: CGKeyCode(keycode)).send() }
case let .appleScript(source: source):
guard let appleScript = source.appleScript else {
print("cannot create apple script for item \(action)")
return {}
}
return {
DispatchQueue.appleScriptQueue.async {
var error: NSDictionary?
appleScript.executeAndReturnError(&error)
if let error = error {
print("error \(error) when handling \(action) ")
}
}
}
case let .shellScript(executable: executable, parameters: parameters):
return {
let task = Process()
task.launchPath = executable
task.arguments = parameters
task.launch()
}
case let .openUrl(url: url):
return {
if let url = URL(string: url), NSWorkspace.shared.open(url) {
#if DEBUG
print("URL was successfully opened")
#endif
} else {
print("error", url)
}
}
case let .custom(closure: closure):
return closure
case .none:
return nil
}
}
func action(forItem item: BarItemDefinition) -> (() -> Void)? {
switch item.action {
switch item.legacyAction {
case let .hidKey(keycode: keycode):
return { HIDPostAuxKey(keycode) }
case let .keyPress(keycode: keycode):
@@ -376,7 +453,7 @@ class TouchBarController: NSObject, NSTouchBarDelegate {
}
func longAction(forItem item: BarItemDefinition) -> (() -> Void)? {
switch item.longAction {
switch item.legacyLongAction {
case let .hidKey(keycode: keycode):
return { HIDPostAuxKey(keycode) }
case let .keyPress(keycode: keycode):
@@ -428,6 +505,12 @@ extension NSCustomTouchBarItem: CanSetWidth {
}
}
extension NSPopoverTouchBarItem: CanSetWidth {
func setWidth(value: CGFloat) {
view?.widthAnchor.constraint(equalToConstant: value).isActive = true
}
}
extension BarItemDefinition {
var align: Align {
if case let .align(result)? = additionalParameters[.align] {
+8 -6
View File
@@ -82,12 +82,14 @@ class AppScrubberTouchBarItem: NSCustomTouchBarItem {
public func createAppButton(for app: DockItem) -> DockBarItem {
let item = DockBarItem(app)
item.isBordered = false
item.tapClosure = { [weak self] in
self?.switchToApp(app: app)
}
item.longTapClosure = { [weak self] in
self?.handleHalfLongPress(item: app)
}
item.actions.append(contentsOf: [
ItemAction(trigger: .singleTap) { [weak self] in
self?.switchToApp(app: app)
},
ItemAction(trigger: .longTap) { [weak self] in
self?.handleHalfLongPress(item: app)
}
])
item.killAppClosure = {[weak self] in
self?.handleLongPress(item: app)
}
+1 -1
View File
@@ -11,7 +11,7 @@ class DarkModeBarItem: CustomButtonTouchBarItem, Widget {
isBordered = false
setWidth(value: 24)
tapClosure = { [weak self] in self?.DarkModeToggle() }
actions.append(ItemAction(trigger: .singleTap) { [weak self] in self?.DarkModeToggle() })
timer = Timer.scheduledTimer(timeInterval: 3, target: self, selector: #selector(refresh), userInfo: nil, repeats: true)
+1 -1
View File
@@ -16,7 +16,7 @@ class DnDBarItem: CustomButtonTouchBarItem {
isBordered = false
setWidth(value: 32)
tapClosure = { [weak self] in self?.DnDToggle() }
actions.append(ItemAction(trigger: .singleTap) { [weak self] in self?.DnDToggle() })
timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(refresh), userInfo: nil, repeats: true)
+3 -2
View File
@@ -18,9 +18,10 @@ class InputSourceBarItem: CustomButtonTouchBarItem {
observeIputSourceChangedNotification()
textInputSourceDidChange()
tapClosure = { [weak self] in
actions.append(ItemAction(trigger: .singleTap) { [weak self] in
self?.switchInputSource()
}
})
}
required init?(coder _: NSCoder) {
+31 -3
View File
@@ -41,9 +41,12 @@ class MusicBarItem: CustomButtonTouchBarItem {
super.init(identifier: identifier, title: "⏳")
isBordered = false
tapClosure = { [weak self] in self?.playPause() }
longTapClosure = { [weak self] in self?.nextTrack() }
actions = [
ItemAction(trigger: .singleTap) { [weak self] in self?.playPause() },
ItemAction(trigger: .doubleTap) { [weak self] in self?.previousTrack() },
ItemAction(trigger: .longTap) { [weak self] in self?.nextTrack() }
]
refreshAndSchedule()
}
@@ -177,6 +180,31 @@ class MusicBarItem: CustomButtonTouchBarItem {
}
}
}
@objc func previousTrack() {
for ident in playerBundleIdentifiers {
if let musicPlayer = SBApplication(bundleIdentifier: ident.rawValue) {
if musicPlayer.isRunning {
if ident == .Spotify {
let mp = (musicPlayer as SpotifyApplication)
mp.previousTrack!()
updatePlayer()
return
} else if ident == .iTunes {
let mp = (musicPlayer as iTunesApplication)
mp.previousTrack!()
updatePlayer()
return
} else if ident == .Music {
let mp = (musicPlayer as MusicApplication)
mp.previousTrack!()
updatePlayer()
return
}
}
}
}
}
func refreshAndSchedule() {
DispatchQueue.main.async {
+2 -2
View File
@@ -30,8 +30,8 @@ class NightShiftBarItem: CustomButtonTouchBarItem {
super.init(identifier: identifier, title: "")
isBordered = false
setWidth(value: 28)
tapClosure = { [weak self] in self?.nightShiftAction() }
actions.append(ItemAction(trigger: .singleTap) { [weak self] in self?.nightShiftAction() })
timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(refresh), userInfo: nil, repeats: true)
+566
View File
@@ -0,0 +1,566 @@
//
// NotificationsTouchBarItem.swift
// MTMR
//
// Created by Matthew Cox on 10/22/19.
// Copyright © 2019 Anton Palgunov. All rights reserved.
//
import Foundation
import Cocoa
import ScriptingBridge
import SQLite
import Contacts
import UserNotifications
class NotificationBarItem: CustomButtonTouchBarItem {
public enum Message: String {
case Messages = "com.apple.iChat"
case Mail = "com.apple.mail"
}
private let notificationIdentifiers = [
Message.Messages,
Message.Mail,
]
private let interval: TimeInterval
private let disableMarquee: Bool
private var messageText: String?
private var messageFrom: String?
private var timer: Timer?
private let iconSize = NSSize(width: 21, height: 21)
private let activity: NSBackgroundActivityScheduler
init(identifier: NSTouchBarItem.Identifier, interval: TimeInterval, disableMarquee: Bool, image: NSImage? = nil) {
self.interval = interval
self.disableMarquee = disableMarquee
//tapClosure = { [weak self] in self?.DarkModeToggle() }
if image == nil {
//sliderItem = CustomSlider()
} else {
// sliderItem = CustomSlider(knob: image!)
}
activity = NSBackgroundActivityScheduler(identifier: "\(identifier.rawValue).updatecheck")
activity.interval = interval
super.init(identifier: identifier, title: "⏳")
//var systemBlue: NSColor { set }
isBordered = false
backgroundColor = NSColor.systemRed
//button = item.view as? NSButton else { continue }
//let textRange = NSRange(location: 0, length: button.title.characters.count)
//let titleColor = useCustomColor.state == NSControl.StateValue.on ? NSColor.black : NSColor.white
//let newTitle = NSMutableAttributedString(string: button.title)
//newTitle.addAttribute(NSAttributedStringKey.foregroundColor, value: titleColor, range: textRange)
//newTitle.addAttribute(NSAttributedStringKey.font, value: button.font!, range: textRange)
//newTitle.setAlignment(.center, range: textRange)
//button.attributedTitle = newTitle
//attributedTitle = "test"
//image = NSImage(name: NSUserGroup)
//title = "\(Noti.0)"
// func getDeliveredNotifications(completionHandler: @escaping ([UNNotification]) -> Void) {
// print("")
// }
// let notificationCenter = UNUserNotificationCenter.current()
// notificationCenter.requestAuthorization(options: [.alert, .badge, .provisional]) {}
// UNUserNotificationCenter.current().getDeliveredNotifications { (notifications) in
// print("Noti")
// print(notifications)
// }
/* let center = UNUserNotificationCenter.current()
let options: UNAuthorizationOptions = [.alert, .badge, .sound]
center.requestAuthorization(options: options) { (granted, error) in
if granted {
application.registerForRemoteNotifications()
}
}
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
let actionIdentifier = response.actionIdentifier
let content = response.notification.request.content
print("\(content)")
switch actionIdentifier {
case UNNotificationDismissActionIdentifier: // Notification was dismissed by user
// Do something
completionHandler()
case UNNotificationDefaultActionIdentifier: // App was opened from notification
// Do something
completionHandler()
case "com.usernotificationstutorial.reply":
if let textResponse = response as? UNTextInputNotificationResponse {
let reply = textResponse.userText
// Send reply message
completionHandler()
}
case "com.usernotificationstutorial.delete":
// Delete message
completionHandler()
default:
completionHandler()
}
*/
/*let replyAction = UNTextInputNotificationAction(identifier: "com.usernotificationstutorial.reply", title: "Reply", options: [], textInputButtonTitle: "Send", textInputPlaceholder: "Type your message")
let deleteAction = UNNotificationAction(identifier: "com.usernotificationstutorial.delete", title: "Delete", options: [.authenticationRequired, .destructive])
let category = UNNotificationCategory(identifier: "com.usernotificationstutorial.message", actions: [replyAction, deleteAction], intentIdentifiers: [], options: [])
center.setNotificationCategories([category])
}
userNotificationCenter(center: center)
*/
tapClosure = { [weak self] in self?.playPause() }
longTapClosure = { [weak self] in self?.nextTrack() }
refreshAndSchedule()
}
@objc func marquee() {
let str = title
if str.count > 10 {
let indexFirst = str.index(str.startIndex, offsetBy: 0)
let indexSecond = str.index(str.startIndex, offsetBy: 1)
title = String(str.suffix(from: indexSecond)) + String(str[indexFirst])
}
}
required init?(coder _: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc func playPause() {
for ident in notificationIdentifiers {
if let messageApplication = SBApplication(bundleIdentifier: ident.rawValue) {
if messageApplication.isRunning {
if ident == .Messages {
let mp = (messageApplication as MessagesApplication)
mp.playpause!()
return
} else if ident == .Mail {
let mp = (messageApplication as MailApplication)
mp.playpause!()
return
}
break
}
}
}
}
@objc func nextTrack() {
for ident in notificationIdentifiers {
if let messageApplication = SBApplication(bundleIdentifier: ident.rawValue) {
if messageApplication.isRunning {
if ident == .Messages {
let mp = (messageApplication as MessagesApplication)
mp.nextTrack!()
updateNotification()
return
} else if ident == .Mail {
let mp = (messageApplication as MailApplication)
mp.nextTrack!()
updateNotification()
return
}
}
}
}
}
func refreshAndSchedule() {
DispatchQueue.main.async {
self.updateNotification()
DispatchQueue.main.asyncAfter(deadline: .now() + self.interval) { [weak self] in
self?.refreshAndSchedule()
}
}
}
func updateNotification() {
var iconUpdated = false
var titleUpdated = false
var notificationbuilder = ""
let name = ""
let message = ""
let durl = ""
let person = ""
let dbpath = ""
let Noti = NotiBar().getAll(name: name, message: message, durl: durl, person: person, dbpath: dbpath)
//let lastNotificationItem = ""
print("\(Noti.0) \(Noti.1) \(Noti.3)")
//icon \(Noti.0)
//Message \(Noti.1)
//button path \(Noti.2)
//Number \(Noti.3)
let fromBox = String(Noti.3)
let messageBox = String(Noti.1)
if fromBox.isEmpty
{
notificationbuilder = ""
}
else
{
notificationbuilder = "\(fromBox) - \(messageBox)"
}
let lastNotificationItem = "\(notificationbuilder)"
for ident in notificationIdentifiers {
if let messageApplication = SBApplication(bundleIdentifier: ident.rawValue) {
if messageApplication.isRunning {
var tempTitle = ""
if ident == .Messages {
tempTitle = lastNotificationItem
} else if ident == .Mail {
tempTitle = (messageApplication as MailApplication).title
}
if tempTitle == self.messageText {
return
} else {
self.messageText = tempTitle
}
if let messageText = self.messageText?.ifNotEmpty {
self.timer?.invalidate()
self.timer = nil
if (disableMarquee) {
self.title = " " + messageText
} else {
self.title = " " + messageText + " "
self.timer = Timer.scheduledTimer(timeInterval: 0.25, target: self, selector: #selector(self.marquee), userInfo: nil, repeats: true)
}
titleUpdated = true
}
if let _ = tempTitle.ifNotEmpty,
let appPath = NSWorkspace.shared.absolutePathForApplication(withBundleIdentifier: ident.rawValue) {
let image = NSWorkspace.shared.icon(forFile: appPath)
image.size = self.iconSize
self.image = image
iconUpdated = true
}
break
}
}
}
DispatchQueue.main.async {
if !iconUpdated {
self.image = nil
}
if !titleUpdated {
self.title = ""
}
}
}
}
class NotiRun {
static func shell(launchPath path: String, arguments args: [String]) -> String {
let task = Process()
task.launchPath = path
task.arguments = args
let pipe = Pipe()
task.standardOutput = pipe
task.standardError = pipe
task.launch()
let data = pipe.fileHandleForReading.readDataToEndOfFile()
let output = String(data: data, encoding: .utf8)
task.waitUntilExit()
return(output!)
}
}
class NotiBar
{
func getAll(name:String, message:String, durl: String, person:String, dbpath: String) -> (String, String, String, String, String)
{
let res = NotiRun.shell(launchPath: "/usr/bin/getconf", arguments: ["DARWIN_USER_DIR"])
let trimmedres = res.trimmingCharacters(in: .whitespacesAndNewlines)
let db = try! Connection("\(trimmedres)com.apple.notificationcenter/db2/db")
let record = Table("record")
let app = Table("app")
let presented = Expression<Int?>("presented")
let identifier = Expression<String?>("identifier")
let delivered_date = Expression<Int?>("delivered_date")
let app_id = Expression<Int?>("app_id")
// let app_id = Expression<Int64>("app_id")
let data = Expression<Data>("data")
var name = ""
var message = ""
var durl = ""
var person = ""
var dbpath = ""
let timestamp = Int(NSDate().timeIntervalSinceReferenceDate - 86400)
//var image = ""
//var reply = ""
//var dismiss = ""
//var other = ""
let Messages = String("com.apple.iChat")
let Mail = String("com.apple.mail")
for app in try! db.prepare(app.select(app[*]))
{
if ("\(app[identifier]!)" == Messages)
{
}
if ("\(app[identifier]!)" == Mail)
{
}
// Messages
// Mail
}
// print("\(timestamp)")
//for record in try! db.prepare(record.join(app, on: record[app_id] == app[app_id]).select(record[*]).filter(delivered_date > timestamp) ) {
//app_id == 4 ||
for record in try! db.prepare(record.select(record[*]).filter(delivered_date > timestamp && app_id == 5) ) {
guard let notifierout = try? PropertyListSerialization.propertyList(from: record[data], options: [], format: nil) else {
fatalError("failed to deserialize")
}
if let notiDescAppName = (notifierout as AnyObject)["app"]! as? String
{
if let ret = self.getPath(appNamedNew: "\(notiDescAppName)") {
let retfix = ("\(ret)")
let rettrimmed = retfix.trimmingCharacters(in: .whitespacesAndNewlines)
let notifierbuilder = Bundle(url: URL(fileURLWithPath: "\(rettrimmed)"))?.infoDictionary?["CFBundleDisplayName"] as? String ?? Bundle(url: URL(fileURLWithPath: "\(rettrimmed)"))?.infoDictionary?["CFBundleName"] as? String
if notifierbuilder == nil
{
}
else
{
if (notifierbuilder == "Messages" || notifierbuilder == "Mail")
{
if (notifierbuilder == "Messages")
{
name = "💬"
}
else if notifierbuilder == "Mail"
{
name = "✉️"
}
if let notiDescAppMessageTop = (notifierout as AnyObject)["req"]! as? AnyObject
{
if let notiDescAppMail = (notiDescAppMessageTop as AnyObject)["subt"]! as? String
{
message = notiDescAppMail
}
else if let notiDescAppMessage = (notiDescAppMessageTop as AnyObject)["body"]! as? String
{
message = notiDescAppMessage
}
}
if let notiDescAppURLTop = (notifierout as AnyObject)["req"]! as? AnyObject
{
if let notiDescAppURL = (notiDescAppURLTop as AnyObject)["durl"]! as? String
{
durl = notiDescAppURL
}
}
if let notiDescAppPersonTop = (notifierout as AnyObject)["req"]! as? AnyObject
{
if let notiDescAppPerson = (notiDescAppPersonTop as AnyObject)["titl"]! as? String
{
person = notiDescAppPerson
}
// let imagee = getContactImage(imgu: "\(person)")
}
}
else
{
name = notifierbuilder!
}
}
}
}
dbpath = "\(trimmedres)com.apple.notificationcenter/db2/db"
}
//print("\(name)-\(message)-\(durl)-\(person)-\(dbpath)")
return (name, message, durl, person, dbpath)
}
// static func getImage(image: String) -> String
//{
//}
//static func getOptionReply(reply: String) -> String
//{
//}
//static func getOptionDismiss(dismiss: String) -> String
//{
//}
//static func getOptionOther(otNSView()her: String) -> String
//{
//}
func getPath(appNamedNew: String) -> String? {
if let absopath = NSWorkspace.shared.absolutePathForApplication(withBundleIdentifier: appNamedNew) {
return "\(absopath)"
} else {
return ""
}
}
func lastModified(path: String) -> NSDate? {
let fileUrl = NSURL(fileURLWithPath: path)
var modified: AnyObject?
do {
try fileUrl.getResourceValue(&modified, forKey: URLResourceKey.contentModificationDateKey)
return modified as? NSDate
} catch let error as NSError {
print("\(#function) Error: \(error)")
return nil
}
}
}
func getContactImage(imgu:String) -> NSImage?
{
let store = CNContactStore()
//let contactStore = CNContactStore()
let keys = [CNContactPhoneNumbersKey, CNContactFamilyNameKey, CNContactGivenNameKey, CNContactNicknameKey, CNContactImageDataKey] as! CNKeyDescriptor
// let request = CNContactFetchRequest(keysToFetch: keys)
// try? contactStore.enumerateContacts(with: request) { (contact, error) in
// Do something with contact
//}
do
{
let contacts = try! store.unifiedContacts(matching: CNContact.predicateForContacts(matchingName: imgu), keysToFetch:[keys])
if contacts.count > 0
{
if let image = contacts[0].imageData
{
return NSImage.init(data: image)
}
}
}
return nil
}
@objc protocol MessagesApplication {
@objc optional var currentTrack: SpotifyTrack { get }
@objc optional func nextTrack()
@objc optional func previousTrack()
@objc optional func playpause()
}
extension SBApplication: MessagesApplication {}
@objc protocol MessagesTrack {
@objc optional var artist: String { get }
@objc optional var name: String { get }
}
extension SBObject: MessagesTrack {}
extension MessagesApplication {
var title: String {
guard let t = currentTrack else { return "" }
return (t.artist ?? "") + " — " + (t.name ?? "")
}
}
@objc protocol MailApplication {
@objc optional var currentTrack: MailTrack { get }
@objc optional func playpause()
@objc optional func nextTrack()
@objc optional func previousTrack()
}
extension SBApplication: MailApplication {}
@objc protocol MailTrack {
@objc optional var artist: String { get }
@objc optional var name: String { get }
}
extension SBObject: MailTrack {}
extension MailApplication {
var title: String {
guard let t = currentTrack else { return "" }
return (t.artist ?? "") + " — " + (t.name ?? "")
}
}
+7 -4
View File
@@ -23,8 +23,9 @@ class PomodoroBarItem: CustomButtonTouchBarItem, Widget {
return (
item: .pomodoro(workTime: workTime ?? 1500.00, restTime: restTime ?? 300),
action: .none,
longAction: .none,
actions: [],
legacyAction: .none,
legacyLongAction: .none,
parameters: [:]
)
}
@@ -50,8 +51,10 @@ class PomodoroBarItem: CustomButtonTouchBarItem, Widget {
self.workTime = workTime
self.restTime = restTime
super.init(identifier: identifier, title: defaultTitle)
tapClosure = { [weak self] in self?.startStopWork() }
longTapClosure = { [weak self] in self?.startStopRest() }
actions.append(contentsOf: [
ItemAction(trigger: .singleTap) { [weak self] in self?.startStopWork() },
ItemAction(trigger: .longTap) { [weak self] in self?.startStopRest() }
])
}
required init?(coder _: NSCoder) {
@@ -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
}
}
+14 -7
View File
@@ -13,12 +13,13 @@ class YandexWeatherBarItem: CustomButtonTouchBarItem, CLLocationManagerDelegate
private let activity: NSBackgroundActivityScheduler
private let unitsStr = "°C"
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 prevLocation: CLLocation!
private var manager: CLLocationManager!
private var updateWeatherTask: URLSessionDataTask?
init(identifier: NSTouchBarItem.Identifier, interval: TimeInterval) {
activity = NSBackgroundActivityScheduler(identifier: "\(identifier.rawValue).updatecheck")
@@ -49,8 +50,13 @@ class YandexWeatherBarItem: CustomButtonTouchBarItem, CLLocationManagerDelegate
manager.delegate = self
manager.desiredAccuracy = kCLLocationAccuracyHundredMeters
manager.startUpdatingLocation()
tapClosure = tapClosure ?? defaultTapAction
if actions.filter({ $0.trigger == .singleTap }).isEmpty {
actions.append(ItemAction(
trigger: .singleTap,
defaultTapAction
))
}
}
required init?(coder _: NSCoder) {
@@ -61,7 +67,8 @@ class YandexWeatherBarItem: CustomButtonTouchBarItem, CLLocationManagerDelegate
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
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 {
return
}
@@ -86,12 +93,12 @@ class YandexWeatherBarItem: CustomButtonTouchBarItem, CLLocationManagerDelegate
}
}
task.resume()
updateWeatherTask?.resume()
}
func getWeatherUrl() -> String {
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 {
return "https://yandex.ru/pogoda/?lang=ru" // Yandex will try to determine your location by default
}
+20 -5
View File
@@ -10,7 +10,7 @@ class ParseConfig: XCTestCase {
XCTFail()
return
}
guard case .none? = result?.first?.action else {
guard result?.first?.actions.count == 0 else {
XCTFail()
return
}
@@ -18,14 +18,29 @@ class ParseConfig: XCTestCase {
func testButtonKeyCodeAction() {
let buttonKeycodeFixture = """
[ { "type": "staticButton", "title": "Pew", "action": "hidKey", "keycode": 123} ]
[ { "type": "staticButton", "title": "Pew", "actions": [ { "trigger": "singleTap", "action": "hidKey", "keycode": 123 } ] } ]
""".data(using: .utf8)!
let result = try? JSONDecoder().decode([BarItemDefinition].self, from: buttonKeycodeFixture)
guard case .staticButton("Pew")? = result?.first?.type else {
XCTFail()
return
}
guard case .hidKey(keycode: 123)? = result?.first?.action else {
guard case .hidKey(keycode: 123)? = result?.first?.actions.filter({ $0.trigger == .singleTap }).first?.value else {
XCTFail()
return
}
}
func testButtonKeyCodeLegacyAction() {
let buttonKeycodeFixture = """
[ { "type": "staticButton", "title": "Pew", "action": "hidKey", "keycode": 123 } ]
""".data(using: .utf8)!
let result = try? JSONDecoder().decode([BarItemDefinition].self, from: buttonKeycodeFixture)
guard case .staticButton("Pew")? = result?.first?.type else {
XCTFail()
return
}
guard case .hidKey(keycode: 123)? = result?.first?.legacyAction else {
XCTFail()
return
}
@@ -40,7 +55,7 @@ class ParseConfig: XCTestCase {
XCTFail()
return
}
guard case .keyPress(keycode: 53)? = result?.first?.action else {
guard case .keyPress(keycode: 53)? = result?.first?.actions.filter({ $0.trigger == .singleTap }).first?.value else {
XCTFail()
return
}
@@ -55,7 +70,7 @@ class ParseConfig: XCTestCase {
XCTFail()
return
}
guard case .keyPress(keycode: 53)? = result?.first?.action else {
guard case .keyPress(keycode: 53)? = result?.first?.actions.filter({ $0.trigger == .singleTap }).first?.value else {
XCTFail()
return
}
+61 -52
View File
@@ -18,7 +18,7 @@ My idea is to create a platform for creating plugins to customize the TouchBar.
<a href="https://t.me/joinchat/AmVYGg8vW38c13_3MxdE_g"><img height="20px" src="https://telegram.org/img/t_logo.png" /> Telegram</a>
</p>
<p align="center"><a href="https://www.paypal.me/toxblh/10" title="Donate via Paypal"><img height="36px" src="Resources/support_paypal.svg" alt="PayPal donate button" /></a>
<p align="center"><a href="https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=WUAAG2HH58WE4" title="Donate via Paypal"><img height="36px" src="Resources/support_paypal.svg" alt="PayPal donate button" /></a>
<a href="https://www.buymeacoffee.com/toxblh" target="_blank"><img src="https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png" alt="Buy Me A Coffee" height="36px" ></a>
<a href="https://www.patreon.com/bePatron?u=9900748"><img height="36px" src="https://c5.patreon.com/external/logo/become_a_patron_button.png" srcset="https://c5.patreon.com/external/logo/become_a_patron_button@2x.png 2x"></a>
<a href="https://www.producthunt.com/posts/my-touchbar-my-rules-mtmr">
@@ -84,6 +84,7 @@ The pre-installed configuration contains less or more than you'll probably want,
- darkMode
- pomodoro
- network
- upnext (Calendar events)
> Media Keys
@@ -116,7 +117,7 @@ You can add custom actions for two/three/four finger swipes. To do it, you need
"type": "swipe",
"fingers": 2, // number of fingers required (2,3 or 4)
"direction": "right", // direction of swipe (right/left)
"minOffset" 10, // optional: minimal required offset for gesture to emit event
"minOffset": 10, // optional: minimal required offset for gesture to emit event
"sourceApple": { // optional: apple script to run
"inline": "beep"
},
@@ -197,10 +198,15 @@ Example of "CPU load" button which also changes color based on load value.
"source": {
"inline": "top -l 2 -n 0 -F | egrep -o ' \\d*\\.\\d+% idle' | tail -1 | awk -F% '{p = 100 - $1; if (p > 30) c = \"\\033[33m\"; if (p > 70) c = \"\\033[30;43m\"; printf \"%s%4.1f%%\\n\", c, p}'"
},
"action": "appleScript",
"actionAppleScript": {
"inline": "activate application \"Activity Monitor\"\rtell application \"System Events\"\r\ttell process \"Activity Monitor\"\r\t\ttell radio button \"CPU\" of radio group 1 of group 2 of toolbar 1 of window 1 to perform action \"AXPress\"\r\tend tell\rend tell"
},
"actions": [
{
"trigger": "singleTap",
"action": "appleScript",
"actionAppleScript": {
"inline": "activate application \"Activity Monitor\"\rtell application \"System Events\"\r\ttell process \"Activity Monitor\"\r\t\ttell radio button \"CPU\" of radio group 1 of group 2 of toolbar 1 of window 1 to perform action \"AXPress\"\r\tend tell\rend tell"
}
}
],
"align": "right",
"image": {
// Or you can specify a filePath here.
@@ -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:
### Example:
```js
"actions": [
{
"trigger": "singleTap",
"action": "hidKey",
"keycode": 53
}
]
```
### Triggers:
- `singleTap`
- `doubleTap`
- `tripleTap`
- `longTap`
### Types
- `hidKey`
> https://github.com/aosm/IOHIDFamily/blob/master/IOHIDSystem/IOKit/hidsystem/ev_keymap.h use only numbers
@@ -386,22 +430,6 @@ To close a group, use the button:
"url": "https://google.com",
```
## LongActions
If you want to longPress for some operations, it is similar to the configuration for Actions but with additional parameters, for example:
```js
"longAction": "hidKey",
"longKeycode": 53,
```
- longAction
- longKeycode
- longActionAppleScript
- longExecutablePath
- longShellArguments
- longUrl
## Additional parameters:
- `width` restrict how much room a particular button will take
@@ -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
- [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?)
Re-tick or check a tick for access 🍏→ System Preferences → Security and Privacy → tab Privacy → Accessibility → MTMR
## Credits
+35
View File
@@ -4,3 +4,38 @@
* try move away from enums when parse preset – enums are hard to extend
* find better way to hide bar items
* 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?)
+2 -2
View File
@@ -63,8 +63,8 @@ echo "<?xml version=\"1.0\" standalone=\"yes\"?>
echo ""
echo "Homebrew https://github.com/Homebrew/homebrew-cask/edit/master/Casks/mtmr.rb"
echo ""
echo " version '${VERSION}'"
echo " sha256 '${SHA256}'"
echo " version \"${VERSION}\""
echo " sha256 \"${SHA256}\""
echo ""
echo "Update MTMR v${VERSION}"