From ac8a05d2e5f9fa0484ceab7fa14541424fefa2da Mon Sep 17 00:00:00 2001 From: ad Date: Sun, 15 Apr 2018 22:55:20 +0300 Subject: [PATCH] + show running apps (like dock) --- MTMR.xcodeproj/project.pbxproj | 10 + MTMR/AppScrubberTouchBarItem.swift | 265 +++++++++++++++++++++ MTMR/AppleScriptTouchBarItem.swift | 2 +- MTMR/CBridge/DeprecatedCarbonAPI.c | 45 ++++ MTMR/CBridge/DeprecatedCarbonAPI.h | 27 +++ MTMR/CBridge/TouchBarPrivateApi-Bridging.h | 1 + MTMR/CustomButtonTouchBarItem.swift | 1 + MTMR/ItemsParsing.swift | 7 + MTMR/TouchBarController.swift | 4 + 9 files changed, 361 insertions(+), 1 deletion(-) create mode 100644 MTMR/AppScrubberTouchBarItem.swift create mode 100644 MTMR/CBridge/DeprecatedCarbonAPI.c create mode 100644 MTMR/CBridge/DeprecatedCarbonAPI.h diff --git a/MTMR.xcodeproj/project.pbxproj b/MTMR.xcodeproj/project.pbxproj index fac7ea6..bd54938 100644 --- a/MTMR.xcodeproj/project.pbxproj +++ b/MTMR.xcodeproj/project.pbxproj @@ -16,6 +16,8 @@ 36C2ECE0207CB1B0003CDA33 /* defaultPreset.json in Resources */ = {isa = PBXBuildFile; fileRef = 36C2ECDF207CB1B0003CDA33 /* defaultPreset.json */; }; 6027D1B92080E52A004FFDC7 /* BrightnessViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6027D1B72080E52A004FFDC7 /* BrightnessViewController.swift */; }; 6027D1BA2080E52A004FFDC7 /* VolumeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6027D1B82080E52A004FFDC7 /* VolumeViewController.swift */; }; + 6042B6A72083E03A00C525C8 /* AppScrubberTouchBarItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6042B6A62083E03A00C525C8 /* AppScrubberTouchBarItem.swift */; }; + 6042B6AA2083E27000C525C8 /* DeprecatedCarbonAPI.c in Sources */ = {isa = PBXBuildFile; fileRef = 6042B6A92083E27000C525C8 /* DeprecatedCarbonAPI.c */; }; B0008E552080286C003AD4DD /* SupportHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0008E542080286C003AD4DD /* SupportHelpers.swift */; }; B059D622205E03F5006E6B86 /* TouchBarController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B059D621205E03F5006E6B86 /* TouchBarController.swift */; }; B059D624205E04F3006E6B86 /* CustomButtonTouchBarItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = B059D623205E04F3006E6B86 /* CustomButtonTouchBarItem.swift */; }; @@ -60,6 +62,9 @@ 36C2ECDF207CB1B0003CDA33 /* defaultPreset.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = defaultPreset.json; sourceTree = ""; }; 6027D1B72080E52A004FFDC7 /* BrightnessViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BrightnessViewController.swift; sourceTree = ""; }; 6027D1B82080E52A004FFDC7 /* VolumeViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VolumeViewController.swift; sourceTree = ""; }; + 6042B6A62083E03A00C525C8 /* AppScrubberTouchBarItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppScrubberTouchBarItem.swift; sourceTree = ""; }; + 6042B6A82083E1F500C525C8 /* DeprecatedCarbonAPI.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = DeprecatedCarbonAPI.h; sourceTree = ""; }; + 6042B6A92083E27000C525C8 /* DeprecatedCarbonAPI.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = DeprecatedCarbonAPI.c; sourceTree = ""; }; B0008E542080286C003AD4DD /* SupportHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SupportHelpers.swift; sourceTree = ""; }; B059D621205E03F5006E6B86 /* TouchBarController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TouchBarController.swift; sourceTree = ""; }; B059D623205E04F3006E6B86 /* CustomButtonTouchBarItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomButtonTouchBarItem.swift; sourceTree = ""; }; @@ -163,6 +168,7 @@ 6027D1B72080E52A004FFDC7 /* BrightnessViewController.swift */, 6027D1B82080E52A004FFDC7 /* VolumeViewController.swift */, 368EDDE620812A1D00E10953 /* ScrollViewItem.swift */, + 6042B6A62083E03A00C525C8 /* AppScrubberTouchBarItem.swift */, ); path = MTMR; sourceTree = ""; @@ -200,6 +206,8 @@ B059D62A205F0E7D006E6B86 /* TouchBarPrivateApi-Bridging.h */, B0F87719207AC1EA00D6E430 /* TouchBarSupport.m */, B0F8771B207AC92700D6E430 /* TouchBarSupport.h */, + 6042B6A82083E1F500C525C8 /* DeprecatedCarbonAPI.h */, + 6042B6A92083E27000C525C8 /* DeprecatedCarbonAPI.c */, ); path = CBridge; sourceTree = ""; @@ -324,9 +332,11 @@ 36C2ECD9207B74B4003CDA33 /* AppleScriptTouchBarItem.swift in Sources */, B0F8771A207AC1EA00D6E430 /* TouchBarSupport.m in Sources */, B0008E552080286C003AD4DD /* SupportHelpers.swift in Sources */, + 6042B6A72083E03A00C525C8 /* AppScrubberTouchBarItem.swift in Sources */, B082B253205C7D8000BC04DC /* AppDelegate.swift in Sources */, B059D624205E04F3006E6B86 /* CustomButtonTouchBarItem.swift in Sources */, 6027D1BA2080E52A004FFDC7 /* VolumeViewController.swift in Sources */, + 6042B6AA2083E27000C525C8 /* DeprecatedCarbonAPI.c in Sources */, B09EB1E4207C082000D5C1E0 /* HapticFeedback.swift in Sources */, 36C2ECDB207C3FE7003CDA33 /* ItemsParsing.swift in Sources */, B0A7E9AA205D6AA400EEF070 /* KeyPress.swift in Sources */, diff --git a/MTMR/AppScrubberTouchBarItem.swift b/MTMR/AppScrubberTouchBarItem.swift new file mode 100644 index 0000000..cd24c4c --- /dev/null +++ b/MTMR/AppScrubberTouchBarItem.swift @@ -0,0 +1,265 @@ +// +// AppScrubberTouchBarItem.swift +// +// This file is part of TouchDock +// Copyright (C) 2017 Xander Deng +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// + +import Cocoa + +let activateKeyIndex = "ActivateKeyIndex" +let appScrubberOrderIndex = "AppScrubberOrderIndex" +let appScrubberModeIndex = "AppScrubberModeIndex" + +class AppScrubberTouchBarItem: NSCustomTouchBarItem, NSScrubberDelegate, NSScrubberDataSource { + + var scrubber: NSScrubber! + + var runningApplications: [NSRunningApplication] = [] + + override init(identifier: NSTouchBarItem.Identifier) { + super.init(identifier: identifier) + + scrubber = NSScrubber().then { + $0.delegate = self + $0.dataSource = self + $0.mode = UserDefaults.standard.appScrubberMode + let layout = NSScrubberFlowLayout().then { + $0.itemSize = NSSize(width: 50, height: 30) + } + $0.scrubberLayout = layout + $0.selectionBackgroundStyle = .roundedBackground + } + view = scrubber + + scrubber.register(NSScrubberImageItemView.self, forItemIdentifier: .scrubberApplicationsItem) + + NSWorkspace.shared.notificationCenter.addObserver(self, selector: #selector(activeApplicationChanged), name: NSWorkspace.didLaunchApplicationNotification, object: nil) + NSWorkspace.shared.notificationCenter.addObserver(self, selector: #selector(activeApplicationChanged), name: NSWorkspace.didTerminateApplicationNotification, object: nil) + NSWorkspace.shared.notificationCenter.addObserver(self, selector: #selector(activeApplicationChanged), name: NSWorkspace.didActivateApplicationNotification, object: nil) + UserDefaults.standard.addObserver(self, forKeyPath: appScrubberOrderIndex, context: nil) + + updateRunningApplication(animated: false) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc func activeApplicationChanged(n: Notification) { + updateRunningApplication(animated: true) + } + + override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { + updateRunningApplication(animated: false) + } + + func updateRunningApplication(animated: Bool) { + let isDockOrder = UserDefaults.standard.integer(forKey: appScrubberOrderIndex) != 0 + let newApplications = (isDockOrder ? dockPersistentApplications() : launchedApplications()).filter { + !$0.isTerminated && $0.bundleIdentifier != nil + } + let frontmost = NSWorkspace.shared.frontmostApplication + let index = newApplications.index { + $0.processIdentifier == frontmost?.processIdentifier + } + if animated { + scrubber.performSequentialBatchUpdates { +// print("-----update-----") + for (index, app) in newApplications.enumerated() { + while runningApplications[safe:index].map(newApplications.contains) == false { + scrubber.removeItems(at: [index]) + let r = runningApplications.remove(at: index) +// print("remove \(r.localizedName!) at \(index)") + } + if let oldIndex = runningApplications.index(of: app) { + guard oldIndex != index else { + return + } + scrubber.moveItem(at: oldIndex, to: index) + runningApplications.move(at: oldIndex, to: index) +// print("move \(app.localizedName!) at \(oldIndex) to \(index)") + } else { + scrubber.insertItems(at: [index]) + runningApplications.insert(app, at: index) +// print("insert \(app.localizedName!) to \(index)") + } + } + assert(runningApplications == newApplications) + } + } else { + runningApplications = newApplications + scrubber.reloadData() + } + scrubber.selectedIndex = index ?? 0 + } + + // MARK: - NSScrubberDataSource + + public func numberOfItems(for scrubber: NSScrubber) -> Int { + return runningApplications.count + } + + public func scrubber(_ scrubber: NSScrubber, viewForItemAt index: Int) -> NSScrubberItemView { + let item = scrubber.makeItem(withIdentifier: .scrubberApplicationsItem, owner: self) as? NSScrubberImageItemView ?? NSScrubberImageItemView() + item.imageView.imageScaling = .scaleProportionallyDown + if let icon = runningApplications[index].icon { + item.image = icon + } + return item + } + + public func didFinishInteracting(with scrubber: NSScrubber) { + guard scrubber.selectedIndex > 0 else { + return + } + runningApplications[scrubber.selectedIndex].activate(options: .activateIgnoringOtherApps) + } + +} + +// MARK: - Applications + +private func launchedApplications() -> [NSRunningApplication] { + let asns = _LSCopyApplicationArrayInFrontToBackOrder(~0)?.takeRetainedValue() + return (0.. [NSRunningApplication] { + let apps = NSWorkspace.shared.runningApplications.filter { + $0.activationPolicy == .regular + } + + guard let dockDefaults = UserDefaults(suiteName: "com.apple.dock"), + let persistentApps = dockDefaults.array(forKey: "persistent-apps") as [AnyObject]?, + let bundleIDs = persistentApps.flatMap({ $0.value(forKeyPath: "tile-data.bundle-identifier") }) as? [String] else { + return apps + } + + return apps.sorted { (lhs, rhs) in + if lhs.bundleIdentifier == "com.apple.finder" { + return true + } + if rhs.bundleIdentifier == "com.apple.finder" { + return false + } + switch ((bundleIDs.index(of: lhs.bundleIdentifier!)), bundleIDs.index(of: rhs.bundleIdentifier!)) { + case (nil, _): + return false; + case (_?, nil): + return true + case let (i1?, i2?): + return i1 < i2; + } + } +} + +public protocol Then {} + +extension Then where Self: Any { + + /// Makes it available to set properties with closures just after initializing and copying the value types. + /// + /// let frame = CGRect().with { + /// $0.origin.x = 10 + /// $0.size.width = 100 + /// } + public func with(_ block: (inout Self) throws -> Void) rethrows -> Self { + var copy = self + try block(©) + return copy + } + + /// Makes it available to execute something with closures. + /// + /// UserDefaults.standard.do { + /// $0.set("devxoul", forKey: "username") + /// $0.set("devxoul@gmail.com", forKey: "email") + /// $0.synchronize() + /// } + public func `do`(_ block: (Self) throws -> Void) rethrows { + try block(self) + } + +} + +extension Then where Self: AnyObject { + + /// Makes it available to set properties with closures just after initializing. + /// + /// let label = UILabel().then { + /// $0.textAlignment = .Center + /// $0.textColor = UIColor.blackColor() + /// $0.text = "Hello, World!" + /// } + public func then(_ block: (Self) throws -> Void) rethrows -> Self { + try block(self) + return self + } + +} + +extension NSObject: Then {} + +extension CGPoint: Then {} +extension CGRect: Then {} +extension CGSize: Then {} +extension CGVector: Then {} + +extension UserDefaults { + + var activateKey: NSEvent.ModifierFlags? { + let keys: [NSEvent.ModifierFlags?] = [.command, .option, .control, .shift, nil] + let index = integer(forKey: activateKeyIndex) + return keys[index] + } + + var appScrubberMode: NSScrubber.Mode { + let actions: [NSScrubber.Mode] = [.fixed, .free] + let index = integer(forKey: appScrubberModeIndex) + return actions[index] + } +} + +extension NSUserInterfaceItemIdentifier { + + static let scrubberApplicationsItem = NSUserInterfaceItemIdentifier("ScrubberApplicationsItemReuseIdentifier") +} + +extension RangeReplaceableCollection { + + mutating func move(at oldIndex: Self.Index, to newIndex: Self.Index) { + guard oldIndex != newIndex else { + return + } + let item = remove(at: oldIndex) + insert(item, at: newIndex) + } +} + +extension Collection { + + subscript(safe index: Self.Index) -> Self.Iterator.Element? { + guard index < endIndex else { + return nil + } + return self[index] + } +} diff --git a/MTMR/AppleScriptTouchBarItem.swift b/MTMR/AppleScriptTouchBarItem.swift index 1e71b04..c49914b 100644 --- a/MTMR/AppleScriptTouchBarItem.swift +++ b/MTMR/AppleScriptTouchBarItem.swift @@ -49,7 +49,7 @@ class AppleScriptTouchBarItem: CustomButtonTouchBarItem { let output = script.executeAndReturnError(&error) if let error = error { print(error) - return "error" + return "" } return output.stringValue ?? "" } diff --git a/MTMR/CBridge/DeprecatedCarbonAPI.c b/MTMR/CBridge/DeprecatedCarbonAPI.c new file mode 100644 index 0000000..92cf1dc --- /dev/null +++ b/MTMR/CBridge/DeprecatedCarbonAPI.c @@ -0,0 +1,45 @@ +// +// DeprecatedCarbonAPI.c +// +// This file is part of TouchDock +// Copyright (C) 2017 Xander Deng +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// + +#include "DeprecatedCarbonAPI.h" + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" + +CFStringRef kPidKey = CFSTR("pid"); + +pid_t pidFromASN(void const *asn) { + pid_t pid = -1; + ProcessSerialNumber psn = {kNoProcess, kNoProcess}; + if (CFGetTypeID(asn) == _LSASNGetTypeID()) { + _LSASNExtractHighAndLowParts(asn, &psn.highLongOfPSN, &psn.lowLongOfPSN); + CFDictionaryRef processInfo = ProcessInformationCopyDictionary(&psn, kProcessDictionaryIncludeAllInformationMask); + if (processInfo) { + CFNumberRef pidNumber = CFDictionaryGetValue(processInfo, kPidKey); + if (pidNumber) { + CFNumberGetValue(pidNumber, kCFNumberSInt32Type, &pid); + } + CFRelease(processInfo); + } + } + return pid; +} + +#pragma GCC diagnostic pop diff --git a/MTMR/CBridge/DeprecatedCarbonAPI.h b/MTMR/CBridge/DeprecatedCarbonAPI.h new file mode 100644 index 0000000..a863ef1 --- /dev/null +++ b/MTMR/CBridge/DeprecatedCarbonAPI.h @@ -0,0 +1,27 @@ +// +// DeprecatedCarbonAPI.h +// +// This file is part of TouchDock +// Copyright (C) 2017 Xander Deng +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// + +#import + +extern CFArrayRef _LSCopyApplicationArrayInFrontToBackOrder(uint32_t sessionID); +extern void _LSASNExtractHighAndLowParts(void const* asn, UInt32* psnHigh, UInt32* psnLow); +extern CFTypeID _LSASNGetTypeID(void); + +pid_t pidFromASN(void const *asn); diff --git a/MTMR/CBridge/TouchBarPrivateApi-Bridging.h b/MTMR/CBridge/TouchBarPrivateApi-Bridging.h index 8c25a7d..1955001 100644 --- a/MTMR/CBridge/TouchBarPrivateApi-Bridging.h +++ b/MTMR/CBridge/TouchBarPrivateApi-Bridging.h @@ -8,6 +8,7 @@ #import "TouchBarPrivateApi.h" #import "TouchBarSupport.h" +#import "DeprecatedCarbonAPI.h" NS_ASSUME_NONNULL_BEGIN diff --git a/MTMR/CustomButtonTouchBarItem.swift b/MTMR/CustomButtonTouchBarItem.swift index d3ba783..7b969e7 100644 --- a/MTMR/CustomButtonTouchBarItem.swift +++ b/MTMR/CustomButtonTouchBarItem.swift @@ -16,6 +16,7 @@ class CustomButtonTouchBarItem: NSCustomTouchBarItem { self.tapClosure = callback super.init(identifier: identifier) button = NSButton(title: title, target: self, action: #selector(didTapped)) + button.font = .systemFont(ofSize: CGFloat(13.0)) self.view = button } diff --git a/MTMR/ItemsParsing.swift b/MTMR/ItemsParsing.swift index 2755489..5851f67 100644 --- a/MTMR/ItemsParsing.swift +++ b/MTMR/ItemsParsing.swift @@ -80,6 +80,9 @@ class SupportedTypesHolder { let item = ItemType.appleScriptTitledButton(source: Source(filePath: scriptPath), refreshInterval: interval ?? 1800.0) return (item: item, action: .none, parameters: [:]) }, + "dock": { decoder in + return (item: .dock(), action: .none, parameters: [:]) + }, "volume": { decoder in enum CodingKeys: String, CodingKey { case image } let container = try decoder.container(keyedBy: CodingKeys.self) @@ -134,6 +137,7 @@ enum ItemType: Decodable { case staticButton(title: String) case appleScriptTitledButton(source: SourceProtocol, refreshInterval: Double) case timeButton(formatTemplate: String) + case dock() case volume() case brightness(refreshInterval: Double) @@ -150,6 +154,7 @@ enum ItemType: Decodable { case staticButton case appleScriptTitledButton case timeButton + case dock case volume case brightness } @@ -168,6 +173,8 @@ enum ItemType: Decodable { case .timeButton: let template = try container.decodeIfPresent(String.self, forKey: .formatTemplate) ?? "HH:mm" self = .timeButton(formatTemplate: template) + case .dock: + self = .dock() case .volume: self = .volume() case .brightness: diff --git a/MTMR/TouchBarController.swift b/MTMR/TouchBarController.swift index 1647f52..adee8b6 100644 --- a/MTMR/TouchBarController.swift +++ b/MTMR/TouchBarController.swift @@ -23,6 +23,8 @@ extension ItemType { return "com.toxblh.mtmr.appleScriptButton." case .timeButton(formatTemplate: _): return "com.toxblh.mtmr.timeButton." + case .dock(): + return "com.toxblh.mtmr.dock" case .volume(): return "com.toxblh.mtmr.volume" case .brightness(refreshInterval: _): @@ -140,6 +142,8 @@ class TouchBarController: NSObject, NSTouchBarDelegate { barItem = AppleScriptTouchBarItem(identifier: identifier, source: source, interval: interval, onTap: action) case .timeButton(formatTemplate: let template): barItem = TimeTouchBarItem(identifier: identifier, formatTemplate: template) + case .dock: + barItem = AppScrubberTouchBarItem(identifier: identifier) case .volume: if case .image(let source)? = item.additionalParameters[.image] { barItem = VolumeViewController(identifier: identifier, image: source.image)