mirror of
https://github.com/sindresorhus/touch-bar-simulator
synced 2025-03-04 06:57:10 +00:00
408 lines
10 KiB
Swift
408 lines
10 KiB
Swift
import Cocoa
|
|
import Defaults
|
|
|
|
final class TouchBarWindow: NSPanel {
|
|
enum Docking: String, Codable {
|
|
case floating
|
|
case dockedToTop
|
|
case dockedToBottom
|
|
|
|
func dock(window: TouchBarWindow) {
|
|
switch self {
|
|
case .floating:
|
|
window.addTitlebar()
|
|
case .dockedToTop:
|
|
window.removeTitlebar()
|
|
case .dockedToBottom:
|
|
window.removeTitlebar()
|
|
}
|
|
|
|
reposition(window: window)
|
|
}
|
|
|
|
func reposition(window: NSWindow) {
|
|
switch self {
|
|
case .floating:
|
|
if let prevPosition = Defaults[.lastFloatingPosition] {
|
|
window.setFrameOrigin(prevPosition)
|
|
}
|
|
case .dockedToTop:
|
|
window.moveTo(x: .center, y: .top)
|
|
case .dockedToBottom:
|
|
window.moveTo(x: .center, y: .bottom)
|
|
}
|
|
}
|
|
}
|
|
|
|
override var canBecomeMain: Bool { false }
|
|
|
|
override var canBecomeKey: Bool { false }
|
|
|
|
var docking: Docking = .floating {
|
|
didSet {
|
|
if oldValue == .floating && docking != .floating {
|
|
Defaults[.lastFloatingPosition] = frame.origin
|
|
}
|
|
|
|
if docking == .floating {
|
|
dockBehavior = false
|
|
}
|
|
|
|
// Prevent the Touch Bar from momentarily becoming visible.
|
|
if docking == .floating || !dockBehavior {
|
|
stopDockBehaviorTimer()
|
|
docking.dock(window: self)
|
|
setIsVisible(true)
|
|
orderFront(nil)
|
|
return
|
|
}
|
|
|
|
// When docking is set to `dockedToTop` or `dockedToBottom` dockBehavior should start.
|
|
if dockBehavior {
|
|
setIsVisible(false)
|
|
docking.dock(window: self)
|
|
startDockBehaviorTimer()
|
|
}
|
|
}
|
|
}
|
|
|
|
@objc
|
|
func didChangeScreenParameters(_ notification: Notification) {
|
|
docking.reposition(window: self)
|
|
}
|
|
|
|
var showOnAllDesktops: Bool = false {
|
|
didSet {
|
|
if showOnAllDesktops {
|
|
collectionBehavior = .canJoinAllSpaces
|
|
} else {
|
|
collectionBehavior = .moveToActiveSpace
|
|
}
|
|
}
|
|
}
|
|
|
|
var dockBehaviorTimer = Timer()
|
|
var showTouchBarTimer = Timer()
|
|
|
|
func startDockBehaviorTimer() {
|
|
stopDockBehaviorTimer()
|
|
|
|
dockBehaviorTimer = Timer.scheduledTimer(
|
|
timeInterval: 0.1,
|
|
target: self,
|
|
selector: #selector(handleDockBehavior),
|
|
userInfo: nil,
|
|
repeats: true
|
|
)
|
|
}
|
|
|
|
func stopDockBehaviorTimer() {
|
|
dockBehaviorTimer.invalidate()
|
|
dockBehaviorTimer = Timer()
|
|
}
|
|
|
|
var dockBehavior: Bool = Defaults[.dockBehavior] {
|
|
didSet {
|
|
Defaults[.dockBehavior] = dockBehavior
|
|
if docking == .dockedToBottom || docking == .dockedToTop {
|
|
Defaults[.lastWindowDockingWithDockBehavior] = docking
|
|
}
|
|
|
|
if dockBehavior {
|
|
if docking == .dockedToBottom || docking == .dockedToTop {
|
|
docking = Defaults[.lastWindowDockingWithDockBehavior]
|
|
startDockBehaviorTimer()
|
|
} else if docking == .floating {
|
|
Defaults[.windowDocking] = Defaults[.lastWindowDockingWithDockBehavior]
|
|
}
|
|
} else {
|
|
stopDockBehaviorTimer()
|
|
setIsVisible(true)
|
|
}
|
|
}
|
|
}
|
|
|
|
@objc
|
|
func handleDockBehavior() {
|
|
guard
|
|
let visibleFrame = NSScreen.main?.visibleFrame,
|
|
let screenFrame = NSScreen.main?.frame
|
|
else {
|
|
return
|
|
}
|
|
|
|
var detectionRect: NSRect = .zero
|
|
if docking == .dockedToBottom {
|
|
if isVisible {
|
|
detectionRect = CGRect(
|
|
x: 0,
|
|
y: 0,
|
|
width: visibleFrame.width,
|
|
height: frame.height + (screenFrame.height - visibleFrame.height - NSStatusBar.system.thickness)
|
|
)
|
|
} else {
|
|
detectionRect = CGRect(x: 0, y: 0, width: visibleFrame.width, height: 1)
|
|
}
|
|
} else if docking == .dockedToTop {
|
|
if isVisible {
|
|
detectionRect = CGRect(
|
|
x: 0,
|
|
// Without `+ 1`, the Touch Bar would glitch (toggling rapidly).
|
|
y: screenFrame.height - frame.height - NSStatusBar.system.thickness + 1,
|
|
width: visibleFrame.width,
|
|
height: frame.height + NSStatusBar.system.thickness)
|
|
} else {
|
|
detectionRect = CGRect(
|
|
x: 0,
|
|
y: screenFrame.height,
|
|
width: visibleFrame.width,
|
|
height: 1)
|
|
}
|
|
}
|
|
|
|
let mouseLocation = NSEvent.mouseLocation
|
|
if detectionRect.contains(mouseLocation) {
|
|
dismissAnimationDidRun = false
|
|
|
|
guard
|
|
!showTouchBarTimer.isValid,
|
|
!showAnimationDidRun
|
|
else {
|
|
return
|
|
}
|
|
|
|
showTouchBarTimer = Timer.scheduledTimer(withTimeInterval: 0.3, repeats: false) { _ in
|
|
self.performActionWithAnimation(action: .show)
|
|
self.showAnimationDidRun = true
|
|
}
|
|
} else {
|
|
showTouchBarTimer.invalidate()
|
|
showTouchBarTimer = Timer()
|
|
showAnimationDidRun = false
|
|
|
|
if isVisible && !dismissAnimationDidRun {
|
|
performActionWithAnimation(action: .dismiss)
|
|
dismissAnimationDidRun = true
|
|
}
|
|
}
|
|
}
|
|
|
|
func moveToStartPoint() {
|
|
if docking == .dockedToTop {
|
|
moveTo(x: .center, y: .top)
|
|
} else if docking == .dockedToBottom {
|
|
moveTo(x: .center, y: .bottom)
|
|
}
|
|
}
|
|
|
|
var showAnimationDidRun = false
|
|
var dismissAnimationDidRun = false
|
|
|
|
func showTouchBarWithAnimation() {
|
|
guard
|
|
docking == .dockedToTop ||
|
|
docking == .dockedToBottom
|
|
else {
|
|
return
|
|
}
|
|
|
|
var startOrigin: CGPoint!
|
|
let endFrame = frame
|
|
setIsVisible(true)
|
|
if docking == .dockedToTop {
|
|
startOrigin = CGPoint(x: frame.origin.x, y: frame.origin.y + frame.height)
|
|
} else if docking == .dockedToBottom {
|
|
startOrigin = CGPoint(x: frame.origin.x, y: 0 - frame.height)
|
|
}
|
|
setFrameOrigin(startOrigin)
|
|
|
|
NSAnimationContext.runAnimationGroup({ context in
|
|
context.duration = TimeInterval(0.3)
|
|
animator().setFrame(endFrame, display: false, animate: true)
|
|
}, completionHandler: {
|
|
self.moveToStartPoint()
|
|
})
|
|
}
|
|
|
|
func dismissTouchBarWithAnimation() {
|
|
guard
|
|
docking == .dockedToTop ||
|
|
docking == .dockedToBottom
|
|
else {
|
|
return
|
|
}
|
|
|
|
var endFrame = frame
|
|
if docking == .dockedToTop {
|
|
endFrame.origin = NSPoint(x: frame.origin.x, y: frame.origin.y + frame.height + NSStatusBar.system.thickness)
|
|
} else if docking == .dockedToBottom {
|
|
endFrame.origin = NSPoint(x: frame.origin.x, y: 0 - frame.height)
|
|
}
|
|
|
|
NSAnimationContext.runAnimationGroup({ context in
|
|
context.duration = TimeInterval(0.3)
|
|
animator().setFrame(endFrame, display: false, animate: true)
|
|
}, completionHandler: {
|
|
self.setIsVisible(false)
|
|
self.moveToStartPoint()
|
|
})
|
|
}
|
|
|
|
func performActionWithAnimation(action: TouchBarAction) {
|
|
guard
|
|
docking == .dockedToTop ||
|
|
docking == .dockedToBottom
|
|
else {
|
|
return
|
|
}
|
|
|
|
var startOrigin: CGPoint!
|
|
var endFrame = frame
|
|
|
|
if action == .show {
|
|
setIsVisible(true)
|
|
|
|
if docking == .dockedToTop {
|
|
startOrigin = CGPoint(x: frame.origin.x, y: frame.origin.y + frame.height)
|
|
} else if docking == .dockedToBottom {
|
|
startOrigin = CGPoint(x: frame.origin.x, y: 0 - frame.height)
|
|
}
|
|
|
|
setFrameOrigin(startOrigin)
|
|
} else if action == .dismiss {
|
|
if docking == .dockedToTop {
|
|
endFrame.origin = NSPoint(x: frame.origin.x, y: frame.origin.y + frame.height + NSStatusBar.system.thickness)
|
|
} else if docking == .dockedToBottom {
|
|
endFrame.origin = NSPoint(x: frame.origin.x, y: 0 - frame.height)
|
|
}
|
|
}
|
|
|
|
NSAnimationContext.runAnimationGroup({ context in
|
|
context.duration = TimeInterval(0.3)
|
|
animator().setFrame(endFrame, display: false, animate: true)
|
|
}, completionHandler: {
|
|
if action == .dismiss {
|
|
self.setIsVisible(false)
|
|
}
|
|
|
|
self.moveToStartPoint()
|
|
})
|
|
}
|
|
|
|
enum TouchBarAction {
|
|
case show
|
|
case dismiss
|
|
}
|
|
|
|
func addTitlebar() {
|
|
styleMask.insert(.titled)
|
|
title = "Touch Bar Simulator"
|
|
|
|
guard let toolbarView = self.toolbarView else {
|
|
return
|
|
}
|
|
|
|
toolbarView.addSubviews(
|
|
makeScreenshotButton(toolbarView),
|
|
makeTransparencySlider(toolbarView)
|
|
)
|
|
}
|
|
|
|
func removeTitlebar() {
|
|
styleMask.remove(.titled)
|
|
}
|
|
|
|
func makeScreenshotButton(_ toolbarView: NSView) -> NSButton {
|
|
let button = NSButton()
|
|
button.image = NSImage(named: "ScreenshotButton")
|
|
button.imageScaling = .scaleProportionallyDown
|
|
button.isBordered = false
|
|
button.bezelStyle = .shadowlessSquare
|
|
button.frame = CGRect(x: toolbarView.frame.width - 19, y: 4, width: 16, height: 11)
|
|
button.action = #selector(AppDelegate.captureScreenshot)
|
|
return button
|
|
}
|
|
|
|
private var transparencySlider: ToolbarSlider?
|
|
|
|
func makeTransparencySlider(_ parentView: NSView) -> ToolbarSlider {
|
|
let slider = ToolbarSlider().alwaysRedisplayOnValueChanged().bindDoubleValue(to: .windowTransparency)
|
|
slider.frame = CGRect(x: parentView.frame.width - 160, y: 4, width: 140, height: 11)
|
|
slider.minValue = 0.5
|
|
return slider
|
|
}
|
|
|
|
private var defaultsObservations: [DefaultsObservation] = []
|
|
|
|
func setUp() {
|
|
let view = contentView!
|
|
view.wantsLayer = true
|
|
view.layer?.backgroundColor = NSColor.black.cgColor
|
|
|
|
let touchBarView = TouchBarView()
|
|
setContentSize(touchBarView.bounds.adding(padding: 5).size)
|
|
touchBarView.frame = touchBarView.frame.centered(in: view.bounds)
|
|
view.addSubview(touchBarView)
|
|
|
|
// TODO: These could use the `observe` method with `tiedToLifetimeOf` so we don't have to manually invalidate them.
|
|
defaultsObservations.append(Defaults.observe(.windowTransparency) { change in
|
|
self.alphaValue = CGFloat(change.newValue)
|
|
})
|
|
|
|
defaultsObservations.append(Defaults.observe(.windowDocking) { change in
|
|
self.docking = change.newValue
|
|
})
|
|
|
|
// TODO: We could maybe simplify this by creating another `Default` extension to bind a default to a KeyPath:
|
|
// `defaults.bind(.showOnAllDesktops, to: \.showOnAllDesktop)`
|
|
defaultsObservations.append(Defaults.observe(.showOnAllDesktops) { change in
|
|
self.showOnAllDesktops = change.newValue
|
|
})
|
|
|
|
defaultsObservations.append(Defaults.observe(.dockBehavior) { change in
|
|
self.dockBehavior = change.newValue
|
|
})
|
|
|
|
center()
|
|
setFrameOrigin(CGPoint(x: frame.origin.x, y: 100))
|
|
|
|
setFrameUsingName(Constants.windowAutosaveName)
|
|
setFrameAutosaveName(Constants.windowAutosaveName)
|
|
|
|
// Prevent the Touch Bar from momentarily becoming visible.
|
|
if !dockBehavior {
|
|
orderFront(nil)
|
|
}
|
|
|
|
NotificationCenter.default.addObserver(self, selector: #selector(didChangeScreenParameters(_:)), name: NSApplication.didChangeScreenParametersNotification, object: nil)
|
|
}
|
|
|
|
deinit {
|
|
for observation in defaultsObservations {
|
|
observation.invalidate()
|
|
}
|
|
}
|
|
|
|
convenience init() {
|
|
self.init(
|
|
contentRect: .zero,
|
|
styleMask: [
|
|
.titled,
|
|
.closable,
|
|
.nonactivatingPanel,
|
|
.hudWindow,
|
|
.utilityWindow
|
|
],
|
|
backing: .buffered,
|
|
defer: false
|
|
)
|
|
|
|
self.level = .assistiveTechHigh
|
|
self._setPreventsActivation(true)
|
|
self.isRestorable = true
|
|
self.hidesOnDeactivate = false
|
|
self.worksWhenModal = true
|
|
self.acceptsMouseMovedEvents = true
|
|
self.isMovableByWindowBackground = false
|
|
}
|
|
}
|