Skip to content

Commit

Permalink
Dark UI improvements (#21)
Browse files Browse the repository at this point in the history
  • Loading branch information
sindresorhus committed Mar 13, 2018
1 parent ddc3b53 commit 67cade2
Show file tree
Hide file tree
Showing 4 changed files with 202 additions and 13 deletions.
8 changes: 7 additions & 1 deletion Gifski/AppDelegate.swift
Expand Up @@ -19,6 +19,8 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
"NSFullScreenMenuItemEverywhere": false,
"outputQuality": 1
])

NSAppearance.app = .dark
}

func applicationDidFinishLaunching(_ notification: Notification) {
Expand All @@ -38,7 +40,11 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
}

guard urls.count == 1 else {
Misc.alert(title: "Max one file", text: "You can only convert a single file at the time")
NSAlert.showModal(
for: mainWindowController.window,
title: "Max one file",
message: "You can only convert a single file at the time"
)
return
}

Expand Down
11 changes: 9 additions & 2 deletions Gifski/MainWindowController.swift
Expand Up @@ -44,8 +44,9 @@ class MainWindowController: NSWindowController {

override func windowDidLoad() {
with(window!) {
$0.delegate = self
$0.appearance = .app
$0.titleVisibility = .hidden
$0.appearance = NSAppearance(named: .vibrantDark)
$0.tabbingMode = .disallowed
$0.titlebarAppearsTransparent = true
$0.isMovableByWindowBackground = true
Expand All @@ -70,11 +71,16 @@ class MainWindowController: NSWindowController {
func convert(_ inputUrl: URL) {
// We already specify the UTIs we support, so this can only happen on invalid but supported files
guard inputUrl.isVideoDecodable else {
Misc.alert(title: "Video not supported", text: "The video you tried to convert could not be read.")
NSAlert.showModal(
for: window,
title: "Video not supported",
message: "The video you tried to convert could not be read."
)
return
}

let panel = NSSavePanel()
panel.appearance = .app
panel.canCreateDirectories = true
panel.directoryURL = inputUrl.directoryURL
panel.nameFieldStringValue = inputUrl.changingFileExtension(to: "gif").filename
Expand Down Expand Up @@ -148,6 +154,7 @@ class MainWindowController: NSWindowController {
@objc
func open(_ sender: AnyObject) {
let panel = NSOpenPanel()
panel.appearance = .app
panel.canChooseDirectories = false
panel.canCreateDirectories = false
panel.allowedFileTypes = System.supportedVideoTypes
Expand Down
3 changes: 3 additions & 0 deletions Gifski/SavePanelAccessoryViewController.swift
Expand Up @@ -14,6 +14,9 @@ final class SavePanelAccessoryViewController: NSViewController {
override func viewDidLoad() {
super.viewDidLoad()

/// TODO: Find a way to create a `NSTextField` extension that adheres to `NSAppearance.app`
view.invertTextColorOnTextFieldsIfDark()

let formatter = ByteCountFormatter()
formatter.zeroPadsFractionDigits = true

Expand Down
193 changes: 183 additions & 10 deletions Gifski/util.swift
Expand Up @@ -46,16 +46,6 @@ struct Meta {
}


struct Misc {
static func alert(title: String, text: String) {
let alert = NSAlert()
alert.messageText = title
alert.informativeText = text
alert.runModal()
}
}


/// This is useful as `awakeFromNib` is not called for programatically created views
class SSView: NSView {
var didAppearWasCalled = false
Expand All @@ -74,6 +64,189 @@ class SSView: NSView {
}


extension NSWindow {
var toolbarView: NSView? {
return standardWindowButton(.closeButton)?.superview
}

var titlebarView: NSView? {
return toolbarView?.superview
}

var titlebarHeight: Double {
return Double(titlebarView?.bounds.height ?? 0)
}
}


extension NSWindowController: NSWindowDelegate {
public func window(_ window: NSWindow, willPositionSheet sheet: NSWindow, using rect: CGRect) -> CGRect {
// Adjust sheet position so it goes below the traffic lights
if window.styleMask.contains(.fullSizeContentView) {
return rect.offsetBy(dx: 0, dy: CGFloat(-window.titlebarHeight))
}

return rect
}
}


extension NSAppearance {
static let aqua = NSAppearance(named: .aqua)!
static let light = NSAppearance(named: .vibrantLight)!
static let dark = NSAppearance(named: .vibrantDark)!

static var system: NSAppearance {
let isDark = UserDefaults.standard.string(forKey: "AppleInterfaceStyle") == "Dark"
return NSAppearance(named: isDark ? .vibrantDark : .vibrantLight)!
}
}


extension NSAppearance {
private struct AssociatedKeys {
static let app = AssociatedObject<NSAppearance>()
}

/// The chosen appearance for the app
/// We're not using `.current` as it doesn't work across threads
static var app: NSAppearance {
get {
return AssociatedKeys.app[self] ?? .aqua
}
set {
current = newValue
AssociatedKeys.app[self] = newValue
}
}
}


extension NSColor {
/// Get the complementary color of the current color
var complementary: NSColor {
guard let ciColor = CIColor(color: self) else {
return self
}

let compRed = 1 - ciColor.red
let compGreen = 1 - ciColor.green
let compBlue = 1 - ciColor.blue

return NSColor(red: compRed, green: compGreen, blue: compBlue, alpha: alphaComponent)
}
}


extension NSView {
/**
Iterate through subviews of a specific type and change properties on them
```
view.forEachSubview(ofType: NSTextField.self) {
$0.textColor = .white
}
```
*/
func forEachSubview<T>(ofType type: T.Type, deep: Bool = true, closure: (T) -> Void) {
for view in subviews {
if let view = view as? T {
closure(view)
} else if deep {
view.forEachSubview(ofType: type, deep: deep, closure: closure)
}
}
}

func invertTextColorOnTextFieldsIfDark() {
guard NSAppearance.app == .dark else {
return
}

forEachSubview(ofType: NSTextField.self) {
$0.textColor = $0.textColor?.complementary
}
}
}


extension NSAlert {
/// Show a modal alert sheet on a window
/// If the window is nil, it will be a app-modal alert
@discardableResult
static func showModal(
for window: NSWindow?,
title: String,
message: String? = nil,
style: NSAlert.Style = .critical
) -> NSApplication.ModalResponse {
guard let window = window else {
return NSAlert(
title: title,
message: message,
style: style
).runModal()
}

return NSAlert(
title: title,
message: message,
style: style
).runModal(for: window)
}

/// Show a app-modal (window indepedendent) alert
@discardableResult
static func showModal(
title: String,
message: String? = nil,
style: NSAlert.Style = .critical
) -> NSApplication.ModalResponse {
return NSAlert(
title: title,
message: message,
style: style
).runModal()
}

convenience init(
title: String,
message: String? = nil,
style: NSAlert.Style = .critical
) {
self.init()
self.messageText = title
self.alertStyle = style

if let message = message {
self.informativeText = message
}

// Adhere to the current app appearance
self.appearance = .app
}

var appearance: NSAppearance {
get {
return window.appearance ?? .aqua
}
set {
window.appearance = newValue
}
}

/// Runs the alert as a window-modal sheel
@discardableResult
func runModal(for window: NSWindow) -> NSApplication.ModalResponse {
beginSheetModal(for: window) { returnCode in
NSApp.stopModal(withCode: returnCode)
}

return NSApp.runModal(for: window)
}
}


extension NSView {
func copyView<T: NSView>() -> T {
return NSKeyedUnarchiver.unarchiveObject(with: NSKeyedArchiver.archivedData(withRootObject: self)) as! T
Expand Down

0 comments on commit 67cade2

Please sign in to comment.