feat: improved PreferencesPanel UX, partially implements #49

- ux/code: adds sliders (maxScreenUsage, iconSize, fontHeight)
- ux: defined sensible min/max/step values for sliders
- code: fixes panelWidth (panel and its content did not grow correctly)
- code: removes inputsMap (NSControl.identifier is now used for that)
- code: adds makeLabelWithProvidedControl() as a helper to create labeled controls with a optional suffix
- code: removes dropdownDidChange() & textDidEndEditing() -> combined in controlDidEndEditing()
- code: adds controlDidEndEditing() as an universal .action for NSControl elements
  - adds a restore of values in UI and Preferences if there is an issue while saving a Preference
- code: adds another little code clarity improvements
This commit is contained in:
Mariusz Bieniek authored and lwouis committed 2019-11-11 09:09:36 +09:00
1 parent fa4d1506b9
commit 59fc712086
1 file changed
+101 -61
+101 -61
View File
@@ -5,12 +5,13 @@ class PreferencesPanel: NSPanel {
let panelWidth = CGFloat(400) let panelWidth = CGFloat(400)
let panelHeight = CGFloat(400) // gets auto adjusted to content height let panelHeight = CGFloat(400) // gets auto adjusted to content height
let panelPadding = CGFloat(40) let panelPadding = CGFloat(40)
let panelWidthToLabelRatio = CGFloat(0.5)
// ui: preferences elements // ui: preferences elements
var maxScreenUsage: NSTextField? var maxScreenUsage: NSSlider?
var maxThumbnailsPerRow: NSTextField? var maxThumbnailsPerRow: NSTextField?
var iconSize: NSTextField? var iconSize: NSSlider?
var fontHeight: NSTextField? var fontHeight: NSSlider?
var tabKeyCode: NSTextField? var tabKeyCode: NSTextField?
var windowDisplayDelay: NSTextField? var windowDisplayDelay: NSTextField?
var metaKey: NSPopUpButton? var metaKey: NSPopUpButton?
@@ -18,10 +19,10 @@ class PreferencesPanel: NSPanel {
var showOnScreen: NSPopUpButton? var showOnScreen: NSPopUpButton?
var invisibleTextField: NSTextField? // default firstResponder and used for triggering of focus loose 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) let initialRect = NSRect(x: 0, y: 0, width: panelWidth, height: panelHeight)
super.init(contentRect: initialRect, styleMask: style, backing: backingStoreType, defer: flag)
title = Application.name + " Preferences" title = Application.name + " Preferences"
titlebarAppearsTransparent = true titlebarAppearsTransparent = true
hidesOnDeactivate = false hidesOnDeactivate = false
@@ -44,26 +45,26 @@ class PreferencesPanel: NSPanel {
wrappingView.alignment = .left wrappingView.alignment = .left
wrappingView.spacing = panelPadding * 0.3 wrappingView.spacing = panelPadding * 0.3
wrappingView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: panelPadding * 0.5).isActive = true 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.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: panelPadding * -0.5).isActive = true
wrappingView.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: panelPadding * 0.5).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 wrappingView.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: panelPadding * -0.5).isActive = true
return contentView return contentView
} }
private func makePreferencesViews() -> [NSView] { private func makePreferencesViews() -> [NSView] {
return [ return [
makeLabelWithDropdown(\PreferencesPanel.metaKey, "Meta key to activate the app", "metaKey", Preferences.metaKeyMacro.labels), makeLabelWithDropdown(\PreferencesPanel.metaKey, "Meta key to activate the app", rawName: "metaKey", values: Preferences.metaKeyMacro.labels),
makeLabelWithInput(\PreferencesPanel.tabKeyCode, "Tab key", "tabKeyCode", "KeyCode"), makeLabelWithInput(\PreferencesPanel.tabKeyCode, "Tab key", rawName: "tabKeyCode", suffixText: "KeyCode"),
makeHorizontalSeparator(), makeHorizontalSeparator(),
makeLabelWithDropdown(\PreferencesPanel.theme, "Main window theme", "theme", Preferences.themeMacro.labels), makeLabelWithDropdown(\PreferencesPanel.theme, "Main window theme", rawName: "theme", values: Preferences.themeMacro.labels),
makeLabelWithInput(\PreferencesPanel.maxScreenUsage, "Max window size", "maxScreenUsage", "% of screen"), makeLabelWithSlider(\PreferencesPanel.maxScreenUsage, "Max window size", rawName: "maxScreenUsage", minValue: 10, maxValue: 100, numberOfTickMarks: 10),
makeLabelWithInput(\PreferencesPanel.maxThumbnailsPerRow, "Max thumbnails per row", "maxThumbnailsPerRow"), makeLabelWithInput(\PreferencesPanel.maxThumbnailsPerRow, "Max thumbnails per row", rawName: "maxThumbnailsPerRow"),
makeLabelWithInput(\PreferencesPanel.iconSize, "Apps icon size", "iconSize", "px"), makeLabelWithSlider(\PreferencesPanel.iconSize, "Apps icon size", rawName: "iconSize", minValue: 12, maxValue: 64, numberOfTickMarks: 16),
makeLabelWithInput(\PreferencesPanel.fontHeight, "Font size", "fontHeight", "px"), makeLabelWithSlider(\PreferencesPanel.fontHeight, "Font size", rawName: "fontHeight", minValue: 12, maxValue: 36, numberOfTickMarks: 16),
makeHorizontalSeparator(), makeHorizontalSeparator(),
makeLabelWithInput(\PreferencesPanel.windowDisplayDelay, "Window apparition delay", "windowDisplayDelay", "ms"), makeLabelWithInput(\PreferencesPanel.windowDisplayDelay, "Window apparition delay", rawName: "windowDisplayDelay", suffixText: "ms"),
makeLabelWithDropdown(\PreferencesPanel.showOnScreen, "Show on", "showOnScreen", Preferences.showOnScreenMacro.labels), makeLabelWithDropdown(\PreferencesPanel.showOnScreen, "Show on", rawName: "showOnScreen", values: Preferences.showOnScreenMacro.labels),
makeHorizontalSeparator(), makeHorizontalSeparator(),
makeRestartHint() makeRestartHint()
] ]
@@ -88,7 +89,7 @@ class PreferencesPanel: NSPanel {
let field = NSTextField(wrappingLabelWithString: "Some settings require restarting the app to apply: ") let field = NSTextField(wrappingLabelWithString: "Some settings require restarting the app to apply: ")
field.textColor = .systemRed field.textColor = .systemRed
field.alignment = .right field.alignment = .right
field.widthAnchor.constraint(equalToConstant: (panelWidth - panelPadding) * 0.5).isActive = true field.widthAnchor.constraint(equalToConstant: calcLabelWidth()).isActive = true
let button = NSButton() let button = NSButton()
button.title = "↻ Restart" button.title = "↻ Restart"
@@ -101,16 +102,51 @@ class PreferencesPanel: NSPanel {
return container return container
} }
private func makeLabelWithInput(_ keyPath: ReferenceWritableKeyPath<PreferencesPanel, NSTextField?>, _ labelText: String, _ rawName: String, _ suffixText: String? = nil) -> NSStackView { private func makeLabelWithInput(_ keyPath: ReferenceWritableKeyPath<PreferencesPanel, NSTextField?>, _ labelText: String, rawName: String, suffixText: String? = nil) -> NSStackView {
let input = NSTextField(string: Preferences.rawValues[rawName]!)
input.widthAnchor.constraint(equalToConstant: 32).isActive = true
self[keyPath: keyPath] = input
return makeLabelWithProvidedControl(labelText, rawName, input, suffixText)
}
private func makeLabelWithDropdown(_ keyPath: ReferenceWritableKeyPath<PreferencesPanel, NSPopUpButton?>, _ labelText: String, rawName: String, values: [String], suffixText: String? = nil) -> NSStackView {
let popUp = NSPopUpButton()
popUp.addItems(withTitles: values)
popUp.selectItem(withTitle: Preferences.rawValues[rawName]!)
self[keyPath: keyPath] = popUp
return makeLabelWithProvidedControl(labelText, rawName, popUp, suffixText)
}
private func makeLabelWithSlider(_ keyPath: ReferenceWritableKeyPath<PreferencesPanel, NSSlider?>, _ labelText: String, rawName: String, minValue: Double, maxValue: Double, numberOfTickMarks: Int, suffixText: String? = nil) -> NSStackView {
let slider = NSSlider(
value: Double(Preferences.rawValues[rawName]!)!,
minValue: minValue,
maxValue: maxValue,
target: self,
action: #selector(controlDidEndEditing)
)
slider.numberOfTickMarks = numberOfTickMarks
slider.allowsTickMarkValuesOnly = numberOfTickMarks > 1
slider.tickMarkPosition = .below
self[keyPath: keyPath] = slider
return makeLabelWithProvidedControl(labelText, rawName, slider, suffixText)
}
private func makeLabelWithProvidedControl(_ labelText: String, _ rawName: String, _ control: NSControl, _ suffixText: String? = nil) -> NSStackView {
let label = NSTextField(wrappingLabelWithString: labelText + ": ") let label = NSTextField(wrappingLabelWithString: labelText + ": ")
label.alignment = .right label.alignment = .right
label.widthAnchor.constraint(equalToConstant: (panelWidth - panelPadding) * 0.5).isActive = true label.widthAnchor.constraint(equalToConstant: calcLabelWidth()).isActive = true
let input = NSTextField(string: Preferences.rawValues[rawName]!) control.identifier = NSUserInterfaceItemIdentifier(rawName)
input.target = self control.target = self
input.action = #selector(textDidEndEditing) control.action = #selector(controlDidEndEditing)
input.widthAnchor.constraint(equalToConstant: 32).isActive = true let containerView = NSStackView(views: [label, control])
let containerView = NSStackView(views: [label, input])
if suffixText != nil { if suffixText != nil {
let suffix = NSTextField(labelWithString: suffixText!) let suffix = NSTextField(labelWithString: suffixText!)
@@ -118,54 +154,58 @@ class PreferencesPanel: NSPanel {
containerView.addView(suffix, in: .leading) containerView.addView(suffix, in: .leading)
} }
self[keyPath: keyPath] = input
inputsMap[input] = rawName
return containerView return containerView
} }
private func makeLabelWithDropdown(_ keyPath: ReferenceWritableKeyPath<PreferencesPanel, NSPopUpButton?>, _ labelText: String, _ rawName: String, _ values: [String]) -> NSStackView { /*
let label = NSTextField(wrappingLabelWithString: labelText + ": ") usage notes:
label.alignment = .right - NSSlider: supports on purpose currently only decimal values
label.widthAnchor.constraint(equalToConstant: (panelWidth - panelPadding) * 0.5).isActive = true */
@objc func controlDidEndEditing(senderControl: NSControl) {
self.makeFirstResponder(invisibleTextField) // deselects any possibly selected NSTextField (so slider & popUp changes deselect them)
let input = NSPopUpButton() let key: String? = senderControl.identifier?.rawValue
input.addItems(withTitles: values) var newValue: String?
input.selectItem(withTitle: Preferences.rawValues[rawName]!)
input.action = #selector(dropdownDidChange)
input.target = self
self[keyPath: keyPath] = input if senderControl is NSPopUpButton {
newValue = (senderControl as! NSPopUpButton).titleOfSelectedItem!
return NSStackView(views: [label, input]) } else if senderControl is NSSlider {
} newValue = String(format: "%.0f", Double(senderControl.stringValue)!) // we are only interested in decimals of the provided double
} else {
@objc func dropdownDidChange(sender: AnyObject) throws { newValue = senderControl.stringValue
if let popUpButton = sender as? NSPopUpButton {
switch popUpButton {
case theme:
try! Preferences.updateAndValidateFromString("theme", popUpButton.titleOfSelectedItem!)
case metaKey:
try! Preferences.updateAndValidateFromString("metaKey", popUpButton.titleOfSelectedItem!)
case showOnScreen:
try! Preferences.updateAndValidateFromString("showOnScreen", popUpButton.titleOfSelectedItem!)
default:
throw "Tried to update an unknown popUpButton: '\(popUpButton)' = '\(popUpButton.titleOfSelectedItem!)'"
}
try! Preferences.saveRawToDisk()
} }
}
@objc func textDidEndEditing(sender: AnyObject) { // debugPrint("PreferencesPanel: save: change", key!, newValue!)
if let textField = sender as? NSTextField {
let key = inputsMap[textField]! if key != nil && newValue != nil {
if newValue == Preferences.rawValues[key!] {
debugPrint("PreferencesPanel: save: abort: value was not changed")
return
}
let previousValue = Preferences.rawValues[key!]!
do { do {
try Preferences.updateAndValidateFromString(key, textField.stringValue) try Preferences.updateAndValidateFromString(key!, newValue!)
try Preferences.saveRawToDisk() try Preferences.saveRawToDisk()
} catch { } catch {
debugPrint(key, error) debugPrint("PreferencesPanel: save: error", key!, error, "previousValue", previousValue, " | newValue", newValue!)
textField.stringValue = Preferences.rawValues[key]!
// restores the previous value in Preferences and senderControl
try! Preferences.updateAndValidateFromString(key!, previousValue)
if senderControl is NSPopUpButton {
(senderControl as! NSPopUpButton).selectItem(withTitle: previousValue)
} else {
senderControl.stringValue = previousValue
}
} }
} else {
debugPrint("PreferencesPanel: save: error: key||newValue = nil", key!, newValue!)
} }
} }
private func calcLabelWidth() -> CGFloat {
return (panelWidth - panelPadding) * panelWidthToLabelRatio
}
} }