Files
Mikhail Filimonov 7a4c99c53d Merge branch 'appstore' into dev
# Conflicts:
#	Telegram-Mac/Info.plist
#	TelegramShare/Info.plist
2024-02-10 09:03:21 +04:00

520 lines
19 KiB
Swift

//
// TokenizedView.swift
// TGUIKit
//
// Created by keepcoder on 07/08/2017.
// Copyright © 2017 Telegram. All rights reserved.
//
import Cocoa
import SwiftSignalKit
import ColorPalette
public struct SearchToken : Equatable {
public let name:String
public let uniqueId:Int64
public init(name:String, uniqueId: Int64) {
self.name = name
self.uniqueId = uniqueId
}
}
public func ==(lhs:SearchToken, rhs: SearchToken) -> Bool {
return lhs.name == rhs.name && lhs.uniqueId == rhs.uniqueId
}
private class TokenView : Control {
fileprivate let token:SearchToken
private var isFailed: Bool = false
private let dismiss: ImageButton = ImageButton()
private let nameView: TextView = TextView()
fileprivate var immediatlyPaste: Bool = true
override var isSelected: Bool {
didSet {
updateLocalizationAndTheme(theme: presentation)
}
}
private let customTheme: ()->TokenizedView.Theme
init(_ token: SearchToken, maxSize: NSSize, onDismiss:@escaping()->Void, onSelect: @escaping()->Void, customTheme:@escaping()->TokenizedView.Theme) {
self.token = token
self.customTheme = customTheme
super.init()
self.layer?.cornerRadius = 6
let layout = TextViewLayout(.initialize(string: token.name, color: customTheme().underSelectColor, font: .normal(.title)), maximumNumberOfLines: 1)
layout.measure(width: maxSize.width - 30)
self.nameView.update(layout)
nameView.userInteractionEnabled = false
nameView.isSelectable = false
setFrameSize(NSMakeSize(layout.layoutSize.width + 30, maxSize.height))
dismiss.autohighlight = false
updateLocalizationAndTheme(theme: presentation)
needsLayout = true
addSubview(nameView)
addSubview(dismiss)
dismiss.set(handler: { _ in
onDismiss()
}, for: .Click)
set(handler: { _ in
onSelect()
}, for: .Click)
}
fileprivate func setFailed(_ isFailed: Bool, animated: Bool) {
self.isFailed = isFailed
self.updateLocalizationAndTheme(theme: presentation)
if isFailed {
self.shake()
}
}
fileprivate var isPerfectSized: Bool {
return nameView.textLayout?.isPerfectSized ?? false
}
override func change(size: NSSize, animated: Bool = true, _ save: Bool = true, removeOnCompletion: Bool = false, duration: Double = 0.2, timingFunction: CAMediaTimingFunctionName = CAMediaTimingFunctionName.easeOut, completion: ((Bool) -> Void)? = nil) {
nameView.textLayout?.measure(width: size.width - 30)
let size = NSMakeSize(min(((nameView.textLayout?.layoutSize.width ?? 0) + 30), size.width), size.height)
super.change(size: size, animated: animated, save, duration: duration, timingFunction: timingFunction)
let point = focus(dismiss.frame.size)
dismiss.change(pos: NSMakePoint(frame.width - 5 - dismiss.frame.width, point.minY), animated: animated)
nameView.update(nameView.textLayout)
}
override func layout() {
super.layout()
nameView.centerY(x: 5)
nameView.setFrameOrigin(5, nameView.frame.minY - 1)
dismiss.centerY(x: frame.width - 5 - dismiss.frame.width)
}
var _backgroundColor: NSColor {
var r:CGFloat = 0, g:CGFloat = 0, b:CGFloat = 0, a:CGFloat = 0
var redSelected = customTheme().redColor
redSelected.getRed(&r, green: &g, blue: &b, alpha: &a)
redSelected = NSColor(red: min(r + 0.1, 1.0), green: min(g + 0.1, 1.0), blue: min(b + 0.1, 1.0), alpha: a)
return isSelected ? (isFailed ? redSelected : customTheme().accentSelectColor) : (isFailed ? customTheme().redColor : customTheme().accentColor)
}
override func updateLocalizationAndTheme(theme: PresentationTheme) {
super.updateLocalizationAndTheme(theme: theme)
dismiss.set(image: #imageLiteral(resourceName: "Icon_SearchClear").precomposed(customTheme().underSelectColor.withAlphaComponent(0.7)), for: .Normal)
dismiss.set(image: #imageLiteral(resourceName: "Icon_SearchClear").precomposed(customTheme().underSelectColor), for: .Highlight)
_ = dismiss.sizeToFit()
nameView.backgroundColor = .clear
self.background = _backgroundColor
}
required public init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
required public init(frame frameRect: NSRect) {
fatalError("init(frame:) has not been implemented")
}
}
public protocol TokenizedProtocol : class {
func tokenizedViewDidChangedHeight(_ view: TokenizedView, height: CGFloat, animated: Bool)
}
public class TokenizedView: ScrollView, AppearanceViewProtocol, NSTextViewDelegate {
public struct Theme {
let background: NSColor
let grayBackground: NSColor
let textColor: NSColor
let grayTextColor: NSColor
let underSelectColor: NSColor
let accentColor: NSColor
let accentSelectColor: NSColor
let redColor: NSColor
public init(_ colors: ColorPalette) {
self.background = colors.background
self.textColor = colors.text
self.grayBackground = colors.grayBackground
self.grayTextColor = colors.grayText
self.underSelectColor = colors.underSelectedColor
self.accentColor = colors.accent
self.accentSelectColor = colors.accentSelect
self.redColor = colors.redUI
}
public init(background: NSColor = presentation.colors.background,
grayBackground: NSColor = presentation.colors.grayBackground,
textColor: NSColor = presentation.colors.text,
grayTextColor: NSColor = presentation.colors.grayText,
underSelectColor: NSColor = presentation.colors.underSelectedColor,
accentColor: NSColor = presentation.colors.accent,
accentSelectColor: NSColor = presentation.colors.accentSelect,
redColor: NSColor = presentation.colors.redUI) {
self.background = background
self.textColor = textColor
self.grayBackground = grayBackground
self.grayTextColor = grayTextColor
self.underSelectColor = underSelectColor
self.accentColor = accentColor
self.accentSelectColor = accentSelectColor
self.redColor = redColor
}
}
private var tokens:[SearchToken] = []
private var failedTokens:[Int64] = []
private let container: View = View()
private let input:SearchTextField = SearchTextField()
private(set) public var state: SearchFieldState = .None {
didSet {
stateValue.set(state)
}
}
public let stateValue: ValuePromise<SearchFieldState> = ValuePromise(.None, ignoreRepeated: true)
private var selectedIndex: Int? = nil {
didSet {
for view in container.subviews {
if let view = view as? TokenView {
view.isSelected = selectedIndex != nil && view.token == tokens[selectedIndex!]
}
}
}
}
private let _tokensUpdater:Promise<[SearchToken]> = Promise([])
public var tokensUpdater:Signal<[SearchToken], NoError> {
return _tokensUpdater.get()
}
private let _textUpdater:ValuePromise<String> = ValuePromise("", ignoreRepeated: true)
public var textUpdater:Signal<String, NoError> {
return _textUpdater.get()
}
public weak var delegate: TokenizedProtocol? = nil
private let placeholder: TextView = TextView()
public func addTokens(tokens: [SearchToken], animated: Bool) -> Void {
self.tokens.append(contentsOf: tokens)
for token in tokens {
let view = TokenView(token, maxSize: NSMakeSize(frame.width - 10, 22), onDismiss: { [weak self] in
self?.removeTokens(uniqueIds: [token.uniqueId], animated: true)
}, onSelect: { [weak self] in
self?.selectedIndex = self?.tokens.firstIndex(of: token)
}, customTheme: self.customTheme)
container.addSubview(view)
}
_tokensUpdater.set(.single(self.tokens))
input.string = ""
textDidChange(Notification(name: NSText.didChangeNotification))
(contentView as? TGClipView)?.scroll(to: NSMakePoint(0, max(0, container.frame.height - frame.height)), animated: animated)
}
public func removeTokens(uniqueIds: [Int64], animated: Bool) {
for uniqueId in uniqueIds {
var index:Int? = nil
for i in 0 ..< tokens.count {
if tokens[i].uniqueId == uniqueId {
index = i
break
}
}
if let index = index {
tokens.remove(at: index)
for view in container.subviews {
if let view = view as? TokenView {
if view.token.uniqueId == uniqueId {
view.change(opacity: 0, animated: animated, completion: { [weak view] completed in
if completed {
view?.removeFromSuperview()
}
})
}
}
}
}
}
layoutContainer(animated: animated)
_tokensUpdater.set(.single(tokens))
}
private func layoutContainer(animated: Bool) {
CATransaction.begin()
let mainw = frame.width
let between = NSMakePoint(5, 4)
var point: NSPoint = between
var extraLine: Bool = false
let count = container.subviews.count
for i in 0 ..< count {
let subview = container.subviews[i]
let next = container.subviews[min(i + 1, count - 1)]
if let token = subview as? TokenView, token.layer?.opacity != 0 {
token.change(pos: point, animated: token.immediatlyPaste ? false : animated)
if animated, token.immediatlyPaste {
token.layer?.animateAlpha(from: 0, to: 1, duration: 0.2)
}
//token.change(size: NSMakeSize(100, token.frame.height), animated: animated)
token.immediatlyPaste = false
point.x += subview.frame.width + between.x
let dif = mainw - (point.x + (i == count - 1 ? mainw/3 : next.frame.width) + between.x)
if dif < between.x {
// if !token.isPerfectSized {
// token.change(size: NSMakeSize(frame.width - startPointX - between.x, token.frame.height), animated: animated)
// }
point.x = between.x
point.y += token.frame.height + between.y
}
}
if subview == container.subviews.last {
if mainw - point.x > mainw/3 {
extraLine = true
}
}
}
input.frame = NSMakeRect(point.x, point.y + 3, mainw - point.x - between.x, 16)
placeholder.change(pos: NSMakePoint(point.x + 6, point.y + 3), animated: animated)
placeholder.change(opacity: tokens.isEmpty ? 1.0 : 0.0, animated: animated)
let contentHeight = max(point.y + between.y + (extraLine ? 22 : 0), 30)
container.change(size: NSMakeSize(container.frame.width, contentHeight), animated: animated)
let height = min(contentHeight, 108)
if height != frame.height {
_change(size: NSMakeSize(mainw, height), animated: animated)
delegate?.tokenizedViewDidChangedHeight(self, height: height, animated: animated)
}
CATransaction.commit()
}
public func textView(_ textView: NSTextView, doCommandBy commandSelector: Selector) -> Bool {
let range = input.selectedRange()
if commandSelector == #selector(insertNewline(_:)) {
return true
}
if range.location == 0 {
if commandSelector == #selector(moveLeft(_:)) {
if let index = selectedIndex {
selectedIndex = max(index - 1, 0)
} else {
selectedIndex = tokens.count - 1
}
return true
} else if commandSelector == #selector(moveRight(_:)) {
if let index = selectedIndex {
if index + 1 == tokens.count {
selectedIndex = nil
input.setSelectedRange(NSMakeRange(input.string.length, 0))
} else {
selectedIndex = index + 1
}
return true
}
}
if commandSelector == #selector(deleteBackward(_:)) {
if let selectedIndex = selectedIndex {
removeTokens(uniqueIds: [tokens[selectedIndex].uniqueId], animated: true)
if selectedIndex != tokens.count {
self.selectedIndex = min(selectedIndex, tokens.count - 1)
} else {
self.selectedIndex = nil
input.setSelectedRange(NSMakeRange(input.string.length, 0))
}
return true
} else {
if !tokens.isEmpty {
self.selectedIndex = tokens.count - 1
return true
}
}
}
}
return false
}
open func textDidChange(_ notification: Notification) {
let pHidden = !input.string.isEmpty
if placeholder.isHidden != pHidden {
placeholder.isHidden = pHidden
}
if self.window?.firstResponder == self.input {
self.state = .Focus
}
_textUpdater.set(input.string)
selectedIndex = nil
}
public func textDidEndEditing(_ notification: Notification) {
self.didResignResponder()
}
public func textDidBeginEditing(_ notification: Notification) {
//self.didBecomeResponder()
}
public override var needsLayout: Bool {
set {
super.needsLayout = false
}
get {
return super.needsLayout
}
}
public override func layout() {
super.layout()
//layoutContainer(animated: false)
}
public override func setFrameSize(_ newSize: NSSize) {
super.setFrameSize(newSize)
}
private let localizationFunc: (String)->String
private let placeholderKey: String
private let customTheme: ()->TokenizedView.Theme
required public init(frame frameRect: NSRect, localizationFunc: @escaping(String)->String, placeholderKey:String, customTheme: @escaping()->TokenizedView.Theme = { TokenizedView.Theme() }) {
self.localizationFunc = localizationFunc
self.placeholderKey = placeholderKey
self.customTheme = customTheme
super.init(frame: frameRect)
hasVerticalScroller = true
container.frame = bounds
container.autoresizingMask = [.width]
contentView.documentView = container
input.focusRingType = .none
input.backgroundColor = NSColor.clear
input.delegate = self
input.isRichText = false
input.textContainer?.widthTracksTextView = true
input.textContainer?.heightTracksTextView = false
input.isHorizontallyResizable = false
input.isVerticallyResizable = false
placeholder.set(handler: { [weak self] _ in
self?.window?.makeFirstResponder(self?.responder)
}, for: .Click)
input.font = .normal(.text)
container.addSubview(input)
container.addSubview(placeholder)
container.layer?.cornerRadius = 10
wantsLayer = true
self.layer?.cornerRadius = 10
self.layer?.backgroundColor = presentation.colors.grayBackground.cgColor
updateLocalizationAndTheme(theme: presentation)
}
public func markAsFailed(_ ids:[Int64], animated: Bool) {
self.failedTokens = ids
self.updateFailed(animated)
}
public func addTooltip(for id: Int64, text: String) {
for token in self.container.subviews {
if let token = token as? TokenView, token.token.uniqueId == id {
tooltip(for: token, text: text)
break
}
}
}
public func removeAllFailed(animated: Bool) {
self.failedTokens = []
self.updateFailed(animated)
}
private func updateFailed(_ animated: Bool) {
for token in self.container.subviews {
if let token = token as? TokenView {
token.setFailed(failedTokens.contains(token.token.uniqueId), animated: animated)
}
}
}
open func didResignResponder() {
state = .None
}
open func didBecomeResponder() {
state = .Focus
}
public var query: String {
return input.string
}
override public func becomeFirstResponder() -> Bool {
window?.makeFirstResponder(input)
return true
}
public var responder: NSResponder? {
return input
}
public func setString(_ string: String) {
input.string = string
self._textUpdater.set(string)
textDidChange(Notification(name: NSTextView.didChangeNotification))
}
public func updateLocalizationAndTheme(theme: PresentationTheme) {
background = customTheme().background
contentView.background = customTheme().background
self.container.backgroundColor = customTheme().grayBackground
input.textColor = customTheme().textColor
input.insertionPointColor = customTheme().textColor
let placeholderLayout = TextViewLayout(.initialize(string: localizedString(placeholderKey), color: customTheme().grayTextColor, font: .normal(.title)), maximumNumberOfLines: 1)
placeholderLayout.measure(width: .greatestFiniteMagnitude)
placeholder.update(placeholderLayout)
placeholder.backgroundColor = customTheme().grayBackground
placeholder.isSelectable = false
layoutContainer(animated: false)
}
required public init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}