1
0
mirror of https://github.com/Toxblh/MTMR.git synced 2026-01-11 09:28:38 +00:00

Merge pull request #234 from ReDetection/dock-improvements

Dock improvements
This commit is contained in:
Anton Palgunov 2019-11-01 15:51:54 +03:00 committed by GitHub
commit fa413f2fa2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 164 additions and 185 deletions

View File

@ -12,6 +12,9 @@ struct AppSettings {
@UserDefault(key: "com.toxblh.mtmr.blackListedApps", defaultValue: []) @UserDefault(key: "com.toxblh.mtmr.blackListedApps", defaultValue: [])
static var blacklistedAppIds: [String] static var blacklistedAppIds: [String]
@UserDefault(key: "com.toxblh.mtmr.dock.persistent", defaultValue: [])
static var dockPersistentAppIds: [String]
} }
@propertyWrapper @propertyWrapper

View File

@ -15,6 +15,7 @@ class CustomButtonTouchBarItem: NSCustomTouchBarItem, NSGestureRecognizerDelegat
longClick.isEnabled = longTapClosure != nil longClick.isEnabled = longTapClosure != nil
} }
} }
var finishViewConfiguration: ()->() = {}
private var button: NSButton! private var button: NSButton!
private var singleClick: HapticClickGestureRecognizer! private var singleClick: HapticClickGestureRecognizer!
@ -100,6 +101,7 @@ class CustomButtonTouchBarItem: NSCustomTouchBarItem, NSGestureRecognizerDelegat
view.addGestureRecognizer(longClick) view.addGestureRecognizer(longClick)
view.addGestureRecognizer(singleClick) view.addGestureRecognizer(singleClick)
finishViewConfiguration()
} }
func gestureRecognizer(_ gestureRecognizer: NSGestureRecognizer, shouldRequireFailureOf otherGestureRecognizer: NSGestureRecognizer) -> Bool { func gestureRecognizer(_ gestureRecognizer: NSGestureRecognizer, shouldRequireFailureOf otherGestureRecognizer: NSGestureRecognizer) -> Bool {
@ -187,7 +189,7 @@ class HapticClickGestureRecognizer: NSClickGestureRecognizer {
} }
class LongPressGestureRecognizer: NSPressGestureRecognizer { class LongPressGestureRecognizer: NSPressGestureRecognizer {
private let recognizeTimeout = 0.4 var recognizeTimeout = 0.4
private var timer: Timer? private var timer: Timer?
override func touchesBegan(with event: NSEvent) { override func touchesBegan(with event: NSEvent) {

View File

@ -211,18 +211,6 @@ class SupportedTypesHolder {
) )
}, },
"dock": { decoder in
enum CodingKeys: String, CodingKey { case autoResize }
let container = try decoder.container(keyedBy: CodingKeys.self)
let autoResize = try container.decodeIfPresent(Bool.self, forKey: .autoResize) ?? false
return (
item: .dock(autoResize: autoResize),
action: .none,
longAction: .none,
parameters: [:]
)
},
"inputsource": { _ in "inputsource": { _ in
( (
item: .inputsource, item: .inputsource,
@ -346,7 +334,7 @@ enum ItemType: Decodable {
case shellScriptTitledButton(source: SourceProtocol, refreshInterval: Double) case shellScriptTitledButton(source: SourceProtocol, refreshInterval: Double)
case timeButton(formatTemplate: String, timeZone: String?, locale: String?) case timeButton(formatTemplate: String, timeZone: String?, locale: String?)
case battery case battery
case dock(autoResize: Bool) case dock(autoResize: Bool, filter: String?)
case volume case volume
case brightness(refreshInterval: Double) case brightness(refreshInterval: Double)
case weather(interval: Double, units: String, api_key: String, icon_type: String) case weather(interval: Double, units: String, api_key: String, icon_type: String)
@ -383,6 +371,7 @@ enum ItemType: Decodable {
case restTime case restTime
case flip case flip
case autoResize case autoResize
case filter
case disableMarquee case disableMarquee
} }
@ -437,7 +426,8 @@ enum ItemType: Decodable {
case .dock: case .dock:
let autoResize = try container.decodeIfPresent(Bool.self, forKey: .autoResize) ?? false let autoResize = try container.decodeIfPresent(Bool.self, forKey: .autoResize) ?? false
self = .dock(autoResize: autoResize) let filterRegexString = try container.decodeIfPresent(String.self, forKey: .filter)
self = .dock(autoResize: autoResize, filter: filterRegexString)
case .volume: case .volume:
self = .volume self = .volume

View File

@ -29,7 +29,7 @@ extension ItemType {
return "com.toxblh.mtmr.timeButton." return "com.toxblh.mtmr.timeButton."
case .battery: case .battery:
return "com.toxblh.mtmr.battery." return "com.toxblh.mtmr.battery."
case .dock(autoResize: _): case .dock(autoResize: _, filter: _):
return "com.toxblh.mtmr.dock" return "com.toxblh.mtmr.dock"
case .volume: case .volume:
return "com.toxblh.mtmr.volume" return "com.toxblh.mtmr.volume"
@ -248,8 +248,16 @@ class TouchBarController: NSObject, NSTouchBarDelegate {
barItem = TimeTouchBarItem(identifier: identifier, formatTemplate: template, timeZone: timeZone, locale: locale) barItem = TimeTouchBarItem(identifier: identifier, formatTemplate: template, timeZone: timeZone, locale: locale)
case .battery: case .battery:
barItem = BatteryBarItem(identifier: identifier) barItem = BatteryBarItem(identifier: identifier)
case let .dock(autoResize: autoResize): case let .dock(autoResize: autoResize, filter: regexString):
barItem = AppScrubberTouchBarItem(identifier: identifier, autoResize: autoResize) if let regexString = regexString {
guard let regex = try? NSRegularExpression(pattern: regexString, options: []) else {
barItem = CustomButtonTouchBarItem(identifier: identifier, title: "Bad regex")
break
}
barItem = AppScrubberTouchBarItem(identifier: identifier, autoResize: autoResize, filter: regex)
} else {
barItem = AppScrubberTouchBarItem(identifier: identifier, autoResize: autoResize)
}
case .volume: case .volume:
if case let .image(source)? = item.additionalParameters[.image] { if case let .image(source)? = item.additionalParameters[.image] {
barItem = VolumeViewController(identifier: identifier, image: source.image) barItem = VolumeViewController(identifier: identifier, image: source.image)

View File

@ -7,212 +7,127 @@
import Cocoa import Cocoa
class AppScrubberTouchBarItem: NSCustomTouchBarItem, NSScrubberDelegate, NSScrubberDataSource { class AppScrubberTouchBarItem: NSCustomTouchBarItem {
private var scrubber: NSScrubber! private var scrollView = NSScrollView()
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 autoResize: Bool = false
private var widthConstraint: NSLayoutConstraint? private var widthConstraint: NSLayoutConstraint?
private let filter: NSRegularExpression?
private var persistentAppIdentifiers: [String] = [] private var persistentAppIdentifiers: [String] = []
private var runningAppsIdentifiers: [String] = [] private var runningAppsIdentifiers: [String] = []
private var frontmostApplicationIdentifier: String? { private var frontmostApplicationIdentifier: String? {
guard let frontmostId = NSWorkspace.shared.frontmostApplication?.bundleIdentifier else { return nil } return NSWorkspace.shared.frontmostApplication?.bundleIdentifier
return frontmostId
} }
private var applications: [DockItem] = [] private var applications: [DockItem] = []
private var items: [DockBarItem] = []
convenience override init(identifier: NSTouchBarItem.Identifier) { init(identifier: NSTouchBarItem.Identifier, autoResize: Bool = false, filter: NSRegularExpression? = nil) {
self.init(identifier: identifier, autoResize: false) self.filter = filter
}
static var iconWidth = 36
static var spacingWidth = 2
init(identifier: NSTouchBarItem.Identifier, autoResize: Bool) {
super.init(identifier: identifier) super.init(identifier: identifier)
self.autoResize = autoResize self.autoResize = autoResize
view = scrollView
scrubber = NSScrubber() NSWorkspace.shared.notificationCenter.addObserver(self, selector: #selector(hardReloadItems), name: NSWorkspace.didLaunchApplicationNotification, object: nil)
scrubber.delegate = self NSWorkspace.shared.notificationCenter.addObserver(self, selector: #selector(hardReloadItems), name: NSWorkspace.didTerminateApplicationNotification, object: nil)
scrubber.dataSource = self NSWorkspace.shared.notificationCenter.addObserver(self, selector: #selector(softReloadItems), name: NSWorkspace.didActivateApplicationNotification, object: nil)
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 persistentAppIdentifiers = AppSettings.dockPersistentAppIds
hardReloadItems()
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) { required init?(coder _: NSCoder) {
fatalError("init(coder:) has not been implemented") fatalError("init(coder:) has not been implemented")
} }
@objc func activeApplicationChanged(n _: Notification) { @objc func hardReloadItems() {
updateRunningApplication() applications = launchedApplications()
}
override func observeValue(forKeyPath _: String?, of _: Any?, change _: [NSKeyValueChangeKey: Any]?, context _: UnsafeMutableRawPointer?) {
updateRunningApplication()
}
func updateRunningApplication() {
let newApplications = launchedApplications()
let index = newApplications.firstIndex {
$0.bundleIdentifier == frontmostApplicationIdentifier
}
applications = newApplications
applications += getDockPersistentAppsList() applications += getDockPersistentAppsList()
scrubber.reloadData() reloadData()
softReloadItems()
updateSize() updateSize()
}
scrubber.selectedIndex = index ?? 0 @objc func softReloadItems() {
let frontMostAppId = self.frontmostApplicationIdentifier
let runningAppsIds = NSWorkspace.shared.runningApplications.map { $0.bundleIdentifier }
for barItem in items {
let bundleId = barItem.dockItem.bundleIdentifier
barItem.isRunning = runningAppsIds.contains(bundleId)
barItem.isFrontmost = frontMostAppId == bundleId
}
} }
func updateSize() { func updateSize() {
if self.autoResize { if self.autoResize {
if let constraint: NSLayoutConstraint = self.widthConstraint { self.widthConstraint?.isActive = false
constraint.isActive = false
self.scrubber.removeConstraint(constraint) let width = self.scrollView.documentView?.fittingSize.width ?? 0
} self.widthConstraint = self.scrollView.widthAnchor.constraint(equalToConstant: width)
let width = (AppScrubberTouchBarItem.iconWidth + AppScrubberTouchBarItem.spacingWidth) * self.applications.count - AppScrubberTouchBarItem.spacingWidth
self.widthConstraint = self.scrubber.widthAnchor.constraint(equalToConstant: CGFloat(width))
self.widthConstraint!.isActive = true self.widthConstraint!.isActive = true
} }
} }
public func numberOfItems(for _: NSScrubber) -> Int { func reloadData() {
return applications.count items = applications.map { self.createAppButton(for: $0) }
let stackView = NSStackView(views: items.compactMap { $0.view })
stackView.spacing = 1
stackView.orientation = .horizontal
let visibleRect = self.scrollView.documentVisibleRect
scrollView.documentView = stackView
stackView.scroll(visibleRect.origin)
} }
public func scrubber(_ scrubber: NSScrubber, viewForItemAt index: Int) -> NSScrubberItemView { public func createAppButton(for app: DockItem) -> DockBarItem {
let item = scrubber.makeItem(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: "ScrubberApplicationsItemReuseIdentifier"), owner: self) as? NSScrubberImageItemView ?? NSScrubberImageItemView() let item = DockBarItem(app)
item.imageView.imageScaling = .scaleProportionallyDown item.isBordered = false
item.tapClosure = { [weak self] in
let app = applications[index] self?.switchToApp(app: app)
if let icon = app.icon {
item.image = icon
item.image.size = NSSize(width: 26, height: 26)
} }
item.longTapClosure = { [weak self] in
item.removeFromSuperview() self?.handleHalfLongPress(item: app)
}
let dotView = NSView(frame: .zero) item.killAppClosure = {[weak self] in
dotView.wantsLayer = true self?.handleLongPress(item: app)
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 return item
} }
public func didBeginInteracting(with _: NSScrubber) { public func switchToApp(app: DockItem) {
stopTimer() let bundleIdentifier = app.bundleIdentifier
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://") { if bundleIdentifier!.contains("file://") {
NSWorkspace.shared.openFile(bundleIdentifier!.replacingOccurrences(of: "file://", with: "")) NSWorkspace.shared.openFile(bundleIdentifier!.replacingOccurrences(of: "file://", with: ""))
} else { } else {
NSWorkspace.shared.launchApplication(withBundleIdentifier: bundleIdentifier!, options: [.default], additionalEventParamDescriptor: nil, launchIdentifier: nil) NSWorkspace.shared.launchApplication(withBundleIdentifier: bundleIdentifier!, options: [.default], additionalEventParamDescriptor: nil, launchIdentifier: nil)
HapticFeedback.shared?.tap(strong: 6)
} }
updateRunningApplication() softReloadItems()
// NB: if you can't open app which on another space, try to check mark // 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" // "When switching to an application, switch to a Space with open windows for the application"
// in Mission control settings // in Mission control settings
} }
private func longPress(selected: Int) { //todo
let bundleIdentifier = applications[selected].bundleIdentifier private func handleLongPress(item: DockItem) {
if let pid = item.pid, let app = NSRunningApplication(processIdentifier: pid) {
if ticks > maxTicks { if !app.terminate() {
if let processIdentifier = applications[selected].pid { app.forceTerminate()
if !(NSRunningApplication(processIdentifier: processIdentifier)?.terminate())! {
NSRunningApplication(processIdentifier: processIdentifier)?.forceTerminate()
}
} }
} else { hardReloadItems()
HapticFeedback.shared?.tap(strong: 6)
if let index = self.persistentAppIdentifiers.firstIndex(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 handleHalfLongPress(item: DockItem) {
if let index = self.persistentAppIdentifiers.firstIndex(of: item.bundleIdentifier) {
persistentAppIdentifiers.remove(at: index)
hardReloadItems()
} else {
persistentAppIdentifiers.append(item.bundleIdentifier)
}
AppSettings.dockPersistentAppIds = persistentAppIdentifiers
} }
private func launchedApplications() -> [DockItem] { private func launchedApplications() -> [DockItem] {
@ -221,24 +136,27 @@ class AppScrubberTouchBarItem: NSCustomTouchBarItem, NSScrubberDelegate, NSScrub
for app in NSWorkspace.shared.runningApplications { for app in NSWorkspace.shared.runningApplications {
guard app.activationPolicy == NSApplication.ActivationPolicy.regular else { continue } guard app.activationPolicy == NSApplication.ActivationPolicy.regular else { continue }
guard let bundleIdentifier = app.bundleIdentifier else { continue } guard let bundleIdentifier = app.bundleIdentifier else { continue }
if let filter = self.filter,
let name = app.localizedName,
filter.numberOfMatches(in: name, options: [], range: NSRange(location: 0, length: name.count)) == 0 {
continue
}
runningAppsIdentifiers.append(bundleIdentifier) runningAppsIdentifiers.append(bundleIdentifier)
let dockItem = DockItem(bundleIdentifier: bundleIdentifier, icon: getIcon(forBundleIdentifier: bundleIdentifier), pid: app.processIdentifier) let dockItem = DockItem(bundleIdentifier: bundleIdentifier, icon: app.icon ?? getIcon(forBundleIdentifier: bundleIdentifier), pid: app.processIdentifier)
returnable.append(dockItem) returnable.append(dockItem)
} }
return returnable return returnable
} }
public func getIcon(forBundleIdentifier bundleIdentifier: String? = nil, orPath path: String? = nil, orType _: String? = nil) -> NSImage { public func getIcon(forBundleIdentifier bundleIdentifier: String? = nil, orPath path: String? = nil) -> NSImage {
if bundleIdentifier != nil { if let bundleIdentifier = bundleIdentifier, let appPath = NSWorkspace.shared.absolutePathForApplication(withBundleIdentifier: bundleIdentifier) {
if let appPath = NSWorkspace.shared.absolutePathForApplication(withBundleIdentifier: bundleIdentifier!) { return NSWorkspace.shared.icon(forFile: appPath)
return NSWorkspace.shared.icon(forFile: appPath)
}
} }
if path != nil { if let path = path {
return NSWorkspace.shared.icon(forFile: path!) return NSWorkspace.shared.icon(forFile: path)
} }
let genericIcon = NSImage(contentsOfFile: "/System/Library/CoreServices/CoreTypes.bundle/Contents/Resources/GenericDocumentIcon.icns") let genericIcon = NSImage(contentsOfFile: "/System/Library/CoreServices/CoreTypes.bundle/Contents/Resources/GenericDocumentIcon.icns")
@ -269,3 +187,60 @@ public class DockItem: NSObject {
self.pid = pid self.pid = pid
} }
} }
private let iconWidth = 32.0
class DockBarItem: CustomButtonTouchBarItem {
let dotView = NSView(frame: .zero)
let dockItem: DockItem
fileprivate var killGestureRecognizer: LongPressGestureRecognizer!
var killAppClosure: () -> Void = { }
var isRunning = false {
didSet {
redrawDotView()
}
}
var isFrontmost = false {
didSet {
redrawDotView()
}
}
init(_ app: DockItem) {
self.dockItem = app
super.init(identifier: .init(app.bundleIdentifier), title: "")
dotView.wantsLayer = true
image = app.icon
image?.size = NSSize(width: iconWidth, height: iconWidth)
killGestureRecognizer = LongPressGestureRecognizer(target: self, action: #selector(firePanGestureRecognizer))
killGestureRecognizer.allowedTouchTypes = .direct
killGestureRecognizer.recognizeTimeout = 1.5
killGestureRecognizer.minimumPressDuration = 1.5
killGestureRecognizer.isEnabled = isRunning
self.finishViewConfiguration = { [weak self] in
guard let selfie = self else { return }
selfie.dotView.layer?.cornerRadius = 1.5
selfie.view.addSubview(selfie.dotView)
selfie.redrawDotView()
selfie.view.addGestureRecognizer(selfie.killGestureRecognizer)
}
}
func redrawDotView() {
dotView.layer?.backgroundColor = isRunning ? NSColor.white.cgColor : NSColor.clear.cgColor
dotView.frame.size = NSSize(width: isFrontmost ? iconWidth - 14 : 3, height: 3)
dotView.setFrameOrigin(NSPoint(x: 18.0 - Double(dotView.frame.size.width) / 2.0, y: iconWidth - 5))
}
@objc func firePanGestureRecognizer() {
self.killAppClosure()
}
required init?(coder _: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}

View File

@ -288,6 +288,7 @@ To close a group, use the button:
```js ```js
{ {
"type": "dock", "type": "dock",
"filter": "(^Xcode$)|(Safari)|(.*player)",
"autoResize": true "autoResize": true
}, },
``` ```