Files

451 lines
19 KiB
Swift

//
// DSFSecureTextField.swift
//
// Created by Darren Ford on 2/1/20.
// Copyright © 2020 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
@IBDesignable
public class DSFSecureTextField: NSSecureTextField {
/// Whether to display a toggle button into the control to control the visibility
@IBInspectable
public dynamic var displayToggleButton: Bool = true {
didSet {
self.updateForPasswordVisibility()
}
}
/// Allow or disallow showing plain text password
@IBInspectable
public dynamic var allowShowPassword: Bool = true {
didSet {
self.passwordIsVisible = false
self.configureButtonForState()
}
}
/// Show or obscure the password
@objc
public dynamic var passwordIsVisible: Bool = false {
didSet {
self.updateForPasswordVisibility()
}
}
// Embedded button if the style requires it
private var visibilityButton: DSFPasswordButton?
public override init(frame frameRect: NSRect) {
super.init(frame: frameRect)
self.setup()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
}
override public func viewDidMoveToWindow() {
super.viewDidMoveToWindow()
self.setup()
}
}
// MARK: - Private (DSFSecureTextField)
private extension DSFSecureTextField {
func configureButtonForState() {
if self.allowShowPassword && self.displayToggleButton {
let button = DSFPasswordButton(frame: NSRect(x: 0, y: 0, width: 16, height: 16))
self.visibilityButton = button
button.action = #selector(visibilityChanged(_:))
button.target = self
self.addSubview(button)
NSLayoutConstraint.activate([
button.centerYAnchor.constraint(equalTo: self.centerYAnchor),
button.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -2),
button.widthAnchor.constraint(equalTo: button.heightAnchor, multiplier: 1),
button.heightAnchor.constraint(equalTo: self.heightAnchor, multiplier: 0.8)
])
button.needsLayout = true
self.needsUpdateConstraints = true
} else {
self.visibilityButton?.removeFromSuperview()
self.visibilityButton = nil
}
self.window?.recalculateKeyViewLoop()
}
func setup() {
self.translatesAutoresizingMaskIntoConstraints = false
// By default, the password should ALWAYS be hidden
self.passwordIsVisible = false
self.configureButtonForState()
self.updateForPasswordVisibility()
}
// MARK: Updates
// Triggered when the user clicks the embedded button
@objc func visibilityChanged(_ sender: NSButton) {
self.passwordIsVisible = (sender.state == .on)
}
func updateForPasswordVisibility() {
let str = self.cell?.stringValue ?? ""
if self.window?.firstResponder == self.currentEditor() {
// Text field has focus.
self.abortEditing()
}
guard let oldCell = self.cell as? NSTextFieldCell else { return }
let newCell: NSTextFieldCell
if !self.displayToggleButton {
// Button should NOT be included
if self.passwordIsVisible {
newCell = NSTextFieldCell()
self.cell = newCell
} else {
newCell = NSSecureTextFieldCell()
self.cell = newCell
}
} else {
if self.allowShowPassword {
newCell = self.passwordIsVisible ? DSFPlainTextFieldCell() : DSFPasswordTextFieldCell()
self.cell = newCell
} else {
newCell = NSSecureTextFieldCell()
self.cell = newCell
}
}
newCell.placeholderAttributedString = oldCell.placeholderAttributedString
newCell.isScrollable = oldCell.isScrollable
newCell.isEditable = oldCell.isEditable
newCell.font = oldCell.font
newCell.textColor = oldCell.textColor
newCell.isBordered = oldCell.isBordered
newCell.isBezeled = oldCell.isBezeled
newCell.backgroundStyle = oldCell.backgroundStyle
newCell.bezelStyle = oldCell.bezelStyle
newCell.drawsBackground = oldCell.drawsBackground
newCell.focusRingType = oldCell.focusRingType
newCell.usesSingleLineMode = oldCell.usesSingleLineMode
newCell.stringValue = str
self.visibilityButton?.needsLayout = true
self.needsUpdateConstraints = true
}
}
// MARK: - Private implementation (Text Field Cells)
private class DSFPasswordTextFieldCell: NSSecureTextFieldCell {
override func select(withFrame rect: NSRect, in controlView: NSView, editor textObj: NSText, delegate: Any?, start selStart: Int, length selLength: Int) {
var newRect = rect
newRect.size.width -= rect.height * 1.25
super.select(withFrame: newRect, in: controlView, editor: textObj, delegate: delegate, start: selStart, length: selLength)
}
override func edit(withFrame rect: NSRect, in controlView: NSView, editor textObj: NSText, delegate: Any?, event: NSEvent?) {
var newRect = rect
newRect.size.width -= rect.height * 1.25
super.edit(withFrame: newRect, in: controlView, editor: textObj, delegate: delegate, event: event)
}
override func drawInterior(withFrame cellFrame: NSRect, in controlView: NSView) {
if self.drawsBackground {
NSColor.controlBackgroundColor.setFill()
cellFrame.fill()
}
var newRect = cellFrame
newRect.size.width -= cellFrame.height * 1.25
super.drawInterior(withFrame: newRect, in: controlView)
}
}
private class DSFPlainTextFieldCell: NSTextFieldCell {
override func select(withFrame rect: NSRect, in controlView: NSView, editor textObj: NSText, delegate: Any?, start selStart: Int, length selLength: Int) {
var newRect = rect
newRect.size.width -= rect.height * 1.25
super.select(withFrame: newRect, in: controlView, editor: textObj, delegate: delegate, start: selStart, length: selLength)
}
override func edit(withFrame rect: NSRect, in controlView: NSView, editor textObj: NSText, delegate: Any?, event: NSEvent?) {
var newRect = rect
newRect.size.width -= rect.height * 1.25
super.edit(withFrame: newRect, in: controlView, editor: textObj, delegate: delegate, event: event)
}
override func drawInterior(withFrame cellFrame: NSRect, in controlView: NSView) {
if self.drawsBackground {
NSColor.controlBackgroundColor.setFill()
cellFrame.fill()
}
var newRect = cellFrame
newRect.size.width -= cellFrame.height * 1.25
super.drawInterior(withFrame: newRect, in: controlView)
}
}
// MARK: - Private implementation (Embedded button)
private class DSFPasswordButton: NSButton {
enum Constants {
enum Color {
static let placeholder = NSColor(calibratedRed: 173.0 / 255, green: 179.0 / 255, blue: 210.0 / 255, alpha: 1.0)
static let neutral = NSColor(calibratedRed: 45.0 / 255, green: 51.0 / 255, blue: 82.0 / 255, alpha: 1.0)
}
}
private class Translation {
static var toggle: String {
NSLocalizedString("Toggle password visibility", comment: "Button used to toggle whether the password is shown in the clear or obscured")
}
}
// State observer for the button. Note that we have to attach it to the _cell's_ state, as the NSButton state is not
// marked as `dynamic` in Swift.
private var stateObserver: NSKeyValueObservation?
override init(frame frameRect: NSRect) {
super.init(frame: frameRect)
self.setup()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
}
override func awakeFromNib() {
super.awakeFromNib()
self.setup()
}
override var cell: NSCell? {
get {
super.cell
}
set {
super.cell = newValue
self.updateObserver()
}
}
private func updateObserver() {
self.stateObserver = nil
self.stateObserver = self.observe(\.cell!.state, options: [.new]) { [weak self] _, _/*button, state*/ in
self?.stateChanged()
}
}
private func stateChanged() {
// Currently doing nothing
}
private func setup() {
self.translatesAutoresizingMaskIntoConstraints = false
self.isBordered = false
self.imagePosition = .noImage
if #available(macOS 10.14, *) {
self.contentTintColor = .clear
}
self.toolTip = Translation.toggle
// Initial observer
self.updateObserver()
}
// MARK: Tracking areas
private var trackingArea: NSTrackingArea?
override func layout() {
super.layout()
if let area = trackingArea {
self.removeTrackingArea(area)
}
let opts: NSTrackingArea.Options = [.inVisibleRect, [.mouseMoved, .mouseEnteredAndExited], .activeInKeyWindow]
let newE = NSTrackingArea(rect: self.bounds, options: opts, owner: self, userInfo: nil)
self.addTrackingArea(newE)
}
override func mouseMoved(with event: NSEvent) {
super.mouseMoved(with: event)
NSCursor.pointingHand.set()
}
override public func mouseEntered(with event: NSEvent) {
NSCursor.pointingHand.set()
}
override public func mouseExited(with event: NSEvent) {
NSCursor.arrow.set()
}
// MARK: Drawing
// swiftlint:disable function_body_length
override func draw(_ dirtyRect: NSRect) {
let dest = self.bounds
let highContrast = NSWorkspace.shared.accessibilityDisplayShouldIncreaseContrast
if state == .off {
let fillColor = Constants.Color.placeholder
// Bezier Drawing
let bezierPath = NSBezierPath()
bezierPath.move(to: NSPoint(x: 25.12, y: 5.21))
bezierPath.line(to: NSPoint(x: 6.03, y: 24.46))
bezierPath.curve(to: NSPoint(x: 6.03, y: 25.42), controlPoint1: NSPoint(x: 5.77, y: 24.72), controlPoint2: NSPoint(x: 5.77, y: 25.16))
bezierPath.curve(to: NSPoint(x: 6.99, y: 25.42), controlPoint1: NSPoint(x: 6.3, y: 25.7), controlPoint2: NSPoint(x: 6.73, y: 25.69))
bezierPath.line(to: NSPoint(x: 26.06, y: 6.17))
bezierPath.curve(to: NSPoint(x: 26.06, y: 5.21), controlPoint1: NSPoint(x: 26.34, y: 5.9), controlPoint2: NSPoint(x: 26.36, y: 5.51))
bezierPath.curve(to: NSPoint(x: 25.12, y: 5.21), controlPoint1: NSPoint(x: 25.79, y: 4.92), controlPoint2: NSPoint(x: 25.38, y: 4.94))
bezierPath.close()
bezierPath.move(to: NSPoint(x: 16.14, y: 24.73))
bezierPath.curve(to: NSPoint(x: 31.27, y: 15.08), controlPoint1: NSPoint(x: 25.03, y: 24.73), controlPoint2: NSPoint(x: 31.27, y: 17.37))
bezierPath.curve(to: NSPoint(x: 25.43, y: 8.27), controlPoint1: NSPoint(x: 31.27, y: 13.72), controlPoint2: NSPoint(x: 29.09, y: 10.59))
bezierPath.line(to: NSPoint(x: 24.35, y: 9.36))
bezierPath.curve(to: NSPoint(x: 29.73, y: 15.08), controlPoint1: NSPoint(x: 27.67, y: 11.38), controlPoint2: NSPoint(x: 29.73, y: 14.09))
bezierPath.curve(to: NSPoint(x: 16.14, y: 23.31), controlPoint1: NSPoint(x: 29.73, y: 16.58), controlPoint2: NSPoint(x: 23.83, y: 23.31))
bezierPath.curve(to: NSPoint(x: 11.7, y: 22.53), controlPoint1: NSPoint(x: 14.51, y: 23.31), controlPoint2: NSPoint(x: 13.1, y: 23.02))
bezierPath.line(to: NSPoint(x: 10.54, y: 23.69))
bezierPath.curve(to: NSPoint(x: 16.14, y: 24.73), controlPoint1: NSPoint(x: 12.28, y: 24.33), controlPoint2: NSPoint(x: 14.07, y: 24.73))
bezierPath.close()
bezierPath.move(to: NSPoint(x: 16.14, y: 5.43))
bezierPath.curve(to: NSPoint(x: 1, y: 15.08), controlPoint1: NSPoint(x: 7.33, y: 5.43), controlPoint2: NSPoint(x: 1, y: 12.79))
bezierPath.curve(to: NSPoint(x: 7.04, y: 21.99), controlPoint1: NSPoint(x: 1, y: 16.45), controlPoint2: NSPoint(x: 3.27, y: 19.64))
bezierPath.line(to: NSPoint(x: 8.14, y: 20.88))
bezierPath.curve(to: NSPoint(x: 2.56, y: 15.08), controlPoint1: NSPoint(x: 4.68, y: 18.8), controlPoint2: NSPoint(x: 2.56, y: 15.99))
bezierPath.curve(to: NSPoint(x: 16.14, y: 6.85), controlPoint1: NSPoint(x: 2.56, y: 13.4), controlPoint2: NSPoint(x: 8.48, y: 6.85))
bezierPath.curve(to: NSPoint(x: 20.89, y: 7.68), controlPoint1: NSPoint(x: 17.87, y: 6.85), controlPoint2: NSPoint(x: 19.41, y: 7.17))
bezierPath.line(to: NSPoint(x: 22.04, y: 6.51))
bezierPath.curve(to: NSPoint(x: 16.14, y: 5.43), controlPoint1: NSPoint(x: 20.25, y: 5.85), controlPoint2: NSPoint(x: 18.31, y: 5.43))
bezierPath.close()
bezierPath.move(to: NSPoint(x: 21.75, y: 12.25))
bezierPath.line(to: NSPoint(x: 13.32, y: 20.75))
bezierPath.curve(to: NSPoint(x: 16.14, y: 21.44), controlPoint1: NSPoint(x: 14.17, y: 21.19), controlPoint2: NSPoint(x: 15.13, y: 21.44))
bezierPath.curve(to: NSPoint(x: 22.47, y: 15.08), controlPoint1: NSPoint(x: 19.63, y: 21.44), controlPoint2: NSPoint(x: 22.47, y: 18.63))
bezierPath.curve(to: NSPoint(x: 21.75, y: 12.25), controlPoint1: NSPoint(x: 22.47, y: 14.06), controlPoint2: NSPoint(x: 22.21, y: 13.09))
bezierPath.close()
bezierPath.move(to: NSPoint(x: 16.14, y: 8.71))
bezierPath.curve(to: NSPoint(x: 9.81, y: 15.08), controlPoint1: NSPoint(x: 12.62, y: 8.71), controlPoint2: NSPoint(x: 9.83, y: 11.61))
bezierPath.curve(to: NSPoint(x: 10.6, y: 18.13), controlPoint1: NSPoint(x: 9.81, y: 16.19), controlPoint2: NSPoint(x: 10.09, y: 17.24))
bezierPath.line(to: NSPoint(x: 19.13, y: 9.51))
bezierPath.curve(to: NSPoint(x: 16.14, y: 8.71), controlPoint1: NSPoint(x: 18.25, y: 9.01), controlPoint2: NSPoint(x: 17.24, y: 8.71))
bezierPath.close()
var transform = AffineTransform()
transform.scale(dest.width / 33.0)
transform.translate(x: 0, y: 1)
bezierPath.transform(using: transform)
fillColor.setFill()
bezierPath.fill()
} else {
let fillColor = NSColor.white
let bezierPath = NSBezierPath()
bezierPath.move(to: NSPoint(x: 16.01, y: 5))
bezierPath.curve(to: NSPoint(x: 1, y: 15), controlPoint1: NSPoint(x: 7.27, y: 5), controlPoint2: NSPoint(x: 1, y: 12.63))
bezierPath.curve(to: NSPoint(x: 16.01, y: 25), controlPoint1: NSPoint(x: 1, y: 17.37), controlPoint2: NSPoint(x: 7.31, y: 25))
bezierPath.curve(to: NSPoint(x: 31, y: 15), controlPoint1: NSPoint(x: 24.78, y: 25), controlPoint2: NSPoint(x: 31, y: 17.37))
bezierPath.curve(to: NSPoint(x: 16.01, y: 5), controlPoint1: NSPoint(x: 31, y: 12.63), controlPoint2: NSPoint(x: 24.81, y: 5))
bezierPath.close()
bezierPath.move(to: NSPoint(x: 16.01, y: 6.47))
bezierPath.curve(to: NSPoint(x: 29.47, y: 15), controlPoint1: NSPoint(x: 23.59, y: 6.47), controlPoint2: NSPoint(x: 29.47, y: 13.27))
bezierPath.curve(to: NSPoint(x: 16.01, y: 23.53), controlPoint1: NSPoint(x: 29.47, y: 16.56), controlPoint2: NSPoint(x: 23.59, y: 23.53))
bezierPath.curve(to: NSPoint(x: 2.54, y: 15), controlPoint1: NSPoint(x: 8.41, y: 23.53), controlPoint2: NSPoint(x: 2.54, y: 16.56))
bezierPath.curve(to: NSPoint(x: 16.01, y: 6.47), controlPoint1: NSPoint(x: 2.54, y: 13.27), controlPoint2: NSPoint(x: 8.41, y: 6.47))
bezierPath.close()
bezierPath.move(to: NSPoint(x: 16.01, y: 8.41))
bezierPath.curve(to: NSPoint(x: 9.73, y: 15), controlPoint1: NSPoint(x: 12.52, y: 8.41), controlPoint2: NSPoint(x: 9.75, y: 11.4))
bezierPath.curve(to: NSPoint(x: 16.01, y: 21.59), controlPoint1: NSPoint(x: 9.72, y: 18.68), controlPoint2: NSPoint(x: 12.52, y: 21.59))
bezierPath.curve(to: NSPoint(x: 22.27, y: 15), controlPoint1: NSPoint(x: 19.46, y: 21.59), controlPoint2: NSPoint(x: 22.27, y: 18.68))
bezierPath.curve(to: NSPoint(x: 16.01, y: 8.41), controlPoint1: NSPoint(x: 22.27, y: 11.4), controlPoint2: NSPoint(x: 19.46, y: 8.41))
bezierPath.close()
bezierPath.move(to: NSPoint(x: 16.02, y: 12.9))
bezierPath.curve(to: NSPoint(x: 18.02, y: 15), controlPoint1: NSPoint(x: 17.11, y: 12.9), controlPoint2: NSPoint(x: 18.02, y: 13.84))
bezierPath.curve(to: NSPoint(x: 16.02, y: 17.1), controlPoint1: NSPoint(x: 18.02, y: 16.17), controlPoint2: NSPoint(x: 17.11, y: 17.1))
bezierPath.curve(to: NSPoint(x: 13.99, y: 15), controlPoint1: NSPoint(x: 14.9, y: 17.1), controlPoint2: NSPoint(x: 13.99, y: 16.17))
bezierPath.curve(to: NSPoint(x: 16.02, y: 12.9), controlPoint1: NSPoint(x: 13.99, y: 13.84), controlPoint2: NSPoint(x: 14.9, y: 12.9))
bezierPath.close()
var transform = AffineTransform()
transform.scale(dest.width / 33.0)
transform.translate(x: 0, y: 1)
bezierPath.transform(using: transform)
if !highContrast {
let shadow = NSShadow()
shadow.shadowColor = NSColor.black.withAlphaComponent(0.6)
shadow.shadowOffset = NSSize(width: 1.0, height: -1.0)
shadow.shadowBlurRadius = 1.0
shadow.set()
}
fillColor.setFill()
bezierPath.fill()
}
}
// swiftlint:enable function_body_length
}