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:
1 parent
59fc712086
commit
65327c2b89
6 files changed
+247
-140
No files matched your search
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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]
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
Reference in new issue
Block a user