This commit is contained in:
Sindre Sorhus 2019-03-06 15:25:53 +07:00
parent 664bf278ce
commit f011775549
10 changed files with 97 additions and 50 deletions

View file

@ -122,6 +122,7 @@ whitelist_rules:
- xct_specific_matcher
- xctfail_message
- yoda_condition
- todo
analyzer_rules:
- unused_import
- unused_private_declaration

View file

@ -45,7 +45,7 @@
/* 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>"; };
3C8493AC21FD3B7F00F12966 /* Glue.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = Glue.swift; sourceTree = "<group>"; usesTabs = 1; };
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>"; };
@ -116,8 +116,8 @@
E35579EA21595EDC001CB642 /* TouchBarWindow.swift */,
E39A158D214D0C4F00F86D5D /* TouchBarView.swift */,
AF6C7BC51E7FAF38004A27E0 /* ToolbarSlider.swift */,
3C8493AC21FD3B7F00F12966 /* Glue.swift */,
E3930B0F216625BE00F66410 /* Constants.swift */,
3C8493AC21FD3B7F00F12966 /* Glue.swift */,
E35831A31F4D7EE0003BE371 /* util.swift */,
E3FE2CC41E726CE800C6713A /* Assets.xcassets */,
E356A16421028DAB000148AD /* Other */,

View file

@ -77,7 +77,7 @@ extension AppDelegate: NSMenuDelegate {
menu.addItem(NSMenuItem.separator())
menu.addItem(NSMenuItem("Take Screenshot", keyEquivalent: "6", keyModifiers: [.shift, .command]) { _ in
menu.addItem(NSMenuItem("Capture Screenshot", keyEquivalent: "6", keyModifiers: [.shift, .command]) { _ in
self.captureScreenshot()
})
@ -108,6 +108,9 @@ extension AppDelegate: NSMenuDelegate {
private func statusItemButtonClicked() {
toggleView()
if window.isVisible { window.orderFront(nil) }
if window.isVisible {
window.orderFront(nil)
}
}
}

View file

@ -1,9 +1,16 @@
import Foundation
import Defaults
// TODO: Upstream all these to https://github.com/sindresorhus/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 {
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 {
@ -12,9 +19,11 @@ extension Defaults {
observation.invalidate()
return
}
_ = temporaryStrongReference
handler(change)
}
return observation
}
}
@ -26,19 +35,22 @@ extension NSMenuItem {
`key`.
```
let menuItem = NSMenuItem(title: "Invert Colors").streamState(to: .invertColors)
let menuItem = NSMenuItem(title: "Invert Colors").bindState(to: .invertColors)
```
*/
func bindState(to key: Defaults.Key<Bool>) -> Self {
self.addAction { _ in
addAction { _ in
defaults[key].toggle()
}
defaults.observe(key, tiedToLifetimeOf: self) { [unowned self] change in
self.isChecked = change.newValue
}
return self
}
// TODO: The doc comments here are out of date
/**
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
@ -48,37 +60,44 @@ extension NSMenuItem {
enum BillingType {
case paper, electronic, duck
}
let menuItem = NSMenuItem(title: "Duck").streamChoice(to: .billingType, value: .duck)
let menuItem = NSMenuItem(title: "Duck").bindChecked(to: .billingType, value: .duck)
```
*/
func bindChecked<Value: Equatable>(to key: Defaults.Key<Value>, value: Value) -> Self {
self.addAction { _ in
addAction { _ in
defaults[key] = value
}
defaults.observe(key, tiedToLifetimeOf: self) { [unowned self] change in
self.isChecked = (change.newValue == value)
}
return self
}
}
// TODO: Generalize this to all `NSControl`s, or maybe even all things with a `.action` and `.doubleValue`?
extension NSSlider {
// TODO: The doc comments here are out of date
/**
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)
let slider = NSSlider().bindDoubleValue(to: .transparency)
```
*/
func bindDoubleValue(to key: Defaults.Key<Double>) -> Self {
self.addAction { sender in
addAction { sender in
defaults[key] = sender.doubleValue
}
defaults.observe(key, tiedToLifetimeOf: self) { [unowned self] change in
self.doubleValue = change.newValue
}
return self
}
}

View file

@ -23,24 +23,24 @@ private final class ToolbarSliderCell: NSSliderCell {
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 *= ((barRect.width - shadow.shadowBlurRadius * 2) / barRect.width)
frame.origin.x += shadow.shadowBlurRadius
}
NSGraphicsContext.saveGraphicsState()
self.shadow?.set()
shadow?.set()
// Circle
let path = NSBezierPath(roundedRect: frame, xRadius: 4, yRadius: 12)
self.fillColor.set()
fillColor.set()
path.fill()
// Border should not draw a shadow
NSShadow().set()
// Border
self.borderColor.set()
borderColor.set()
path.lineWidth = 0.8
path.stroke()
@ -50,14 +50,16 @@ private final class ToolbarSliderCell: NSSliderCell {
private var barRect = CGRect.zero
override func drawBar(inside rect: CGRect, flipped: Bool) {
self.barRect = rect
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
// 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)
barRect = barRect.insetBy(dx: shadow.shadowBlurRadius * 2, dy: 0)
}
super.drawBar(inside: self.barRect, flipped: flipped)
super.drawBar(inside: barRect, flipped: flipped)
}
}
@ -66,13 +68,14 @@ extension NSSlider {
// 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
// itself brighter for some reason.
func alwaysRedisplayOnValueChanged() -> Self {
self.addAction { sender in
addAction { sender in
if (defaults[.windowTransparency] - sender.doubleValue) != 0 {
sender.needsDisplay = true
}
}
return self
}
}

View file

@ -31,7 +31,7 @@ final class TouchBarView: NSView {
}
self.layer!.contents = frameSurface
} .takeUnretainedValue()
}.takeUnretainedValue()
DFRSetStatus(2)
stream?.start()

View file

@ -48,9 +48,11 @@ final class TouchBarWindow: NSPanel {
func addTitlebar() {
styleMask.insert(.titled)
title = "Touch Bar Simulator"
guard let toolbarView = self.toolbarView else {
return
}
toolbarView.addSubviews(
makeScreenshotButton(toolbarView),
makeTransparencySlider(toolbarView)
@ -92,32 +94,37 @@ final class TouchBarWindow: NSPanel {
private var defaultsObservations: [DefaultsObservation] = []
func setUp() {
let view = self.contentView!
let view = contentView!
view.wantsLayer = true
view.layer?.backgroundColor = NSColor.black.cgColor
let touchBarView = TouchBarView()
self.setContentSize(touchBarView.bounds.adding(padding: 5).size)
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
})
self.center()
self.setFrameOrigin(CGPoint(x: self.frame.origin.x, y: 100))
center()
setFrameOrigin(CGPoint(x: frame.origin.x, y: 100))
self.setFrameUsingName(Constants.windowAutosaveName)
self.setFrameAutosaveName(Constants.windowAutosaveName)
setFrameUsingName(Constants.windowAutosaveName)
setFrameAutosaveName(Constants.windowAutosaveName)
self.orderFront(nil)
orderFront(nil)
}
deinit {

View file

@ -51,6 +51,7 @@ extension NSWindow {
enum MoveXPositioning {
case left, center, right
}
enum MoveYPositioning {
case top, center, bottom
}
@ -100,12 +101,21 @@ extension NSMenuItem {
}
extension NSMenuItem {
convenience init(_ title: String, keyEquivalent: String = "", keyModifiers: NSEvent.ModifierFlags? = nil, isChecked: Bool = false, action: ((NSMenuItem) -> Void)? = nil) {
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
}
@ -144,7 +154,7 @@ private final class ActionTrampoline<Sender>: NSObject {
@objc
fileprivate func performAction(_ sender: TargetActionSender) {
action(sender as! Sender)
}
}
}
private struct TargetActionSenderAssociatedKeys {
@ -153,7 +163,7 @@ private struct TargetActionSenderAssociatedKeys {
extension TargetActionSender {
/**
Closure version of `.action`
Closure version of `.action`.
```
let menuItem = NSMenuItem(title: "Unicorn")
@ -168,20 +178,23 @@ extension TargetActionSender {
}
set {
guard let newValue = newValue else {
self.target = nil
self.action = nil
target = nil
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)
target = trampoline
action = #selector(ActionTrampoline<Self>.performAction)
}
}
func addAction(_ action: @escaping ((Self) -> Void)) {
let lastAction = self.onAction
self.onAction = { sender in
// TODO: The problem with doing it like this is that there's no way to add ability to remove an action. I think a better solution would be to store an array of action handlers using associated object.
let lastAction = onAction
onAction = { sender in
lastAction?(sender)
action(sender)
}
@ -198,6 +211,7 @@ extension NSApplication {
}
}
// TODO: Find a namespace to put this onto. I don't like free-floating functions.
func pressKey(keyCode: CGKeyCode, flags: CGEventFlags = []) {
let eventSource = CGEventSource(stateID: .hidSystemState)
let keyDown = CGEvent(keyboardEventSource: eventSource, virtualKey: keyCode, keyDown: true)

View file

@ -4,29 +4,23 @@
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.
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.
<img src="screenshot-menu-bar.png" width="379" height="283" align="left">
Clicking the menu bar icon toggles the Touch Bar window.
Right-clicking 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.
**[Website](https://sindresorhus.com/touch-bar-simulator/)** &nbsp;&nbsp; **[Discuss it on Product Hunt](https://www.producthunt.com/posts/touch-bar-simulator)**
<img src="screenshot.png" width="1129">
*Check out my other [macOS apps](https://sindresorhus.com/apps)*
<a href="https://www.patreon.com/sindresorhus">
<img src="https://c5.patreon.com/external/logo/become_a_patron_button@2x.png" width="160">
</a>
## Getting started
#### [Download the latest release](https://sindresorhus.com/touch-bar-simulator)
---
Or install it with [Homebrew-Cask](https://caskroom.github.io):
```
@ -36,6 +30,10 @@ $ brew cask install touch-bar-simulator
*Requires macOS 10.14 or later.*
<a href="https://www.patreon.com/sindresorhus">
<img src="https://c5.patreon.com/external/logo/become_a_patron_button@2x.png" width="160">
</a>
## Screenshot
@ -68,6 +66,8 @@ Xcode 10 moved the required private symbols needed to trigger the Touch Bar simu
## Related
- [Website](https://sindresorhus.com/touch-bar-simulator/)
- [Product Hunt submission](https://www.producthunt.com/posts/touch-bar-simulator)
- [Gifski](https://github.com/sindresorhus/gifski-app) - Convert videos to high-quality GIFs on your Mac
- [More apps…](https://sindresorhus.com/#apps)

BIN
screenshot-menu-bar.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 400 KiB