diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
index 4f6cea0..6d99015 100644
--- a/.github/FUNDING.yml
+++ b/.github/FUNDING.yml
@@ -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
diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml
new file mode 100644
index 0000000..d341f6c
--- /dev/null
+++ b/.github/workflows/build-test.yml
@@ -0,0 +1,22 @@
+name: Build-and-test
+
+on: [push, pull_request]
+
+jobs:
+ test:
+ runs-on: macOS-latest
+
+ steps:
+ - uses: actions/checkout@v1
+
+ - name: Run tests
+ run: xcodebuild test -project MTMR.xcodeproj -scheme 'UnitTests' | xcpretty -c && exit ${PIPESTATUS[0]}
+
+ build:
+ runs-on: macOS-latest
+
+ steps:
+ - uses: actions/checkout@v1
+
+ - name: Build
+ run: xcodebuild archive -project "MTMR.xcodeproj" -scheme "MTMR" -archivePath Release/App.xcarchive DEVELOPMENT_TEAM="" CODE_SIGN_IDENTITY="" | xcpretty -c && exit ${PIPESTATUS[0]}
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
new file mode 100644
index 0000000..d1b03be
--- /dev/null
+++ b/.github/workflows/publish.yml
@@ -0,0 +1,41 @@
+name: Publish unsign version
+
+on:
+ push:
+ branches:
+ - master
+ tags:
+ - "v*"
+
+jobs:
+ Build-and-release:
+ runs-on: macOS-latest
+
+ steps:
+ - uses: actions/checkout@v1
+
+ - name: Set up Node.js
+ uses: actions/setup-node@v1
+ with:
+ node-version: 12.x
+
+ - name: Install create-dmg
+ run: npm i -g create-dmg
+
+ - name: Build Archive
+ run: xcodebuild archive -project "MTMR.xcodeproj" -scheme "MTMR" -archivePath Release/App.xcarchive DEVELOPMENT_TEAM="" CODE_SIGN_IDENTITY="" | xcpretty -c && exit ${PIPESTATUS[0]}
+
+ - name: Build App
+ run: xcodebuild -project "MTMR.xcodeproj" -exportArchive -archivePath Release/App.xcarchive -exportOptionsPlist export-options.plist -exportPath Release | xcpretty -c && exit ${PIPESTATUS[0]}
+
+ - name: Create DMG
+ run: |
+ cd Release
+ create-dmg MTMR.app || true
+
+ - name: GitHub Release
+ uses: "marvinpinto/action-automatic-releases@latest"
+ with:
+ repo_token: "${{ secrets.GITHUB_TOKEN }}"
+ prerelease: false
+ files: Release/MTMR*.dmg
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index 8fd2c35..0000000
--- a/.travis.yml
+++ /dev/null
@@ -1,6 +0,0 @@
-language: swift
-xcode_project: MTMR.xcodeproj
-xcode_scheme: UnitTests
-osx_image: xcode11.1
-install: gem install xcpretty
-script: "xcodebuild test -project MTMR.xcodeproj -scheme 'UnitTests' | xcpretty -c && exit ${PIPESTATUS[0]}"
diff --git a/MTMR.xcodeproj/project.pbxproj b/MTMR.xcodeproj/project.pbxproj
index ff15983..ed1f6d7 100644
--- a/MTMR.xcodeproj/project.pbxproj
+++ b/MTMR.xcodeproj/project.pbxproj
@@ -22,6 +22,8 @@
4CC9FEDC22FDEA65001512EB /* AMR_ANSIEscapeHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = 4CC9FEDB22FDEA65001512EB /* AMR_ANSIEscapeHelper.m */; };
4CDC6E5022FCA93F0069ADD4 /* ShellScriptTouchBarItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CDC6E4F22FCA93F0069ADD4 /* ShellScriptTouchBarItem.swift */; };
4CFF5E5C22E623DD00BFB1EE /* YandexWeatherBarItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CFF5E5B22E623DD00BFB1EE /* YandexWeatherBarItem.swift */; };
+ 5DC6CA02241F92CB005CD5E8 /* Music.nowPlaying.scpt in Resources */ = {isa = PBXBuildFile; fileRef = 5DC6CA00241F92CB005CD5E8 /* Music.nowPlaying.scpt */; };
+ 5DC6CA03241F92CB005CD5E8 /* Music.next.scpt in Resources */ = {isa = PBXBuildFile; fileRef = 5DC6CA01241F92CB005CD5E8 /* Music.next.scpt */; };
60173D3E20C0031B002C305F /* LaunchAtLoginController.m in Sources */ = {isa = PBXBuildFile; fileRef = 60173D3D20C0031B002C305F /* LaunchAtLoginController.m */; };
6027D1B92080E52A004FFDC7 /* BrightnessViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6027D1B72080E52A004FFDC7 /* BrightnessViewController.swift */; };
6027D1BA2080E52A004FFDC7 /* VolumeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6027D1B82080E52A004FFDC7 /* VolumeViewController.swift */; };
@@ -71,6 +73,9 @@
B0F3112520C9E35F0076BB88 /* SupportNSTouchBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0F3112420C9E35F0076BB88 /* SupportNSTouchBar.swift */; };
B0F54A7A2295AC7D00B4C509 /* DarkModeBarItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0F54A792295AC7D00B4C509 /* DarkModeBarItem.swift */; };
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 */
@@ -106,6 +111,8 @@
4CC9FEDB22FDEA65001512EB /* AMR_ANSIEscapeHelper.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AMR_ANSIEscapeHelper.m; sourceTree = ""; };
4CDC6E4F22FCA93F0069ADD4 /* ShellScriptTouchBarItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShellScriptTouchBarItem.swift; sourceTree = ""; };
4CFF5E5B22E623DD00BFB1EE /* YandexWeatherBarItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = YandexWeatherBarItem.swift; sourceTree = ""; };
+ 5DC6CA00241F92CB005CD5E8 /* Music.nowPlaying.scpt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = Music.nowPlaying.scpt; sourceTree = ""; };
+ 5DC6CA01241F92CB005CD5E8 /* Music.next.scpt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = Music.next.scpt; sourceTree = ""; };
60173D3C20C0031B002C305F /* LaunchAtLoginController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = LaunchAtLoginController.h; sourceTree = ""; };
60173D3D20C0031B002C305F /* LaunchAtLoginController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = LaunchAtLoginController.m; sourceTree = ""; };
6027D1B72080E52A004FFDC7 /* BrightnessViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BrightnessViewController.swift; sourceTree = ""; };
@@ -165,6 +172,9 @@
B0F54A792295AC7D00B4C509 /* DarkModeBarItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DarkModeBarItem.swift; sourceTree = ""; };
B0F87719207AC1EA00D6E430 /* TouchBarSupport.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TouchBarSupport.m; sourceTree = ""; };
B0F8771B207AC92700D6E430 /* TouchBarSupport.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TouchBarSupport.h; sourceTree = ""; };
+ BAF5AB5624317B4300B04904 /* BasicView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BasicView.swift; sourceTree = ""; };
+ BAF5AB5824317CAF00B04904 /* SwipeItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwipeItem.swift; sourceTree = ""; };
+ F29F6A2424BC7148004FF8E4 /* UpNextScrubberTouchBarItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpNextScrubberTouchBarItem.swift; sourceTree = ""; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@@ -242,8 +252,10 @@
36FEF871235A1CFC00A0ABCE /* AppSettings.swift */,
B059D623205E04F3006E6B86 /* CustomButtonTouchBarItem.swift */,
36C2ECD8207B74B4003CDA33 /* AppleScriptTouchBarItem.swift */,
+ BAF5AB5824317CAF00B04904 /* SwipeItem.swift */,
4CDC6E4F22FCA93F0069ADD4 /* ShellScriptTouchBarItem.swift */,
B059D621205E03F5006E6B86 /* TouchBarController.swift */,
+ BAF5AB5624317B4300B04904 /* BasicView.swift */,
36C2ECDA207C3FE7003CDA33 /* ItemsParsing.swift */,
B0008E542080286C003AD4DD /* SupportHelpers.swift */,
B09EB1E3207C082000D5C1E0 /* HapticFeedback.swift */,
@@ -270,6 +282,8 @@
B0B17426207D6AFE0004B740 /* AppleScripts */ = {
isa = PBXGroup;
children = (
+ 5DC6CA01241F92CB005CD5E8 /* Music.next.scpt */,
+ 5DC6CA00241F92CB005CD5E8 /* Music.nowPlaying.scpt */,
B0B1742A207D6B580004B740 /* Battery.scpt */,
B0B17429207D6B580004B740 /* Finder.scpt */,
B0B1742C207D6B590004B740 /* iTunes.next.scpt */,
@@ -325,6 +339,7 @@
B08126F0217BE19000A98970 /* WidgetProtocol.swift */,
B0F54A792295AC7D00B4C509 /* DarkModeBarItem.swift */,
839ECD8423600BF500BE2DA5 /* NotificationTouchBarItem.swift */,
+ F29F6A2424BC7148004FF8E4 /* UpNextScrubberTouchBarItem.swift */,
);
path = Widgets;
sourceTree = "";
@@ -422,6 +437,8 @@
B0B1743A207D6B590004B740 /* iTunes.nowPlaying.scpt in Resources */,
B082B257205C7D8000BC04DC /* Assets.xcassets in Resources */,
B0B17437207D6B590004B740 /* Spotify.nowPlaying.scpt in Resources */,
+ 5DC6CA02241F92CB005CD5E8 /* Music.nowPlaying.scpt in Resources */,
+ 5DC6CA03241F92CB005CD5E8 /* Music.next.scpt in Resources */,
B0B17436207D6B590004B740 /* iTunes.next.scpt in Resources */,
B082B25A205C7D8000BC04DC /* Main.storyboard in Resources */,
B0B17435207D6B590004B740 /* Spotify.next.scpt in Resources */,
@@ -472,11 +489,13 @@
B059D622205E03F5006E6B86 /* TouchBarController.swift in Sources */,
B05600D32083E9BB00EB218D /* CustomSlider.swift in Sources */,
36C2ECD9207B74B4003CDA33 /* AppleScriptTouchBarItem.swift in Sources */,
+ BAF5AB5724317B4300B04904 /* BasicView.swift in Sources */,
B0846A752220C968000288A7 /* NetworkBarItem.swift in Sources */,
B0F8771A207AC1EA00D6E430 /* TouchBarSupport.m in Sources */,
B08126F1217BE19000A98970 /* WidgetProtocol.swift in Sources */,
B0008E552080286C003AD4DD /* SupportHelpers.swift in Sources */,
6042B6A72083E03A00C525C8 /* AppScrubberTouchBarItem.swift in Sources */,
+ BAF5AB5924317CAF00B04904 /* SwipeItem.swift in Sources */,
B082B253205C7D8000BC04DC /* AppDelegate.swift in Sources */,
B0F54A7A2295AC7D00B4C509 /* DarkModeBarItem.swift in Sources */,
B08126EF217BD0B900A98970 /* PomodoroBarItem.swift in Sources */,
@@ -487,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 */,
diff --git a/MTMR/AppDelegate.swift b/MTMR/AppDelegate.swift
index ab27818..f9c5084 100644
--- a/MTMR/AppDelegate.swift
+++ b/MTMR/AppDelegate.swift
@@ -92,7 +92,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
@objc func toggleMultitouch(_ item: NSMenuItem) {
item.state = item.state == .on ? .off : .on
AppSettings.multitouchGestures = item.state == .on
- TouchBarController.shared.scrollArea?.gesturesEnabled = item.state == .on
+ TouchBarController.shared.basicView?.legacyGesturesEnabled = item.state == .on
}
@objc func openPreset(_: Any?) {
diff --git a/MTMR/AppleScriptTouchBarItem.swift b/MTMR/AppleScriptTouchBarItem.swift
index 33d89cf..1ee67a7 100644
--- a/MTMR/AppleScriptTouchBarItem.swift
+++ b/MTMR/AppleScriptTouchBarItem.swift
@@ -4,9 +4,11 @@ class AppleScriptTouchBarItem: CustomButtonTouchBarItem {
private var script: NSAppleScript!
private let interval: TimeInterval
private var forceHideConstraint: NSLayoutConstraint!
+ private let alternativeImages: [String: SourceProtocol]
- init?(identifier: NSTouchBarItem.Identifier, source: SourceProtocol, interval: TimeInterval) {
+ init?(identifier: NSTouchBarItem.Identifier, source: SourceProtocol, interval: TimeInterval, alternativeImages: [String: SourceProtocol]) {
self.interval = interval
+ self.alternativeImages = alternativeImages
super.init(identifier: identifier, title: "⏳")
forceHideConstraint = view.widthAnchor.constraint(equalToConstant: 0)
title = "scheduled"
@@ -57,6 +59,16 @@ class AppleScriptTouchBarItem: CustomButtonTouchBarItem {
}
}
+ func updateIcon(iconLabel: String) {
+ if alternativeImages[iconLabel] != nil {
+ DispatchQueue.main.async {
+ self.image = self.alternativeImages[iconLabel]!.image
+ }
+ } else {
+ print("Cannot find icon with label \"\(iconLabel)\"")
+ }
+ }
+
func execute() -> String {
var error: NSDictionary?
let output = script.executeAndReturnError(&error)
@@ -64,6 +76,18 @@ class AppleScriptTouchBarItem: CustomButtonTouchBarItem {
print(error)
return "error"
}
+ if output.descriptorType == typeAEList {
+ let arr = Array(1...output.numberOfItems).compactMap({ output.atIndex($0)!.stringValue ?? "" })
+
+ if arr.count <= 0 {
+ return ""
+ } else if arr.count == 1 {
+ return arr[0]
+ } else {
+ updateIcon(iconLabel: arr[1])
+ return arr[0]
+ }
+ }
return output.stringValue ?? ""
}
}
diff --git a/MTMR/AppleScripts/Music.next.scpt b/MTMR/AppleScripts/Music.next.scpt
new file mode 100644
index 0000000..9101064
--- /dev/null
+++ b/MTMR/AppleScripts/Music.next.scpt
@@ -0,0 +1,7 @@
+if application "Music" is running then
+ tell application "Music"
+ if player state is playing then
+ next track
+ end if
+ end tell
+end if
diff --git a/MTMR/AppleScripts/Music.nowPlaying.scpt b/MTMR/AppleScripts/Music.nowPlaying.scpt
new file mode 100644
index 0000000..81644b4
--- /dev/null
+++ b/MTMR/AppleScripts/Music.nowPlaying.scpt
@@ -0,0 +1,10 @@
+if application "Music" is running then
+ tell application "Music"
+ 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 ""
diff --git a/MTMR/AppleScripts/PlaySmart.scpt b/MTMR/AppleScripts/PlaySmart.scpt
index eed3e02..2807287 100644
--- a/MTMR/AppleScripts/PlaySmart.scpt
+++ b/MTMR/AppleScripts/PlaySmart.scpt
@@ -2,6 +2,10 @@ if application "iTunes" is running then
tell application "iTunes" to playpause
end if
+if application "Music" is running then
+ tell application "Music" to playpause
+end if
+
if application "Spotify" is running then
tell application "Spotify" to playpause
end if
diff --git a/MTMR/BasicView.swift b/MTMR/BasicView.swift
new file mode 100644
index 0000000..5845380
--- /dev/null
+++ b/MTMR/BasicView.swift
@@ -0,0 +1,107 @@
+//
+// BasicView.swift
+// MTMR
+//
+// Created by Fedor Zaitsev on 3/29/20.
+// Copyright © 2020 Anton Palgunov. All rights reserved.
+//
+
+import Foundation
+
+
+class BasicView: NSCustomTouchBarItem, NSGestureRecognizerDelegate {
+ var twofingers: NSPanGestureRecognizer!
+ var threefingers: NSPanGestureRecognizer!
+ 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 = 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:
+ prevPositions[fingers] = position
+ legacyPrevPositions[fingers] = position
+ case .changed:
+ if self.legacyGesturesEnabled {
+ if fingers == 2 {
+ let prevPos = legacyPrevPositions[fingers]!
+ if ((position - prevPos) > 10) || ((prevPos - position) > 10) {
+ if position > prevPos {
+ HIDPostAuxKey(NX_KEYTYPE_SOUND_UP)
+ } else if position < prevPos {
+ HIDPostAuxKey(NX_KEYTYPE_SOUND_DOWN)
+ }
+ legacyPrevPositions[fingers] = position
+ }
+ }
+ if fingers == 3 {
+ let prevPos = legacyPrevPositions[fingers]!
+ if ((position - prevPos) > 15) || ((prevPos - position) > 15) {
+ if position > prevPos {
+ HIDPostAuxKey(NX_KEYTYPE_BRIGHTNESS_UP)
+ } else if position < prevPos {
+ HIDPostAuxKey(NX_KEYTYPE_BRIGHTNESS_DOWN)
+ }
+ legacyPrevPositions[fingers] = position
+ }
+ }
+ }
+ case .ended:
+ print("gesture ended \(position - prevPositions[fingers]!) \(fingers)")
+ for item in swipeItems {
+ item.processEvent(offset: position - prevPositions[fingers]!, fingers: fingers)
+ }
+ default:
+ break
+ }
+ }
+
+ @objc func twofingersHandler(_ sender: NSGestureRecognizer?) {
+ let position = (sender?.location(in: sender?.view).x)!
+ self.gestureHandler(position: position, fingers: 2, state: sender!.state)
+ }
+
+ @objc func threefingersHandler(_ sender: NSGestureRecognizer?) {
+ let position = (sender?.location(in: sender?.view).x)!
+ self.gestureHandler(position: position, fingers: 3, state: sender!.state)
+ }
+
+ @objc func fourfingersHandler(_ sender: NSGestureRecognizer?) {
+ let position = (sender?.location(in: sender?.view).x)!
+ self.gestureHandler(position: position, fingers: 4, state: sender!.state)
+ }
+}
diff --git a/MTMR/CustomButtonTouchBarItem.swift b/MTMR/CustomButtonTouchBarItem.swift
index 6b734d2..a10976d 100644
--- a/MTMR/CustomButtonTouchBarItem.swift
+++ b/MTMR/CustomButtonTouchBarItem.swift
@@ -8,18 +8,32 @@
import Cocoa
+struct ItemAction {
+ typealias TriggerClosure = (() -> Void)?
+
+ let trigger: Action.Trigger
+ let closure: TriggerClosure
+
+ init(trigger: Action.Trigger, _ closure: TriggerClosure) {
+ self.trigger = trigger
+ self.closure = closure
+ }
+}
+
class CustomButtonTouchBarItem: NSCustomTouchBarItem, NSGestureRecognizerDelegate {
- var tapClosure: (() -> Void)?
- var longTapClosure: (() -> Void)? {
+
+ var actions: [ItemAction] = [] {
didSet {
- longClick.isEnabled = longTapClosure != nil
+ multiClick.isDoubleClickEnabled = actions.filter({ $0.trigger == .doubleTap }).count > 0
+ multiClick.isTripleClickEnabled = actions.filter({ $0.trigger == .tripleTap }).count > 0
+ longClick.isEnabled = actions.filter({ $0.trigger == .longTap }).count > 0
}
}
var finishViewConfiguration: ()->() = {}
private var button: NSButton!
- private var singleClick: HapticClickGestureRecognizer!
private var longClick: LongPressGestureRecognizer!
+ private var multiClick: MultiClickGestureRecognizer!
init(identifier: NSTouchBarItem.Identifier, title: String) {
attributedTitle = title.defaultTouchbarAttributedString
@@ -31,10 +45,17 @@ class CustomButtonTouchBarItem: NSCustomTouchBarItem, NSGestureRecognizerDelegat
longClick.isEnabled = false
longClick.allowedTouchTypes = .direct
longClick.delegate = self
-
- singleClick = HapticClickGestureRecognizer(target: self, action: #selector(handleGestureSingle))
- singleClick.allowedTouchTypes = .direct
- singleClick.delegate = self
+
+ multiClick = MultiClickGestureRecognizer(
+ target: self,
+ action: #selector(handleGestureSingleTap),
+ doubleAction: #selector(handleGestureDoubleTap),
+ tripleAction: #selector(handleGestureTripleTap)
+ )
+ multiClick.allowedTouchTypes = .direct
+ multiClick.delegate = self
+ multiClick.isDoubleClickEnabled = false
+ multiClick.isTripleClickEnabled = false
reinstallButton()
button.attributedTitle = attributedTitle
@@ -100,33 +121,43 @@ class CustomButtonTouchBarItem: NSCustomTouchBarItem, NSGestureRecognizerDelegat
view = button
view.addGestureRecognizer(longClick)
- view.addGestureRecognizer(singleClick)
+ // view.addGestureRecognizer(singleClick)
+ view.addGestureRecognizer(multiClick)
finishViewConfiguration()
}
func gestureRecognizer(_ gestureRecognizer: NSGestureRecognizer, shouldRequireFailureOf otherGestureRecognizer: NSGestureRecognizer) -> Bool {
- if gestureRecognizer == singleClick && otherGestureRecognizer == longClick
- || gestureRecognizer == longClick && otherGestureRecognizer == singleClick // need it
+ if gestureRecognizer == multiClick && otherGestureRecognizer == longClick
+ || gestureRecognizer == longClick && otherGestureRecognizer == multiClick // need it
{
return false
}
return true
}
-
- @objc func handleGestureSingle(gr: NSClickGestureRecognizer) {
- switch gr.state {
- case .ended:
- tapClosure?()
- break
- default:
- break
+
+ func callActions(for trigger: Action.Trigger) {
+ let itemActions = self.actions.filter { $0.trigger == trigger }
+ for itemAction in itemActions {
+ itemAction.closure?()
}
}
+
+ @objc func handleGestureSingleTap() {
+ callActions(for: .singleTap)
+ }
+
+ @objc func handleGestureDoubleTap() {
+ callActions(for: .doubleTap)
+ }
+
+ @objc func handleGestureTripleTap() {
+ callActions(for: .tripleTap)
+ }
@objc func handleGestureLong(gr: NSPressGestureRecognizer) {
switch gr.state {
case .possible: // tiny hack because we're calling action manually
- (self.longTapClosure ?? self.tapClosure)?()
+ callActions(for: .longTap)
break
default:
break
@@ -176,15 +207,78 @@ class CustomButtonCell: NSButtonCell {
}
}
-class HapticClickGestureRecognizer: NSClickGestureRecognizer {
+// Thanks to https://stackoverflow.com/a/49843893
+final class MultiClickGestureRecognizer: NSClickGestureRecognizer {
+
+ private let _action: Selector
+ private let _doubleAction: Selector
+ private let _tripleAction: Selector
+ private var _clickCount: Int = 0
+
+ public var isDoubleClickEnabled = true
+ public var isTripleClickEnabled = true
+
+ override var action: Selector? {
+ get {
+ return nil /// prevent base class from performing any actions
+ } set {
+ if newValue != nil { // if they are trying to assign an actual action
+ fatalError("Only use init(target:action:doubleAction) for assigning actions")
+ }
+ }
+ }
+
+ required init(target: AnyObject, action: Selector, doubleAction: Selector, tripleAction: Selector) {
+ _action = action
+ _doubleAction = doubleAction
+ _tripleAction = tripleAction
+ super.init(target: target, action: nil)
+ }
+
+ required init?(coder: NSCoder) {
+ fatalError("init(target:action:doubleAction:tripleAction) is only support atm")
+ }
+
override func touchesBegan(with event: NSEvent) {
HapticFeedback.shared?.tap(strong: 2)
super.touchesBegan(with: event)
}
-
+
override func touchesEnded(with event: NSEvent) {
HapticFeedback.shared?.tap(strong: 1)
super.touchesEnded(with: event)
+ _clickCount += 1
+
+ var delayThreshold: TimeInterval // fine tune this as needed
+
+ guard isDoubleClickEnabled || isTripleClickEnabled else {
+ _ = target?.perform(_action)
+ return
+ }
+
+ if (isTripleClickEnabled) {
+ delayThreshold = 0.4
+ perform(#selector(_resetAndPerformActionIfNecessary), with: nil, afterDelay: delayThreshold)
+ if _clickCount == 3 {
+ _ = target?.perform(_tripleAction)
+ }
+ } else {
+ delayThreshold = 0.3
+ perform(#selector(_resetAndPerformActionIfNecessary), with: nil, afterDelay: delayThreshold)
+ if _clickCount == 2 {
+ _ = target?.perform(_doubleAction)
+ }
+ }
+ }
+
+ @objc private func _resetAndPerformActionIfNecessary() {
+ if _clickCount == 1 {
+ _ = target?.perform(_action)
+ }
+ if isTripleClickEnabled && _clickCount == 2 {
+ _ = target?.perform(_doubleAction)
+ }
+ _clickCount = 0
}
}
diff --git a/MTMR/Info.plist b/MTMR/Info.plist
index 99603a5..90a0507 100644
--- a/MTMR/Info.plist
+++ b/MTMR/Info.plist
@@ -17,9 +17,9 @@
CFBundlePackageType
APPL
CFBundleShortVersionString
- 0.25
+ 0.27
CFBundleVersion
- 297
+ 434
LSApplicationCategoryType
public.app-category.utilities
LSMinimumSystemVersion
@@ -39,7 +39,7 @@
NSHomeKitUsageDescription
MTMR needs access to HomeKit for work
NSHumanReadableCopyright
- Copyright © 2018 Anton Palgunov. All rights reserved.
+ Copyright © 2018 - 2020 Anton Palgunov. All rights reserved.
NSLocationAlwaysAndWhenInUseUsageDescription
Weather widget need your location for correct work
NSLocationAlwaysUsageDescription
diff --git a/MTMR/ItemsParsing.swift b/MTMR/ItemsParsing.swift
index 3990921..74c7cc5 100644
--- a/MTMR/ItemsParsing.swift
+++ b/MTMR/ItemsParsing.swift
@@ -3,47 +3,52 @@ import Foundation
extension Data {
func barItemDefinitions() -> [BarItemDefinition]? {
- return try? JSONDecoder().decode([BarItemDefinition].self, from: utf8string!.stripComments().data(using: .utf8)!)
+ return try! JSONDecoder().decode([BarItemDefinition].self, from: utf8string!.stripComments().data(using: .utf8)!)
}
}
struct BarItemDefinition: Decodable {
let type: ItemType
- let action: ActionType
- let longAction: LongActionType
+ let actions: [Action]
+ let legacyAction: LegacyActionType
+ let legacyLongAction: LegacyLongActionType
let additionalParameters: [GeneralParameters.CodingKeys: GeneralParameter]
private enum CodingKeys: String, CodingKey {
case type
+ case actions
}
- init(type: ItemType, action: ActionType, longAction: LongActionType, additionalParameters: [GeneralParameters.CodingKeys: GeneralParameter]) {
+ init(type: ItemType, actions: [Action], action: LegacyActionType, legacyLongAction: LegacyLongActionType, additionalParameters: [GeneralParameters.CodingKeys: GeneralParameter]) {
self.type = type
- self.action = action
- self.longAction = longAction
+ self.actions = actions
+ self.legacyAction = action
+ self.legacyLongAction = legacyLongAction
self.additionalParameters = additionalParameters
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let type = try container.decode(String.self, forKey: .type)
- let parametersDecoder = SupportedTypesHolder.sharedInstance.lookup(by: type)
+ let actions = try container.decodeIfPresent([Action].self, forKey: .actions)
+ let parametersDecoder = SupportedTypesHolder.sharedInstance.lookup(by: type, actions: actions ?? [])
var additionalParameters = try GeneralParameters(from: decoder).parameters
if let result = try? parametersDecoder(decoder),
- case let (itemType, action, longAction, parameters) = result {
+ case let (itemType, actions, action, longAction, parameters) = result {
parameters.forEach { additionalParameters[$0] = $1 }
- self.init(type: itemType, action: action, longAction: longAction, additionalParameters: additionalParameters)
+ self.init(type: itemType, actions: actions, action: action, legacyLongAction: longAction, additionalParameters: additionalParameters)
} else {
- self.init(type: .staticButton(title: "unknown"), action: .none, longAction: .none, additionalParameters: additionalParameters)
+ self.init(type: .staticButton(title: "unknown"), actions: [], action: .none, legacyLongAction: .none, additionalParameters: additionalParameters)
}
}
}
typealias ParametersDecoder = (Decoder) throws -> (
item: ItemType,
- action: ActionType,
- longAction: LongActionType,
+ actions: [Action],
+ legacyAction: LegacyActionType,
+ legacyLongAction: LegacyLongActionType,
parameters: [GeneralParameters.CodingKeys: GeneralParameter]
)
@@ -51,8 +56,11 @@ 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)]
) },
@@ -65,8 +73,11 @@ class SupportedTypesHolder {
"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: [:]
) },
@@ -74,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]
)
},
@@ -84,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]
)
},
@@ -94,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]
)
},
@@ -104,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]
)
},
@@ -114,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]
)
},
@@ -124,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]
)
},
@@ -134,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]
)
},
@@ -144,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]
)
},
@@ -154,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]
)
},
@@ -164,23 +202,32 @@ class SupportedTypesHolder {
let imageParameter = GeneralParameter.image(source: NSImage(named: NSImage.touchBarFastForwardTemplateName)!)
return (
item: .staticButton(title: ""),
- action: .hidKey(keycode: NX_KEYTYPE_NEXT),
- longAction: .none,
+ actions: [
+ Action(trigger: .singleTap, value: .hidKey(keycode: NX_KEYTYPE_NEXT))
+ ],
+ legacyAction: .none,
+ legacyLongAction: .none,
parameters: [.image: imageParameter]
)
},
"sleep": { _ in (
item: .staticButton(title: "☕️"),
- action: .shellScript(executable: "/usr/bin/pmset", parameters: ["sleepnow"]),
- longAction: .none,
+ actions: [
+ Action(trigger: .singleTap, value: .shellScript(executable: "/usr/bin/pmset", parameters: ["sleepnow"]))
+ ],
+ legacyAction: .none,
+ legacyLongAction: .none,
parameters: [:]
) },
"displaySleep": { _ in (
item: .staticButton(title: "☕️"),
- action: .shellScript(executable: "/usr/bin/pmset", parameters: ["displaysleepnow"]),
- longAction: .none,
+ actions: [
+ Action(trigger: .singleTap, value: .shellScript(executable: "/usr/bin/pmset", parameters: ["displaysleepnow"]))
+ ],
+ legacyAction: .none,
+ legacyLongAction: .none,
parameters: [:]
) },
@@ -209,11 +256,12 @@ class SupportedTypesHolder {
static let sharedInstance = SupportedTypesHolder()
- func lookup(by type: String) -> ParametersDecoder {
+ func lookup(by type: String, actions: [Action]) -> ParametersDecoder {
return supportedTypes[type] ?? { decoder in (
item: try ItemType(from: decoder),
- action: try ActionType(from: decoder),
- longAction: try LongActionType(from: decoder),
+ actions: actions,
+ legacyAction: try LegacyActionType(from: decoder),
+ legacyLongAction: try LegacyLongActionType(from: decoder),
parameters: [:]
) }
}
@@ -222,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: [:]
)
}
@@ -236,7 +285,7 @@ class SupportedTypesHolder {
enum ItemType: Decodable {
case staticButton(title: String)
- case appleScriptTitledButton(source: SourceProtocol, refreshInterval: Double)
+ case appleScriptTitledButton(source: SourceProtocol, refreshInterval: Double, alternativeImages: [String: SourceProtocol])
case shellScriptTitledButton(source: SourceProtocol, refreshInterval: Double)
case timeButton(formatTemplate: String, timeZone: String?, locale: String?)
case battery
@@ -255,6 +304,8 @@ enum ItemType: Decodable {
case pomodoro(workTime: Double, restTime: Double)
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
@@ -280,6 +331,13 @@ enum ItemType: Decodable {
case autoResize
case filter
case disableMarquee
+ case alternativeImages
+ case sourceApple
+ case sourceBash
+ case direction
+ case fingers
+ case minOffset
+ case maxToShow
}
enum ItemTypeRaw: String, Decodable {
@@ -303,6 +361,8 @@ enum ItemType: Decodable {
case pomodoro
case network
case darkMode
+ case swipe
+ case upnext
}
init(from decoder: Decoder) throws {
@@ -312,7 +372,8 @@ enum ItemType: Decodable {
case .appleScriptTitledButton:
let source = try container.decode(Source.self, forKey: .source)
let interval = try container.decodeIfPresent(Double.self, forKey: .refreshInterval) ?? 1800.0
- self = .appleScriptTitledButton(source: source, refreshInterval: interval)
+ 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)
@@ -396,11 +457,114 @@ 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)
+ let direction = try container.decode(String.self, forKey: .direction)
+ 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 : 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)
@@ -458,7 +622,7 @@ enum ActionType: Decodable {
}
}
-enum LongActionType: Decodable {
+enum LegacyLongActionType: Decodable {
case none
case hidKey(keycode: Int32)
case keyPress(keycode: Int)
diff --git a/MTMR/ScrollViewItem.swift b/MTMR/ScrollViewItem.swift
index 96b0691..8a39af1 100644
--- a/MTMR/ScrollViewItem.swift
+++ b/MTMR/ScrollViewItem.swift
@@ -1,10 +1,6 @@
import Foundation
-class ScrollViewItem: NSCustomTouchBarItem, NSGestureRecognizerDelegate {
- var twofingersPrev: CGFloat = 0.0
- var threefingersPrev: CGFloat = 0.0
- var twofingers: NSPanGestureRecognizer!
- var threefingers: NSPanGestureRecognizer!
+class ScrollViewItem: NSCustomTouchBarItem/*, NSGestureRecognizerDelegate*/ {
init(identifier: NSTouchBarItem.Identifier, items: [NSTouchBarItem]) {
super.init(identifier: identifier)
@@ -15,70 +11,10 @@ class ScrollViewItem: NSCustomTouchBarItem, NSGestureRecognizerDelegate {
let scrollView = NSScrollView(frame: CGRect(origin: .zero, size: stackView.fittingSize))
scrollView.documentView = stackView
view = scrollView
-
- twofingers = NSPanGestureRecognizer(target: self, action: #selector(twofingersHandler(_:)))
- twofingers.allowedTouchTypes = .direct
- twofingers.numberOfTouchesRequired = 2
- view.addGestureRecognizer(twofingers)
-
- threefingers = NSPanGestureRecognizer(target: self, action: #selector(threefingersHandler(_:)))
- threefingers.allowedTouchTypes = .direct
- threefingers.numberOfTouchesRequired = 3
- view.addGestureRecognizer(threefingers)
- }
-
- var gesturesEnabled = true {
- didSet {
- twofingers.isEnabled = gesturesEnabled
- threefingers.isEnabled = gesturesEnabled
- }
}
required init?(coder _: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
- @objc func twofingersHandler(_ sender: NSGestureRecognizer?) { // Volume
- let position = (sender?.location(in: sender?.view).x)!
-
- switch sender!.state {
- case .began:
- twofingersPrev = position
- case .changed:
- if ((position - twofingersPrev) > 10) || ((twofingersPrev - position) > 10) {
- if position > twofingersPrev {
- HIDPostAuxKey(NX_KEYTYPE_SOUND_UP)
- } else if position < twofingersPrev {
- HIDPostAuxKey(NX_KEYTYPE_SOUND_DOWN)
- }
- twofingersPrev = position
- }
- case .ended:
- twofingersPrev = 0.0
- default:
- break
- }
- }
-
- @objc func threefingersHandler(_ sender: NSGestureRecognizer?) { // Brightness
- let position = (sender?.location(in: sender?.view).x)!
-
- switch sender!.state {
- case .began:
- threefingersPrev = position
- case .changed:
- if ((position - threefingersPrev) > 15) || ((threefingersPrev - position) > 15) {
- if position > threefingersPrev {
- GenericKeyPress(keyCode: CGKeyCode(144)).send()
- } else if position < threefingersPrev {
- GenericKeyPress(keyCode: CGKeyCode(145)).send()
- }
- threefingersPrev = position
- }
- case .ended:
- threefingersPrev = 0.0
- default:
- break
- }
- }
}
diff --git a/MTMR/ShellScriptTouchBarItem.swift b/MTMR/ShellScriptTouchBarItem.swift
index 94d51ec..4fd51f3 100644
--- a/MTMR/ShellScriptTouchBarItem.swift
+++ b/MTMR/ShellScriptTouchBarItem.swift
@@ -62,11 +62,19 @@ class ShellScriptTouchBarItem: CustomButtonTouchBarItem {
let pipe = Pipe()
task.standardOutput = pipe
+
+ // kill process if it is over update interval
+ DispatchQueue.main.asyncAfter(deadline: .now() + interval) { [weak task] in
+ task?.terminate()
+ }
+
task.launch()
let data = pipe.fileHandleForReading.readDataToEndOfFile()
var output: String = NSString(data: data, encoding: String.Encoding.utf8.rawValue) as String? ?? ""
+ //always wait until task end or you can catch "task still running" error while accessing task.terminationStatus variable
+ task.waitUntilExit()
if (output == "" && task.terminationStatus != 0) {
output = "error"
}
diff --git a/MTMR/SwipeItem.swift b/MTMR/SwipeItem.swift
new file mode 100644
index 0000000..baf7949
--- /dev/null
+++ b/MTMR/SwipeItem.swift
@@ -0,0 +1,66 @@
+//
+// SwipeItem.swift
+// MTMR
+//
+// Created by Fedor Zaitsev on 3/29/20.
+// Copyright © 2020 Anton Palgunov. All rights reserved.
+//
+
+import Foundation
+import Foundation
+
+class SwipeItem: NSCustomTouchBarItem {
+ private var scriptApple: NSAppleScript?
+ private var scriptBash: String?
+ private var direction: String
+ private var fingers: Int
+ private var minOffset: Float
+ init?(identifier: NSTouchBarItem.Identifier, direction: String, fingers: Int, minOffset: Float, sourceApple: SourceProtocol?, sourceBash: SourceProtocol?) {
+ self.direction = direction
+ self.fingers = fingers
+ self.scriptBash = sourceBash?.string
+ self.scriptApple = sourceApple?.appleScript
+ self.minOffset = minOffset
+ super.init(identifier: identifier)
+ }
+
+ required init?(coder _: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ func processEvent(offset: CGFloat, fingers: Int) {
+ if direction == "right" && Float(offset) > self.minOffset && self.fingers == fingers {
+ self.execute()
+ }
+ if direction == "left" && Float(offset) < -self.minOffset && self.fingers == fingers {
+ self.execute()
+ }
+ }
+
+ func execute() {
+ if scriptApple != nil {
+ DispatchQueue.appleScriptQueue.async {
+ var error: NSDictionary?
+ self.scriptApple?.executeAndReturnError(&error)
+ if let error = error {
+ print("SwipeItem apple script error: \(error)")
+ return
+ }
+ }
+ }
+ if scriptBash != nil {
+ DispatchQueue.shellScriptQueue.async {
+ let task = Process()
+ task.launchPath = "/bin/bash"
+ task.arguments = ["-c", self.scriptBash!]
+ task.launch()
+ task.waitUntilExit()
+
+
+ if (task.terminationStatus != 0) {
+ print("SwipeItem bash script error. Status: \(task.terminationStatus)")
+ }
+ }
+ }
+ }
+}
diff --git a/MTMR/TouchBarController.swift b/MTMR/TouchBarController.swift
index b71c066..95ecf0e 100644
--- a/MTMR/TouchBarController.swift
+++ b/MTMR/TouchBarController.swift
@@ -59,6 +59,10 @@ extension ItemType {
return NetworkBarItem.identifier
case .darkMode:
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."
}
}
}
@@ -78,10 +82,10 @@ class TouchBarController: NSObject, NSTouchBarDelegate {
var items: [NSTouchBarItem.Identifier: NSTouchBarItem] = [:]
var leftIdentifiers: [NSTouchBarItem.Identifier] = []
var centerIdentifiers: [NSTouchBarItem.Identifier] = []
- var centerItems: [NSTouchBarItem] = []
var rightIdentifiers: [NSTouchBarItem.Identifier] = []
- var scrollArea: ScrollViewItem?
- var centerScrollArea = NSTouchBarItem.Identifier("com.toxblh.mtmr.scrollArea.".appending(UUID().uuidString))
+ var basicViewIdentifier = NSTouchBarItem.Identifier("com.toxblh.mtmr.scrollView.".appending(UUID().uuidString))
+ var basicView: BasicView?
+ var swipeItems: [SwipeItem] = []
var blacklistAppIdentifiers: [String] = []
var frontmostApplicationIdentifier: String? {
@@ -90,13 +94,28 @@ class TouchBarController: NSObject, NSTouchBarDelegate {
private override init() {
super.init()
- SupportedTypesHolder.sharedInstance.register(typename: "exitTouchbar", item: .staticButton(title: "exit"), action: .custom(closure: { [weak self] in self?.dismissTouchBar() }), longAction: .none)
+ SupportedTypesHolder.sharedInstance.register(
+ typename: "exitTouchbar",
+ item: .staticButton(title: "exit"),
+ actions: [
+ Action(trigger: .singleTap, value: .custom(closure: { [weak self] in self?.dismissTouchBar() }))
+ ],
+ legacyAction: .none,
+ legacyLongAction: .none
+ )
SupportedTypesHolder.sharedInstance.register(typename: "close") { _ in
- (item: .staticButton(title: ""), action: .custom(closure: { [weak self] in
- guard let `self` = self else { return }
- self.reloadPreset(path: self.lastPresetPath)
- }), longAction: .none, parameters: [.width: .width(30), .image: .image(source: (NSImage(named: NSImage.stopProgressFreestandingTemplateName))!)])
+ (
+ item: .staticButton(title: ""),
+ actions: [
+ Action(trigger: .singleTap, value: .custom(closure: { [weak self] in
+ guard let `self` = self else { return }
+ self.reloadPreset(path: self.lastPresetPath)
+ }))
+ ],
+ legacyAction: .none,
+ legacyLongAction: .none,
+ parameters: [.width: .width(30), .image: .image(source: (NSImage(named: NSImage.stopProgressFreestandingTemplateName))!)])
}
blacklistAppIdentifiers = AppSettings.blacklistedAppIds
@@ -116,24 +135,29 @@ class TouchBarController: NSObject, NSTouchBarDelegate {
jsonItems = newJsonItems
itemDefinitions = [:]
items = [:]
- leftIdentifiers = []
- centerItems = []
- rightIdentifiers = []
loadItemDefinitions(jsonItems: jsonItems)
createItems()
- centerItems = centerIdentifiers.compactMap({ (identifier) -> NSTouchBarItem? in
+ let centerItems = centerIdentifiers.compactMap({ (identifier) -> NSTouchBarItem? in
items[identifier]
})
- centerScrollArea = NSTouchBarItem.Identifier("com.toxblh.mtmr.scrollArea.".appending(UUID().uuidString))
- scrollArea = ScrollViewItem(identifier: centerScrollArea, items: centerItems)
- scrollArea?.gesturesEnabled = AppSettings.multitouchGestures
+ let centerScrollArea = NSTouchBarItem.Identifier("com.toxblh.mtmr.scrollArea.".appending(UUID().uuidString))
+ let scrollArea = ScrollViewItem(identifier: centerScrollArea, items: centerItems)
touchBar.delegate = self
- touchBar.defaultItemIdentifiers = []
- touchBar.defaultItemIdentifiers = leftIdentifiers + [centerScrollArea] + rightIdentifiers
+ 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
updateActiveApp()
}
@@ -163,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)
}
@@ -189,7 +213,12 @@ class TouchBarController: NSObject, NSTouchBarDelegate {
func createItems() {
for (identifier, definition) in itemDefinitions {
- items[identifier] = createItem(forIdentifier: identifier, definition: definition)
+ let item = createItem(forIdentifier: identifier, definition: definition)
+ if item is SwipeItem {
+ swipeItems.append(item as! SwipeItem)
+ } else {
+ items[identifier] = item
+ }
}
}
@@ -225,16 +254,11 @@ class TouchBarController: NSObject, NSTouchBarDelegate {
}
func touchBar(_: NSTouchBar, makeItemForIdentifier identifier: NSTouchBarItem.Identifier) -> NSTouchBarItem? {
- if identifier == centerScrollArea {
- return scrollArea
+ if identifier == basicViewIdentifier {
+ return basicView
}
- guard let item = self.items[identifier],
- let definition = self.itemDefinitions[identifier],
- definition.align != .center else {
- return nil
- }
- return item
+ return nil
}
func createItem(forIdentifier identifier: NSTouchBarItem.Identifier, definition item: BarItemDefinition) -> NSTouchBarItem? {
@@ -242,8 +266,8 @@ class TouchBarController: NSObject, NSTouchBarDelegate {
switch item.type {
case let .staticButton(title: title):
barItem = CustomButtonTouchBarItem(identifier: identifier, title: title)
- case let .appleScriptTitledButton(source: source, refreshInterval: interval):
- barItem = AppleScriptTouchBarItem(identifier: identifier, source: source, interval: interval)
+ case let .appleScriptTitledButton(source: source, refreshInterval: interval, alternativeImages: alternativeImages):
+ barItem = AppleScriptTouchBarItem(identifier: identifier, source: source, interval: interval, alternativeImages: alternativeImages)
case let .shellScriptTitledButton(source: source, refreshInterval: interval):
barItem = ShellScriptTouchBarItem(identifier: identifier, source: source, interval: interval)
case let .timeButton(formatTemplate: template, timeZone: timeZone, locale: locale):
@@ -300,13 +324,23 @@ class TouchBarController: NSObject, NSTouchBarDelegate {
barItem = NetworkBarItem(identifier: identifier, flip: flip)
case .darkMode:
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
@@ -329,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):
@@ -375,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):
@@ -427,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] {
diff --git a/MTMR/Widgets/AppScrubberTouchBarItem.swift b/MTMR/Widgets/AppScrubberTouchBarItem.swift
index adc5541..b8a0300 100644
--- a/MTMR/Widgets/AppScrubberTouchBarItem.swift
+++ b/MTMR/Widgets/AppScrubberTouchBarItem.swift
@@ -82,12 +82,14 @@ class AppScrubberTouchBarItem: NSCustomTouchBarItem {
public func createAppButton(for app: DockItem) -> DockBarItem {
let item = DockBarItem(app)
item.isBordered = false
- item.tapClosure = { [weak self] in
- self?.switchToApp(app: app)
- }
- item.longTapClosure = { [weak self] in
- self?.handleHalfLongPress(item: app)
- }
+ item.actions.append(contentsOf: [
+ ItemAction(trigger: .singleTap) { [weak self] in
+ self?.switchToApp(app: app)
+ },
+ ItemAction(trigger: .longTap) { [weak self] in
+ self?.handleHalfLongPress(item: app)
+ }
+ ])
item.killAppClosure = {[weak self] in
self?.handleLongPress(item: app)
}
diff --git a/MTMR/Widgets/CurrencyBarItem.swift b/MTMR/Widgets/CurrencyBarItem.swift
index dfe3479..71e5599 100644
--- a/MTMR/Widgets/CurrencyBarItem.swift
+++ b/MTMR/Widgets/CurrencyBarItem.swift
@@ -15,6 +15,9 @@ class CurrencyBarItem: CustomButtonTouchBarItem {
private var postfix: String
private var from: String
private var to: String
+ private var decimal: Int
+ private var decimalValue: Float32!
+ private var decimalString: String!
private var oldValue: Float32!
private var full: Bool = false
@@ -32,11 +35,29 @@ class CurrencyBarItem: CustomButtonTouchBarItem {
"IDR": "Rp",
"MXN": "$",
"SGD": "$",
- "CHF": "Fr.",
"BTC": "฿",
"LTC": "Ł",
"ETH": "Ξ",
]
+ private let decimals = [
+ "USD": 4,
+ "EUR": 4,
+ "RUB": 2,
+ "JPY": 2,
+ "GBP": 4,
+ "CAD": 4,
+ "KRW": 4,
+ "CNY": 4,
+ "AUD": 4,
+ "BRL": 4,
+ "IDR": 1,
+ "MXN": 2,
+ "SGD": 4,
+ "CHF": 4,
+ "BTC": 2,
+ "LTC": 2,
+ "ETH": 2,
+ ]
init(identifier: NSTouchBarItem.Identifier, interval: TimeInterval, from: String, to: String, full: Bool) {
activity = NSBackgroundActivityScheduler(identifier: "\(identifier.rawValue).updatecheck")
@@ -57,6 +78,14 @@ class CurrencyBarItem: CustomButtonTouchBarItem {
postfix = to
}
+
+ if let decimal = decimals[to] {
+ self.decimal = decimal
+ } else {
+ decimal = 2
+ }
+
+
super.init(identifier: identifier, title: "⏳")
activity.repeats = true
@@ -114,10 +143,12 @@ class CurrencyBarItem: CustomButtonTouchBarItem {
}
oldValue = value
+ decimalValue = (value * pow(10,Float(decimal))).rounded() / pow(10,Float(decimal))
+ decimalString = String(decimalValue)
var title = ""
if full {
- title = String(format: "%@‣%.2f%@", prefix, value, postfix)
+ title = String(format: "%@%@‣%@", prefix, postfix, decimalString)
} else {
title = String(format: "%@%.2f", prefix, value)
}
diff --git a/MTMR/Widgets/DarkModeBarItem.swift b/MTMR/Widgets/DarkModeBarItem.swift
index ceaba78..ecad30b 100644
--- a/MTMR/Widgets/DarkModeBarItem.swift
+++ b/MTMR/Widgets/DarkModeBarItem.swift
@@ -11,7 +11,7 @@ class DarkModeBarItem: CustomButtonTouchBarItem, Widget {
isBordered = false
setWidth(value: 24)
- tapClosure = { [weak self] in self?.DarkModeToggle() }
+ actions.append(ItemAction(trigger: .singleTap) { [weak self] in self?.DarkModeToggle() })
timer = Timer.scheduledTimer(timeInterval: 3, target: self, selector: #selector(refresh), userInfo: nil, repeats: true)
diff --git a/MTMR/Widgets/DnDBarItem.swift b/MTMR/Widgets/DnDBarItem.swift
index 18344ca..eb15f7a 100644
--- a/MTMR/Widgets/DnDBarItem.swift
+++ b/MTMR/Widgets/DnDBarItem.swift
@@ -16,7 +16,7 @@ class DnDBarItem: CustomButtonTouchBarItem {
isBordered = false
setWidth(value: 32)
- tapClosure = { [weak self] in self?.DnDToggle() }
+ actions.append(ItemAction(trigger: .singleTap) { [weak self] in self?.DnDToggle() })
timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(refresh), userInfo: nil, repeats: true)
diff --git a/MTMR/Widgets/InputSourceBarItem.swift b/MTMR/Widgets/InputSourceBarItem.swift
index 6e8b15b..1d3e367 100644
--- a/MTMR/Widgets/InputSourceBarItem.swift
+++ b/MTMR/Widgets/InputSourceBarItem.swift
@@ -18,9 +18,10 @@ class InputSourceBarItem: CustomButtonTouchBarItem {
observeIputSourceChangedNotification()
textInputSourceDidChange()
- tapClosure = { [weak self] in
+
+ actions.append(ItemAction(trigger: .singleTap) { [weak self] in
self?.switchInputSource()
- }
+ })
}
required init?(coder _: NSCoder) {
diff --git a/MTMR/Widgets/MusicBarItem.swift b/MTMR/Widgets/MusicBarItem.swift
index 5bc8e7b..a1d1e11 100644
--- a/MTMR/Widgets/MusicBarItem.swift
+++ b/MTMR/Widgets/MusicBarItem.swift
@@ -11,6 +11,7 @@ import ScriptingBridge
class MusicBarItem: CustomButtonTouchBarItem {
private enum Player: String {
+ case Music = "com.apple.Music"
case iTunes = "com.apple.iTunes"
case Spotify = "com.spotify.client"
case VOX = "com.coppertino.Vox"
@@ -19,6 +20,7 @@ class MusicBarItem: CustomButtonTouchBarItem {
}
private let playerBundleIdentifiers = [
+ Player.Music,
Player.iTunes,
Player.Spotify,
Player.VOX,
@@ -39,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()
}
@@ -71,6 +76,10 @@ class MusicBarItem: CustomButtonTouchBarItem {
let mp = (musicPlayer as iTunesApplication)
mp.playpause!()
return
+ } else if ident == .Music {
+ let mp = (musicPlayer as MusicApplication)
+ mp.playpause!()
+ return
} else if ident == .VOX {
let mp = (musicPlayer as VoxApplication)
mp.playpause!()
@@ -134,6 +143,11 @@ class MusicBarItem: CustomButtonTouchBarItem {
mp.nextTrack!()
updatePlayer()
return
+ } else if ident == .Music {
+ let mp = (musicPlayer as MusicApplication)
+ mp.nextTrack!()
+ updatePlayer()
+ return
} else if ident == .VOX {
let mp = (musicPlayer as VoxApplication)
mp.next!()
@@ -166,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 {
@@ -188,6 +227,8 @@ class MusicBarItem: CustomButtonTouchBarItem {
tempTitle = (musicPlayer as SpotifyApplication).title
} else if ident == .iTunes {
tempTitle = (musicPlayer as iTunesApplication).title
+ } else if ident == .Music {
+ tempTitle = (musicPlayer as MusicApplication).title
} else if ident == .VOX {
tempTitle = (musicPlayer as VoxApplication).title
} else if ident == .Safari {
@@ -321,6 +362,29 @@ extension iTunesApplication {
}
}
+@objc protocol MusicApplication {
+ @objc optional var currentTrack: MusicTrack { get }
+ @objc optional func playpause()
+ @objc optional func nextTrack()
+ @objc optional func previousTrack()
+}
+
+extension SBApplication: MusicApplication {}
+
+@objc protocol MusicTrack {
+ @objc optional var artist: String { get }
+ @objc optional var name: String { get }
+}
+
+extension SBObject: MusicTrack {}
+
+extension MusicApplication {
+ var title: String {
+ guard let t = currentTrack else { return "" }
+ return (t.artist ?? "") + " — " + (t.name ?? "")
+ }
+}
+
@objc protocol VoxApplication {
@objc optional func playpause()
@objc optional func next()
diff --git a/MTMR/Widgets/NightShiftBarItem.swift b/MTMR/Widgets/NightShiftBarItem.swift
index 517a8e7..04715ed 100644
--- a/MTMR/Widgets/NightShiftBarItem.swift
+++ b/MTMR/Widgets/NightShiftBarItem.swift
@@ -30,8 +30,8 @@ class NightShiftBarItem: CustomButtonTouchBarItem {
super.init(identifier: identifier, title: "")
isBordered = false
setWidth(value: 28)
-
- tapClosure = { [weak self] in self?.nightShiftAction() }
+
+ actions.append(ItemAction(trigger: .singleTap) { [weak self] in self?.nightShiftAction() })
timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(refresh), userInfo: nil, repeats: true)
diff --git a/MTMR/Widgets/PomodoroBarItem.swift b/MTMR/Widgets/PomodoroBarItem.swift
index 7b824f7..9f7974f 100644
--- a/MTMR/Widgets/PomodoroBarItem.swift
+++ b/MTMR/Widgets/PomodoroBarItem.swift
@@ -23,8 +23,9 @@ class PomodoroBarItem: CustomButtonTouchBarItem, Widget {
return (
item: .pomodoro(workTime: workTime ?? 1500.00, restTime: restTime ?? 300),
- action: .none,
- longAction: .none,
+ actions: [],
+ legacyAction: .none,
+ legacyLongAction: .none,
parameters: [:]
)
}
@@ -35,7 +36,7 @@ class PomodoroBarItem: CustomButtonTouchBarItem, Widget {
case none
}
- private let defaultTitle = "🍅"
+ private let defaultTitle = "🍅 "
private let workTime: TimeInterval
private let restTime: TimeInterval
private var typeTime: TimeTypes = .none
@@ -50,8 +51,10 @@ class PomodoroBarItem: CustomButtonTouchBarItem, Widget {
self.workTime = workTime
self.restTime = restTime
super.init(identifier: identifier, title: defaultTitle)
- tapClosure = { [weak self] in self?.startStopWork() }
- longTapClosure = { [weak self] in self?.startStopRest() }
+ actions.append(contentsOf: [
+ ItemAction(trigger: .singleTap) { [weak self] in self?.startStopWork() },
+ ItemAction(trigger: .longTap) { [weak self] in self?.startStopRest() }
+ ])
}
required init?(coder _: NSCoder) {
diff --git a/MTMR/Widgets/UpNextScrubberTouchBarItem.swift b/MTMR/Widgets/UpNextScrubberTouchBarItem.swift
new file mode 100644
index 0000000..6ea7e78
--- /dev/null
+++ b/MTMR/Widgets/UpNextScrubberTouchBarItem.swift
@@ -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
+ }
+}
diff --git a/MTMR/Widgets/YandexWeatherBarItem.swift b/MTMR/Widgets/YandexWeatherBarItem.swift
index c6a050b..c59309d 100644
--- a/MTMR/Widgets/YandexWeatherBarItem.swift
+++ b/MTMR/Widgets/YandexWeatherBarItem.swift
@@ -12,10 +12,14 @@ import CoreLocation
class YandexWeatherBarItem: CustomButtonTouchBarItem, CLLocationManagerDelegate {
private let activity: NSBackgroundActivityScheduler
private let unitsStr = "°C"
- private let iconsSource = ["Ясно": "☀️", "Малооблачно": "🌤", "Облачно с прояснениями": "⛅️", "Пасмурно": "☁️", "Небольшой дождь": "🌦", "Дождь": "🌧", "Ливень": "⛈", "Гроза": "🌩", "Дождь со снегом": "☔️", "Небольшой снег": "❄️", "Снег": "🌨", "Туман": "🌫"]
+ private let iconsSource = [
+ "Ясно": "☀️", "Малооблачно": "🌤", "Облачно с прояснениями": "⛅️", "Пасмурно": "☁️", "Небольшой дождь": "🌦", "Дождь": "🌧", "Ливень": "⛈", "Гроза": "🌩", "Дождь с грозой": "⛈", "Дождь со снегом": "☔️", "Небольшой снег": "❄️", "Снег": "🌨", "Туман": "🌫",
+ "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")
@@ -46,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) {
@@ -58,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
}
@@ -83,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
}
diff --git a/MTMR/defaultPreset.json b/MTMR/defaultPreset.json
index 62b43d8..dfa019e 100644
--- a/MTMR/defaultPreset.json
+++ b/MTMR/defaultPreset.json
@@ -28,6 +28,26 @@
}
},
+ // Music
+ {
+ "type": "appleScriptTitledButton",
+ "source": {
+ "inline": "if application \"Music\" is running then\rtell application \"Music\"\rif player state is playing then\rreturn (get artist of current track) & \" – \" & (get name of current track)\relse\rreturn \"\"\rend if\rend tell\rend if\rreturn \"\"\r"
+ },
+ "action": "appleScript",
+ "actionAppleScript": {
+ "inline": "if application \"Music\" is running then\rtell application \"Music\"\rif player state is playing then\rnext track\rend if\rend tell\rend if\r"
+ },
+ "longAction": "appleScript",
+ "longActionAppleScript": {
+ "inline": "if application \"Music\" is running then\rtell application \"Music\"\rif player state is playing then\rprev track\rend if\rend tell\rend if\r"
+ },
+ "refreshInterval": 2,
+ "image": {
+ "base64": "iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAMAAACdt4HsAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAADAUExURUdwTOVVZCzB+3qc0gkDBgEBAgcKEwAAAA4EBP5aVU2V95iJv7V3rtBOvH5W6jaOyclScKZGX3wuQCMuUqZN7+NQYXtDvFd9/sxYni2z6UhBhyhvnIp7sf38/PXz9ePm69/k6fHv8/j29+3q7/v6+ufq7uvu8fTw8+1egOFki/dbboVj/HNy/T+j/dNtnEul81vC8Vmf3OeRqOBVofK4xZfF7sDb7PLe6LxU1KKK79PL6vrW3fh4g5Zi4bi16daa0A3Qc90AAAAddFJOUwD3/v0uOlYNG/z+/v7998OYYztt/Le2/eDqi5jEo2rNTwAABMtJREFUWMPdl1tjqjoQhauC0Hqvta26JVxUQBQFRPBS/f//as8kISBqd/twXs5KjBDyrZkEFfP09D+WoigqFxz+mlbVXncweEZVBoNuD1x+E1vtMVZoURn01J/mob52KRNuj4k5mZjJcRueFotFpfv6EwdVRfy0tSdXSranheN0/zkRSB7x42Q6udExdBy3972Fog4o/kBg4X6bhPJaeX7eTqeT6b0MJtPpdOu6n49XQgX+dOQ8vpWEvYn/2AHiL052PpoROcsOtND17ztQ3rwTuCQz9O/moKiMf6BkG/puKBzurKQ64PmbU2bDzUxk3Uql4lZcl3Vpvt9VbxLoLZwjY7E1WcNZoB0XpbELie/3Sg6KVHG2jGPs1LTCE2UXFfgIgtBgyq8d/E/pehJq1zmZGc7kAPsMX4Ec932T25uX5vUklFcHJlDU1OT4wllkvOtn9lrSbF7dCUggNEtaMOXhQZq4WkpBcksJQBCOnyjvM4P8KqQgFW9BFJka2NMKB1gw+VMxvN9smnwI1EuzpxS+g9FWYySjsTpOtnq+H162iW01m/wyXLUPzT/5HKQoSjQmU8vE6TAElvWggbhuNRpScQa002bVtJmBCz9qNusWBkJmoyHmoHajC4yybVujhR26mJVha7lDo2FrhnA4N0aq+BpE24zjgsMoEsfU0AADaKCwemiIRZA+o6N9oygyMi/EAWk0DMNgFvCmN/5IwqCV3PCGzzIwbINrzgwykVq2iorUalm2UTZotXKWqVYz5uBjzDUoxrxWyzKQWy061LZsNIJ3PAMDIcbVauwdGmxrNblgYNnCgStN54ylBSsYoAxerwwgJsCWTS0sepym0Mdp1gYBw5lmwgDXIDEoaeHLYE36BafzuQWFQ9RASM/XQPpMD5YQ2gA/AwPArQJyZWDsgo64C+/pBRkDG4s31hdmwFNAGz1mBjPukBs8qdSgLDBA1LJm1lw/14IgWAdAQ5nhax4HY/FR7qfpHQMPUChzS0c6eFmv17MZo7HZBP3MQJHTNCnzxPMgOFUteAGtMwMmfRPIijBgc+AmNK6+9zw+9Iw05YsG8aaT/7Kro7eUcoSWw3n/1W57SxgGOZCYxl+VDDabcf6LpNTf3g6IQ4XY7TbiyyUBmhqsViswWK02cE7ITIdmtxnWlcJz6f1tPyMsha+2R4UGyJPdywotwGSHOKHdm+IMYA40BRhtkXOb42DAh8crppjw8CyB4nMBlvFtD/0WSfZebkDHguLNar2JdyTXptqRrx6OmMKZkISQc4Yv9yyDXHiiEx1qXL1OAFdhBJPAAQeRQDEiRZEm+kwnu2p1XHo64yQ8j47bL1kCZ87pDKWxuW4mQJ9O9ba31xE5Y/rnA4VoTCJQwvnyBNgk+pkDi8sSJjlKRPxhX7r3Lytz0LPMi1H1Qv7VuzwuAzh4h1ukKFi/YV9+9E8THZbne2Ezxd/xsNGQ6u+wgoeH4SH9Tl367t+2Ko/acA8Oj/DhWP7X/30Zkvj4WMYlj10MOISXf7DlkPvvH6g43u0oCzDS1U5f/sHWC3d7cn3UAQf4HeHfwxXQY4yu/HTDKNXro3Gngw4vw2FnPKrXJfUXu0fqIdeFZOnXm08FTRSxcf391pW7oNGT8vRf6i9jqljwYzAm6AAAAABJRU5ErkJggg=="
+ }
+ },
+
// iTunes
{
"type": "appleScriptTitledButton",
diff --git a/MTMRTests/AppleScriptDefinitionTests.swift b/MTMRTests/AppleScriptDefinitionTests.swift
index 98101b6..6e0d425 100644
--- a/MTMRTests/AppleScriptDefinitionTests.swift
+++ b/MTMRTests/AppleScriptDefinitionTests.swift
@@ -6,7 +6,7 @@ class AppleScriptDefinitionTests: XCTestCase {
[ { "type": "appleScriptTitledButton", "source": { "inline": "tell everything fine" } } ]
""".data(using: .utf8)!
let result = try? JSONDecoder().decode([BarItemDefinition].self, from: buttonNoActionFixture)
- guard case .appleScriptTitledButton(let source, _)? = result?.first?.type else {
+ guard case .appleScriptTitledButton(let source, _, _)? = result?.first?.type else {
XCTFail()
return
}
@@ -18,7 +18,7 @@ class AppleScriptDefinitionTests: XCTestCase {
[ { "type": "appleScriptTitledButton", "source": { "filePath": "/ololo/pew" } } ]
""".data(using: .utf8)!
let result = try? JSONDecoder().decode([BarItemDefinition].self, from: buttonNoActionFixture)
- guard case .appleScriptTitledButton(let source, _)? = result?.first?.type else {
+ guard case .appleScriptTitledButton(let source, _, _)? = result?.first?.type else {
XCTFail()
return
}
@@ -32,7 +32,7 @@ class AppleScriptDefinitionTests: XCTestCase {
[ { "type": "appleScriptTitledButton", "source": { "filePath": "~/pew" } } ]
""".data(using: .utf8)!
let result = try? JSONDecoder().decode([BarItemDefinition].self, from: buttonNoActionFixture)
- guard case .appleScriptTitledButton(let source, _)? = result?.first?.type else {
+ guard case .appleScriptTitledButton(let source, _, _)? = result?.first?.type else {
XCTFail()
return
}
@@ -58,7 +58,7 @@ class AppleScriptDefinitionTests: XCTestCase {
[ { "type": "appleScriptTitledButton", "source": { "inline": "tell everything fine" }, "refreshInterval": 305} ]
""".data(using: .utf8)!
let result = try? JSONDecoder().decode([BarItemDefinition].self, from: buttonNoActionFixture)
- guard case .appleScriptTitledButton(_, 305)? = result?.first?.type else {
+ guard case .appleScriptTitledButton(_, 305, _)? = result?.first?.type else {
XCTFail()
return
}
diff --git a/MTMRTests/ParseConfigTests.swift b/MTMRTests/ParseConfigTests.swift
index a3321cc..6cec324 100644
--- a/MTMRTests/ParseConfigTests.swift
+++ b/MTMRTests/ParseConfigTests.swift
@@ -10,7 +10,7 @@ class ParseConfig: XCTestCase {
XCTFail()
return
}
- guard case .none? = result?.first?.action else {
+ guard result?.first?.actions.count == 0 else {
XCTFail()
return
}
@@ -18,14 +18,29 @@ class ParseConfig: XCTestCase {
func testButtonKeyCodeAction() {
let buttonKeycodeFixture = """
- [ { "type": "staticButton", "title": "Pew", "action": "hidKey", "keycode": 123} ]
+ [ { "type": "staticButton", "title": "Pew", "actions": [ { "trigger": "singleTap", "action": "hidKey", "keycode": 123 } ] } ]
""".data(using: .utf8)!
let result = try? JSONDecoder().decode([BarItemDefinition].self, from: buttonKeycodeFixture)
guard case .staticButton("Pew")? = result?.first?.type else {
XCTFail()
return
}
- guard case .hidKey(keycode: 123)? = result?.first?.action else {
+ guard case .hidKey(keycode: 123)? = result?.first?.actions.filter({ $0.trigger == .singleTap }).first?.value else {
+ XCTFail()
+ return
+ }
+ }
+
+ func testButtonKeyCodeLegacyAction() {
+ let buttonKeycodeFixture = """
+ [ { "type": "staticButton", "title": "Pew", "action": "hidKey", "keycode": 123 } ]
+ """.data(using: .utf8)!
+ let result = try? JSONDecoder().decode([BarItemDefinition].self, from: buttonKeycodeFixture)
+ guard case .staticButton("Pew")? = result?.first?.type else {
+ XCTFail()
+ return
+ }
+ guard case .hidKey(keycode: 123)? = result?.first?.legacyAction else {
XCTFail()
return
}
@@ -40,7 +55,7 @@ class ParseConfig: XCTestCase {
XCTFail()
return
}
- guard case .keyPress(keycode: 53)? = result?.first?.action else {
+ guard case .keyPress(keycode: 53)? = result?.first?.actions.filter({ $0.trigger == .singleTap }).first?.value else {
XCTFail()
return
}
@@ -55,7 +70,7 @@ class ParseConfig: XCTestCase {
XCTFail()
return
}
- guard case .keyPress(keycode: 53)? = result?.first?.action else {
+ guard case .keyPress(keycode: 53)? = result?.first?.actions.filter({ $0.trigger == .singleTap }).first?.value else {
XCTFail()
return
}
diff --git a/README.md b/README.md
index e985d68..354d442 100644
--- a/README.md
+++ b/README.md
@@ -18,7 +18,7 @@ My idea is to create a platform for creating plugins to customize the TouchBar.
Telegram
-
+
@@ -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
@@ -102,11 +103,31 @@ The pre-installed configuration contains less or more than you'll probably want,
- appleScriptTitledButton
- shellScriptTitledButton
-## Gestures on central part:
+## Gestures
+By default you can enable basic gestures from application menu (status bar -> MTMR icon -> Volume/Brightness gestures):
- two finger slide: change you Volume
- three finger slide: change you Brightness
+### Custom gestures
+
+You can add custom actions for two/three/four finger swipes. To do it, you need to use `swipe` type:
+
+```json
+ "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
+ "sourceApple": { // optional: apple script to run
+ "inline": "beep"
+ },
+ "sourceBash": { // optional: bash script to run
+ "inline": "touch /Users/lobster/test"
+ }
+```
+
+You may create as many `swipe` objects in the preset as you want.
+
## Built-in slider types:
- brightness
@@ -133,10 +154,37 @@ The pre-installed configuration contains less or more than you'll probably want,
"inline": "tell application \"Finder\"\rif not (exists window 1) then\rmake new Finder window\rset target of front window to path to home folder as string\rend if\ractivate\rend tell",
// or
"base64": "StringInbase64"
- }
+ },
}
```
+> Note: appleScriptTitledButton can change its icon. To do it, you need to do the following things:
+1. Declarate dictionary of icons in `alternativeImages` field
+2. Make you script return array of two values - `{"TITLE", "IMAGE_LABEL"}`
+3. Make sure that your `IMAGE_LABEL` is declared in `alternativeImages` field
+
+Example:
+```js
+ {
+ "type": "appleScriptTitledButton",
+ "source": {
+ "inline": "if (random number from 1 to 2) = 1 then\n\tset val to {\"title\", \"play\"}\nelse\n\tset val to {\"title\", \"pause\"}\nend if\nreturn val"
+ },
+ "refreshInterval": 1,
+ "image": {
+ "base64": "iVBORw0KGgoAAAANSUhEUgA..."
+ },
+ "alternativeImages": {
+ "play": {
+ "base64": "iVBORw0KGgoAAAANSUhEUgAAAAAA..."
+ },
+ "pause": {
+ "base64": "iVBORw0KGgoAAAANSUhEUgAAAIAA..."
+ }
+ }
+ },
+```
+
#### `shellScriptTitledButton`
> Note: script may return also colors using escape sequences (read more here https://misc.flogisoft.com/bash/tip_colors_and_formatting)
> Only "16 Colors" mode supported atm. If background color returned, button will pick it up as own background color.
@@ -150,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.
@@ -296,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
@@ -339,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
@@ -375,39 +450,45 @@ If you want to longPress for some operations, it is similar to the configuration
"bordered": "false" // "true" or "false"
```
-### Roadmap
+- `background` allow to specify you button background color
-- [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
+```js
+ "background": "#FF0000",
+```
+by using background with color "#000000" and bordered == false you can create button without gray background but with background when the button is pressed
-Settings:
+- `title` specify button title
-- [ ] Interface for plugins and export like presets
-- [x] Startup at login
-- [ ] Show on/off in Dock
-- [ ] Show on/off in StatusBar
-- [ ] On/off Haptic Feedback
+```js
+ "title": "hello"
+```
-Maybe:
+- `image` specify button icon
-- [ ] Refactoring the application into packages (AppleScript, JavaScript? and Swift?)
+```js
+ "image": {
+ //Can be either of those
+ "base64": "iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAMAAACdt4HsAAAABGdB...."
+ //or
+ "filePath": "~/img.png"
+ }
+```
+
+## Troubleshooting
+
+#### 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 Enter
+
+
+#### 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"
+
+Re-tick or check a tick for access 🍏→ System Preferences → Security and Privacy → tab Privacy → Accessibility → MTMR
## Credits
diff --git a/TECHNICAL_DEBT.md b/TECHNICAL_DEBT.md
index 94f77b3..2b6df1d 100644
--- a/TECHNICAL_DEBT.md
+++ b/TECHNICAL_DEBT.md
@@ -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?)
diff --git a/build.sh b/build.sh
index cc5f8ee..28283cd 100755
--- a/build.sh
+++ b/build.sh
@@ -1,16 +1,18 @@
+# INSTALL xcpretty: sudo gem install xcpretty
+
NAME='MTMR'
rm -r Release 2>/dev/null
xcodebuild archive \
-scheme "$NAME" \
- -archivePath Release/App.xcarchive
+ -archivePath Release/App.xcarchive | xcpretty -c
xcodebuild \
-exportArchive \
-archivePath Release/App.xcarchive \
-exportOptionsPlist export-options.plist \
- -exportPath Release
+ -exportPath Release | xcpretty -c
cd Release
rm -r App.xcarchive
@@ -20,7 +22,7 @@ NAME_DMG="${NAME}.app"
echo $NAME_DMG
create-dmg $NAME_DMG
-DATE=`date +"%a, %d %b %Y %H:%M:%S %z"`
+DATE=`LC_ALL=en_US.utf8 date +"%a, %d %b %Y %H:%M:%S %z"`
BUILD=`/usr/libexec/PlistBuddy -c "Print CFBundleVersion" ${NAME}.app/Contents/Info.plist`
VERSION=`/usr/libexec/PlistBuddy -c "Print CFBundleShortVersionString" ${NAME}.app/Contents/Info.plist`
MINIMUM=`/usr/libexec/PlistBuddy -c "Print LSMinimumSystemVersion" ${NAME}.app/Contents/Info.plist`
@@ -61,8 +63,8 @@ echo "
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}"
diff --git a/test.sh b/test.sh
index ade0dd7..c140089 100755
--- a/test.sh
+++ b/test.sh
@@ -1,3 +1,4 @@
+# INSTALL xcpretty: sudo gem install xcpretty
NAME='MTMR'
killall $NAME
@@ -11,13 +12,13 @@ rm -r Release 2>/dev/null
xcodebuild archive \
-scheme "$NAME" \
- -archivePath Release/App.xcarchive
+ -archivePath Release/App.xcarchive | xcpretty
xcodebuild \
-exportArchive \
-archivePath Release/App.xcarchive \
-exportOptionsPlist export-options.plist \
- -exportPath Release
+ -exportPath Release | xcpretty
cd Release
rm -r App.xcarchive