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 */; };
D04BAD4DE538FDF7E7532EE2 /* Labels.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BAD32E130E4A061DC8332 /* Labels.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 */
/* 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>"; };
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>"; };
F0298F4D7927608A986C320D /* PreferencesPanelNSTextFieldEditable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PreferencesPanelNSTextFieldEditable.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@@ -172,6 +174,7 @@
D04BA3F15EAE8D8C39B6F2CF /* Screen.swift */,
D04BA2D2AD6B1CCA3F3A4DD7 /* SystemPermissions.swift */,
D04BA86768C6503A11ED81FC /* Extensions.swift */,
F0298F4D7927608A986C320D /* PreferencesPanelNSTextFieldEditable.swift */,
);
path = logic;
sourceTree = "<group>";
@@ -302,6 +305,7 @@
D04BA02DD4152997C32CF50B /* StatusItem.swift in Sources */,
D04BA0F3D46BC79544E2B930 /* Extensions.swift in Sources */,
D04BAD4DE538FDF7E7532EE2 /* Labels.swift in Sources */,
F02981D5D1E1F62074801CAE /* PreferencesPanelNSTextFieldEditable.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
+23 -5
View File
@@ -19,15 +19,11 @@ extension Optional {
return value
case .none:
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
extension String {
init?(_ cgFloat: CGFloat) {
@@ -39,3 +35,25 @@ extension 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()
showOnScreen = p.preferences
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
}
@@ -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 {
static let name = "AltTab"
var statusItem: NSStatusItem?
var backgroundView: NSVisualEffectView?
var thumbnailsPanel: ThumbnailsPanel?
var preferencesPanel: PreferencesPanel?
var selectedOpenWindow: Int = 0
@@ -28,11 +27,15 @@ class Application: NSApplication, NSApplicationDelegate, NSWindowDelegate {
SystemPermissions.ensureAccessibilityCheckboxIsChecked()
Preferences.loadFromDiskAndUpdateValues()
statusItem = StatusItem.make(self)
thumbnailsPanel = ThumbnailsPanel(self)
preferencesPanel = PreferencesPanel()
initPreferencesDependentComponents()
Keyboard.listenToGlobalEvents(self)
}
// we put application code here which should be executed on init() and Preferences change
func initPreferencesDependentComponents() {
thumbnailsPanel = ThumbnailsPanel(self)
}
func showUiOrSelectNext() {
debugPrint("showUiOrSelectNext")
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)
}
@@ -125,12 +132,4 @@ class Application: NSApplication, NSApplicationDelegate, NSWindowDelegate {
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 Foundation
class PreferencesPanel: NSPanel {
// ui: base layout
let panelWidth = CGFloat(400)
let panelHeight = CGFloat(400) // gets auto adjusted to content height
class PreferencesPanel: NSPanel, NSWindowDelegate {
let panelWidth = CGFloat(496)
let panelHeight = CGFloat(256) // auto expands to content height (but does not auto shrink)
let panelPadding = CGFloat(40)
let panelWidthToLabelRatio = CGFloat(0.5)
// ui: preferences elements
var maxScreenUsage: NSSlider?
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
var labelWidth: CGFloat { get { // derived convenience variable
return (panelWidth - panelPadding) * CGFloat(0.45)
}}
var windowCloseRequested = false
override init(contentRect: NSRect, styleMask style: StyleMask, backing backingStoreType: BackingStoreType, defer flag: Bool) {
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"
titlebarAppearsTransparent = true
hidesOnDeactivate = false
contentView = makeContentView()
NSApp.activate(ignoringOtherApps: true)
}
// setup hidden element
invisibleTextField = NSTextField()
invisibleTextField?.isHidden = true
contentView?.subviews.append(invisibleTextField!)
self.initialFirstResponder = invisibleTextField
override func close() {
NSApp.deactivate()
(NSApp as! Application).preferencesPanel = nil
super.close()
}
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 {
@@ -53,20 +63,34 @@ class PreferencesPanel: NSPanel {
}
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 [
makeLabelWithDropdown(\PreferencesPanel.metaKey, "Meta key to activate the app", rawName: "metaKey", values: Preferences.metaKeyMacro.labels),
makeLabelWithInput(\PreferencesPanel.tabKeyCode, "Tab key", rawName: "tabKeyCode", suffixText: "KeyCode"),
makeLabelWithDropdown("Alt key", rawName: "metaKey", values: Preferences.metaKeyMacro.labels),
makeLabelWithInput("Tab key", rawName: "tabKeyCode", width: 33, suffixText: "KeyCode (48 = Tab)", validator: tabKeyCodeValidator),
makeHorizontalSeparator(),
makeLabelWithDropdown(\PreferencesPanel.theme, "Main window theme", rawName: "theme", values: Preferences.themeMacro.labels),
makeLabelWithSlider(\PreferencesPanel.maxScreenUsage, "Max window size", rawName: "maxScreenUsage", minValue: 10, maxValue: 100, numberOfTickMarks: 10),
makeLabelWithInput(\PreferencesPanel.maxThumbnailsPerRow, "Max thumbnails per row", rawName: "maxThumbnailsPerRow"),
makeLabelWithSlider(\PreferencesPanel.iconSize, "Apps icon size", rawName: "iconSize", minValue: 12, maxValue: 64, numberOfTickMarks: 16),
makeLabelWithSlider(\PreferencesPanel.fontHeight, "Font size", rawName: "fontHeight", minValue: 12, maxValue: 36, numberOfTickMarks: 16),
makeLabelWithDropdown("Theme", rawName: "theme", values: Preferences.themeMacro.labels),
makeLabelWithSlider("Max screen usage", rawName: "maxScreenUsage", minValue: 10, maxValue: 100, numberOfTickMarks: 10, unitText: "%"),
makeLabelWithInput("Max thumbnails per row", rawName: "maxThumbnailsPerRow", width: 25, validator: maxThumbnailsPerRowValidator),
makeLabelWithSlider("Apps icon size", rawName: "iconSize", minValue: 12, maxValue: 62, numberOfTickMarks: 16, unitText: "px"),
makeLabelWithSlider("Window font size", rawName: "fontHeight", minValue: 12, maxValue: 36, numberOfTickMarks: 16, unitText: "px"),
makeHorizontalSeparator(),
makeLabelWithInput(\PreferencesPanel.windowDisplayDelay, "Window apparition delay", rawName: "windowDisplayDelay", suffixText: "ms"),
makeLabelWithDropdown(\PreferencesPanel.showOnScreen, "Show on", rawName: "showOnScreen", values: Preferences.showOnScreenMacro.labels),
makeHorizontalSeparator(),
makeRestartHint()
makeLabelWithInput("Window apparition delay", rawName: "windowDisplayDelay", width: 41, suffixText: "ms", validator: windowDisplayDelayValidator),
makeLabelWithDropdown("Show on", rawName: "showOnScreen", values: Preferences.showOnScreenMacro.labels)
]
}
@@ -77,135 +101,163 @@ class PreferencesPanel: NSPanel {
return view
}
@objc private func restartButtonAction() {
self.makeFirstResponder(invisibleTextField)
if let delegate = Application.shared.delegate as? Application {
delegate.relaunch()
private func makeLabelWithInput(_ labelText: String, rawName: String, width: CGFloat? = nil, suffixText: String? = nil, validator: ((String)->Bool)? = nil) -> NSStackView {
let input = PreferencesPanelNSTextFieldEditable(string: Preferences.rawValues[rawName]!)
input.validationHandler = validator
input.delegate = input
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 {
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 {
private func makeLabelWithDropdown(_ 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)
return makeLabelWithProvidedControl(labelText, rawName: rawName, control: popUp, suffixText: 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)
)
private func makeLabelWithSlider(_ labelText: String, rawName: String, minValue: Double, maxValue: Double, numberOfTickMarks: Int, unitText: String) -> NSStackView {
let value = Preferences.rawValues[rawName]!
let suffixText = value + unitText
let slider = NSSlider()
slider.minValue = minValue
slider.maxValue = maxValue
slider.stringValue = value
slider.numberOfTickMarks = numberOfTickMarks
slider.allowsTickMarkValuesOnly = numberOfTickMarks > 1
slider.tickMarkPosition = .below
slider.isContinuous = true
self[keyPath: keyPath] = slider
return makeLabelWithProvidedControl(labelText, rawName, slider, suffixText)
return makeLabelWithProvidedControl(labelText, rawName: rawName, control: slider, suffixText: suffixText, suffixWidth: 40)
}
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 + ": ")
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.target = self
control.action = #selector(controlDidEndEditing)
control.action = #selector(controlWasChanged)
let containerView = NSStackView(views: [label, control])
if suffixText != nil {
let suffix = NSTextField(labelWithString: suffixText!)
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)
}
return containerView
}
/*
usage notes:
- NSSlider: supports on purpose currently only decimal values
*/
@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
private func updateSuffixWithValue(_ control: NSControl, _ value: String) {
let suffixIdentifierPredicate = {(view: NSView) -> Bool in
view.identifier?.rawValue == control.identifier!.rawValue + ControlIdentifierDiscriminator.SUFFIX.rawValue
}
// 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 {
if newValue == Preferences.rawValues[key!] {
debugPrint("PreferencesPanel: save: abort: value was not changed")
return
@objc
private func controlWasChanged(senderControl: NSControl) {
let key: String = senderControl.identifier!.rawValue
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 {
try Preferences.updateAndValidateFromString(key!, newValue!)
try Preferences.saveRawToDisk()
} catch {
debugPrint("PreferencesPanel: save: error", key!, error, "previousValue", previousValue, " | newValue", newValue!)
// 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
alert.beginSheetModal(for: self, completionHandler: {(modalResponse: NSApplication.ModalResponse) -> Void in
if modalResponse == NSApplication.ModalResponse.alertFirstButtonReturn {
debugPrint("PreferencesPanel: save: error: user choice: edit")
self.windowCloseRequested = false
}
if modalResponse == NSApplication.ModalResponse.alertSecondButtonReturn {
debugPrint("PreferencesPanel: save: error: user choice: cancel -> revert value and eventually close window")
try! Preferences.updateAndValidateFromString(key, previousValue)
self.setControlValue(control, previousValue)
self.updateControlExtras(control, previousValue)
if self.windowCloseRequested {
self.close()
}
}
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 {
debugPrint("PreferencesPanel: save: error: key||newValue = nil", key!, newValue!)
return control.stringValue
}
}
private func calcLabelWidth() -> CGFloat {
return (panelWidth - panelPadding) * panelWidthToLabelRatio
private func setControlValue(_ control: NSControl, _ value: String) {
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"
}