Moved 'isSecure' out into two different classes
In later versions of Xcode changing the NSTextFieldCell to an NSSecureTextFieldCell generates an error (no longer secure). Rather than maintain two almost identical field types, GYB is being used to templatize the primary source file 'DSFFloatLabelledTextField.swift.gyb' into two different classes. As a result, changes _must_ be made to the template file rather than the sources.
This commit is contained in:
parent
0e89b0c162
commit
ad85f71bd2
|
@ -2,3 +2,4 @@ DSFFloatLabelledTextControls.xcodeproj/xcuserdata
|
|||
xcschememanagement.plist
|
||||
.swiftpm
|
||||
Demos/DSFFloatLabelledTextControls.xcodeproj/xcuserdata
|
||||
gyb.cpython-310.pyc
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
// ** Automatically generated from DSFFloatLabelledTextField.swift.gyb **
|
||||
//
|
||||
// DSFFloatLabelledSecureTextField.swift
|
||||
// (generated using tools/regenerate-sources.sh)
|
||||
//
|
||||
// Copyright © 2023 Darren Ford. All rights reserved.
|
||||
//
|
||||
|
@ -167,11 +169,6 @@ import AppKit
|
|||
self.setup()
|
||||
}
|
||||
|
||||
open override func awakeFromNib() {
|
||||
super.awakeFromNib()
|
||||
|
||||
}
|
||||
|
||||
open override func viewDidMoveToWindow() {
|
||||
super.viewDidMoveToWindow()
|
||||
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
// ** Automatically generated from DSFFloatLabelledTextField.swift.gyb **
|
||||
//
|
||||
// DSFFloatLabelledTextField.swift
|
||||
// (generated using tools/regenerate-sources.sh)
|
||||
//
|
||||
// Copyright © 2023 Darren Ford. All rights reserved.
|
||||
//
|
||||
|
@ -141,7 +143,6 @@ import AppKit
|
|||
customCell.alignment = self.alignment
|
||||
customCell.formatter = self.formatter
|
||||
customCell.topOffset = self.placeholderHeight
|
||||
|
||||
self.cell = customCell
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,448 @@
|
|||
// ** Automatically generated from DSFFloatLabelledTextField.swift.gyb **
|
||||
//
|
||||
// DSFFloatLabelled${field_type}Field.swift
|
||||
// (generated using tools/regenerate-sources.sh)
|
||||
//
|
||||
// Copyright © 2023 Darren Ford. All rights reserved.
|
||||
//
|
||||
// MIT license
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a
|
||||
// copy of this software and associated documentation files (the "Software"),
|
||||
// to deal in the Software without restriction, including without limitation
|
||||
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
// and/or sell copies of the Software, and to permit persons to whom the
|
||||
// Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
//
|
||||
|
||||
import AppKit
|
||||
|
||||
/// DSFFloatLabelledTextField delegate protocol
|
||||
@objc public protocol DSFFloatLabelled${field_type}FieldDelegate: NSObjectProtocol {
|
||||
/// Called when the label is shown or hidden
|
||||
@objc optional func floatLabelledTextField(_ field: DSFFloatLabelled${field_type}Field, didShowFloatingLabel didShow: Bool)
|
||||
/// Called when the field becomes or loses first responder status
|
||||
@objc optional func floatLabelledTextField(_ field: DSFFloatLabelled${field_type}Field, didFocus: Bool)
|
||||
/// Called when the content of the field changes
|
||||
@objc optional func floatLabelledTextFieldContentChanged(_ field: DSFFloatLabelled${field_type}Field)
|
||||
}
|
||||
|
||||
/// An NS${field_type}Field that implements the Float Label Pattern
|
||||
@IBDesignable open class DSFFloatLabelled${field_type}Field: NS${field_type}Field {
|
||||
|
||||
/// Optional delegate to provide callbacks for the floating label state.
|
||||
///
|
||||
/// This delegate can be set via Interface Builder or programatically
|
||||
@IBOutlet @objc public weak var floatLabelDelegate: DSFFloatLabelled${field_type}FieldDelegate?
|
||||
|
||||
/// The size (in pt) of the floating label text
|
||||
@IBInspectable public var placeholderTextSize: CGFloat = NSFont.smallSystemFontSize {
|
||||
didSet {
|
||||
self.floatingLabel.font = NSFont.systemFont(ofSize: self.placeholderTextSize)
|
||||
self.reconfigureControl()
|
||||
}
|
||||
}
|
||||
|
||||
/// Spacing between the floating label and the text field text
|
||||
@IBInspectable public var placeholderSpacing: CGFloat = 0.0 {
|
||||
didSet {
|
||||
self.floatingLabel.font = NSFont.systemFont(ofSize: self.placeholderTextSize)
|
||||
self.reconfigureControl()
|
||||
}
|
||||
}
|
||||
|
||||
// Override so that we can notify when the developer changes the text programatically too
|
||||
open override var stringValue: String {
|
||||
get {
|
||||
return super.stringValue
|
||||
}
|
||||
set {
|
||||
super.stringValue = newValue
|
||||
NotificationCenter.default.post(name: NSControl.textDidChangeNotification, object: self)
|
||||
}
|
||||
}
|
||||
|
||||
// Override so that we can update when the developer changes the placeholder string
|
||||
open override var placeholderString: String? {
|
||||
get {
|
||||
return super.placeholderString
|
||||
}
|
||||
set {
|
||||
super.placeholderString = newValue
|
||||
self.floatingLabel.stringValue = newValue ?? ""
|
||||
}
|
||||
}
|
||||
|
||||
/// Floating label
|
||||
private let floatingLabel = NSTextField()
|
||||
|
||||
/// Is the label currently showing
|
||||
private var isShowing: Bool = false {
|
||||
didSet {
|
||||
self.floatLabelDelegate?.floatLabelledTextField?(self, didShowFloatingLabel: self.isShowing)
|
||||
}
|
||||
}
|
||||
|
||||
/// Constraint to tie the label to the top of the control
|
||||
private var floatingTop: NSLayoutConstraint?
|
||||
|
||||
/// Height of the control
|
||||
private var heightConstraint: NSLayoutConstraint?
|
||||
|
||||
/// Observers for the font and placeholder text
|
||||
private var fontObserver: NSKeyValueObservation?
|
||||
private var placeholderObserver: NSKeyValueObservation?
|
||||
|
||||
/// Returns the height of the placeholder text
|
||||
var placeholderHeight: CGFloat {
|
||||
let layoutManager = NSLayoutManager()
|
||||
return layoutManager.defaultLineHeight(for: self.floatingLabel.font!) + 1
|
||||
}
|
||||
|
||||
/// Returns the height of the primary (editable) text
|
||||
private var textHeight: CGFloat {
|
||||
let layoutManager = NSLayoutManager()
|
||||
return layoutManager.defaultLineHeight(for: self.font!) + 1
|
||||
}
|
||||
|
||||
/// Returns the total height of the control given the font settings
|
||||
private var controlHeight: CGFloat {
|
||||
return self.textHeight + self.placeholderSpacing + self.placeholderHeight
|
||||
}
|
||||
|
||||
open override var intrinsicContentSize: NSSize {
|
||||
var sz = super.intrinsicContentSize
|
||||
sz.height = self.controlHeight
|
||||
return sz
|
||||
}
|
||||
|
||||
func configureCell() {
|
||||
let customCell = DSFFloatLabelled${field_type}FieldCell()
|
||||
customCell.isEditable = true
|
||||
customCell.wraps = false
|
||||
customCell.usesSingleLineMode = true
|
||||
customCell.placeholderString = self.placeholderString
|
||||
customCell.title = self.stringValue
|
||||
customCell.font = self.font
|
||||
customCell.isBordered = self.isBordered
|
||||
customCell.isBezeled = self.isBezeled
|
||||
customCell.bezelStyle = self.bezelStyle
|
||||
customCell.isScrollable = true
|
||||
customCell.isContinuous = self.isContinuous
|
||||
customCell.alignment = self.alignment
|
||||
customCell.formatter = self.formatter
|
||||
customCell.topOffset = self.placeholderHeight
|
||||
self.cell = customCell
|
||||
}
|
||||
|
||||
func setTopOffset(_ value: CGFloat) {
|
||||
guard var f = self.cell as? DSFFloatLabelledTextFieldCellProtocol else {
|
||||
return
|
||||
}
|
||||
f.topOffset = value
|
||||
}
|
||||
|
||||
/// Set the fonts to be used in the control
|
||||
open func setFonts(primary: NSFont, secondary: NSFont) {
|
||||
self.floatingLabel.font = secondary
|
||||
self.font = primary
|
||||
}
|
||||
|
||||
public override init(frame frameRect: NSRect) {
|
||||
super.init(frame: frameRect)
|
||||
self.setup()
|
||||
}
|
||||
|
||||
required public init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
self.setup()
|
||||
}
|
||||
|
||||
open override func viewDidMoveToWindow() {
|
||||
super.viewDidMoveToWindow()
|
||||
|
||||
// When we're added to the view, make sure that the floating label alignment matches us
|
||||
// (as the user might have changed the alignment BEFORE adding to the superview
|
||||
self.floatingLabel.alignment = self.alignment
|
||||
}
|
||||
|
||||
private func setup() {
|
||||
// Setup the common elements of the control
|
||||
self.commonSetup()
|
||||
|
||||
// Configure a default text cell
|
||||
self.configureCell()
|
||||
|
||||
// Listen to changes in the primary font so we can reconfigure to match
|
||||
self.fontObserver = self.observe(\.font, options: [.new]) { [weak self] _, _ in
|
||||
self?.reconfigureControl()
|
||||
}
|
||||
|
||||
// Listen to changes in the placeholder text so we can reflect it in the floater
|
||||
self.placeholderObserver = self.observe(\.placeholderString, options: [.new]) { [weak self] _, _ in
|
||||
guard let `self` = self else { return }
|
||||
self.floatingLabel.stringValue = self.placeholderString!
|
||||
self.reconfigureControl()
|
||||
}
|
||||
}
|
||||
|
||||
/// Build the floating label
|
||||
private func createFloatingLabel() {
|
||||
if self.floatingLabel.superview == nil {
|
||||
self.addSubview(self.floatingLabel)
|
||||
}
|
||||
|
||||
self.floatingLabel.wantsLayer = true
|
||||
self.floatingLabel.isEditable = false
|
||||
self.floatingLabel.isSelectable = false
|
||||
self.floatingLabel.isEnabled = true
|
||||
self.floatingLabel.isBezeled = false
|
||||
self.floatingLabel.isBordered = false
|
||||
self.floatingLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
self.floatingLabel.font = NSFont.systemFont(ofSize: self.placeholderTextSize)
|
||||
self.floatingLabel.textColor = NSColor.placeholderTextColor
|
||||
self.floatingLabel.stringValue = self.placeholderString ?? ""
|
||||
self.floatingLabel.alphaValue = 0.0
|
||||
self.floatingLabel.alignment = self.alignment
|
||||
self.floatingLabel.drawsBackground = false
|
||||
|
||||
self.floatingTop = NSLayoutConstraint(
|
||||
item: self.floatingLabel, attribute: .top,
|
||||
relatedBy: .equal,
|
||||
toItem: self, attribute: .top,
|
||||
multiplier: 1.0, constant: 10
|
||||
)
|
||||
self.addConstraint(self.floatingTop!)
|
||||
|
||||
var x = NSLayoutConstraint(
|
||||
item: self.floatingLabel, attribute: .leading,
|
||||
relatedBy: .equal,
|
||||
toItem: self, attribute: .leading,
|
||||
multiplier: 1.0, constant: self.isBezeled ? 4 : 0
|
||||
)
|
||||
self.addConstraint(x)
|
||||
|
||||
x = NSLayoutConstraint(
|
||||
item: self.floatingLabel, attribute: .trailing,
|
||||
relatedBy: .equal,
|
||||
toItem: self, attribute: .trailing,
|
||||
multiplier: 1.0, constant: self.isBezeled ? -4 : 0
|
||||
)
|
||||
self.addConstraint(x)
|
||||
|
||||
self.floatingLabel.setContentHuggingPriority(NSLayoutConstraint.Priority(10), for: .horizontal)
|
||||
self.floatingLabel.setContentCompressionResistancePriority(NSLayoutConstraint.Priority(10), for: .horizontal)
|
||||
}
|
||||
|
||||
private func commonSetup() {
|
||||
self.wantsLayer = true
|
||||
self.translatesAutoresizingMaskIntoConstraints = false
|
||||
self.usesSingleLineMode = true
|
||||
self.delegate = self
|
||||
|
||||
// Default to natural layout
|
||||
self.alignment = .natural
|
||||
|
||||
self.createFloatingLabel()
|
||||
|
||||
self.heightConstraint = NSLayoutConstraint(
|
||||
item: self, attribute: .height,
|
||||
relatedBy: .equal,
|
||||
toItem: nil, attribute: .notAnAttribute,
|
||||
multiplier: 1.0, constant: self.controlHeight
|
||||
)
|
||||
self.addConstraint(self.heightConstraint!)
|
||||
|
||||
// If the field already has text, make sure the placeholder is shown
|
||||
if self.stringValue.count > 0 {
|
||||
self.showPlaceholder(animated: false)
|
||||
}
|
||||
}
|
||||
|
||||
/// Change the layout if any changes occur
|
||||
private func reconfigureControl() {
|
||||
if self.isCurrentFocus() {
|
||||
/// If we are currently editing, then finish before changing.
|
||||
self.window?.endEditing(for: nil)
|
||||
}
|
||||
self.expandFrame()
|
||||
}
|
||||
|
||||
/// Rebuild the frame of the text field to match the new settings
|
||||
private func expandFrame() {
|
||||
self.heightConstraint?.constant = self.controlHeight
|
||||
self.setTopOffset(self.placeholderHeight + self.placeholderSpacing)
|
||||
self.needsLayout = true
|
||||
}
|
||||
|
||||
open override func layout() {
|
||||
super.layout()
|
||||
|
||||
self.setTopOffset(self.placeholderHeight + self.placeholderSpacing)
|
||||
self.needsLayout = true
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Focus and editing
|
||||
|
||||
extension DSFFloatLabelled${field_type}Field: NSTextFieldDelegate {
|
||||
|
||||
// Change the floating label color to represent active state
|
||||
private func setFloatingLabelActive(_ active: Bool) {
|
||||
if active {
|
||||
self.floatingLabel.textColor = NSColor.simulatedAccentColor
|
||||
}
|
||||
else {
|
||||
self.floatingLabel.textColor = NSColor.placeholderTextColor
|
||||
}
|
||||
|
||||
self.floatLabelDelegate?.floatLabelledTextField?(self, didFocus: active)
|
||||
}
|
||||
|
||||
public func controlTextDidChange(_ obj: Notification) {
|
||||
guard let field = obj.object as? NSTextField else {
|
||||
return
|
||||
}
|
||||
|
||||
if field.stringValue.count > 0, !self.isShowing {
|
||||
self.showPlaceholder(animated: true)
|
||||
}
|
||||
else if field.stringValue.count == 0, self.isShowing {
|
||||
self.hidePlaceholder()
|
||||
}
|
||||
|
||||
self.floatLabelDelegate?.floatLabelledTextFieldContentChanged?(self)
|
||||
}
|
||||
|
||||
open override func becomeFirstResponder() -> Bool {
|
||||
let becomeResult = super.becomeFirstResponder()
|
||||
if becomeResult, self.isCurrentFocus() {
|
||||
// Set the color of the 'label' to match the current focus color.
|
||||
// We need to perform this on the main thread after the current set of notifications are processed
|
||||
// Why? We (occasionally) receive a 'controlTextDidEndEditing' message AFTER we receive a
|
||||
// 'becomeFirstResponder'. I've read that this is related to the text field automatically selecting
|
||||
// text when taking focus, but I haven't been able to verify this in any useful manner.
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
if let `self` = self {
|
||||
self.setFloatingLabelActive(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
return becomeResult
|
||||
}
|
||||
|
||||
open func controlTextDidEndEditing(_: Notification) {
|
||||
// When we lose focus, set the label color back to the placeholder color
|
||||
self.setFloatingLabelActive(false)
|
||||
}
|
||||
|
||||
/// Does our text field currently have input focus?
|
||||
private func isCurrentFocus() -> Bool {
|
||||
// 1. Get the window's first responder
|
||||
// 2. Check is has an active field editor
|
||||
// 3. Is the delegate of the field editor us?
|
||||
if let fr = self.window?.firstResponder as? NSTextView,
|
||||
self.window?.fieldEditor(false, for: nil) != nil,
|
||||
fr.delegate === self {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Animations
|
||||
|
||||
extension DSFFloatLabelled${field_type}Field {
|
||||
|
||||
/// Duration of the fade in/out of the secondary label
|
||||
private var animationDuration: TimeInterval {
|
||||
return shouldAnimate() ? 0.4 : 0.0
|
||||
}
|
||||
|
||||
/// Returns true if the system is NOT set to reduce motion (accessibility settings)
|
||||
private func shouldAnimate() -> Bool {
|
||||
if #available(OSX 10.12, *) {
|
||||
return !NSWorkspace.shared.accessibilityDisplayShouldReduceMotion
|
||||
} else {
|
||||
// Fallback on earlier versions. Just animate
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
private func showPlaceholder(animated: Bool) {
|
||||
self.isShowing = true
|
||||
if animated {
|
||||
NSAnimationContext.runAnimationGroup({ [weak self] context in
|
||||
guard let `self` = self else { return }
|
||||
context.allowsImplicitAnimation = true
|
||||
context.duration = self.animationDuration
|
||||
self.floatingTop?.constant = 0
|
||||
self.floatingLabel.alphaValue = 1.0
|
||||
self.layoutSubtreeIfNeeded()
|
||||
}, completionHandler: {
|
||||
//
|
||||
})
|
||||
}
|
||||
else {
|
||||
self.setFloatingLabelActive(false)
|
||||
self.floatingTop?.constant = 0
|
||||
self.floatingLabel.alphaValue = 1.0
|
||||
}
|
||||
}
|
||||
|
||||
private func hidePlaceholder() {
|
||||
self.isShowing = false
|
||||
let duration = self.animationDuration
|
||||
NSAnimationContext.runAnimationGroup({ [weak self] context in
|
||||
guard let `self` = self else { return }
|
||||
context.allowsImplicitAnimation = true
|
||||
context.duration = duration
|
||||
self.floatingTop?.constant = self.textHeight / 1.5
|
||||
self.floatingLabel.alphaValue = 0.0
|
||||
self.layoutSubtreeIfNeeded()
|
||||
}, completionHandler: {
|
||||
//
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Cell definition
|
||||
|
||||
private class DSFFloatLabelled${field_type}FieldCell: NS${field_type}FieldCell, DSFFloatLabelledTextFieldCellProtocol {
|
||||
var topOffset: CGFloat = 0
|
||||
|
||||
private func offset() -> CGFloat {
|
||||
return self.topOffset - (self.isBezeled ? 5 : 1)
|
||||
}
|
||||
|
||||
override func titleRect(forBounds rect: NSRect) -> NSRect {
|
||||
return NSRect(x: rect.origin.x, y: rect.origin.y + self.offset(), width: rect.width, height: rect.height)
|
||||
}
|
||||
|
||||
override func edit(withFrame rect: NSRect, in controlView: NSView, editor textObj: NSText, delegate: Any?, event: NSEvent?) {
|
||||
let insetRect = NSRect(x: rect.origin.x, y: rect.origin.y + self.offset(), width: rect.width, height: rect.height)
|
||||
super.edit(withFrame: insetRect, in: controlView, editor: textObj, delegate: delegate, event: event)
|
||||
}
|
||||
|
||||
override func select(withFrame rect: NSRect, in controlView: NSView, editor textObj: NSText, delegate: Any?, start selStart: Int, length selLength: Int) {
|
||||
let insetRect = NSRect(x: rect.origin.x, y: rect.origin.y + self.offset(), width: rect.width, height: rect.height)
|
||||
super.select(withFrame: insetRect, in: controlView, editor: textObj, delegate: delegate, start: selStart, length: selLength)
|
||||
}
|
||||
|
||||
override func drawInterior(withFrame cellFrame: NSRect, in controlView: NSView) {
|
||||
let insetRect = NSRect(x: cellFrame.origin.x, y: cellFrame.origin.y + self.offset(), width: cellFrame.width, height: cellFrame.height)
|
||||
super.drawInterior(withFrame: insetRect, in: controlView)
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,26 @@
|
|||
#!/bin/sh
|
||||
|
||||
# A script to re-build the text field sources from the source template file
|
||||
|
||||
# Stop on error
|
||||
set -e
|
||||
|
||||
# The path where this script resides
|
||||
script_path="$( cd -- "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )"
|
||||
|
||||
# The path for the package (one level up)
|
||||
package_path="$( cd -- "${script_path}/../" >/dev/null 2>&1 ; pwd -P )"
|
||||
|
||||
pushd .
|
||||
|
||||
# We have to run docbuild in the root of the source package
|
||||
cd "${package_path}"
|
||||
|
||||
# Generate the NSTextField implementation
|
||||
python3 ${script_path}/gyb --line-directive '' -D field_type="Text" -o "DSFFloatLabelledTextField.swift" "Sources/DSFFloatLabelledTextField/DSFFloatLabelledTextField.swift.gyb"
|
||||
# Generate the NSSecureTextField implementation
|
||||
python3 ${script_path}/gyb --line-directive '' -D field_type="SecureText" -o "DSFFloatLabelledSecureTextField.swift" "Sources/DSFFloatLabelledTextField/DSFFloatLabelledTextField.swift.gyb"
|
||||
|
||||
popd
|
||||
|
||||
echo "Done!"
|
Loading…
Reference in New Issue