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:
1 parent
fa4d1506b9
commit
59fc712086
1 file changed
+101
-61
@@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user