feat: improves PreferencesPanel UX, partially implements #49

implements requested changes from Code-Review and contains also refactoring and more features and improvements
- Application Wide: changes all exceptions to NSError (required for error handling via sheet in panel)
- PreferencesPanel: adds display of current value of Sliders
- PreferencesPanel: adds validation with visualisation for TextFields (whitelist for KeyCode and ranges for maxScreenUsage & windowDisplayDelay) during editing, window open and close
- PreferencesPanel: adds error sheet (in-window modal) at all known possible errors to display error information and ask the user how to proceed
- PreferencesPanel: removes Restart button (and relaunch code in Application)
- PreferencesPanel: adds Preferences being now live
- PreferencesPanel: removes invisibleText workaround
- PreferencesPanel: adds TextField value save on each keyDown event (if valid and changed)
- PreferencesPanel: adds window activation/deactivation
- PreferencesPanel: changes label texts of some Preferences
- ...
This commit is contained in:
Mariusz Bieniek authored and lwouis committed 2019-11-11 09:09:36 +09:00
1 parent 59fc712086
commit 65327c2b89
6 files changed
+247 -140

No files matched your search

+4
View File
@@ -21,6 +21,7 @@
D04BA9CCE02D30C8164A552A /* SystemPermissions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BA2D2AD6B1CCA3F3A4DD7 /* SystemPermissions.swift */; }; D04BA9CCE02D30C8164A552A /* SystemPermissions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BA2D2AD6B1CCA3F3A4DD7 /* SystemPermissions.swift */; };
D04BAD4DE538FDF7E7532EE2 /* Labels.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BAD32E130E4A061DC8332 /* Labels.swift */; }; D04BAD4DE538FDF7E7532EE2 /* Labels.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BAD32E130E4A061DC8332 /* Labels.swift */; };
D04BAEF78503D7A2CEFB9E9E /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BAA44C837F3A67403B9DB /* main.swift */; }; D04BAEF78503D7A2CEFB9E9E /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BAA44C837F3A67403B9DB /* main.swift */; };
F02981D5D1E1F62074801CAE /* PreferencesPanelNSTextFieldEditable.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0298F4D7927608A986C320D /* PreferencesPanelNSTextFieldEditable.swift */; };
/* End PBXBuildFile section */ /* End PBXBuildFile section */
/* Begin PBXFileReference section */ /* Begin PBXFileReference section */
@@ -72,6 +73,7 @@
D04BAF076A30A1BAFEDBEA66 /* 5 windows - 2 lines.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = "5 windows - 2 lines.jpg"; sourceTree = "<group>"; }; D04BAF076A30A1BAFEDBEA66 /* 5 windows - 2 lines.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = "5 windows - 2 lines.jpg"; sourceTree = "<group>"; };
D04BAF249324297C07E31164 /* frontpage.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = frontpage.jpg; sourceTree = "<group>"; }; D04BAF249324297C07E31164 /* frontpage.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = frontpage.jpg; sourceTree = "<group>"; };
D04BAFA277EAE3BDDDB61110 /* CHANGELOG.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = CHANGELOG.md; sourceTree = "<group>"; }; D04BAFA277EAE3BDDDB61110 /* CHANGELOG.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = CHANGELOG.md; sourceTree = "<group>"; };
F0298F4D7927608A986C320D /* PreferencesPanelNSTextFieldEditable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PreferencesPanelNSTextFieldEditable.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */ /* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */ /* Begin PBXFrameworksBuildPhase section */
@@ -172,6 +174,7 @@
D04BA3F15EAE8D8C39B6F2CF /* Screen.swift */, D04BA3F15EAE8D8C39B6F2CF /* Screen.swift */,
D04BA2D2AD6B1CCA3F3A4DD7 /* SystemPermissions.swift */, D04BA2D2AD6B1CCA3F3A4DD7 /* SystemPermissions.swift */,
D04BA86768C6503A11ED81FC /* Extensions.swift */, D04BA86768C6503A11ED81FC /* Extensions.swift */,
F0298F4D7927608A986C320D /* PreferencesPanelNSTextFieldEditable.swift */,
); );
path = logic; path = logic;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -302,6 +305,7 @@
D04BA02DD4152997C32CF50B /* StatusItem.swift in Sources */, D04BA02DD4152997C32CF50B /* StatusItem.swift in Sources */,
D04BA0F3D46BC79544E2B930 /* Extensions.swift in Sources */, D04BA0F3D46BC79544E2B930 /* Extensions.swift in Sources */,
D04BAD4DE538FDF7E7532EE2 /* Labels.swift in Sources */, D04BAD4DE538FDF7E7532EE2 /* Labels.swift in Sources */,
F02981D5D1E1F62074801CAE /* PreferencesPanelNSTextFieldEditable.swift in Sources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
+23 -5
View File
@@ -19,15 +19,11 @@ extension Optional {
return value return value
case .none: case .none:
Thread.callStackSymbols.forEach { print($0) } Thread.callStackSymbols.forEach { print($0) }
throw "Optional contained nil" throw NSError.make(domain: "Optional", message: "Optional contained nil")
} }
} }
} }
// allow String to be treated as Error (e.g. throw "explanation")
extension String: Error {
}
// add String constructor from CGFloat that round up at 1 decimal // add String constructor from CGFloat that round up at 1 decimal
extension String { extension String {
init?(_ cgFloat: CGFloat) { init?(_ cgFloat: CGFloat) {
@@ -39,3 +35,25 @@ extension String {
self.init(string) self.init(string)
} }
} }
// add recursive lookup in subviews for specific type
extension NSView {
func findNestedViews<T: NSView>(subclassOf: T.Type) -> [T] {
return recursiveSubviews.compactMap { $0 as? T }
}
var recursiveSubviews: [NSView] {
return subviews + subviews.flatMap { $0.recursiveSubviews }
}
}
// add convenience to NSError
extension NSError {
class func make(domain: String, message: String, code: Int = 9999) -> NSError {
return NSError(
domain: domain,
code: code,
userInfo: [NSLocalizedDescriptionKey: message, NSLocalizedFailureReasonErrorKey: message]
)
}
}
+1 -1
View File
@@ -102,7 +102,7 @@ class Preferences {
let p = try showOnScreenMacro.labelToMacro[value].orThrow() let p = try showOnScreenMacro.labelToMacro[value].orThrow()
showOnScreen = p.preferences showOnScreen = p.preferences
default: default:
throw "Tried to update an unknown preference: '\(valueName)' = '\(value)'" throw NSError.make(domain: "Preferences", message: "Tried to update an unknown preference: '\(valueName)' = '\(value)'")
} }
rawValues[valueName] = value rawValues[valueName] = value
} }
@@ -0,0 +1,34 @@
import Cocoa
class PreferencesPanelNSTextFieldEditable: NSTextField, NSTextFieldDelegate {
var validationHandler: ((String)->Bool)?
// protocol method
func controlTextDidChange(_ obj: Notification) {
visualizeValidationState(isValid())
let textField = obj.object as! PreferencesPanelNSTextFieldEditable
sendAction(textField.action, to: textField.target)
}
// custom method
func visualizeValidationState(_ isValid: Bool) -> Void {
if !isValid {
wantsLayer = true
layer?.borderColor = NSColor.systemRed.cgColor
layer?.borderWidth = 1
} else {
wantsLayer = false
}
}
// custom method
func isValid() -> Bool {
if let handler = validationHandler {
return handler(stringValue)
}
return true
}
}
+11 -12
View File
@@ -4,7 +4,6 @@ import Cocoa
class Application: NSApplication, NSApplicationDelegate, NSWindowDelegate { class Application: NSApplication, NSApplicationDelegate, NSWindowDelegate {
static let name = "AltTab" static let name = "AltTab"
var statusItem: NSStatusItem? var statusItem: NSStatusItem?
var backgroundView: NSVisualEffectView?
var thumbnailsPanel: ThumbnailsPanel? var thumbnailsPanel: ThumbnailsPanel?
var preferencesPanel: PreferencesPanel? var preferencesPanel: PreferencesPanel?
var selectedOpenWindow: Int = 0 var selectedOpenWindow: Int = 0
@@ -28,11 +27,15 @@ class Application: NSApplication, NSApplicationDelegate, NSWindowDelegate {
SystemPermissions.ensureAccessibilityCheckboxIsChecked() SystemPermissions.ensureAccessibilityCheckboxIsChecked()
Preferences.loadFromDiskAndUpdateValues() Preferences.loadFromDiskAndUpdateValues()
statusItem = StatusItem.make(self) statusItem = StatusItem.make(self)
thumbnailsPanel = ThumbnailsPanel(self) initPreferencesDependentComponents()
preferencesPanel = PreferencesPanel()
Keyboard.listenToGlobalEvents(self) Keyboard.listenToGlobalEvents(self)
} }
// we put application code here which should be executed on init() and Preferences change
func initPreferencesDependentComponents() {
thumbnailsPanel = ThumbnailsPanel(self)
}
func showUiOrSelectNext() { func showUiOrSelectNext() {
debugPrint("showUiOrSelectNext") debugPrint("showUiOrSelectNext")
showUiOrCycleSelection(1) showUiOrCycleSelection(1)
@@ -58,7 +61,11 @@ class Application: NSApplication, NSApplicationDelegate, NSWindowDelegate {
} }
} }
@objc func showPreferencesPanel() { @objc
func showPreferencesPanel() {
if preferencesPanel == nil {
preferencesPanel = PreferencesPanel()
}
Screen.showPanel(preferencesPanel!, Screen.preferredScreen(), .appleCentered) Screen.showPanel(preferencesPanel!, Screen.preferredScreen(), .appleCentered)
} }
@@ -125,12 +132,4 @@ class Application: NSApplication, NSApplicationDelegate, NSWindowDelegate {
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)
}
} }
+174 -122
View File
@@ -1,38 +1,48 @@
import Cocoa import Cocoa
import Foundation
class PreferencesPanel: NSPanel { class PreferencesPanel: NSPanel, NSWindowDelegate {
// ui: base layout let panelWidth = CGFloat(496)
let panelWidth = CGFloat(400) let panelHeight = CGFloat(256) // auto expands to content height (but does not auto shrink)
let panelHeight = CGFloat(400) // gets auto adjusted to content height
let panelPadding = CGFloat(40) let panelPadding = CGFloat(40)
let panelWidthToLabelRatio = CGFloat(0.5) var labelWidth: CGFloat { get { // derived convenience variable
return (panelWidth - panelPadding) * CGFloat(0.45)
// ui: preferences elements }}
var maxScreenUsage: NSSlider? var windowCloseRequested = false
var maxThumbnailsPerRow: NSTextField?
var iconSize: NSSlider?
var fontHeight: NSSlider?
var tabKeyCode: NSTextField?
var windowDisplayDelay: NSTextField?
var metaKey: NSPopUpButton?
var theme: NSPopUpButton?
var showOnScreen: NSPopUpButton?
var invisibleTextField: NSTextField? // default firstResponder and used for triggering of focus loose
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) {
let initialRect = NSRect(x: 0, y: 0, width: panelWidth, height: panelHeight) let initialRect = NSRect(x: 0, y: 0, width: panelWidth, height: panelHeight)
super.init(contentRect: initialRect, styleMask: style, backing: backingStoreType, defer: flag) super.init(contentRect: initialRect, styleMask: style, backing: backingStoreType, defer: flag)
title = Application.name + " Preferences" title = Application.name + " Preferences"
titlebarAppearsTransparent = true
hidesOnDeactivate = false hidesOnDeactivate = false
contentView = makeContentView() contentView = makeContentView()
NSApp.activate(ignoringOtherApps: true)
}
// setup hidden element override func close() {
invisibleTextField = NSTextField() NSApp.deactivate()
invisibleTextField?.isHidden = true (NSApp as! Application).preferencesPanel = nil
contentView?.subviews.append(invisibleTextField!) super.close()
self.initialFirstResponder = invisibleTextField }
public func windowShouldClose(_ sender: NSWindow) -> Bool {
windowCloseRequested = true
challengeNextInvalidEditableTextField()
return attachedSheet == nil // user is challenged
}
private func challengeNextInvalidEditableTextField() {
let invalidFields = (contentView?
.findNestedViews(subclassOf: PreferencesPanelNSTextFieldEditable.self)
.filter({ !$0.isValid() })
)
let focusedField = invalidFields?.filter({ $0.currentEditor() != nil }).first
let fieldToNotify = focusedField ?? invalidFields?.first
fieldToNotify?.delegate?.controlTextDidChange?(Notification(name: NSControl.textDidChangeNotification, object: fieldToNotify))
if fieldToNotify != focusedField {
makeFirstResponder(fieldToNotify)
}
} }
private func makeContentView() -> NSView { private func makeContentView() -> NSView {
@@ -53,20 +63,34 @@ class PreferencesPanel: NSPanel {
} }
private func makePreferencesViews() -> [NSView] { private func makePreferencesViews() -> [NSView] {
// TODO: make the validators be a part of each Preference
let tabKeyCodeValidator: ((String)->Bool) = {
guard let int = Int($0) else {
return false
}
// non-special keys (mac & pc keyboards): https://eastmanreference.com/complete-list-of-applescript-key-codes
var whitelistedKeycodes: [Int] = Array(0...53)
whitelistedKeycodes.append(contentsOf: [65, 67, 69, 75, 76, 78, ])
whitelistedKeycodes.append(contentsOf: Array(81...89))
whitelistedKeycodes.append(contentsOf: [91, 92, 115, 116, 117, 119, 121])
whitelistedKeycodes.append(contentsOf: Array(123...126))
return whitelistedKeycodes.contains(int)
}
let maxThumbnailsPerRowValidator: ((String)->Bool) = { (3...16).contains(Int($0) ?? -1) }
let windowDisplayDelayValidator: ((String)->Bool) = { (0..<10000).contains(Int($0) ?? -1) }
return [ return [
makeLabelWithDropdown(\PreferencesPanel.metaKey, "Meta key to activate the app", rawName: "metaKey", values: Preferences.metaKeyMacro.labels), makeLabelWithDropdown("Alt key", rawName: "metaKey", values: Preferences.metaKeyMacro.labels),
makeLabelWithInput(\PreferencesPanel.tabKeyCode, "Tab key", rawName: "tabKeyCode", suffixText: "KeyCode"), makeLabelWithInput("Tab key", rawName: "tabKeyCode", width: 33, suffixText: "KeyCode (48 = Tab)", validator: tabKeyCodeValidator),
makeHorizontalSeparator(), makeHorizontalSeparator(),
makeLabelWithDropdown(\PreferencesPanel.theme, "Main window theme", rawName: "theme", values: Preferences.themeMacro.labels), makeLabelWithDropdown("Theme", rawName: "theme", values: Preferences.themeMacro.labels),
makeLabelWithSlider(\PreferencesPanel.maxScreenUsage, "Max window size", rawName: "maxScreenUsage", minValue: 10, maxValue: 100, numberOfTickMarks: 10), makeLabelWithSlider("Max screen usage", rawName: "maxScreenUsage", minValue: 10, maxValue: 100, numberOfTickMarks: 10, unitText: "%"),
makeLabelWithInput(\PreferencesPanel.maxThumbnailsPerRow, "Max thumbnails per row", rawName: "maxThumbnailsPerRow"), makeLabelWithInput("Max thumbnails per row", rawName: "maxThumbnailsPerRow", width: 25, validator: maxThumbnailsPerRowValidator),
makeLabelWithSlider(\PreferencesPanel.iconSize, "Apps icon size", rawName: "iconSize", minValue: 12, maxValue: 64, numberOfTickMarks: 16), makeLabelWithSlider("Apps icon size", rawName: "iconSize", minValue: 12, maxValue: 62, numberOfTickMarks: 16, unitText: "px"),
makeLabelWithSlider(\PreferencesPanel.fontHeight, "Font size", rawName: "fontHeight", minValue: 12, maxValue: 36, numberOfTickMarks: 16), makeLabelWithSlider("Window font size", rawName: "fontHeight", minValue: 12, maxValue: 36, numberOfTickMarks: 16, unitText: "px"),
makeHorizontalSeparator(), makeHorizontalSeparator(),
makeLabelWithInput(\PreferencesPanel.windowDisplayDelay, "Window apparition delay", rawName: "windowDisplayDelay", suffixText: "ms"), makeLabelWithInput("Window apparition delay", rawName: "windowDisplayDelay", width: 41, suffixText: "ms", validator: windowDisplayDelayValidator),
makeLabelWithDropdown(\PreferencesPanel.showOnScreen, "Show on", rawName: "showOnScreen", values: Preferences.showOnScreenMacro.labels), makeLabelWithDropdown("Show on", rawName: "showOnScreen", values: Preferences.showOnScreenMacro.labels)
makeHorizontalSeparator(),
makeRestartHint()
] ]
} }
@@ -77,135 +101,163 @@ class PreferencesPanel: NSPanel {
return view return view
} }
@objc private func restartButtonAction() { private func makeLabelWithInput(_ labelText: String, rawName: String, width: CGFloat? = nil, suffixText: String? = nil, validator: ((String)->Bool)? = nil) -> NSStackView {
self.makeFirstResponder(invisibleTextField) let input = PreferencesPanelNSTextFieldEditable(string: Preferences.rawValues[rawName]!)
input.validationHandler = validator
if let delegate = Application.shared.delegate as? Application { input.delegate = input
delegate.relaunch() input.visualizeValidationState(input.isValid())
if width != nil {
input.widthAnchor.constraint(equalToConstant: width!).isActive = true
} }
return makeLabelWithProvidedControl(labelText, rawName: rawName, control: input, suffixText: suffixText)
} }
private func makeRestartHint() -> NSStackView { private func makeLabelWithDropdown(_ labelText: String, rawName: String, values: [String], suffixText: String? = nil) -> NSStackView {
let field = NSTextField(wrappingLabelWithString: "Some settings require restarting the app to apply: ")
field.textColor = .systemRed
field.alignment = .right
field.widthAnchor.constraint(equalToConstant: calcLabelWidth()).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 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() let popUp = NSPopUpButton()
popUp.addItems(withTitles: values) popUp.addItems(withTitles: values)
popUp.selectItem(withTitle: Preferences.rawValues[rawName]!) popUp.selectItem(withTitle: Preferences.rawValues[rawName]!)
self[keyPath: keyPath] = popUp return makeLabelWithProvidedControl(labelText, rawName: rawName, control: popUp, suffixText: suffixText)
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 { private func makeLabelWithSlider(_ labelText: String, rawName: String, minValue: Double, maxValue: Double, numberOfTickMarks: Int, unitText: String) -> NSStackView {
let slider = NSSlider( let value = Preferences.rawValues[rawName]!
value: Double(Preferences.rawValues[rawName]!)!, let suffixText = value + unitText
minValue: minValue, let slider = NSSlider()
maxValue: maxValue, slider.minValue = minValue
target: self, slider.maxValue = maxValue
action: #selector(controlDidEndEditing) slider.stringValue = value
)
slider.numberOfTickMarks = numberOfTickMarks slider.numberOfTickMarks = numberOfTickMarks
slider.allowsTickMarkValuesOnly = numberOfTickMarks > 1 slider.allowsTickMarkValuesOnly = numberOfTickMarks > 1
slider.tickMarkPosition = .below slider.tickMarkPosition = .below
slider.isContinuous = true
self[keyPath: keyPath] = slider return makeLabelWithProvidedControl(labelText, rawName: rawName, control: slider, suffixText: suffixText, suffixWidth: 40)
return makeLabelWithProvidedControl(labelText, rawName, slider, suffixText)
} }
private func makeLabelWithProvidedControl(_ labelText: String, _ rawName: String, _ control: NSControl, _ suffixText: String? = nil) -> NSStackView { private func makeLabelWithProvidedControl(_ labelText: String, rawName: String, control: NSControl, suffixText: String? = nil, suffixWidth: CGFloat? = nil) -> NSStackView {
let label = NSTextField(wrappingLabelWithString: labelText + ": ") let label = NSTextField(wrappingLabelWithString: labelText + ": ")
label.alignment = .right label.alignment = .right
label.widthAnchor.constraint(equalToConstant: calcLabelWidth()).isActive = true label.widthAnchor.constraint(equalToConstant: labelWidth).isActive = true
label.identifier = NSUserInterfaceItemIdentifier(rawName + ControlIdentifierDiscriminator.LABEL.rawValue)
label.isSelectable = false
control.identifier = NSUserInterfaceItemIdentifier(rawName) control.identifier = NSUserInterfaceItemIdentifier(rawName)
control.target = self control.target = self
control.action = #selector(controlDidEndEditing) control.action = #selector(controlWasChanged)
let containerView = NSStackView(views: [label, control]) let containerView = NSStackView(views: [label, control])
if suffixText != nil { if suffixText != nil {
let suffix = NSTextField(labelWithString: suffixText!) let suffix = NSTextField(labelWithString: suffixText!)
suffix.textColor = .gray suffix.textColor = .gray
suffix.identifier = NSUserInterfaceItemIdentifier(rawName + ControlIdentifierDiscriminator.SUFFIX.rawValue)
if suffixWidth != nil {
suffix.widthAnchor.constraint(equalToConstant: suffixWidth!).isActive = true
}
containerView.addView(suffix, in: .leading) containerView.addView(suffix, in: .leading)
} }
return containerView return containerView
} }
/* private func updateSuffixWithValue(_ control: NSControl, _ value: String) {
usage notes: let suffixIdentifierPredicate = {(view: NSView) -> Bool in
- NSSlider: supports on purpose currently only decimal values view.identifier?.rawValue == control.identifier!.rawValue + ControlIdentifierDiscriminator.SUFFIX.rawValue
*/
@objc func controlDidEndEditing(senderControl: NSControl) {
self.makeFirstResponder(invisibleTextField) // deselects any possibly selected NSTextField (so slider & popUp changes deselect them)
let key: String? = senderControl.identifier?.rawValue
var newValue: String?
if senderControl is NSPopUpButton {
newValue = (senderControl as! NSPopUpButton).titleOfSelectedItem!
} else if senderControl is NSSlider {
newValue = String(format: "%.0f", Double(senderControl.stringValue)!) // we are only interested in decimals of the provided double
} else {
newValue = senderControl.stringValue
} }
// debugPrint("PreferencesPanel: save: change", key!, newValue!) if let suffixView: NSTextField = control.superview?.subviews.first(where: suffixIdentifierPredicate) as? NSTextField {
let regex = try! NSRegularExpression(pattern: "^[0-9]+") // first decimal
let range = NSMakeRange(0, suffixView.stringValue.count)
suffixView.stringValue = regex.stringByReplacingMatches(in: suffixView.stringValue, range: range, withTemplate: value)
}
}
if key != nil && newValue != nil { @objc
if newValue == Preferences.rawValues[key!] { private func controlWasChanged(senderControl: NSControl) {
debugPrint("PreferencesPanel: save: abort: value was not changed") let key: String = senderControl.identifier!.rawValue
return let previousValue: String = Preferences.rawValues[key]!
let newValue: String = getControlValue(senderControl)
let invalidTextField = senderControl is PreferencesPanelNSTextFieldEditable && !(senderControl as! PreferencesPanelNSTextFieldEditable).isValid()
if (invalidTextField && !windowCloseRequested) || (newValue == previousValue && !invalidTextField) {
return
}
updateControlExtras(senderControl, newValue)
do {
// TODO: remove conditional as soon a Preference does validation on its own
if invalidTextField && windowCloseRequested {
throw NSError.make(domain: "Preferences", message: "Please enter a valid value for '" + key + "'")
} }
try Preferences.updateAndValidateFromString(key, newValue)
(NSApp as! Application).initPreferencesDependentComponents()
try Preferences.saveRawToDisk()
} catch let error {
debugPrint("PreferencesPanel: save: error", key, newValue, error)
showSaveErrorSheetModal(error as NSError, senderControl, key, previousValue) // allows recursive call by user choice
}
}
let previousValue = Preferences.rawValues[key!]! private func showSaveErrorSheetModal(_ nsError: NSError, _ control: NSControl, _ key: String, _ previousValue: String) {
let alert = NSAlert()
alert.messageText = "Could not save Preference"
alert.informativeText = nsError.localizedDescription + "\n"
alert.addButton(withTitle: "Edit")
alert.addButton(withTitle: "Cancel")
alert.addButton(withTitle: "Check again")
do { alert.beginSheetModal(for: self, completionHandler: {(modalResponse: NSApplication.ModalResponse) -> Void in
try Preferences.updateAndValidateFromString(key!, newValue!) if modalResponse == NSApplication.ModalResponse.alertFirstButtonReturn {
try Preferences.saveRawToDisk() debugPrint("PreferencesPanel: save: error: user choice: edit")
} catch { self.windowCloseRequested = false
debugPrint("PreferencesPanel: save: error", key!, error, "previousValue", previousValue, " | newValue", newValue!) }
if modalResponse == NSApplication.ModalResponse.alertSecondButtonReturn {
// restores the previous value in Preferences and senderControl debugPrint("PreferencesPanel: save: error: user choice: cancel -> revert value and eventually close window")
try! Preferences.updateAndValidateFromString(key!, previousValue) try! Preferences.updateAndValidateFromString(key, previousValue)
self.setControlValue(control, previousValue)
if senderControl is NSPopUpButton { self.updateControlExtras(control, previousValue)
(senderControl as! NSPopUpButton).selectItem(withTitle: previousValue) if self.windowCloseRequested {
} else { self.close()
senderControl.stringValue = previousValue
} }
} }
if modalResponse == NSApplication.ModalResponse.alertThirdButtonReturn {
debugPrint("PreferencesPanel: save: error: user choice: check again")
self.controlWasChanged(senderControl: control)
}
})
}
private func getControlValue(_ control: NSControl) -> String {
if control is NSPopUpButton {
return (control as! NSPopUpButton).titleOfSelectedItem!
} else if control is NSSlider {
return String(format: "%.0f", control.doubleValue) // we are only interested in decimals of the provided double
} else { } else {
debugPrint("PreferencesPanel: save: error: key||newValue = nil", key!, newValue!) return control.stringValue
} }
} }
private func calcLabelWidth() -> CGFloat { private func setControlValue(_ control: NSControl, _ value: String) {
return (panelWidth - panelPadding) * panelWidthToLabelRatio if control is NSPopUpButton {
(control as! NSPopUpButton).selectItem(withTitle: value)
} else if control is NSTextField{
control.stringValue = value
(control as! NSTextField).delegate?.controlTextDidChange?(Notification(name: NSControl.textDidChangeNotification, object: control))
} else {
control.stringValue = value
}
}
private func updateControlExtras(_ control: NSControl, _ value: String) {
if control is NSSlider {
updateSuffixWithValue(control as! NSSlider, value)
}
} }
} }
enum ControlIdentifierDiscriminator: String {
case LABEL = "_label"
case SUFFIX = "_suffix"
}