mirror of
https://github.com/sindresorhus/touch-bar-simulator
synced 2025-02-17 13:18:24 +00:00
parent
81fabef672
commit
7109f14f81
10 changed files with 549 additions and 87 deletions
1
Cartfile
1
Cartfile
|
@ -1 +1,2 @@
|
|||
github "sparkle-project/Sparkle"
|
||||
github "sindresorhus/Defaults"
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
github "sindresorhus/Defaults" "v1.0.0"
|
||||
github "sparkle-project/Sparkle" "1.21.2"
|
||||
|
|
|
@ -7,6 +7,9 @@
|
|||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
3C37035421FBEBD300177657 /* Defaults.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3C37035321FBEBD300177657 /* Defaults.framework */; };
|
||||
3C37035521FBEBD300177657 /* Defaults.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3C37035321FBEBD300177657 /* Defaults.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
||||
3C8493AD21FD3B7F00F12966 /* Glue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C8493AC21FD3B7F00F12966 /* Glue.swift */; };
|
||||
AF6C7BC61E7FAF38004A27E0 /* ToolbarSlider.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF6C7BC51E7FAF38004A27E0 /* ToolbarSlider.swift */; };
|
||||
E30988DF1E88DD060078CA9E /* Sparkle.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E30988DE1E88DD060078CA9E /* Sparkle.framework */; };
|
||||
E30988E01E88DD060078CA9E /* Sparkle.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = E30988DE1E88DD060078CA9E /* Sparkle.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
||||
|
@ -32,6 +35,7 @@
|
|||
files = (
|
||||
E30988E01E88DD060078CA9E /* Sparkle.framework in Embed Frameworks */,
|
||||
E39A158B214D011F00F86D5D /* SkyLight.framework in Embed Frameworks */,
|
||||
3C37035521FBEBD300177657 /* Defaults.framework in Embed Frameworks */,
|
||||
E39A157F214CFE7100F86D5D /* DFRFoundation.framework in Embed Frameworks */,
|
||||
);
|
||||
name = "Embed Frameworks";
|
||||
|
@ -40,6 +44,8 @@
|
|||
/* End PBXCopyFilesBuildPhase section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
3C37035321FBEBD300177657 /* Defaults.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Defaults.framework; path = Carthage/Build/Mac/Defaults.framework; sourceTree = "<group>"; };
|
||||
3C8493AC21FD3B7F00F12966 /* Glue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Glue.swift; sourceTree = "<group>"; };
|
||||
AF6C7BC51E7FAF38004A27E0 /* ToolbarSlider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = ToolbarSlider.swift; sourceTree = "<group>"; usesTabs = 1; };
|
||||
E30988DE1E88DD060078CA9E /* Sparkle.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Sparkle.framework; path = Carthage/Build/Mac/Sparkle.framework; sourceTree = "<group>"; };
|
||||
E34D6548214BBDAE00786C24 /* Touch Bar Simulator.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "Touch Bar Simulator.entitlements"; sourceTree = "<group>"; };
|
||||
|
@ -64,6 +70,7 @@
|
|||
files = (
|
||||
E39A157E214CFE7100F86D5D /* DFRFoundation.framework in Frameworks */,
|
||||
E39A158A214D011F00F86D5D /* SkyLight.framework in Frameworks */,
|
||||
3C37035421FBEBD300177657 /* Defaults.framework in Frameworks */,
|
||||
E30988DF1E88DD060078CA9E /* Sparkle.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
|
@ -109,6 +116,7 @@
|
|||
E35579EA21595EDC001CB642 /* TouchBarWindow.swift */,
|
||||
E39A158D214D0C4F00F86D5D /* TouchBarView.swift */,
|
||||
AF6C7BC51E7FAF38004A27E0 /* ToolbarSlider.swift */,
|
||||
3C8493AC21FD3B7F00F12966 /* Glue.swift */,
|
||||
E3930B0F216625BE00F66410 /* Constants.swift */,
|
||||
E35831A31F4D7EE0003BE371 /* util.swift */,
|
||||
E3FE2CC41E726CE800C6713A /* Assets.xcassets */,
|
||||
|
@ -121,6 +129,7 @@
|
|||
isa = PBXGroup;
|
||||
children = (
|
||||
E30988DE1E88DD060078CA9E /* Sparkle.framework */,
|
||||
3C37035321FBEBD300177657 /* Defaults.framework */,
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
|
@ -227,6 +236,7 @@
|
|||
files = (
|
||||
E35831A41F4D7EE0003BE371 /* util.swift in Sources */,
|
||||
AF6C7BC61E7FAF38004A27E0 /* ToolbarSlider.swift in Sources */,
|
||||
3C8493AD21FD3B7F00F12966 /* Glue.swift in Sources */,
|
||||
E356A16321028D81000148AD /* main.swift in Sources */,
|
||||
E35579EB21595EDC001CB642 /* TouchBarWindow.swift in Sources */,
|
||||
E3930B10216625BE00F66410 /* Constants.swift in Sources */,
|
||||
|
|
|
@ -1,71 +1,30 @@
|
|||
import Cocoa
|
||||
import Sparkle
|
||||
|
||||
private let defaults = UserDefaults.standard
|
||||
import Defaults
|
||||
|
||||
final class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
lazy var window = with(TouchBarWindow()) {
|
||||
$0.delegate = self
|
||||
$0.alphaValue = CGFloat(defaults.double(forKey: "windowTransparency"))
|
||||
$0.alphaValue = CGFloat(defaults[.windowTransparency])
|
||||
$0.setUp()
|
||||
}
|
||||
|
||||
lazy var toolbarView: NSView = self.window.toolbarView!
|
||||
lazy var statusItem = with(NSStatusBar.system.statusItem(withLength: NSStatusItem.squareLength)) {
|
||||
$0.menu = with(NSMenu()) { $0.delegate = self }
|
||||
$0.button!.image = NSImage(named: "AppIcon") // TODO: Add proper icon
|
||||
$0.button!.toolTip = "Right-click or option-click for menu"
|
||||
}
|
||||
|
||||
func applicationWillFinishLaunching(_ notification: Notification) {
|
||||
defaults.register(defaults: [
|
||||
"NSApplicationCrashOnExceptions": true,
|
||||
"windowTransparency": 0.75
|
||||
UserDefaults.standard.register(defaults: [
|
||||
"NSApplicationCrashOnExceptions": true
|
||||
])
|
||||
}
|
||||
|
||||
func applicationDidFinishLaunching(_ notification: Notification) {
|
||||
NSApp.servicesProvider = self
|
||||
|
||||
_ = SUUpdater()
|
||||
|
||||
let view = window.contentView!
|
||||
view.wantsLayer = true
|
||||
view.layer?.backgroundColor = NSColor.black.cgColor
|
||||
|
||||
let touchBarView = TouchBarView()
|
||||
window.setContentSize(touchBarView.bounds.adding(padding: 5).size)
|
||||
touchBarView.frame = touchBarView.frame.centered(in: view.bounds)
|
||||
view.addSubview(touchBarView)
|
||||
|
||||
toolbarView.addSubviews(
|
||||
makeScreenshotButton(),
|
||||
makeTransparencySlider()
|
||||
)
|
||||
|
||||
window.center()
|
||||
var origin = window.frame.origin
|
||||
origin.y = 100
|
||||
window.setFrameOrigin(origin)
|
||||
|
||||
window.setFrameUsingName(Constants.windowAutosaveName)
|
||||
window.setFrameAutosaveName(Constants.windowAutosaveName)
|
||||
|
||||
window.orderFront(nil)
|
||||
}
|
||||
|
||||
func makeScreenshotButton() -> NSButton {
|
||||
let button = NSButton()
|
||||
button.image = #imageLiteral(resourceName: "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(captureScreenshot)
|
||||
return button
|
||||
}
|
||||
|
||||
func makeTransparencySlider() -> ToolbarSlider {
|
||||
let slider = ToolbarSlider()
|
||||
slider.frame = CGRect(x: toolbarView.frame.width - 150, y: 4, width: 120, height: 11)
|
||||
slider.action = #selector(setTransparency)
|
||||
slider.minValue = 0.5
|
||||
slider.doubleValue = defaults.double(forKey: "windowTransparency")
|
||||
return slider
|
||||
_ = window
|
||||
_ = statusItem
|
||||
}
|
||||
|
||||
@objc
|
||||
|
@ -75,19 +34,80 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
|
|||
}
|
||||
|
||||
@objc
|
||||
func setTransparency(sender: ToolbarSlider) {
|
||||
window.alphaValue = CGFloat(sender.doubleValue)
|
||||
defaults.set(sender.doubleValue, forKey: "windowTransparency")
|
||||
func toggleView(_ pboard: NSPasteboard, userData: String, error: NSErrorPointer) {
|
||||
toggleView()
|
||||
}
|
||||
|
||||
@objc
|
||||
func toggleView(_ pboard: NSPasteboard, userData: String, error: NSErrorPointer) {
|
||||
func toggleView() {
|
||||
window.setIsVisible(!window.isVisible)
|
||||
}
|
||||
}
|
||||
|
||||
extension AppDelegate: NSWindowDelegate {
|
||||
func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
|
||||
return true
|
||||
extension AppDelegate: NSMenuDelegate {
|
||||
private func update(menu: NSMenu) {
|
||||
menu.removeAllItems()
|
||||
|
||||
guard statusItemShouldShowMenu() else {
|
||||
return
|
||||
}
|
||||
|
||||
menu.addItem(NSMenuItem(title: "Docking", action: nil, keyEquivalent: ""))
|
||||
var statusMenuDockingItems: [NSMenuItem] = []
|
||||
statusMenuDockingItems.append(NSMenuItem("Floating").bindChecked(to: .windowDocking, value: .floating))
|
||||
statusMenuDockingItems.append(NSMenuItem("Docked to Top").bindChecked(to: .windowDocking, value: .dockedToTop))
|
||||
statusMenuDockingItems.append(NSMenuItem("Docked to Bottom").bindChecked(to: .windowDocking, value: .dockedToBottom))
|
||||
for item in statusMenuDockingItems {
|
||||
item.indentationLevel = 1
|
||||
}
|
||||
menu.items.append(contentsOf: statusMenuDockingItems)
|
||||
|
||||
menu.addItem(NSMenuItem(title: "Transparency", action: nil, keyEquivalent: ""))
|
||||
let transparencyItem = NSMenuItem("Transparency")
|
||||
let transparencyView = NSView(frame: CGRect(origin: .zero, size: CGSize(width: 200, height: 20)))
|
||||
let slider = MenubarSlider().alwaysRedisplayOnValueChanged().bindDoubleValue(to: .windowTransparency)
|
||||
slider.frame = CGRect(x: 20, y: 4, width: 180, height: 11)
|
||||
slider.minValue = 0.5
|
||||
transparencyView.addSubview(slider)
|
||||
slider.translatesAutoresizingMaskIntoConstraints = false
|
||||
slider.leadingAnchor.constraint(equalTo: transparencyView.leadingAnchor, constant: 24).isActive = true
|
||||
slider.trailingAnchor.constraint(equalTo: transparencyView.trailingAnchor, constant: -9).isActive = true
|
||||
slider.centerYAnchor.constraint(equalTo: transparencyView.centerYAnchor).isActive = true
|
||||
transparencyItem.view = transparencyView
|
||||
menu.addItem(transparencyItem)
|
||||
|
||||
menu.addItem(NSMenuItem.separator())
|
||||
|
||||
menu.addItem(NSMenuItem("Take Screenshot", keyEquivalent: "6", keyModifiers: [.shift, .command]) { _ in
|
||||
self.captureScreenshot()
|
||||
})
|
||||
|
||||
menu.addItem(NSMenuItem.separator())
|
||||
|
||||
menu.addItem(NSMenuItem("Show on All Desktops").bindState(to: .showOnAllDesktops))
|
||||
|
||||
menu.addItem(NSMenuItem.separator())
|
||||
|
||||
menu.addItem(NSMenuItem("Quit Touch Bar Simulator", keyEquivalent: "q") { _ in
|
||||
NSApp.terminate(nil)
|
||||
})
|
||||
}
|
||||
|
||||
private func statusItemShouldShowMenu() -> Bool {
|
||||
return !NSApp.isLeftMouseDown || NSApp.isOptionKeyDown
|
||||
}
|
||||
|
||||
func menuNeedsUpdate(_ menu: NSMenu) {
|
||||
update(menu: menu)
|
||||
}
|
||||
|
||||
func menuWillOpen(_ menu: NSMenu) {
|
||||
if !statusItemShouldShowMenu() {
|
||||
statusItemButtonClicked()
|
||||
}
|
||||
}
|
||||
|
||||
private func statusItemButtonClicked() {
|
||||
toggleView()
|
||||
if window.isVisible { window.orderFront(nil) }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,13 @@
|
|||
import Foundation
|
||||
import Defaults
|
||||
|
||||
struct Constants {
|
||||
static let windowAutosaveName = "TouchBarWindow"
|
||||
}
|
||||
|
||||
extension Defaults.Keys {
|
||||
static let windowTransparency = Key<Double>("windowTransparency", default: 0.75)
|
||||
static let windowDocking = Key<TouchBarWindow.Docking>("windowDocking", default: .floating)
|
||||
static let showOnAllDesktops = Key<Bool>("showOnAllDesktops", default: false)
|
||||
static let lastFloatingPosition = OptionalKey<CGPoint>("lastFloatingPosition")
|
||||
}
|
||||
|
|
84
Touch Bar Simulator/Glue.swift
Normal file
84
Touch Bar Simulator/Glue.swift
Normal file
|
@ -0,0 +1,84 @@
|
|||
import Foundation
|
||||
import Defaults
|
||||
|
||||
extension Defaults {
|
||||
@discardableResult
|
||||
func observe<T: Codable, Weak: AnyObject>(_ key: Key<T>, tiedToLifetimeOf weaklyHeldObject: Weak, options: NSKeyValueObservingOptions = [.initial, .new, .old], handler: @escaping (KeyChange<T>) -> Void) -> DefaultsObservation {
|
||||
var observation: DefaultsObservation!
|
||||
observation = observe(key, options: options) { [weak weaklyHeldObject] change in
|
||||
guard let temporaryStrongReference = weaklyHeldObject else {
|
||||
// Will never occur on first call (outer function holds a strong reference),
|
||||
// so observation will never be nil
|
||||
observation.invalidate()
|
||||
return
|
||||
}
|
||||
_ = temporaryStrongReference
|
||||
handler(change)
|
||||
}
|
||||
return observation
|
||||
}
|
||||
}
|
||||
|
||||
extension NSMenuItem {
|
||||
/**
|
||||
Adds an action to this menu item that toggles the value of `key` in the
|
||||
defaults system, and initializes this item's state to the current value of
|
||||
`key`.
|
||||
|
||||
```
|
||||
let menuItem = NSMenuItem(title: "Invert Colors").streamState(to: .invertColors)
|
||||
```
|
||||
*/
|
||||
func bindState(to key: Defaults.Key<Bool>) -> Self {
|
||||
self.addAction { _ in
|
||||
defaults[key].toggle()
|
||||
}
|
||||
defaults.observe(key, tiedToLifetimeOf: self) { [unowned self] change in
|
||||
self.isChecked = change.newValue
|
||||
}
|
||||
return self
|
||||
}
|
||||
|
||||
/**
|
||||
Adds an action to this menu item that sets the value of `key` in the
|
||||
defaults system to `value`, and initializes this item's state based on
|
||||
whether the current value of `key` matches `value`.
|
||||
|
||||
```
|
||||
enum BillingType {
|
||||
case paper, electronic, duck
|
||||
}
|
||||
let menuItem = NSMenuItem(title: "Duck").streamChoice(to: .billingType, value: .duck)
|
||||
```
|
||||
*/
|
||||
func bindChecked<Value: Equatable>(to key: Defaults.Key<Value>, value: Value) -> Self {
|
||||
self.addAction { _ in
|
||||
defaults[key] = value
|
||||
}
|
||||
defaults.observe(key, tiedToLifetimeOf: self) { [unowned self] change in
|
||||
self.isChecked = (change.newValue == value)
|
||||
}
|
||||
return self
|
||||
}
|
||||
}
|
||||
|
||||
extension NSSlider {
|
||||
/**
|
||||
Adds an action to this slider that sets the value of `key` in the defaults
|
||||
system to the slider's `doubleValue`, and initializes its value to the
|
||||
current value of `key`.
|
||||
|
||||
```
|
||||
let slider = NSSlider().streamDoubleValue(to: .transparency)
|
||||
```
|
||||
*/
|
||||
func bindDoubleValue(to key: Defaults.Key<Double>) -> Self {
|
||||
self.addAction { sender in
|
||||
defaults[key] = sender.doubleValue
|
||||
}
|
||||
defaults.observe(key, tiedToLifetimeOf: self) { [unowned self] change in
|
||||
self.doubleValue = change.newValue
|
||||
}
|
||||
return self
|
||||
}
|
||||
}
|
|
@ -1,35 +1,110 @@
|
|||
import Cocoa
|
||||
|
||||
private let knob: NSImage = {
|
||||
let frame = CGRect(x: 0, y: 0, width: 32, height: 32)
|
||||
|
||||
let image = NSImage(size: frame.size)
|
||||
image.lockFocus()
|
||||
|
||||
// Circle
|
||||
let path = NSBezierPath(roundedRect: frame, xRadius: 4, yRadius: 12)
|
||||
NSColor.lightGray.set()
|
||||
path.fill()
|
||||
|
||||
// Border
|
||||
NSColor.black.set()
|
||||
path.lineWidth = 2
|
||||
path.stroke()
|
||||
|
||||
image.unlockFocus()
|
||||
return image
|
||||
}()
|
||||
import Defaults
|
||||
|
||||
private final class ToolbarSliderCell: NSSliderCell {
|
||||
var fillColor: NSColor
|
||||
var borderColor: NSColor
|
||||
var shadow: NSShadow?
|
||||
|
||||
init(fillColor: NSColor, borderColor: NSColor, shadow: NSShadow? = nil) {
|
||||
self.fillColor = fillColor
|
||||
self.borderColor = borderColor
|
||||
self.shadow = shadow
|
||||
super.init()
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func drawKnob(_ knobRect: CGRect) {
|
||||
knob.draw(in: knobRect.insetBy(dx: 0, dy: 6.5))
|
||||
var frame = knobRect.insetBy(dx: 0, dy: 6.5)
|
||||
if let shadow = self.shadow {
|
||||
// Make room on either side of the view for the shadow to spill into,
|
||||
// rather than clip on the edges
|
||||
frame.origin.x *= ((self.barRect.width - shadow.shadowBlurRadius * 2) / self.barRect.width)
|
||||
frame.origin.x += shadow.shadowBlurRadius
|
||||
}
|
||||
|
||||
NSGraphicsContext.saveGraphicsState()
|
||||
|
||||
self.shadow?.set()
|
||||
|
||||
// Circle
|
||||
let path = NSBezierPath(roundedRect: frame, xRadius: 4, yRadius: 12)
|
||||
self.fillColor.set()
|
||||
path.fill()
|
||||
|
||||
// Border should not draw a shadow
|
||||
NSShadow().set()
|
||||
|
||||
// Border
|
||||
self.borderColor.set()
|
||||
path.lineWidth = 0.8
|
||||
path.stroke()
|
||||
|
||||
NSGraphicsContext.restoreGraphicsState()
|
||||
}
|
||||
|
||||
private var barRect = CGRect.zero
|
||||
|
||||
override func drawBar(inside rect: CGRect, flipped: Bool) {
|
||||
self.barRect = rect
|
||||
// A knob shadow requires a small skew in the origin of the knob (see above),
|
||||
// which causes the knob to not go all the way to the ends of the bar
|
||||
// Fix this by shortening the bar
|
||||
if let shadow = self.shadow {
|
||||
self.barRect = self.barRect.insetBy(dx: shadow.shadowBlurRadius * 2, dy: 0)
|
||||
}
|
||||
super.drawBar(inside: self.barRect, flipped: flipped)
|
||||
}
|
||||
}
|
||||
|
||||
extension NSSlider {
|
||||
// Redisplaying the slider prevents shadow artifacts that result
|
||||
// from moving a knob that draws a shadow
|
||||
// However, only do so if its value has changed, because if a
|
||||
// redisplay is attempted without a change, then the slider draws
|
||||
// itself brighter for some reason
|
||||
func alwaysRedisplayOnValueChanged() -> Self {
|
||||
self.addAction { sender in
|
||||
if (defaults[.windowTransparency] - sender.doubleValue) != 0 {
|
||||
sender.needsDisplay = true
|
||||
}
|
||||
}
|
||||
return self
|
||||
}
|
||||
}
|
||||
|
||||
final class ToolbarSlider: NSSlider {
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
cell = ToolbarSliderCell()
|
||||
|
||||
let knobShadow = NSShadow()
|
||||
knobShadow.shadowColor = NSColor.black.withAlphaComponent(0.7)
|
||||
knobShadow.shadowOffset = CGSize(width: 0.8, height: -0.8)
|
||||
knobShadow.shadowBlurRadius = 5
|
||||
|
||||
self.cell = ToolbarSliderCell(fillColor: .lightGray, borderColor: .black, shadow: knobShadow)
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
}
|
||||
|
||||
final class MenubarSlider: NSSlider {
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
|
||||
let knobShadow = NSShadow()
|
||||
knobShadow.shadowColor = NSColor.black.withAlphaComponent(0.6)
|
||||
knobShadow.shadowOffset = CGSize(width: 0.8, height: -0.8)
|
||||
knobShadow.shadowBlurRadius = 4
|
||||
|
||||
self.cell = ToolbarSliderCell(fillColor: .controlTextColor, borderColor: .clear, shadow: knobShadow)
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
|
|
|
@ -1,6 +1,86 @@
|
|||
import Cocoa
|
||||
import Defaults
|
||||
|
||||
final class TouchBarWindow: NSPanel {
|
||||
enum Docking: String, Codable {
|
||||
case floating, dockedToTop, dockedToBottom
|
||||
|
||||
func dock(window: TouchBarWindow) {
|
||||
switch self {
|
||||
case .floating:
|
||||
window.addTitlebar()
|
||||
if let prevPosition = defaults[.lastFloatingPosition] {
|
||||
window.setFrameOrigin(prevPosition)
|
||||
}
|
||||
case .dockedToTop:
|
||||
window.removeTitlebar()
|
||||
window.moveTo(x: .center, y: .top)
|
||||
case .dockedToBottom:
|
||||
window.removeTitlebar()
|
||||
window.moveTo(x: .center, y: .bottom)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var docking: Docking? {
|
||||
didSet {
|
||||
if oldValue == .floating && docking != .floating {
|
||||
defaults[.lastFloatingPosition] = frame.origin
|
||||
}
|
||||
|
||||
docking?.dock(window: self)
|
||||
|
||||
setIsVisible(true)
|
||||
orderFront(nil)
|
||||
}
|
||||
}
|
||||
|
||||
var showOnAllDesktops: Bool = false {
|
||||
didSet {
|
||||
if showOnAllDesktops {
|
||||
collectionBehavior = .canJoinAllSpaces
|
||||
} else {
|
||||
collectionBehavior = .moveToActiveSpace
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 = #imageLiteral(resourceName: "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
|
||||
}
|
||||
|
||||
override var canBecomeMain: Bool {
|
||||
return false
|
||||
}
|
||||
|
@ -9,6 +89,43 @@ final class TouchBarWindow: NSPanel {
|
|||
return false
|
||||
}
|
||||
|
||||
private var defaultsObservations: [DefaultsObservation] = []
|
||||
|
||||
func setUp() {
|
||||
let view = self.contentView!
|
||||
view.wantsLayer = true
|
||||
view.layer?.backgroundColor = NSColor.black.cgColor
|
||||
|
||||
let touchBarView = TouchBarView()
|
||||
self.setContentSize(touchBarView.bounds.adding(padding: 5).size)
|
||||
touchBarView.frame = touchBarView.frame.centered(in: view.bounds)
|
||||
view.addSubview(touchBarView)
|
||||
|
||||
defaultsObservations.append(defaults.observe(.windowTransparency) { change in
|
||||
self.alphaValue = CGFloat(change.newValue)
|
||||
})
|
||||
defaultsObservations.append(defaults.observe(.windowDocking) { change in
|
||||
self.docking = change.newValue
|
||||
})
|
||||
defaultsObservations.append(defaults.observe(.showOnAllDesktops) { change in
|
||||
self.showOnAllDesktops = change.newValue
|
||||
})
|
||||
|
||||
self.center()
|
||||
self.setFrameOrigin(CGPoint(x: self.frame.origin.x, y: 100))
|
||||
|
||||
self.setFrameUsingName(Constants.windowAutosaveName)
|
||||
self.setFrameAutosaveName(Constants.windowAutosaveName)
|
||||
|
||||
self.orderFront(nil)
|
||||
}
|
||||
|
||||
deinit {
|
||||
for observation in defaultsObservations {
|
||||
observation.invalidate()
|
||||
}
|
||||
}
|
||||
|
||||
convenience init() {
|
||||
self.init(
|
||||
contentRect: .zero,
|
||||
|
@ -23,7 +140,6 @@ final class TouchBarWindow: NSPanel {
|
|||
)
|
||||
|
||||
self._setPreventsActivation(true)
|
||||
self.title = "Touch Bar Simulator"
|
||||
self.isRestorable = true
|
||||
self.hidesOnDeactivate = false
|
||||
self.worksWhenModal = true
|
||||
|
|
|
@ -47,12 +47,157 @@ extension NSWindow {
|
|||
}
|
||||
}
|
||||
|
||||
extension NSWindow {
|
||||
enum MoveXPositioning {
|
||||
case left, center, right
|
||||
}
|
||||
enum MoveYPositioning {
|
||||
case top, center, bottom
|
||||
}
|
||||
|
||||
func moveTo(x xPositioning: MoveXPositioning, y yPositioning: MoveYPositioning) {
|
||||
guard let visibleFrame = NSScreen.main?.visibleFrame else {
|
||||
return
|
||||
}
|
||||
|
||||
let x: CGFloat, y: CGFloat
|
||||
switch xPositioning {
|
||||
case .left:
|
||||
x = visibleFrame.minX
|
||||
case .center:
|
||||
x = visibleFrame.midX - frame.width / 2
|
||||
case .right:
|
||||
x = visibleFrame.maxX - frame.width
|
||||
}
|
||||
switch yPositioning {
|
||||
case .top:
|
||||
y = visibleFrame.maxY - frame.height
|
||||
case .center:
|
||||
y = visibleFrame.midY - frame.height / 2
|
||||
case .bottom:
|
||||
y = visibleFrame.minY
|
||||
}
|
||||
|
||||
setFrameOrigin(CGPoint(x: x, y: y))
|
||||
}
|
||||
}
|
||||
|
||||
extension NSView {
|
||||
func addSubviews(_ subviews: NSView...) {
|
||||
subviews.forEach { addSubview($0) }
|
||||
}
|
||||
}
|
||||
|
||||
extension NSMenuItem {
|
||||
var isChecked: Bool {
|
||||
get {
|
||||
return state == .on
|
||||
}
|
||||
set {
|
||||
state = newValue ? .on : .off
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension NSMenuItem {
|
||||
convenience init(_ title: String, keyEquivalent: String = "", keyModifiers: NSEvent.ModifierFlags? = nil, isChecked: Bool = false, action: ((NSMenuItem) -> Void)? = nil) {
|
||||
self.init(title: title, action: nil, keyEquivalent: keyEquivalent)
|
||||
if let keyModifiers = keyModifiers {
|
||||
self.keyEquivalentModifierMask = keyModifiers
|
||||
}
|
||||
self.isChecked = isChecked
|
||||
if let action = action {
|
||||
self.onAction = action
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final class AssociatedObject<T: Any> {
|
||||
subscript(index: Any) -> T? {
|
||||
get {
|
||||
return objc_getAssociatedObject(index, Unmanaged.passUnretained(self).toOpaque()) as! T?
|
||||
} set {
|
||||
objc_setAssociatedObject(index, Unmanaged.passUnretained(self).toOpaque(), newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc
|
||||
protocol TargetActionSender: AnyObject {
|
||||
var target: AnyObject? { get set }
|
||||
var action: Selector? { get set }
|
||||
}
|
||||
|
||||
extension NSControl: TargetActionSender {}
|
||||
extension NSMenuItem: TargetActionSender {}
|
||||
extension NSGestureRecognizer: TargetActionSender {}
|
||||
|
||||
private final class ActionTrampoline<Sender>: NSObject {
|
||||
typealias ActionClosure = ((Sender) -> Void)
|
||||
|
||||
let action: ActionClosure
|
||||
|
||||
init(action: @escaping ActionClosure) {
|
||||
self.action = action
|
||||
}
|
||||
|
||||
@objc
|
||||
fileprivate func performAction(_ sender: TargetActionSender) {
|
||||
action(sender as! Sender)
|
||||
}
|
||||
}
|
||||
|
||||
private struct TargetActionSenderAssociatedKeys {
|
||||
fileprivate static let trampoline = AssociatedObject<AnyObject>()
|
||||
}
|
||||
|
||||
extension TargetActionSender {
|
||||
/**
|
||||
Closure version of `.action`
|
||||
|
||||
```
|
||||
let menuItem = NSMenuItem(title: "Unicorn")
|
||||
menuItem.onAction = { sender in
|
||||
print("NSMenuItem action: \(sender)")
|
||||
}
|
||||
```
|
||||
*/
|
||||
var onAction: ((Self) -> Void)? {
|
||||
get {
|
||||
return (TargetActionSenderAssociatedKeys.trampoline[self] as? ActionTrampoline<Self>)?.action
|
||||
}
|
||||
set {
|
||||
guard let newValue = newValue else {
|
||||
self.target = nil
|
||||
self.action = nil
|
||||
TargetActionSenderAssociatedKeys.trampoline[self] = nil
|
||||
return
|
||||
}
|
||||
let trampoline = ActionTrampoline(action: newValue)
|
||||
TargetActionSenderAssociatedKeys.trampoline[self] = trampoline
|
||||
self.target = trampoline
|
||||
self.action = #selector(ActionTrampoline<Self>.performAction)
|
||||
}
|
||||
}
|
||||
func addAction(_ action: @escaping ((Self) -> Void)) {
|
||||
let lastAction = self.onAction
|
||||
self.onAction = { sender in
|
||||
lastAction?(sender)
|
||||
action(sender)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension NSApplication {
|
||||
var isLeftMouseDown: Bool {
|
||||
return currentEvent?.type == .leftMouseDown
|
||||
}
|
||||
|
||||
var isOptionKeyDown: Bool {
|
||||
return NSEvent.modifierFlags.contains(.option)
|
||||
}
|
||||
}
|
||||
|
||||
func pressKey(keyCode: CGKeyCode, flags: CGEventFlags = []) {
|
||||
let eventSource = CGEventSource(stateID: .hidSystemState)
|
||||
let keyDown = CGEvent(keyboardEventSource: eventSource, virtualKey: keyCode, keyDown: true)
|
||||
|
|
|
@ -2,9 +2,11 @@
|
|||
|
||||
> Use the Touch Bar on any Mac
|
||||
|
||||
Launch the Touch Bar simulator from anywhere without needing to have Xcode installed, whereas Apple requires you to launch it from inside Xcode. It also comes with a handy transparency slider, a screenshot button, and a service to toggle the Touch Bar in the Services menu or with a keyboard shortcut.
|
||||
Launch the Touch Bar simulator from anywhere without needing to have Xcode installed, whereas Apple requires you to launch it from inside Xcode. It also comes with a handy transparency slider, a screenshot button, and a menu bar icon and system service to toggle the Touch Bar with a click or keyboard shortcut.
|
||||
|
||||
You can add a shortcut in `System Preferences` → `Keyboard` → `Shortcuts` → `Services` → `Toggle Touch Bar`.
|
||||
Right- or option-clicking the menu bar icon displays a menu with options to dock the window to the top or bottom of the screen, make it show on all desktops at once, access toolbar features in docked mode, or quit the app.
|
||||
|
||||
You can add a toggle shortcut in `System Preferences` → `Keyboard` → `Shortcuts` → `Services` → `Toggle Touch Bar`.
|
||||
|
||||
**Important:** If clicking in the simulator or the screenshot button is not working, you need to go to "System Preferences" → "Security & Privacy" → "Accessibility", and ensure "Touch Bar Simulator.app" is checked. If it's already checked, try unchecking and checking it again.
|
||||
|
||||
|
@ -39,7 +41,7 @@ $ brew cask install touch-bar-simulator
|
|||
|
||||
You can capture a screenshot of the Touch Bar by either:
|
||||
|
||||
1. Clicking the screenshot button in the Touch Bar window which saves it to `~/Desktop`.
|
||||
1. Clicking the screenshot button in the Touch Bar window or options menu which saves it to `~/Desktop`.
|
||||
2. Pressing <kbd>⇧⌘6</kbd> which saves it to `~/Desktop`.
|
||||
3. Pressing <kbd>⌃⇧⌘6</kbd> which saves it to the clipboard.
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue