1
0
mirror of https://github.com/Toxblh/MTMR.git synced 2026-01-10 17:08:39 +00:00

+ show running apps (like dock)

This commit is contained in:
ad 2018-04-15 22:55:20 +03:00
parent 75ea408370
commit ac8a05d2e5
9 changed files with 361 additions and 1 deletions

View File

@ -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 = "<group>"; };
6027D1B72080E52A004FFDC7 /* BrightnessViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BrightnessViewController.swift; sourceTree = "<group>"; };
6027D1B82080E52A004FFDC7 /* VolumeViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VolumeViewController.swift; sourceTree = "<group>"; };
6042B6A62083E03A00C525C8 /* AppScrubberTouchBarItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppScrubberTouchBarItem.swift; sourceTree = "<group>"; };
6042B6A82083E1F500C525C8 /* DeprecatedCarbonAPI.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = DeprecatedCarbonAPI.h; sourceTree = "<group>"; };
6042B6A92083E27000C525C8 /* DeprecatedCarbonAPI.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = DeprecatedCarbonAPI.c; sourceTree = "<group>"; };
B0008E542080286C003AD4DD /* SupportHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SupportHelpers.swift; sourceTree = "<group>"; };
B059D621205E03F5006E6B86 /* TouchBarController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TouchBarController.swift; sourceTree = "<group>"; };
B059D623205E04F3006E6B86 /* CustomButtonTouchBarItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomButtonTouchBarItem.swift; sourceTree = "<group>"; };
@ -163,6 +168,7 @@
6027D1B72080E52A004FFDC7 /* BrightnessViewController.swift */,
6027D1B82080E52A004FFDC7 /* VolumeViewController.swift */,
368EDDE620812A1D00E10953 /* ScrollViewItem.swift */,
6042B6A62083E03A00C525C8 /* AppScrubberTouchBarItem.swift */,
);
path = MTMR;
sourceTree = "<group>";
@ -200,6 +206,8 @@
B059D62A205F0E7D006E6B86 /* TouchBarPrivateApi-Bridging.h */,
B0F87719207AC1EA00D6E430 /* TouchBarSupport.m */,
B0F8771B207AC92700D6E430 /* TouchBarSupport.h */,
6042B6A82083E1F500C525C8 /* DeprecatedCarbonAPI.h */,
6042B6A92083E27000C525C8 /* DeprecatedCarbonAPI.c */,
);
path = CBridge;
sourceTree = "<group>";
@ -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 */,

View File

@ -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 <http://www.gnu.org/licenses/>.
//
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..<CFArrayGetCount(asns)).flatMap { index in
let asn = CFArrayGetValueAtIndex(asns, index)
let pid = pidFromASN(asn)
return NSRunningApplication(processIdentifier: pid)
}
}
private func dockPersistentApplications() -> [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(&copy)
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]
}
}

View File

@ -49,7 +49,7 @@ class AppleScriptTouchBarItem: CustomButtonTouchBarItem {
let output = script.executeAndReturnError(&error)
if let error = error {
print(error)
return "error"
return ""
}
return output.stringValue ?? ""
}

View File

@ -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 <http://www.gnu.org/licenses/>.
//
#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

View File

@ -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 <http://www.gnu.org/licenses/>.
//
#import <Carbon/Carbon.h>
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);

View File

@ -8,6 +8,7 @@
#import "TouchBarPrivateApi.h"
#import "TouchBarSupport.h"
#import "DeprecatedCarbonAPI.h"
NS_ASSUME_NONNULL_BEGIN

View File

@ -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
}

View File

@ -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:

View File

@ -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)