Add menu bar item with hide/show and docking options (#33)

Fixes #30
This commit is contained in:
ThatsJustCheesy 2019-03-06 02:41:36 -05:00 committed by Sindre Sorhus
parent 81fabef672
commit 7109f14f81
10 changed files with 549 additions and 87 deletions

View file

@ -1 +1,2 @@
github "sparkle-project/Sparkle"
github "sindresorhus/Defaults"

View file

@ -1 +1,2 @@
github "sindresorhus/Defaults" "v1.0.0"
github "sparkle-project/Sparkle" "1.21.2"

View file

@ -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 */,

View file

@ -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) }
}
}

View file

@ -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")
}

View 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
}
}

View file

@ -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)

View file

@ -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

View file

@ -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)

View file

@ -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.