Skip to content

Commit

Permalink
feat: improved PreferencesPanel UX, partially implements #49
Browse files Browse the repository at this point in the history
- ux: first pass on UI
- ux: groups available preferences by keys-, visual-, behaviour-preferences (vertical separation without labels)
- ux: adds Restart action
- code: removes unused variables
- code: PreferencesPanel is no longer a Delegate of an view child
  • Loading branch information
gingerr authored and lwouis committed Nov 11, 2019
1 parent 177d2cf commit fa4d150
Show file tree
Hide file tree
Showing 2 changed files with 120 additions and 50 deletions.
9 changes: 9 additions & 0 deletions alt-tab-macos/ui/Application.swift
Original file line number Diff line number Diff line change
Expand Up @@ -124,4 +124,13 @@ class Application: NSApplication, NSApplicationDelegate, NSWindowDelegate {
func currentlySelectedWindow() -> OpenWindow? {
return openWindows.count > selectedOpenWindow ? openWindows[selectedOpenWindow] : nil
}

func relaunch(afterDelay seconds: TimeInterval = 0.5) -> Never {
let task = Process()
task.launchPath = "/bin/sh"
task.arguments = ["-c", "sleep \(seconds); open \"\(Bundle.main.bundlePath)\""]
task.launch()
self.terminate(nil)
exit(0)
}
}
161 changes: 111 additions & 50 deletions alt-tab-macos/ui/PreferencesPanel.swift
Original file line number Diff line number Diff line change
@@ -1,82 +1,143 @@
import Cocoa

class PreferencesPanel: NSPanel, NSTextViewDelegate {
var maxScreenUsage: NSTextView?
var windowPadding: NSTextView?
var cellPadding: NSTextView?
var cellBorderWidth: NSTextView?
var maxThumbnailsPerRow: NSTextView?
var iconSize: NSTextView?
var fontHeight: NSTextView?
var interItemPadding: NSTextView?
var tabKeyCode: NSTextView?
var windowDisplayDelay: NSTextView?
class PreferencesPanel: NSPanel {
// ui: base layout
let panelWidth = CGFloat(400)
let panelHeight = CGFloat(400) // gets auto adjusted to content height
let panelPadding = CGFloat(40)

// ui: preferences elements
var maxScreenUsage: NSTextField?
var maxThumbnailsPerRow: NSTextField?
var iconSize: NSTextField?
var fontHeight: NSTextField?
var tabKeyCode: NSTextField?
var windowDisplayDelay: NSTextField?
var metaKey: NSPopUpButton?
var theme: NSPopUpButton?
var showOnScreen: NSPopUpButton?
var inputsMap = [NSTextView: String]()

var invisibleTextField: NSTextField? // default firstResponder and used for triggering of focus loose
var inputsMap = [NSTextField: String]()

override init(contentRect: NSRect, styleMask style: StyleMask, backing backingStoreType: BackingStoreType, defer flag: Bool) {
super.init(contentRect: contentRect, styleMask: style, backing: backingStoreType, defer: flag)
title = Application.name + " Preferences"
titlebarAppearsTransparent = true
hidesOnDeactivate = false
contentView = makeGridView(makeLabelsAndInputs(), makeWarningLabel())
contentView = makeContentView()

// setup hidden element
invisibleTextField = NSTextField()
invisibleTextField?.isHidden = true
contentView?.subviews.append(invisibleTextField!)
self.initialFirstResponder = invisibleTextField
}

private func makeContentView() -> NSView {
let wrappingView = NSStackView(views: makePreferencesViews())
let contentView = NSView()
contentView.addSubview(wrappingView)

// visual setup
wrappingView.orientation = .vertical
wrappingView.alignment = .left
wrappingView.spacing = panelPadding * 0.3
wrappingView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: panelPadding * 0.5).isActive = true
wrappingView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: panelPadding * 0.5 * -1).isActive = true
wrappingView.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: panelPadding * 0.5).isActive = true
wrappingView.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: panelPadding * 0.5 * -1).isActive = true

return contentView
}

private func makeLabelsAndInputs() -> [[NSView]] {
private func makePreferencesViews() -> [NSView] {
return [
makeLabelWithDropdown(\PreferencesPanel.theme, "Main window theme", "theme", Preferences.themeMacro.labels),
makeLabelWithDropdown(\PreferencesPanel.metaKey, "Meta key to activate the app", "metaKey", Preferences.metaKeyMacro.labels),
makeLabelWithInput(\PreferencesPanel.tabKeyCode, "Tab key (HIToolbox.Events)", "tabKeyCode"),
makeLabelWithInput(\PreferencesPanel.maxScreenUsage, "Max window size (screen %)", "maxScreenUsage"),
makeLabelWithInput(\PreferencesPanel.tabKeyCode, "Tab key", "tabKeyCode", "KeyCode"),
makeHorizontalSeparator(),
makeLabelWithDropdown(\PreferencesPanel.theme, "Main window theme", "theme", Preferences.themeMacro.labels),
makeLabelWithInput(\PreferencesPanel.maxScreenUsage, "Max window size", "maxScreenUsage", "% of screen"),
makeLabelWithInput(\PreferencesPanel.maxThumbnailsPerRow, "Max thumbnails per row", "maxThumbnailsPerRow"),
makeLabelWithInput(\PreferencesPanel.iconSize, "Apps icon size (px)", "iconSize"),
makeLabelWithInput(\PreferencesPanel.fontHeight, "Font size (px)", "fontHeight"),
makeLabelWithInput(\PreferencesPanel.windowDisplayDelay, "Window apparition delay (ms)", "windowDisplayDelay"),
makeLabelWithDropdown(\PreferencesPanel.showOnScreen, "Show on", "showOnScreen", Preferences.showOnScreenMacro.labels)
makeLabelWithInput(\PreferencesPanel.iconSize, "Apps icon size", "iconSize", "px"),
makeLabelWithInput(\PreferencesPanel.fontHeight, "Font size", "fontHeight", "px"),
makeHorizontalSeparator(),
makeLabelWithInput(\PreferencesPanel.windowDisplayDelay, "Window apparition delay", "windowDisplayDelay", "ms"),
makeLabelWithDropdown(\PreferencesPanel.showOnScreen, "Show on", "showOnScreen", Preferences.showOnScreenMacro.labels),
makeHorizontalSeparator(),
makeRestartHint()
]
}

private func makeGridView(_ rows: [[NSView]], _ warningLabel: BaseLabel) -> NSGridView {
let gridView = NSGridView(views: rows)
gridView.setContentHuggingPriority(.defaultLow, for: .horizontal)
gridView.setContentHuggingPriority(.defaultLow, for: .vertical)
gridView.widthAnchor.constraint(greaterThanOrEqualToConstant: 380).isActive = true
gridView.addRow(with: [warningLabel, NSGridCell.emptyContentView])
gridView.mergeCells(inHorizontalRange: NSRange(location: 0, length: 2), verticalRange: NSRange(location: gridView.numberOfRows - 1, length: 1))
return gridView
private func makeHorizontalSeparator() -> NSView {
let view = NSBox()
view.boxType = .separator

return view
}

private func makeWarningLabel() -> BaseLabel {
let warningLabel = BaseLabel("Some settings require restarting the app to apply")
warningLabel.textColor = .systemRed
warningLabel.alignment = .center
return warningLabel
@objc private func restartButtonAction() {
self.makeFirstResponder(invisibleTextField)

if let delegate = Application.shared.delegate as? Application {
delegate.relaunch()
}
}

private func makeLabelWithInput(_ keyPath: ReferenceWritableKeyPath<PreferencesPanel, NSTextView?>, _ labelText: String, _ rawName: String) -> [NSTextView] {
let label = BaseLabel(labelText)
private func makeRestartHint() -> NSStackView {
let field = NSTextField(wrappingLabelWithString: "Some settings require restarting the app to apply: ")
field.textColor = .systemRed
field.alignment = .right
field.widthAnchor.constraint(equalToConstant: (panelWidth - panelPadding) * 0.5).isActive = true

let button = NSButton()
button.title = "↻ Restart"
button.bezelStyle = .rounded
button.target = self
button.action = #selector(restartButtonAction)

let container = NSStackView(views: [field, button])
container.alignment = .bottom
return container
}

private func makeLabelWithInput(_ keyPath: ReferenceWritableKeyPath<PreferencesPanel, NSTextField?>, _ labelText: String, _ rawName: String, _ suffixText: String? = nil) -> NSStackView {
let label = NSTextField(wrappingLabelWithString: labelText + ": ")
label.alignment = .right
let input = NSTextView()
input.delegate = self
input.font = Preferences.font
input.string = Preferences.rawValues[rawName]!
input.widthAnchor.constraint(equalToConstant: 40).isActive = true
label.widthAnchor.constraint(equalToConstant: (panelWidth - panelPadding) * 0.5).isActive = true

let input = NSTextField(string: Preferences.rawValues[rawName]!)
input.target = self
input.action = #selector(textDidEndEditing)
input.widthAnchor.constraint(equalToConstant: 32).isActive = true
let containerView = NSStackView(views: [label, input])

if suffixText != nil {
let suffix = NSTextField(labelWithString: suffixText!)
suffix.textColor = .gray
containerView.addView(suffix, in: .leading)
}

self[keyPath: keyPath] = input
inputsMap[input] = rawName
return [label, input]

return containerView
}

private func makeLabelWithDropdown(_ keyPath: ReferenceWritableKeyPath<PreferencesPanel, NSPopUpButton?>, _ labelText: String, _ rawName: String, _ values: [String]) -> [NSView] {
let label = BaseLabel(labelText)
private func makeLabelWithDropdown(_ keyPath: ReferenceWritableKeyPath<PreferencesPanel, NSPopUpButton?>, _ labelText: String, _ rawName: String, _ values: [String]) -> NSStackView {
let label = NSTextField(wrappingLabelWithString: labelText + ": ")
label.alignment = .right
label.widthAnchor.constraint(equalToConstant: (panelWidth - panelPadding) * 0.5).isActive = true

let input = NSPopUpButton()
input.addItems(withTitles: values)
input.selectItem(withTitle: Preferences.rawValues[rawName]!)
input.action = #selector(dropdownDidChange)
input.target = self

self[keyPath: keyPath] = input
return [label, input]

return NSStackView(views: [label, input])
}

@objc func dropdownDidChange(sender: AnyObject) throws {
Expand All @@ -95,15 +156,15 @@ class PreferencesPanel: NSPanel, NSTextViewDelegate {
}
}

func textDidEndEditing(_ notification: Notification) {
if let textView = notification.object as? NSTextView {
let key = inputsMap[textView]!
@objc func textDidEndEditing(sender: AnyObject) {
if let textField = sender as? NSTextField {
let key = inputsMap[textField]!
do {
try Preferences.updateAndValidateFromString(key, textView.string)
try Preferences.updateAndValidateFromString(key, textField.stringValue)
try Preferences.saveRawToDisk()
} catch {
debugPrint(key, error)
textView.string = Preferences.rawValues[key]!
textField.stringValue = Preferences.rawValues[key]!
}
}
}
Expand Down

0 comments on commit fa4d150

Please sign in to comment.