feat: improves PreferencesPanel UX, partially implements #49
implements requested changes from Code-Review and contains also refactoring, additions & improvements - PreferencesPanel: does not handle NSApp.activate & .deactivate anymore - PreferencesPanel: maxThumbnailsPerRow & windowDisplayDelay are now sliders - PreferencesPanel: Sliders do not use tickmarks anymore (to allow finer settings) - PreferencesPanel: tweaked Sliders min/max values - PreferencesPanel: adds Hyperlink to KeyCodes Reference as suffix for Tab key control (with additional required sub-class of NSTextField) - PreferencesPanel: adds makeSuffix() method for code clarity
This commit is contained in:
1 parent
65327c2b89
commit
21a4587e77
5 files changed
+99
-66
No files matched your search
@@ -21,7 +21,8 @@
|
||||
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 */; };
|
||||
F029861A378EC1417106FEC3 /* TextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0298E42A818112B290FF6C7 /* TextField.swift */; };
|
||||
F0298AB28A3CE5DBEC385730 /* HyperlinkLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0298708E2B13DBD4738AE76 /* HyperlinkLabel.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
@@ -73,7 +74,8 @@
|
||||
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>"; };
|
||||
F0298708E2B13DBD4738AE76 /* HyperlinkLabel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HyperlinkLabel.swift; sourceTree = "<group>"; };
|
||||
F0298E42A818112B290FF6C7 /* TextField.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextField.swift; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
@@ -174,7 +176,6 @@
|
||||
D04BA3F15EAE8D8C39B6F2CF /* Screen.swift */,
|
||||
D04BA2D2AD6B1CCA3F3A4DD7 /* SystemPermissions.swift */,
|
||||
D04BA86768C6503A11ED81FC /* Extensions.swift */,
|
||||
F0298F4D7927608A986C320D /* PreferencesPanelNSTextFieldEditable.swift */,
|
||||
);
|
||||
path = logic;
|
||||
sourceTree = "<group>";
|
||||
@@ -189,6 +190,8 @@
|
||||
D04BAE5BBE182DD5DDFE2E3E /* ThumbnailsPanel.swift */,
|
||||
D04BA0AF7C5DCF367FBB663C /* StatusItem.swift */,
|
||||
D04BAD32E130E4A061DC8332 /* Labels.swift */,
|
||||
F0298E42A818112B290FF6C7 /* TextField.swift */,
|
||||
F0298708E2B13DBD4738AE76 /* HyperlinkLabel.swift */,
|
||||
);
|
||||
path = ui;
|
||||
sourceTree = "<group>";
|
||||
@@ -305,7 +308,8 @@
|
||||
D04BA02DD4152997C32CF50B /* StatusItem.swift in Sources */,
|
||||
D04BA0F3D46BC79544E2B930 /* Extensions.swift in Sources */,
|
||||
D04BAD4DE538FDF7E7532EE2 /* Labels.swift in Sources */,
|
||||
F02981D5D1E1F62074801CAE /* PreferencesPanelNSTextFieldEditable.swift in Sources */,
|
||||
F029861A378EC1417106FEC3 /* TextField.swift in Sources */,
|
||||
F0298AB28A3CE5DBEC385730 /* HyperlinkLabel.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
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
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import Cocoa
|
||||
|
||||
class HyperlinkLabel: NSTextField {
|
||||
|
||||
public convenience init(labelWithUrl stringValue: String, nsUrl: NSURL) {
|
||||
self.init(labelWithString: stringValue)
|
||||
isSelectable = true
|
||||
allowsEditingTextAttributes = true
|
||||
let linkTextAttributes: [NSAttributedString.Key: Any] = [
|
||||
NSAttributedString.Key.link: nsUrl as Any,
|
||||
NSAttributedString.Key.font: NSFont.labelFont(ofSize: NSFont.systemFontSize),
|
||||
]
|
||||
|
||||
attributedStringValue = NSAttributedString(string: stringValue, attributes: linkTextAttributes)
|
||||
}
|
||||
|
||||
// the whole point for this sub-class: always display a pointing-hand cursor (not only when the TextField is focused)
|
||||
override func resetCursorRects() {
|
||||
addCursorRect(bounds, cursor: NSCursor.pointingHand)
|
||||
}
|
||||
}
|
||||
@@ -5,9 +5,9 @@ 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)
|
||||
var labelWidth: CGFloat { get { // derived convenience variable
|
||||
var labelWidth: CGFloat {
|
||||
return (panelWidth - panelPadding) * CGFloat(0.45)
|
||||
}}
|
||||
}
|
||||
var windowCloseRequested = false
|
||||
|
||||
override init(contentRect: NSRect, styleMask style: StyleMask, backing backingStoreType: BackingStoreType, defer flag: Bool) {
|
||||
@@ -16,11 +16,9 @@ class PreferencesPanel: NSPanel, NSWindowDelegate {
|
||||
title = Application.name + " Preferences"
|
||||
hidesOnDeactivate = false
|
||||
contentView = makeContentView()
|
||||
NSApp.activate(ignoringOtherApps: true)
|
||||
}
|
||||
|
||||
override func close() {
|
||||
NSApp.deactivate()
|
||||
(NSApp as! Application).preferencesPanel = nil
|
||||
super.close()
|
||||
}
|
||||
@@ -28,12 +26,12 @@ class PreferencesPanel: NSPanel, NSWindowDelegate {
|
||||
public func windowShouldClose(_ sender: NSWindow) -> Bool {
|
||||
windowCloseRequested = true
|
||||
challengeNextInvalidEditableTextField()
|
||||
return attachedSheet == nil // user is challenged
|
||||
return attachedSheet == nil // depends if user is challenged with a sheet
|
||||
}
|
||||
|
||||
private func challengeNextInvalidEditableTextField() {
|
||||
let invalidFields = (contentView?
|
||||
.findNestedViews(subclassOf: PreferencesPanelNSTextFieldEditable.self)
|
||||
.findNestedViews(subclassOf: TextField.self)
|
||||
.filter({ !$0.isValid() })
|
||||
)
|
||||
let focusedField = invalidFields?.filter({ $0.currentEditor() != nil }).first
|
||||
@@ -76,20 +74,18 @@ class PreferencesPanel: NSPanel, NSWindowDelegate {
|
||||
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("Alt key", rawName: "metaKey", values: Preferences.metaKeyMacro.labels),
|
||||
makeLabelWithInput("Tab key", rawName: "tabKeyCode", width: 33, suffixText: "KeyCode (48 = Tab)", validator: tabKeyCodeValidator),
|
||||
makeLabelWithInput("Tab key", rawName: "tabKeyCode", width: 33, suffixText: "KeyCodes Reference", suffixUrl: "https://eastmanreference.com/complete-list-of-applescript-key-codes", validator: tabKeyCodeValidator),
|
||||
makeHorizontalSeparator(),
|
||||
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"),
|
||||
makeLabelWithSlider("Max screen usage", rawName: "maxScreenUsage", minValue: 10, maxValue: 100, numberOfTickMarks: 0, unitText: "%"),
|
||||
makeLabelWithSlider("Max thumbnails per row", rawName: "maxThumbnailsPerRow", minValue: 3, maxValue: 16, numberOfTickMarks: 0),
|
||||
makeLabelWithSlider("Apps icon size", rawName: "iconSize", minValue: 12, maxValue: 64, numberOfTickMarks: 0, unitText: "px"),
|
||||
makeLabelWithSlider("Window font size", rawName: "fontHeight", minValue: 12, maxValue: 64, numberOfTickMarks: 0, unitText: "px"),
|
||||
makeHorizontalSeparator(),
|
||||
makeLabelWithInput("Window apparition delay", rawName: "windowDisplayDelay", width: 41, suffixText: "ms", validator: windowDisplayDelayValidator),
|
||||
makeLabelWithSlider("Window apparition delay", rawName: "windowDisplayDelay", minValue: 0, maxValue: 2000, numberOfTickMarks: 0, unitText: "ms"),
|
||||
makeLabelWithDropdown("Show on", rawName: "showOnScreen", values: Preferences.showOnScreenMacro.labels)
|
||||
]
|
||||
}
|
||||
@@ -101,16 +97,16 @@ class PreferencesPanel: NSPanel, NSWindowDelegate {
|
||||
return view
|
||||
}
|
||||
|
||||
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]!)
|
||||
private func makeLabelWithInput(_ labelText: String, rawName: String, width: CGFloat? = nil, suffixText: String? = nil, suffixUrl: String? = nil, validator: ((String)->Bool)? = nil) -> NSStackView {
|
||||
let input = TextField(Preferences.rawValues[rawName]!)
|
||||
input.validationHandler = validator
|
||||
input.delegate = input
|
||||
input.visualizeValidationState(input.isValid())
|
||||
input.visualizeValidationState()
|
||||
if width != nil {
|
||||
input.widthAnchor.constraint(equalToConstant: width!).isActive = true
|
||||
}
|
||||
|
||||
return makeLabelWithProvidedControl(labelText, rawName: rawName, control: input, suffixText: suffixText)
|
||||
return makeLabelWithProvidedControl(labelText, rawName: rawName, control: input, suffixText: suffixText, suffixUrl: suffixUrl)
|
||||
}
|
||||
|
||||
private func makeLabelWithDropdown(_ labelText: String, rawName: String, values: [String], suffixText: String? = nil) -> NSStackView {
|
||||
@@ -121,7 +117,7 @@ class PreferencesPanel: NSPanel, NSWindowDelegate {
|
||||
return makeLabelWithProvidedControl(labelText, rawName: rawName, control: popUp, suffixText: suffixText)
|
||||
}
|
||||
|
||||
private func makeLabelWithSlider(_ labelText: String, rawName: String, minValue: Double, maxValue: Double, numberOfTickMarks: Int, unitText: String) -> NSStackView {
|
||||
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()
|
||||
@@ -133,10 +129,10 @@ class PreferencesPanel: NSPanel, NSWindowDelegate {
|
||||
slider.tickMarkPosition = .below
|
||||
slider.isContinuous = true
|
||||
|
||||
return makeLabelWithProvidedControl(labelText, rawName: rawName, control: slider, suffixText: suffixText, suffixWidth: 40)
|
||||
return makeLabelWithProvidedControl(labelText, rawName: rawName, control: slider, suffixText: suffixText, suffixWidth: 60)
|
||||
}
|
||||
|
||||
private func makeLabelWithProvidedControl(_ labelText: String, rawName: String, control: NSControl, suffixText: String? = nil, suffixWidth: CGFloat? = nil) -> NSStackView {
|
||||
private func makeLabelWithProvidedControl(_ labelText: String, rawName: String, control: NSControl, suffixText: String? = nil, suffixWidth: CGFloat? = nil, suffixUrl: String? = nil) -> NSStackView {
|
||||
let label = NSTextField(wrappingLabelWithString: labelText + ": ")
|
||||
label.alignment = .right
|
||||
label.widthAnchor.constraint(equalToConstant: labelWidth).isActive = true
|
||||
@@ -149,18 +145,29 @@ class PreferencesPanel: NSPanel, NSWindowDelegate {
|
||||
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
|
||||
}
|
||||
let suffix = makeSuffix(controlName: rawName, text: suffixText!, width: suffixWidth, url: suffixUrl)
|
||||
containerView.addView(suffix, in: .leading)
|
||||
}
|
||||
|
||||
return containerView
|
||||
}
|
||||
|
||||
private func makeSuffix(controlName: String, text: String, width: CGFloat? = nil, url: String? = nil) -> NSTextField {
|
||||
let suffix: NSTextField
|
||||
if url == nil {
|
||||
suffix = NSTextField(labelWithString: text)
|
||||
} else {
|
||||
suffix = HyperlinkLabel(labelWithUrl: text, nsUrl: NSURL(string: url!)!)
|
||||
}
|
||||
suffix.textColor = .gray
|
||||
suffix.identifier = NSUserInterfaceItemIdentifier(controlName + ControlIdentifierDiscriminator.SUFFIX.rawValue)
|
||||
if width != nil {
|
||||
suffix.widthAnchor.constraint(equalToConstant: width!).isActive = true
|
||||
}
|
||||
|
||||
return suffix
|
||||
}
|
||||
|
||||
private func updateSuffixWithValue(_ control: NSControl, _ value: String) {
|
||||
let suffixIdentifierPredicate = {(view: NSView) -> Bool in
|
||||
view.identifier?.rawValue == control.identifier!.rawValue + ControlIdentifierDiscriminator.SUFFIX.rawValue
|
||||
@@ -178,7 +185,7 @@ class PreferencesPanel: NSPanel, NSWindowDelegate {
|
||||
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()
|
||||
let invalidTextField = senderControl is TextField && !(senderControl as! TextField).isValid()
|
||||
|
||||
if (invalidTextField && !windowCloseRequested) || (newValue == previousValue && !invalidTextField) {
|
||||
return
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import Cocoa
|
||||
|
||||
class TextField: NSTextField, NSTextFieldDelegate {
|
||||
|
||||
var validationHandler: ((String)->Bool)?
|
||||
|
||||
public convenience init(_ value: String) {
|
||||
self.init(string: value)
|
||||
wantsLayer = true
|
||||
layer?.borderWidth = 1
|
||||
}
|
||||
|
||||
func controlTextDidChange(_ obj: Notification) {
|
||||
visualizeValidationState()
|
||||
let textField = obj.object as! TextField
|
||||
sendAction(textField.action, to: textField.target)
|
||||
}
|
||||
|
||||
func visualizeValidationState() -> Void {
|
||||
if !isValid() {
|
||||
layer?.borderColor = NSColor.systemRed.cgColor
|
||||
} else {
|
||||
layer?.borderColor = .clear
|
||||
}
|
||||
}
|
||||
|
||||
func isValid() -> Bool {
|
||||
if let handler = validationHandler {
|
||||
return handler(stringValue)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
}
|
||||
Reference in new issue
Block a user