diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..e2b9d50 Binary files /dev/null and b/.DS_Store differ diff --git a/MTMR.xcodeproj/project.pbxproj b/MTMR.xcodeproj/project.pbxproj index ebed5ca..2958cd6 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 */; }; B05600D32083E9BB00EB218D /* CustomSlider.swift in Sources */ = {isa = PBXBuildFile; fileRef = B05600D22083E9BB00EB218D /* CustomSlider.swift */; }; B059D622205E03F5006E6B86 /* TouchBarController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B059D621205E03F5006E6B86 /* TouchBarController.swift */; }; @@ -61,6 +63,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 = ""; }; B05600D22083E9BB00EB218D /* CustomSlider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomSlider.swift; sourceTree = ""; }; B059D621205E03F5006E6B86 /* TouchBarController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TouchBarController.swift; sourceTree = ""; }; @@ -165,6 +170,7 @@ 6027D1B72080E52A004FFDC7 /* BrightnessViewController.swift */, 6027D1B82080E52A004FFDC7 /* VolumeViewController.swift */, 368EDDE620812A1D00E10953 /* ScrollViewItem.swift */, + 6042B6A62083E03A00C525C8 /* AppScrubberTouchBarItem.swift */, B05600D22083E9BB00EB218D /* CustomSlider.swift */, ); path = MTMR; @@ -203,6 +209,8 @@ B059D62A205F0E7D006E6B86 /* TouchBarPrivateApi-Bridging.h */, B0F87719207AC1EA00D6E430 /* TouchBarSupport.m */, B0F8771B207AC92700D6E430 /* TouchBarSupport.h */, + 6042B6A82083E1F500C525C8 /* DeprecatedCarbonAPI.h */, + 6042B6A92083E27000C525C8 /* DeprecatedCarbonAPI.c */, ); path = CBridge; sourceTree = ""; @@ -328,9 +336,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..3c5a80a --- /dev/null +++ b/MTMR/AppScrubberTouchBarItem.swift @@ -0,0 +1,118 @@ +// +// 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 + +class AppScrubberTouchBarItem: NSCustomTouchBarItem, NSScrubberDelegate, NSScrubberDataSource { + + var scrubber: NSScrubber! + + var runningApplications: [NSRunningApplication] = [] + + override init(identifier: NSTouchBarItem.Identifier) { + super.init(identifier: identifier) + + scrubber = NSScrubber(); + scrubber.delegate = self + scrubber.dataSource = self + scrubber.mode = .free // .fixed + let layout = NSScrubberFlowLayout(); + layout.itemSize = NSSize(width: 44, height: 30) + scrubber.scrubberLayout = layout + scrubber.selectionBackgroundStyle = .roundedBackground + + view = scrubber + + scrubber.register(NSScrubberImageItemView.self, forItemIdentifier: NSUserInterfaceItemIdentifier(rawValue: "ScrubberApplicationsItemReuseIdentifier")) + + 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) + + updateRunningApplication() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc func activeApplicationChanged(n: Notification) { + updateRunningApplication() + } + + override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { + updateRunningApplication() + } + + func updateRunningApplication() { + let newApplications = launchedApplications().filter { + !$0.isTerminated && $0.bundleIdentifier != nil + } + let frontmost = NSWorkspace.shared.frontmostApplication + let index = newApplications.index { + $0.processIdentifier == frontmost?.processIdentifier + } + + runningApplications = newApplications + scrubber.reloadData() + + scrubber.selectedIndex = index ?? 0 + } + + public func numberOfItems(for scrubber: NSScrubber) -> Int { + return runningApplications.count + } + + public func scrubber(_ scrubber: NSScrubber, viewForItemAt index: Int) -> NSScrubberItemView { + let item = scrubber.makeItem(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: "ScrubberApplicationsItemReuseIdentifier"), 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) { + runningApplications[scrubber.selectedIndex].activate(options: [ .activateIgnoringOtherApps ]) + + // 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 + + // TODO: deminiaturize app +// if let info = CGWindowListCopyWindowInfo(.optionOnScreenOnly, kCGNullWindowID) as? [[ String : Any]] { +// for dict in info { +// if dict["kCGWindowOwnerName"] as! String == runningApplications[scrubber.selectedIndex].localizedName { +// print(dict["kCGWindowNumber"], dict["kCGWindowOwnerName"]) +// } +// } +// } + } +} + +private func launchedApplications() -> [NSRunningApplication] { + let asns = _LSCopyApplicationArrayInFrontToBackOrder(~0)?.takeRetainedValue() + return (0... +// + +#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/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 7b9ea30..ada3da1 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: _): @@ -150,6 +152,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)