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:
1 parent
177d2cf049
commit
fa4d1506b9
2 files changed
+120
-50
No files matched your search
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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 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 [
|
||||
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 {
|
||||
@@ -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]!
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user