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? {
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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]!
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user