Skip to content

Commit

Permalink
feat: improves PreferencesPanel UX, partially implements #49
Browse files Browse the repository at this point in the history
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
  • Loading branch information
gingerr authored and lwouis committed Nov 11, 2019
1 parent 65327c2 commit 21a4587
Show file tree
Hide file tree
Showing 5 changed files with 99 additions and 66 deletions.
12 changes: 8 additions & 4 deletions alt-tab-macos.xcodeproj/project.pbxproj
Expand Up @@ -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 */
Expand Down Expand Up @@ -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 */
Expand Down Expand Up @@ -174,7 +176,6 @@
D04BA3F15EAE8D8C39B6F2CF /* Screen.swift */,
D04BA2D2AD6B1CCA3F3A4DD7 /* SystemPermissions.swift */,
D04BA86768C6503A11ED81FC /* Extensions.swift */,
F0298F4D7927608A986C320D /* PreferencesPanelNSTextFieldEditable.swift */,
);
path = logic;
sourceTree = "<group>";
Expand All @@ -189,6 +190,8 @@
D04BAE5BBE182DD5DDFE2E3E /* ThumbnailsPanel.swift */,
D04BA0AF7C5DCF367FBB663C /* StatusItem.swift */,
D04BAD32E130E4A061DC8332 /* Labels.swift */,
F0298E42A818112B290FF6C7 /* TextField.swift */,
F0298708E2B13DBD4738AE76 /* HyperlinkLabel.swift */,
);
path = ui;
sourceTree = "<group>";
Expand Down Expand Up @@ -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;
};
Expand Down
34 changes: 0 additions & 34 deletions alt-tab-macos/logic/PreferencesPanelNSTextFieldEditable.swift

This file was deleted.

21 changes: 21 additions & 0 deletions alt-tab-macos/ui/HyperlinkLabel.swift
@@ -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)
}
}
63 changes: 35 additions & 28 deletions alt-tab-macos/ui/PreferencesPanel.swift
Expand Up @@ -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) {
Expand All @@ -16,24 +16,22 @@ 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()
}

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
Expand Down Expand Up @@ -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)
]
}
Expand All @@ -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 {
Expand All @@ -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()
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down
35 changes: 35 additions & 0 deletions alt-tab-macos/ui/TextField.swift
@@ -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
}

}

0 comments on commit 21a4587

Please sign in to comment.