mirror of
https://github.com/Toxblh/MTMR.git
synced 2026-01-10 17:08:39 +00:00
272 lines
9.9 KiB
Swift
272 lines
9.9 KiB
Swift
//
|
|
// AppScrubberTouchBarItem.swift
|
|
// MTMR
|
|
//
|
|
// Created by Daniel Apatin on 18.04.2018.
|
|
// Copyright © 2018 Anton Palgunov. All rights reserved.
|
|
|
|
import Cocoa
|
|
|
|
class AppScrubberTouchBarItem: NSCustomTouchBarItem, NSScrubberDelegate, NSScrubberDataSource {
|
|
private var scrubber: NSScrubber!
|
|
|
|
private var timer: Timer!
|
|
private var ticks: Int = 0
|
|
private let minTicks: Int = 5
|
|
private let maxTicks: Int = 20
|
|
private var lastSelected: Int = 0
|
|
private var autoResize: Bool = false
|
|
private var widthConstraint: NSLayoutConstraint?
|
|
|
|
private var persistentAppIdentifiers: [String] = []
|
|
private var runningAppsIdentifiers: [String] = []
|
|
|
|
private var frontmostApplicationIdentifier: String? {
|
|
guard let frontmostId = NSWorkspace.shared.frontmostApplication?.bundleIdentifier else { return nil }
|
|
return frontmostId
|
|
}
|
|
|
|
private var applications: [DockItem] = []
|
|
|
|
convenience override init(identifier: NSTouchBarItem.Identifier) {
|
|
self.init(identifier: identifier, autoResize: false)
|
|
}
|
|
|
|
static var iconWidth = 36
|
|
static var spacingWidth = 2
|
|
|
|
init(identifier: NSTouchBarItem.Identifier, autoResize: Bool) {
|
|
super.init(identifier: identifier)
|
|
self.autoResize = autoResize
|
|
|
|
scrubber = NSScrubber()
|
|
scrubber.delegate = self
|
|
scrubber.dataSource = self
|
|
scrubber.mode = .free // .fixed
|
|
let layout = NSScrubberFlowLayout()
|
|
layout.itemSize = NSSize(width: AppScrubberTouchBarItem.iconWidth, height: 32)
|
|
layout.itemSpacing = CGFloat(AppScrubberTouchBarItem.spacingWidth)
|
|
scrubber.scrubberLayout = layout
|
|
scrubber.selectionBackgroundStyle = .roundedBackground
|
|
scrubber.showsAdditionalContentIndicators = true
|
|
|
|
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)
|
|
|
|
if let persistent = UserDefaults.standard.stringArray(forKey: "com.toxblh.mtmr.dock.persistent") {
|
|
persistentAppIdentifiers = persistent
|
|
}
|
|
|
|
updateRunningApplication()
|
|
}
|
|
|
|
required init?(coder _: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
@objc func activeApplicationChanged(n _: Notification) {
|
|
updateRunningApplication()
|
|
}
|
|
|
|
override func observeValue(forKeyPath _: String?, of _: Any?, change _: [NSKeyValueChangeKey: Any]?, context _: UnsafeMutableRawPointer?) {
|
|
updateRunningApplication()
|
|
}
|
|
|
|
func updateRunningApplication() {
|
|
let newApplications = launchedApplications()
|
|
|
|
let index = newApplications.index {
|
|
$0.bundleIdentifier == frontmostApplicationIdentifier
|
|
}
|
|
|
|
applications = newApplications
|
|
applications += getDockPersistentAppsList()
|
|
scrubber.reloadData()
|
|
updateSize()
|
|
|
|
scrubber.selectedIndex = index ?? 0
|
|
}
|
|
|
|
func updateSize() {
|
|
if self.autoResize {
|
|
if let constraint: NSLayoutConstraint = self.widthConstraint {
|
|
constraint.isActive = false
|
|
self.scrubber.removeConstraint(constraint)
|
|
}
|
|
let width = (AppScrubberTouchBarItem.iconWidth + AppScrubberTouchBarItem.spacingWidth) * self.applications.count - AppScrubberTouchBarItem.spacingWidth
|
|
self.widthConstraint = self.scrubber.widthAnchor.constraint(equalToConstant: CGFloat(width))
|
|
self.widthConstraint!.isActive = true
|
|
}
|
|
}
|
|
|
|
public func numberOfItems(for _: NSScrubber) -> Int {
|
|
return applications.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
|
|
|
|
let app = applications[index]
|
|
|
|
if let icon = app.icon {
|
|
item.image = icon
|
|
item.image.size = NSSize(width: 26, height: 26)
|
|
}
|
|
|
|
item.removeFromSuperview()
|
|
|
|
let dotView = NSView(frame: .zero)
|
|
dotView.wantsLayer = true
|
|
if runningAppsIdentifiers.contains(app.bundleIdentifier!) {
|
|
dotView.layer?.backgroundColor = NSColor.white.cgColor
|
|
} else {
|
|
dotView.layer?.backgroundColor = NSColor.black.cgColor
|
|
}
|
|
dotView.layer?.cornerRadius = 1.5
|
|
dotView.setFrameOrigin(NSPoint(x: 17, y: 1))
|
|
dotView.frame.size = NSSize(width: 3, height: 3)
|
|
item.addSubview(dotView)
|
|
|
|
return item
|
|
}
|
|
|
|
public func didBeginInteracting(with _: NSScrubber) {
|
|
stopTimer()
|
|
ticks = 0
|
|
timer = Timer.scheduledTimer(timeInterval: 0.1, target: self, selector: #selector(checkTimer), userInfo: nil, repeats: true)
|
|
}
|
|
|
|
@objc private func checkTimer() {
|
|
ticks += 1
|
|
|
|
if ticks == minTicks {
|
|
HapticFeedback.shared.tap(strong: 2)
|
|
}
|
|
|
|
if ticks > maxTicks {
|
|
stopTimer()
|
|
HapticFeedback.shared.tap(strong: 6)
|
|
}
|
|
}
|
|
|
|
private func stopTimer() {
|
|
timer?.invalidate()
|
|
timer = nil
|
|
lastSelected = 0
|
|
}
|
|
|
|
public func didCancelInteracting(with _: NSScrubber) {
|
|
stopTimer()
|
|
}
|
|
|
|
public func didFinishInteracting(with scrubber: NSScrubber) {
|
|
stopTimer()
|
|
|
|
if ticks == 0 {
|
|
return
|
|
}
|
|
|
|
if ticks >= minTicks && scrubber.selectedIndex > 0 {
|
|
longPress(selected: scrubber.selectedIndex)
|
|
return
|
|
}
|
|
|
|
let bundleIdentifier = applications[scrubber.selectedIndex].bundleIdentifier
|
|
if bundleIdentifier!.contains("file://") {
|
|
NSWorkspace.shared.openFile(bundleIdentifier!.replacingOccurrences(of: "file://", with: ""))
|
|
} else {
|
|
NSWorkspace.shared.launchApplication(withBundleIdentifier: bundleIdentifier!, options: [.default], additionalEventParamDescriptor: nil, launchIdentifier: nil)
|
|
HapticFeedback.shared.tap(strong: 6)
|
|
}
|
|
updateRunningApplication()
|
|
|
|
// 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
|
|
}
|
|
|
|
private func longPress(selected: Int) {
|
|
let bundleIdentifier = applications[selected].bundleIdentifier
|
|
|
|
if ticks > maxTicks {
|
|
if let processIdentifier = applications[selected].pid {
|
|
if !(NSRunningApplication(processIdentifier: processIdentifier)?.terminate())! {
|
|
NSRunningApplication(processIdentifier: processIdentifier)?.forceTerminate()
|
|
}
|
|
}
|
|
} else {
|
|
HapticFeedback.shared.tap(strong: 6)
|
|
if let index = self.persistentAppIdentifiers.index(of: bundleIdentifier!) {
|
|
persistentAppIdentifiers.remove(at: index)
|
|
} else {
|
|
persistentAppIdentifiers.append(bundleIdentifier!)
|
|
}
|
|
|
|
UserDefaults.standard.set(persistentAppIdentifiers, forKey: "com.toxblh.mtmr.dock.persistent")
|
|
UserDefaults.standard.synchronize()
|
|
}
|
|
ticks = 0
|
|
updateRunningApplication()
|
|
}
|
|
|
|
private func launchedApplications() -> [DockItem] {
|
|
runningAppsIdentifiers = []
|
|
var returnable: [DockItem] = []
|
|
for app in NSWorkspace.shared.runningApplications {
|
|
guard app.activationPolicy == NSApplication.ActivationPolicy.regular else { continue }
|
|
guard let bundleIdentifier = app.bundleIdentifier else { continue }
|
|
|
|
runningAppsIdentifiers.append(bundleIdentifier)
|
|
|
|
let dockItem = DockItem(bundleIdentifier: bundleIdentifier, icon: getIcon(forBundleIdentifier: bundleIdentifier), pid: app.processIdentifier)
|
|
returnable.append(dockItem)
|
|
}
|
|
return returnable
|
|
}
|
|
|
|
public func getIcon(forBundleIdentifier bundleIdentifier: String? = nil, orPath path: String? = nil, orType _: String? = nil) -> NSImage {
|
|
if bundleIdentifier != nil {
|
|
if let appPath = NSWorkspace.shared.absolutePathForApplication(withBundleIdentifier: bundleIdentifier!) {
|
|
return NSWorkspace.shared.icon(forFile: appPath)
|
|
}
|
|
}
|
|
|
|
if path != nil {
|
|
return NSWorkspace.shared.icon(forFile: path!)
|
|
}
|
|
|
|
let genericIcon = NSImage(contentsOfFile: "/System/Library/CoreServices/CoreTypes.bundle/Contents/Resources/GenericDocumentIcon.icns")
|
|
return genericIcon ?? NSImage(size: .zero)
|
|
}
|
|
|
|
public func getDockPersistentAppsList() -> [DockItem] {
|
|
var returnable: [DockItem] = []
|
|
|
|
for bundleIdentifier in persistentAppIdentifiers {
|
|
if !runningAppsIdentifiers.contains(bundleIdentifier) {
|
|
let dockItem = DockItem(bundleIdentifier: bundleIdentifier, icon: getIcon(forBundleIdentifier: bundleIdentifier))
|
|
returnable.append(dockItem)
|
|
}
|
|
}
|
|
|
|
return returnable
|
|
}
|
|
}
|
|
|
|
public class DockItem: NSObject {
|
|
var bundleIdentifier: String!, icon: NSImage!, pid: Int32!
|
|
|
|
convenience init(bundleIdentifier: String, icon: NSImage, pid: Int32? = nil) {
|
|
self.init()
|
|
self.bundleIdentifier = bundleIdentifier
|
|
self.icon = icon
|
|
self.pid = pid
|
|
}
|
|
}
|