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

424 lines
17 KiB
Swift

//
// TouchBar.swift
// MTMR
//
// Created by Anton Palgunov on 18/03/2018.
// Copyright © 2018 Anton Palgunov. All rights reserved.
//
import Cocoa
struct ExactItem {
let identifier: NSTouchBarItem.Identifier
let presetItem: BarItemDefinition
}
let appSupportDirectory = NSSearchPathForDirectoriesInDomains(.applicationSupportDirectory, .userDomainMask, true).first!.appending("/MTMR")
let standardConfigPath = appSupportDirectory.appending("/items.json")
extension ItemType {
var identifierBase: String {
switch self {
case .staticButton(title: _):
return "com.toxblh.mtmr.staticButton."
case .appleScriptTitledButton(source: _):
return "com.toxblh.mtmr.appleScriptButton."
case .timeButton(formatTemplate: _, timeZone: _):
return "com.toxblh.mtmr.timeButton."
case .battery():
return "com.toxblh.mtmr.battery."
case .dock():
return "com.toxblh.mtmr.dock"
case .volume():
return "com.toxblh.mtmr.volume"
case .brightness(refreshInterval: _):
return "com.toxblh.mtmr.brightness"
case .weather(interval: _, units: _, api_key: _, icon_type: _):
return "com.toxblh.mtmr.weather"
case .currency(interval: _, from: _, to: _, full: _):
return "com.toxblh.mtmr.currency"
case .inputsource():
return "com.toxblh.mtmr.inputsource."
case .music(interval: _):
return "com.toxblh.mtmr.music."
case .groupBar(items: _):
return "com.toxblh.mtmr.groupBar."
case .nightShift(items: _):
return "com.toxblh.mtmr.nightShift."
case .dnd(items: _):
return "com.toxblh.mtmr.dnd."
case .pomodoro(interval: _):
return PomodoroBarItem.identifier
}
}
}
extension NSTouchBarItem.Identifier {
static let controlStripItem = NSTouchBarItem.Identifier("com.toxblh.mtmr.controlStrip")
}
class TouchBarController: NSObject, NSTouchBarDelegate {
static let shared = TouchBarController()
var touchBar: NSTouchBar!
fileprivate var lastPresetPath = ""
var jsonItems: [BarItemDefinition] = []
var itemDefinitions: [NSTouchBarItem.Identifier: BarItemDefinition] = [:]
var items: [NSTouchBarItem.Identifier: NSTouchBarItem] = [:]
var leftIdentifiers: [NSTouchBarItem.Identifier] = []
var centerIdentifiers: [NSTouchBarItem.Identifier] = []
var centerItems: [NSTouchBarItem] = []
var rightIdentifiers: [NSTouchBarItem.Identifier] = []
var scrollArea: NSCustomTouchBarItem?
var centerScrollArea = NSTouchBarItem.Identifier("com.toxblh.mtmr.scrollArea.".appending(UUID().uuidString))
var showControlStripState: Bool {
get {
return UserDefaults.standard.bool(forKey: "com.toxblh.mtmr.settings.showControlStrip")
}
set {
UserDefaults.standard.set(newValue, forKey: "com.toxblh.mtmr.settings.showControlStrip")
}
}
var blacklistAppIdentifiers: [String] = []
var frontmostApplicationIdentifier: String? {
get {
guard let frontmostId = NSWorkspace.shared.frontmostApplication?.bundleIdentifier else { return nil }
return frontmostId
}
}
private override init() {
super.init()
SupportedTypesHolder.sharedInstance.register(typename: "exitTouchbar", item: .staticButton(title: "exit"), action: .custom(closure: { [weak self] in self?.dismissTouchBar()}), longAction: .none)
SupportedTypesHolder.sharedInstance.register(typename: "close") { _ in
return (item: .staticButton(title: ""), action: .custom(closure: { [weak self] in
guard let `self` = self else { return }
self.reloadPreset(path: self.lastPresetPath)
}), longAction: .none, parameters: [.width: .width(30), .image: .image(source: (NSImage(named: NSImage.stopProgressFreestandingTemplateName))!)])
}
if let blackListed = UserDefaults.standard.stringArray(forKey: "com.toxblh.mtmr.blackListedApps") {
self.blacklistAppIdentifiers = blackListed
}
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)
reloadStandardConfig()
}
func createAndUpdatePreset(newJsonItems: [BarItemDefinition]) {
if let oldBar = self.touchBar {
minimizeSystemModal(oldBar)
}
self.touchBar = NSTouchBar()
self.jsonItems = newJsonItems
self.itemDefinitions = [:]
self.items = [:]
self.leftIdentifiers = []
self.centerItems = []
self.rightIdentifiers = []
loadItemDefinitions(jsonItems: self.jsonItems)
createItems()
centerItems = centerIdentifiers.compactMap({ (identifier) -> NSTouchBarItem? in
return items[identifier]
})
self.centerScrollArea = NSTouchBarItem.Identifier("com.toxblh.mtmr.scrollArea.".appending(UUID().uuidString))
self.scrollArea = ScrollViewItem(identifier: centerScrollArea, items: centerItems)
touchBar.delegate = self
touchBar.defaultItemIdentifiers = []
touchBar.defaultItemIdentifiers = self.leftIdentifiers + [centerScrollArea] + self.rightIdentifiers
self.updateActiveApp()
}
@objc func activeApplicationChanged(_ n: Notification) {
updateActiveApp()
}
func updateActiveApp() {
if self.blacklistAppIdentifiers.index(of: self.frontmostApplicationIdentifier!) != nil {
dismissTouchBar()
} else {
presentTouchBar()
}
}
func reloadStandardConfig() {
let presetPath = standardConfigPath
if !FileManager.default.fileExists(atPath: presetPath),
let defaultPreset = Bundle.main.path(forResource: "defaultPreset", ofType: "json") {
try? FileManager.default.createDirectory(atPath: appSupportDirectory, withIntermediateDirectories: true, attributes: nil)
try? FileManager.default.copyItem(atPath: defaultPreset, toPath: presetPath)
}
reloadPreset(path: presetPath)
}
func reloadPreset(path: String) {
lastPresetPath = path
let items = path.fileData?.barItemDefinitions() ?? [BarItemDefinition(type: .staticButton(title: "bad preset"), action: .none, longAction: .none, additionalParameters: [:])]
createAndUpdatePreset(newJsonItems: items)
}
func loadItemDefinitions(jsonItems: [BarItemDefinition]) {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "HH-mm-ss"
let time = dateFormatter.string(from: Date())
for item in jsonItems {
let identifierString = item.type.identifierBase.appending(time + "--" + UUID().uuidString)
let identifier = NSTouchBarItem.Identifier(identifierString)
itemDefinitions[identifier] = item
if item.align == .left {
leftIdentifiers.append(identifier)
}
if item.align == .right {
rightIdentifiers.append(identifier)
}
if item.align == .center {
centerIdentifiers.append(identifier)
}
}
}
func createItems() {
for (identifier, definition) in self.itemDefinitions {
self.items[identifier] = self.createItem(forIdentifier: identifier, definition: definition)
}
}
@objc func setupControlStripPresence() {
DFRSystemModalShowsCloseBoxWhenFrontMost(false)
let item = NSCustomTouchBarItem(identifier: .controlStripItem)
item.view = NSButton(image: #imageLiteral(resourceName: "StatusImage"), target: self, action: #selector(presentTouchBar))
NSTouchBarItem.addSystemTrayItem(item)
updateControlStripPresence()
}
func updateControlStripPresence() {
DFRElementSetControlStripPresenceForIdentifier(.controlStripItem, true)
}
@objc private func presentTouchBar() {
if self.showControlStripState {
updateControlStripPresence()
presentSystemModal(touchBar, systemTrayItemIdentifier: .controlStripItem)
} else {
presentSystemModal(touchBar, placement: 1, systemTrayItemIdentifier: .controlStripItem)
}
}
@objc private func dismissTouchBar() {
minimizeSystemModal(touchBar)
updateControlStripPresence()
}
@objc func resetControlStrip() {
dismissTouchBar()
presentTouchBar()
}
func touchBar(_ touchBar: NSTouchBar, makeItemForIdentifier identifier: NSTouchBarItem.Identifier) -> NSTouchBarItem? {
if identifier == centerScrollArea {
return self.scrollArea
}
guard let item = self.items[identifier],
let definition = self.itemDefinitions[identifier],
definition.align != .center else {
return nil
}
return item
}
func createItem(forIdentifier identifier: NSTouchBarItem.Identifier, definition item: BarItemDefinition) -> NSTouchBarItem? {
var barItem: NSTouchBarItem!
switch item.type {
case .staticButton(title: let title):
barItem = CustomButtonTouchBarItem(identifier: identifier, title: title)
case .appleScriptTitledButton(source: let source, refreshInterval: let interval):
barItem = AppleScriptTouchBarItem(identifier: identifier, source: source, interval: interval)
case .timeButton(formatTemplate: let template, timeZone: let timeZone):
barItem = TimeTouchBarItem(identifier: identifier, formatTemplate: template, timeZone: timeZone)
case .battery():
barItem = BatteryBarItem(identifier: identifier)
case .dock:
barItem = AppScrubberTouchBarItem(identifier: identifier)
case .volume:
if case .image(let source)? = item.additionalParameters[.image] {
barItem = VolumeViewController(identifier: identifier, image: source.image)
} else {
barItem = VolumeViewController(identifier: identifier)
}
case .brightness(refreshInterval: let interval):
if case .image(let source)? = item.additionalParameters[.image] {
barItem = BrightnessViewController(identifier: identifier, refreshInterval: interval, image: source.image)
} else {
barItem = BrightnessViewController(identifier: identifier, refreshInterval: interval)
}
case .weather(interval: let interval, units: let units, api_key: let api_key, icon_type: let icon_type):
barItem = WeatherBarItem(identifier: identifier, interval: interval, units: units, api_key: api_key, icon_type: icon_type)
case .currency(interval: let interval, from: let from, to: let to, full: let full):
barItem = CurrencyBarItem(identifier: identifier, interval: interval, from: from, to: to, full: full)
case .inputsource():
barItem = InputSourceBarItem(identifier: identifier)
case .music(interval: let interval):
barItem = MusicBarItem(identifier: identifier, interval: interval)
case .groupBar(items: let items):
barItem = GroupBarItem(identifier: identifier, items: items)
case .nightShift():
barItem = NightShiftBarItem(identifier: identifier)
case .dnd():
barItem = DnDBarItem(identifier: identifier)
case .pomodoro(workTime: let workTime, restTime: let restTime):
barItem = PomodoroBarItem(identifier: identifier, workTime: workTime, restTime: restTime)
}
if let action = self.action(forItem: item), let item = barItem as? CustomButtonTouchBarItem {
item.tapClosure = action
}
if let longAction = self.longAction(forItem: item), let item = barItem as? CustomButtonTouchBarItem {
item.longTapClosure = longAction
}
if case .bordered(let bordered)? = item.additionalParameters[.bordered], let item = barItem as? CustomButtonTouchBarItem {
item.isBordered = bordered
}
if case .background(let color)? = item.additionalParameters[.background], let item = barItem as? CustomButtonTouchBarItem {
item.backgroundColor = color
}
if case .width(let value)? = item.additionalParameters[.width], let widthBarItem = barItem as? CanSetWidth {
widthBarItem.setWidth(value: value)
}
if case .image(let source)? = item.additionalParameters[.image], let item = barItem as? CustomButtonTouchBarItem {
item.image = source.image
}
if case .title(let value)? = item.additionalParameters[.title] {
if let item = barItem as? GroupBarItem {
item.collapsedRepresentationLabel = value
} else if let item = barItem as? CustomButtonTouchBarItem {
item.title = value
}
}
return barItem
}
func action(forItem item: BarItemDefinition) -> (()->())? {
switch item.action {
case .hidKey(keycode: let keycode):
return { HIDPostAuxKey(keycode) }
case .keyPress(keycode: let keycode):
return { GenericKeyPress(keyCode: CGKeyCode(keycode)).send() }
case .appleScript(source: let source):
guard let appleScript = source.appleScript else {
print("cannot create apple script for item \(item)")
return {}
}
return {
DispatchQueue.appleScriptQueue.async {
var error: NSDictionary?
appleScript.executeAndReturnError(&error)
if let error = error {
print("error \(error) when handling \(item) ")
}
}
}
case .shellScript(executable: let executable, parameters: let parameters):
return {
let task = Process()
task.launchPath = executable
task.arguments = parameters
task.launch()
}
case .openUrl(url: let url):
return {
if let url = URL(string: url), NSWorkspace.shared.open(url) {
#if DEBUG
print("URL was successfully opened")
#endif
} else {
print("error", url)
}
}
case .custom(closure: let closure):
return closure
case .none:
return nil
}
}
func longAction(forItem item: BarItemDefinition) -> (()->())? {
switch item.longAction {
case .hidKey(keycode: let keycode):
return { HIDPostAuxKey(keycode) }
case .keyPress(keycode: let keycode):
return { GenericKeyPress(keyCode: CGKeyCode(keycode)).send() }
case .appleScript(source: let source):
guard let appleScript = source.appleScript else {
print("cannot create apple script for item \(item)")
return {}
}
return {
var error: NSDictionary?
appleScript.executeAndReturnError(&error)
if let error = error {
print("error \(error) when handling \(item) ")
}
}
case .shellScript(executable: let executable, parameters: let parameters):
return {
let task = Process()
task.launchPath = executable
task.arguments = parameters
task.launch()
}
case .openUrl(url: let url):
return {
if let url = URL(string: url), NSWorkspace.shared.open(url) {
#if DEBUG
print("URL was successfully opened")
#endif
} else {
print("error", url)
}
}
case .custom(closure: let closure):
return closure
case .none:
return nil
}
}
}
protocol CanSetWidth {
func setWidth(value: CGFloat)
}
extension NSCustomTouchBarItem: CanSetWidth {
func setWidth(value: CGFloat) {
self.view.widthAnchor.constraint(equalToConstant: value).isActive = true
}
}
extension BarItemDefinition {
var align: Align {
if case .align(let result)? = self.additionalParameters[.align] {
return result
}
return .center
}
}