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

-

PayPal donate button +

PayPal donate button Buy Me A Coffee @@ -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