feat: improved PreferencesPanel UX, partially implements #49

- 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
This commit is contained in:
Mariusz Bieniek authored and lwouis committed 2019-11-11 09:09:36 +09:00
1 parent 177d2cf049
commit fa4d1506b9
2 files changed
+120 -50

No files matched your search

+9
View File
@@ -124,4 +124,13 @@ class Application: NSApplication, NSApplicationDelegate, NSWindowDelegate {
func currentlySelectedWindow() -> OpenWindow? { func currentlySelectedWindow() -> OpenWindow? {
return openWindows.count > selectedOpenWindow ? openWindows[selectedOpenWindow] : nil 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)
}
} }
+111 -50
View File
@@ -1,82 +1,143 @@
import Cocoa import Cocoa
class PreferencesPanel: NSPanel, NSTextViewDelegate { class PreferencesPanel: NSPanel {
var maxScreenUsage: NSTextView? // ui: base layout
var windowPadding: NSTextView? let panelWidth = CGFloat(400)
var cellPadding: NSTextView? let panelHeight = CGFloat(400) // gets auto adjusted to content height
var cellBorderWidth: NSTextView? let panelPadding = CGFloat(40)
var maxThumbnailsPerRow: NSTextView?
var iconSize: NSTextView? // ui: preferences elements
var fontHeight: NSTextView? var maxScreenUsage: NSTextField?
var interItemPadding: NSTextView? var maxThumbnailsPerRow: NSTextField?
var tabKeyCode: NSTextView? var iconSize: NSTextField?
var windowDisplayDelay: NSTextView? var fontHeight: NSTextField?
var tabKeyCode: NSTextField?
var windowDisplayDelay: NSTextField?
var metaKey: NSPopUpButton? var metaKey: NSPopUpButton?
var theme: NSPopUpButton? var theme: NSPopUpButton?
var showOnScreen: 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) { override init(contentRect: NSRect, styleMask style: StyleMask, backing backingStoreType: BackingStoreType, defer flag: Bool) {
super.init(contentRect: contentRect, styleMask: style, backing: backingStoreType, defer: flag) super.init(contentRect: contentRect, styleMask: style, backing: backingStoreType, defer: flag)
title = Application.name + " Preferences" title = Application.name + " Preferences"
titlebarAppearsTransparent = true
hidesOnDeactivate = false 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 makeLabelsAndInputs() -> [[NSView]] { 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 makePreferencesViews() -> [NSView] {
return [ return [
makeLabelWithDropdown(\PreferencesPanel.theme, "Main window theme", "theme", Preferences.themeMacro.labels),
makeLabelWithDropdown(\PreferencesPanel.metaKey, "Meta key to activate the app", "metaKey", Preferences.metaKeyMacro.labels), makeLabelWithDropdown(\PreferencesPanel.metaKey, "Meta key to activate the app", "metaKey", Preferences.metaKeyMacro.labels),
makeLabelWithInput(\PreferencesPanel.tabKeyCode, "Tab key (HIToolbox.Events)", "tabKeyCode"), makeLabelWithInput(\PreferencesPanel.tabKeyCode, "Tab key", "tabKeyCode", "KeyCode"),
makeLabelWithInput(\PreferencesPanel.maxScreenUsage, "Max window size (screen %)", "maxScreenUsage"), 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.maxThumbnailsPerRow, "Max thumbnails per row", "maxThumbnailsPerRow"),
makeLabelWithInput(\PreferencesPanel.iconSize, "Apps icon size (px)", "iconSize"), makeLabelWithInput(\PreferencesPanel.iconSize, "Apps icon size", "iconSize", "px"),
makeLabelWithInput(\PreferencesPanel.fontHeight, "Font size (px)", "fontHeight"), makeLabelWithInput(\PreferencesPanel.fontHeight, "Font size", "fontHeight", "px"),
makeLabelWithInput(\PreferencesPanel.windowDisplayDelay, "Window apparition delay (ms)", "windowDisplayDelay"), makeHorizontalSeparator(),
makeLabelWithDropdown(\PreferencesPanel.showOnScreen, "Show on", "showOnScreen", Preferences.showOnScreenMacro.labels) 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 { private func makeHorizontalSeparator() -> NSView {
let gridView = NSGridView(views: rows) let view = NSBox()
gridView.setContentHuggingPriority(.defaultLow, for: .horizontal) view.boxType = .separator
gridView.setContentHuggingPriority(.defaultLow, for: .vertical)
gridView.widthAnchor.constraint(greaterThanOrEqualToConstant: 380).isActive = true return view
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 makeWarningLabel() -> BaseLabel { @objc private func restartButtonAction() {
let warningLabel = BaseLabel("Some settings require restarting the app to apply") self.makeFirstResponder(invisibleTextField)
warningLabel.textColor = .systemRed
warningLabel.alignment = .center if let delegate = Application.shared.delegate as? Application {
return warningLabel delegate.relaunch()
}
} }
private func makeLabelWithInput(_ keyPath: ReferenceWritableKeyPath<PreferencesPanel, NSTextView?>, _ labelText: String, _ rawName: String) -> [NSTextView] { private func makeRestartHint() -> NSStackView {
let label = BaseLabel(labelText) 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 label.alignment = .right
let input = NSTextView() label.widthAnchor.constraint(equalToConstant: (panelWidth - panelPadding) * 0.5).isActive = true
input.delegate = self
input.font = Preferences.font let input = NSTextField(string: Preferences.rawValues[rawName]!)
input.string = Preferences.rawValues[rawName]! input.target = self
input.widthAnchor.constraint(equalToConstant: 40).isActive = true 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 self[keyPath: keyPath] = input
inputsMap[input] = rawName inputsMap[input] = rawName
return [label, input]
return containerView
} }
private func makeLabelWithDropdown(_ keyPath: ReferenceWritableKeyPath<PreferencesPanel, NSPopUpButton?>, _ labelText: String, _ rawName: String, _ values: [String]) -> [NSView] { private func makeLabelWithDropdown(_ keyPath: ReferenceWritableKeyPath<PreferencesPanel, NSPopUpButton?>, _ labelText: String, _ rawName: String, _ values: [String]) -> NSStackView {
let label = BaseLabel(labelText) let label = NSTextField(wrappingLabelWithString: labelText + ": ")
label.alignment = .right label.alignment = .right
label.widthAnchor.constraint(equalToConstant: (panelWidth - panelPadding) * 0.5).isActive = true
let input = NSPopUpButton() let input = NSPopUpButton()
input.addItems(withTitles: values) input.addItems(withTitles: values)
input.selectItem(withTitle: Preferences.rawValues[rawName]!) input.selectItem(withTitle: Preferences.rawValues[rawName]!)
input.action = #selector(dropdownDidChange) input.action = #selector(dropdownDidChange)
input.target = self input.target = self
self[keyPath: keyPath] = input self[keyPath: keyPath] = input
return [label, input]
return NSStackView(views: [label, input])
} }
@objc func dropdownDidChange(sender: AnyObject) throws { @objc func dropdownDidChange(sender: AnyObject) throws {
@@ -95,15 +156,15 @@ class PreferencesPanel: NSPanel, NSTextViewDelegate {
} }
} }
func textDidEndEditing(_ notification: Notification) { @objc func textDidEndEditing(sender: AnyObject) {
if let textView = notification.object as? NSTextView { if let textField = sender as? NSTextField {
let key = inputsMap[textView]! let key = inputsMap[textField]!
do { do {
try Preferences.updateAndValidateFromString(key, textView.string) try Preferences.updateAndValidateFromString(key, textField.stringValue)
try Preferences.saveRawToDisk() try Preferences.saveRawToDisk()
} catch { } catch {
debugPrint(key, error) debugPrint(key, error)
textView.string = Preferences.rawValues[key]! textField.stringValue = Preferences.rawValues[key]!
} }
} }
} }