335 lines
11 KiB
Swift
335 lines
11 KiB
Swift
//
|
|
// BackgroundTextField.swift.swift
|
|
// PrivadoVPN
|
|
//
|
|
// Created by Murad Shabanov on 31.03.2022.
|
|
// Copyright © 2022 Privado LLC. All rights reserved.
|
|
//
|
|
|
|
import Foundation
|
|
|
|
enum TextFieldType {
|
|
case login
|
|
case password
|
|
}
|
|
|
|
final class BackgroundTextField: NSView, NSTextFieldDelegate {
|
|
|
|
typealias EnterHandlerClosure = () -> Bool
|
|
typealias TextDidChageClosure = ((String) -> ())
|
|
typealias RightImageTappedClosure = () -> ()
|
|
|
|
enum Constants {
|
|
|
|
static let lineHeight: CGFloat = 2.0
|
|
|
|
enum Font {
|
|
static let name = PrivadoConstants.Font.regular
|
|
static let size: CGFloat = 14.0
|
|
}
|
|
|
|
enum Color {
|
|
static let text = NSColor.white
|
|
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)
|
|
static let beginEditing = NSColor(red: 40, green: 215, blue: 153)
|
|
}
|
|
}
|
|
|
|
// MARK: - Public
|
|
|
|
public var textField: NSTextField
|
|
public var enterHandler: EnterHandlerClosure?
|
|
public var textDidChangeClosure: TextDidChageClosure?
|
|
public var rightImageTappedClosure: RightImageTappedClosure?
|
|
|
|
public var backgroundColor: NSColor? {
|
|
didSet {
|
|
self.backgroundView.viewBackgroundColor = self.backgroundColor
|
|
}
|
|
}
|
|
|
|
public var highlightBackgroundColor: NSColor?
|
|
|
|
public var iconImage: NSImage? {
|
|
didSet {
|
|
self.leftIconView.image = self.iconImage
|
|
}
|
|
}
|
|
|
|
public var rightImage: NSImage? {
|
|
didSet {
|
|
self.rightIconView.image = self.rightImage
|
|
}
|
|
}
|
|
|
|
public var backgroundRadius: CGFloat? {
|
|
didSet {
|
|
self.backgroundView.layer?.cornerRadius = self.backgroundRadius ?? 0
|
|
}
|
|
}
|
|
|
|
public var strokeWidth: CGFloat? {
|
|
didSet {
|
|
self.backgroundView.layer?.borderWidth = self.strokeWidth ?? 0
|
|
}
|
|
}
|
|
|
|
public var strokeColor: NSColor? {
|
|
didSet {
|
|
self.backgroundView.layer?.borderColor = self.strokeColor?.cgColor
|
|
}
|
|
}
|
|
|
|
public var iconSize: CGSize {
|
|
didSet {
|
|
self.layoutUI()
|
|
}
|
|
}
|
|
|
|
public var rightIconSize: CGSize? {
|
|
didSet {
|
|
self.clearConstraints()
|
|
self.layoutUI()
|
|
}
|
|
}
|
|
|
|
public var textFieldYTopOffset: CGFloat = 11 {
|
|
didSet {
|
|
self.clearConstraints()
|
|
self.layoutUI()
|
|
}
|
|
}
|
|
|
|
public var textFieldTrailingOffset: CGFloat = 10 {
|
|
didSet {
|
|
self.clearConstraints()
|
|
self.layoutUI()
|
|
}
|
|
}
|
|
|
|
public var textFieldYBottomOffset: CGFloat = 10 {
|
|
didSet {
|
|
self.clearConstraints()
|
|
self.layoutUI()
|
|
}
|
|
}
|
|
|
|
public var coloredPlaceHolder: String? {
|
|
get {
|
|
return self.textField.placeholderAttributedString?.string
|
|
}
|
|
set {
|
|
if let value = newValue {
|
|
let attrs: [NSAttributedString.Key : Any] = [NSAttributedString.Key.foregroundColor: Constants.Color.placeholder,
|
|
NSAttributedString.Key.font: self.placeHolderFont ?? .systemFont(ofSize: 14)]
|
|
self.textField.placeholderAttributedString = NSAttributedString(string: value, attributes: attrs)
|
|
} else {
|
|
self.textField.placeholderAttributedString = nil
|
|
}
|
|
}
|
|
}
|
|
|
|
public var placeHolderFont: NSFont?
|
|
|
|
public func highlight(with color: NSColor?) {
|
|
self.backgroundView.layer?.borderWidth = 1
|
|
self.backgroundView.layer?.borderColor = color?.cgColor ?? .clear
|
|
}
|
|
|
|
// MARK: - Init
|
|
|
|
convenience init(frame frameRect: NSRect, textFieldType: TextFieldType, iconSize: CGSize) {
|
|
self.init(frame: .zero)
|
|
self.iconSize = iconSize
|
|
switch textFieldType {
|
|
case .login:
|
|
self.textField = NSTextField()
|
|
case .password:
|
|
self.textField = DSFSecureTextField()
|
|
}
|
|
self.setupUI()
|
|
self.layoutUI()
|
|
}
|
|
|
|
override init(frame frameRect: NSRect) {
|
|
self.textField = NSTextField()
|
|
self.backgroundView = NSView()
|
|
self.leftIconView = ImageView()
|
|
self.rightIconView = ImageView()
|
|
self.iconSize = CGSize(width: 15, height: 15)
|
|
super.init(frame: frameRect)
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
// MARK: - Private
|
|
|
|
private var backgroundView: NSView
|
|
private var leftIconView: ImageView
|
|
private var rightIconView: ImageView
|
|
|
|
private func setupUI() {
|
|
self.translatesAutoresizingMaskIntoConstraints = false
|
|
self.backgroundView.viewBackgroundColor = self.backgroundColor ?? NSColor.gray
|
|
self.backgroundView.translatesAutoresizingMaskIntoConstraints = false
|
|
self.backgroundView.wantsLayer = true
|
|
self.addSubview(self.backgroundView)
|
|
|
|
self.textField.cell?.isScrollable = true
|
|
self.textField.isBezeled = false
|
|
self.textField.drawsBackground = false
|
|
self.textField.translatesAutoresizingMaskIntoConstraints = false
|
|
self.textField.focusRingType = .none
|
|
self.textField.usesSingleLineMode = true
|
|
self.textField.delegate = self
|
|
self.backgroundView.addSubview(self.textField)
|
|
|
|
self.leftIconView.translatesAutoresizingMaskIntoConstraints = false
|
|
self.backgroundView.addSubview(self.leftIconView)
|
|
|
|
self.rightIconView.translatesAutoresizingMaskIntoConstraints = false
|
|
self.rightIconView.useHandCursor = true
|
|
self.rightIconView.target = self
|
|
self.rightIconView.action = #selector(self.rightImageTapped)
|
|
self.addSubview(self.rightIconView)
|
|
}
|
|
|
|
@objc func rightImageTapped() {
|
|
self.rightImageTappedClosure?()
|
|
}
|
|
|
|
private func layoutUI() {
|
|
NSLayoutConstraint.activate([
|
|
self.backgroundView.leadingAnchor.constraint(equalTo: self.leadingAnchor),
|
|
self.backgroundView.topAnchor.constraint(equalTo: self.topAnchor),
|
|
self.backgroundView.trailingAnchor.constraint(equalTo: self.trailingAnchor),
|
|
self.backgroundView.bottomAnchor.constraint(equalTo: self.bottomAnchor),
|
|
self.leftIconView.leadingAnchor.constraint(equalTo: self.backgroundView.leadingAnchor, constant: 13),
|
|
self.leftIconView.topAnchor.constraint(equalTo: self.backgroundView.topAnchor, constant: 13),
|
|
self.leftIconView.widthAnchor.constraint(equalToConstant: self.iconSize.width),
|
|
self.leftIconView.heightAnchor.constraint(equalToConstant: self.iconSize.height),
|
|
self.textField.leadingAnchor.constraint(equalTo: self.leftIconView.trailingAnchor, constant: 10),
|
|
self.textField.topAnchor.constraint(equalTo: self.backgroundView.topAnchor, constant: self.textFieldYTopOffset),
|
|
self.textField.bottomAnchor.constraint(equalTo: self.backgroundView.bottomAnchor, constant: -self.textFieldYBottomOffset),
|
|
self.textField.trailingAnchor.constraint(equalTo: self.backgroundView.trailingAnchor, constant: -self.textFieldTrailingOffset),
|
|
])
|
|
|
|
guard let rightIconSize = rightIconSize else { return }
|
|
NSLayoutConstraint.activate([
|
|
self.rightIconView.widthAnchor.constraint(equalToConstant: rightIconSize.width),
|
|
self.rightIconView.heightAnchor.constraint(equalToConstant: rightIconSize.height),
|
|
self.rightIconView.topAnchor.constraint(equalTo: self.topAnchor, constant: 13),
|
|
self.rightIconView.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -10)
|
|
])
|
|
}
|
|
|
|
private func changeBackground(with color: NSColor?) {
|
|
guard let color = color else {
|
|
return
|
|
}
|
|
|
|
NSAnimationContext.runAnimationGroup { inputContext in
|
|
inputContext.duration = 0.2
|
|
inputContext.allowsImplicitAnimation = true
|
|
self.backgroundView.animator().viewBackgroundColor = color
|
|
} completionHandler: {
|
|
print("highlightBackground")
|
|
}
|
|
}
|
|
|
|
// MARK: - NSTEextFieldDelegate
|
|
|
|
func controlTextDidBeginEditing(_ obj: Notification) {
|
|
self.changeBackground(with: self.highlightBackgroundColor)
|
|
self.highlight(with: nil)
|
|
}
|
|
|
|
func controlTextDidEndEditing(_ obj: Notification) {
|
|
self.changeBackground(with: self.backgroundColor)
|
|
}
|
|
|
|
func controlTextDidChange(_ obj: Notification) {
|
|
self.textDidChangeClosure?(self.textField.stringValue)
|
|
}
|
|
|
|
func control(_ control: NSControl, textView: NSTextView, doCommandBy commandSelector: Selector) -> Bool {
|
|
|
|
if commandSelector == #selector(NSResponder.insertNewline(_:)) {
|
|
|
|
guard let handler = self.enterHandler else { return true }
|
|
return handler()
|
|
}
|
|
|
|
return false
|
|
}
|
|
}
|
|
|
|
extension NSTextField {
|
|
|
|
private func performEditingKeyEquivalent(with event: NSEvent) -> Bool {
|
|
|
|
guard event.type == NSEvent.EventType.keyDown else { return false }
|
|
|
|
let commandKey = NSEvent.ModifierFlags.command.rawValue
|
|
let commandShiftKey = NSEvent.ModifierFlags.command.rawValue | NSEvent.ModifierFlags.shift.rawValue
|
|
|
|
let key = event.modifierFlags.rawValue & NSEvent.ModifierFlags.deviceIndependentFlagsMask.rawValue
|
|
switch key {
|
|
case commandKey: return self.handleCommandKey(keyCode: event.keyCode)
|
|
case commandShiftKey: return self.handleShiftKey(keyCode: event.keyCode)
|
|
default:
|
|
break
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
private func handleCommandKey(keyCode: UInt16) -> Bool {
|
|
|
|
switch keyCode {
|
|
case KeyCode.x:
|
|
if NSApp.sendAction(#selector(NSText.cut(_:)), to: nil, from: self) { return true }
|
|
case KeyCode.c:
|
|
if NSApp.sendAction(#selector(NSText.copy(_:)), to: nil, from: self) { return true }
|
|
case KeyCode.v:
|
|
if NSApp.sendAction(#selector(NSText.paste(_:)), to: nil, from: self) { return true }
|
|
case KeyCode.z:
|
|
if NSApp.sendAction(Selector(("undo:")), to: nil, from: self) { return true }
|
|
case KeyCode.a:
|
|
if NSApp.sendAction(#selector(NSResponder.selectAll(_:)), to: nil, from: self) { return true }
|
|
default:
|
|
break
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
private func handleShiftKey(keyCode: UInt16) -> Bool {
|
|
if keyCode == KeyCode.z {
|
|
if NSApp.sendAction(Selector(("redo:")), to: nil, from: self) { return true }
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
public override func performKeyEquivalent(with event: NSEvent) -> Bool {
|
|
if self.performEditingKeyEquivalent(with: event) {
|
|
return true
|
|
}
|
|
|
|
return super.performKeyEquivalent(with: event)
|
|
}
|
|
}
|
|
|
|
extension NSView {
|
|
func clearConstraints() {
|
|
for subview in self.subviews {
|
|
subview.clearConstraints()
|
|
}
|
|
self.removeConstraints(self.constraints)
|
|
}
|
|
}
|