mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
787 lines
32 KiB
Swift
787 lines
32 KiB
Swift
import Foundation
|
|
import UIKit
|
|
import Display
|
|
import AsyncDisplayKit
|
|
import SwiftSignalKit
|
|
import TelegramPresentationData
|
|
import ItemListUI
|
|
import PresentationDataUtils
|
|
import TelegramStringFormatting
|
|
|
|
class BotCheckoutTipItem: ListViewItem, ItemListItem {
|
|
let theme: PresentationTheme
|
|
let strings: PresentationStrings
|
|
let title: String
|
|
let currency: String
|
|
let value: String
|
|
let numericValue: Int64
|
|
let maxValue: Int64
|
|
let availableVariants: [(String, Int64)]
|
|
let updateValue: (Int64) -> Void
|
|
let updatedFocus: (Bool) -> Void
|
|
|
|
let sectionId: ItemListSectionId
|
|
|
|
let requestsNoInset: Bool = true
|
|
|
|
init(theme: PresentationTheme, strings: PresentationStrings, title: String, currency: String, value: String, numericValue: Int64, maxValue: Int64, availableVariants: [(String, Int64)], sectionId: ItemListSectionId, updateValue: @escaping (Int64) -> Void, updatedFocus: @escaping (Bool) -> Void) {
|
|
self.theme = theme
|
|
self.strings = strings
|
|
self.title = title
|
|
self.currency = currency
|
|
self.value = value
|
|
self.numericValue = numericValue
|
|
self.maxValue = maxValue
|
|
self.availableVariants = availableVariants
|
|
self.updateValue = updateValue
|
|
self.updatedFocus = updatedFocus
|
|
self.sectionId = sectionId
|
|
}
|
|
|
|
func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
|
|
async {
|
|
let node = BotCheckoutTipItemNode()
|
|
let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
|
|
|
|
node.contentSize = layout.contentSize
|
|
node.insets = layout.insets
|
|
|
|
Queue.mainQueue().async {
|
|
completion(node, {
|
|
return (nil, { _ in apply() })
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
|
|
Queue.mainQueue().async {
|
|
if let nodeValue = node() as? BotCheckoutTipItemNode {
|
|
let makeLayout = nodeValue.asyncLayout()
|
|
|
|
async {
|
|
let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
|
|
Queue.mainQueue().async {
|
|
completion(layout, { _ in
|
|
apply()
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
let selectable: Bool = false
|
|
}
|
|
|
|
private let titleFont = Font.regular(17.0)
|
|
private let finalFont = Font.semibold(17.0)
|
|
|
|
private func priceItemInsets(_ neighbors: ItemListNeighbors) -> UIEdgeInsets {
|
|
var insets = UIEdgeInsets()
|
|
switch neighbors.top {
|
|
case .otherSection:
|
|
insets.top += 8.0
|
|
case .none, .sameSection:
|
|
break
|
|
}
|
|
switch neighbors.bottom {
|
|
case .none, .otherSection:
|
|
insets.bottom += 8.0
|
|
case .sameSection:
|
|
break
|
|
}
|
|
return insets
|
|
}
|
|
|
|
private final class TipValueNode: ASDisplayNode {
|
|
private let backgroundNode: ASImageNode
|
|
private let titleNode: ImmediateTextNode
|
|
|
|
private let button: HighlightTrackingButtonNode
|
|
|
|
private var currentBackgroundColor: UIColor?
|
|
|
|
var action: (() -> Void)?
|
|
|
|
override init() {
|
|
self.backgroundNode = ASImageNode()
|
|
self.titleNode = ImmediateTextNode()
|
|
|
|
self.button = HighlightTrackingButtonNode()
|
|
|
|
super.init()
|
|
|
|
self.addSubnode(self.backgroundNode)
|
|
self.addSubnode(self.titleNode)
|
|
self.addSubnode(self.button)
|
|
self.button.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside)
|
|
}
|
|
|
|
@objc private func buttonPressed() {
|
|
self.action?()
|
|
}
|
|
|
|
func update(theme: PresentationTheme, text: String, isHighlighted: Bool, height: CGFloat) -> (CGFloat, (CGFloat) -> Void) {
|
|
var updateBackground = false
|
|
let backgroundColor = isHighlighted ? theme.list.paymentOption.activeFillColor : theme.list.paymentOption.inactiveFillColor
|
|
if let currentBackgroundColor = self.currentBackgroundColor {
|
|
if !currentBackgroundColor.isEqual(backgroundColor) {
|
|
updateBackground = true
|
|
}
|
|
} else {
|
|
updateBackground = true
|
|
}
|
|
if updateBackground {
|
|
self.currentBackgroundColor = backgroundColor
|
|
self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 20.0, color: backgroundColor)
|
|
}
|
|
|
|
self.titleNode.attributedText = NSAttributedString(string: text, font: Font.semibold(15.0), textColor: isHighlighted ? theme.list.paymentOption.activeForegroundColor : theme.list.paymentOption.inactiveForegroundColor)
|
|
let titleSize = self.titleNode.updateLayout(CGSize(width: 200.0, height: height))
|
|
|
|
let minWidth: CGFloat = 80.0
|
|
|
|
let calculatedWidth = max(titleSize.width + 16.0 * 2.0, minWidth)
|
|
|
|
return (calculatedWidth, { calculatedWidth in
|
|
self.titleNode.frame = CGRect(origin: CGPoint(x: floor((calculatedWidth - titleSize.width) / 2.0), y: floor((height - titleSize.height) / 2.0)), size: titleSize)
|
|
|
|
let size = CGSize(width: calculatedWidth, height: height)
|
|
self.backgroundNode.frame = CGRect(origin: CGPoint(), size: size)
|
|
|
|
self.button.frame = CGRect(origin: CGPoint(), size: size)
|
|
})
|
|
}
|
|
}
|
|
|
|
private final class FormatterImpl: NSObject, UITextFieldDelegate {
|
|
private struct Representation {
|
|
private let format: CurrencyFormat
|
|
private var caretIndex: Int = 0
|
|
private var wholePart: [Int] = []
|
|
private var decimalPart: [Int] = []
|
|
|
|
init(string: String, format: CurrencyFormat) {
|
|
self.format = format
|
|
|
|
var isDecimalPart = false
|
|
for c in string {
|
|
if c.isNumber {
|
|
if let value = Int(String(c)) {
|
|
if isDecimalPart {
|
|
self.decimalPart.append(value)
|
|
} else {
|
|
self.wholePart.append(value)
|
|
}
|
|
}
|
|
} else if String(c) == format.decimalSeparator {
|
|
isDecimalPart = true
|
|
}
|
|
}
|
|
|
|
while self.wholePart.count > 1 {
|
|
if self.wholePart[0] != 0 {
|
|
break
|
|
} else {
|
|
self.wholePart.removeFirst()
|
|
}
|
|
}
|
|
if self.wholePart.isEmpty {
|
|
self.wholePart = [0]
|
|
}
|
|
|
|
while self.decimalPart.count > 1 {
|
|
if self.decimalPart[self.decimalPart.count - 1] != 0 {
|
|
break
|
|
} else {
|
|
self.decimalPart.removeLast()
|
|
}
|
|
}
|
|
while self.decimalPart.count < format.decimalDigits {
|
|
self.decimalPart.append(0)
|
|
}
|
|
|
|
self.caretIndex = self.wholePart.count
|
|
}
|
|
|
|
var minCaretIndex: Int {
|
|
for i in 0 ..< self.wholePart.count {
|
|
if self.wholePart[i] != 0 {
|
|
return i
|
|
}
|
|
}
|
|
return self.wholePart.count
|
|
}
|
|
|
|
mutating func moveCaret(offset: Int) {
|
|
self.caretIndex = max(self.minCaretIndex, min(self.caretIndex + offset, self.wholePart.count + self.decimalPart.count))
|
|
}
|
|
|
|
mutating func normalize() {
|
|
while self.wholePart.count > 1 {
|
|
if self.wholePart[0] != 0 {
|
|
break
|
|
} else {
|
|
self.wholePart.removeFirst()
|
|
self.moveCaret(offset: -1)
|
|
}
|
|
}
|
|
if self.wholePart.isEmpty {
|
|
self.wholePart = [0]
|
|
}
|
|
|
|
while self.decimalPart.count < format.decimalDigits {
|
|
self.decimalPart.append(0)
|
|
}
|
|
while self.decimalPart.count > format.decimalDigits {
|
|
self.decimalPart.removeLast()
|
|
}
|
|
|
|
self.caretIndex = max(self.minCaretIndex, min(self.caretIndex, self.wholePart.count + self.decimalPart.count))
|
|
}
|
|
|
|
mutating func backspace() {
|
|
if self.caretIndex > self.wholePart.count {
|
|
let decimalIndex = self.caretIndex - self.wholePart.count
|
|
if decimalIndex > 0 {
|
|
self.decimalPart.remove(at: decimalIndex - 1)
|
|
|
|
self.moveCaret(offset: -1)
|
|
self.normalize()
|
|
}
|
|
} else {
|
|
if self.caretIndex > 0 {
|
|
self.wholePart.remove(at: self.caretIndex - 1)
|
|
|
|
self.moveCaret(offset: -1)
|
|
self.normalize()
|
|
}
|
|
}
|
|
}
|
|
|
|
mutating func insert(letter: String) {
|
|
if letter == "." || letter == "," {
|
|
if self.caretIndex == self.wholePart.count {
|
|
return
|
|
} else if self.caretIndex < self.wholePart.count {
|
|
for i in (self.caretIndex ..< self.wholePart.count).reversed() {
|
|
self.decimalPart.insert(self.wholePart[i], at: 0)
|
|
self.wholePart.remove(at: i)
|
|
}
|
|
}
|
|
|
|
self.normalize()
|
|
} else if letter.count == 1 && letter[letter.startIndex].isNumber {
|
|
if let value = Int(letter) {
|
|
if self.caretIndex <= self.wholePart.count {
|
|
self.wholePart.insert(value, at: self.caretIndex)
|
|
} else {
|
|
let decimalIndex = self.caretIndex - self.wholePart.count
|
|
self.decimalPart.insert(value, at: decimalIndex)
|
|
}
|
|
self.moveCaret(offset: 1)
|
|
self.normalize()
|
|
}
|
|
}
|
|
}
|
|
|
|
var string: String {
|
|
var result = ""
|
|
|
|
for digit in self.wholePart {
|
|
result.append("\(digit)")
|
|
}
|
|
result.append(self.format.decimalSeparator)
|
|
for digit in self.decimalPart {
|
|
result.append("\(digit)")
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
var stringCaretIndex: Int {
|
|
var logicalIndex = 0
|
|
var resolvedIndex = 0
|
|
|
|
if logicalIndex == self.caretIndex {
|
|
return resolvedIndex
|
|
}
|
|
|
|
for _ in self.wholePart {
|
|
logicalIndex += 1
|
|
resolvedIndex += 1
|
|
|
|
if logicalIndex == self.caretIndex {
|
|
return resolvedIndex
|
|
}
|
|
}
|
|
|
|
resolvedIndex += 1
|
|
|
|
for _ in self.decimalPart {
|
|
logicalIndex += 1
|
|
resolvedIndex += 1
|
|
|
|
if logicalIndex == self.caretIndex {
|
|
return resolvedIndex
|
|
}
|
|
}
|
|
|
|
return resolvedIndex
|
|
}
|
|
|
|
var numericalValue: Int64 {
|
|
var result: Int64 = 0
|
|
|
|
for digit in self.wholePart {
|
|
result *= 10
|
|
result += Int64(digit)
|
|
}
|
|
for digit in self.decimalPart {
|
|
result *= 10
|
|
result += Int64(digit)
|
|
}
|
|
|
|
return result
|
|
}
|
|
}
|
|
|
|
private let format: CurrencyFormat
|
|
private let currency: String
|
|
private let maxNumericalValue: Int64
|
|
private let updated: (Int64) -> Void
|
|
private let focusUpdated: (Bool) -> Void
|
|
|
|
private var representation: Representation
|
|
|
|
private var previousResolvedCaretIndex: Int = 0
|
|
private var ignoreTextSelection: Bool = false
|
|
private var enableTextSelectionProcessing: Bool = false
|
|
|
|
init?(textField: UITextField, currency: String, maxNumericalValue: Int64, initialValue: String, updated: @escaping (Int64) -> Void, focusUpdated: @escaping (Bool) -> Void) {
|
|
guard let format = CurrencyFormat(currency: currency) else {
|
|
return nil
|
|
}
|
|
self.format = format
|
|
self.currency = currency
|
|
self.maxNumericalValue = maxNumericalValue
|
|
self.updated = updated
|
|
self.focusUpdated = focusUpdated
|
|
|
|
self.representation = Representation(string: initialValue, format: format)
|
|
|
|
super.init()
|
|
|
|
textField.text = self.representation.string
|
|
self.previousResolvedCaretIndex = self.representation.stringCaretIndex
|
|
}
|
|
|
|
func reset(textField: UITextField, initialValue: String) {
|
|
self.representation = Representation(string: initialValue, format: self.format)
|
|
self.resetFromRepresentation(textField: textField, notifyUpdated: false)
|
|
}
|
|
|
|
private func resetFromRepresentation(textField: UITextField, notifyUpdated: Bool) {
|
|
self.ignoreTextSelection = true
|
|
|
|
if self.representation.numericalValue > self.maxNumericalValue {
|
|
self.representation = Representation(string: formatCurrencyAmountCustom(self.maxNumericalValue, currency: self.currency).0, format: self.format)
|
|
}
|
|
|
|
textField.text = self.representation.string
|
|
self.previousResolvedCaretIndex = self.representation.stringCaretIndex
|
|
|
|
if self.enableTextSelectionProcessing {
|
|
let stringCaretIndex = self.representation.stringCaretIndex
|
|
if let caretPosition = textField.position(from: textField.beginningOfDocument, offset: stringCaretIndex) {
|
|
textField.selectedTextRange = textField.textRange(from: caretPosition, to: caretPosition)
|
|
}
|
|
}
|
|
self.ignoreTextSelection = false
|
|
|
|
if notifyUpdated {
|
|
self.updated(self.representation.numericalValue)
|
|
}
|
|
}
|
|
|
|
@objc public func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
|
|
if string.count == 1 {
|
|
self.representation.insert(letter: string)
|
|
self.resetFromRepresentation(textField: textField, notifyUpdated: true)
|
|
} else if string.count == 0 {
|
|
self.representation.backspace()
|
|
self.resetFromRepresentation(textField: textField, notifyUpdated: true)
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
@objc public func textFieldShouldReturn(_ textField: UITextField) -> Bool {
|
|
return false
|
|
}
|
|
|
|
@objc public func textFieldDidBeginEditing(_ textField: UITextField) {
|
|
self.enableTextSelectionProcessing = true
|
|
self.focusUpdated(true)
|
|
|
|
let stringCaretIndex = self.representation.stringCaretIndex
|
|
self.previousResolvedCaretIndex = stringCaretIndex
|
|
if let caretPosition = textField.position(from: textField.beginningOfDocument, offset: stringCaretIndex) {
|
|
self.ignoreTextSelection = true
|
|
textField.selectedTextRange = textField.textRange(from: caretPosition, to: caretPosition)
|
|
DispatchQueue.main.async {
|
|
textField.selectedTextRange = textField.textRange(from: caretPosition, to: caretPosition)
|
|
self.ignoreTextSelection = false
|
|
}
|
|
}
|
|
}
|
|
|
|
@objc public func textFieldDidChangeSelection(_ textField: UITextField) {
|
|
if self.ignoreTextSelection {
|
|
return
|
|
}
|
|
if !self.enableTextSelectionProcessing {
|
|
return
|
|
}
|
|
|
|
if let selectedTextRange = textField.selectedTextRange {
|
|
let index = textField.offset(from: textField.beginningOfDocument, to: selectedTextRange.end)
|
|
if self.previousResolvedCaretIndex != index {
|
|
self.representation.moveCaret(offset: self.previousResolvedCaretIndex < index ? 1 : -1)
|
|
|
|
let stringCaretIndex = self.representation.stringCaretIndex
|
|
self.previousResolvedCaretIndex = stringCaretIndex
|
|
if let caretPosition = textField.position(from: textField.beginningOfDocument, offset: stringCaretIndex) {
|
|
textField.selectedTextRange = textField.textRange(from: caretPosition, to: caretPosition)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@objc public func textFieldDidEndEditing(_ textField: UITextField) {
|
|
self.enableTextSelectionProcessing = false
|
|
self.focusUpdated(false)
|
|
}
|
|
}
|
|
|
|
class BotCheckoutTipItemNode: ListViewItemNode, UITextFieldDelegate {
|
|
private let backgroundNode: ASDisplayNode
|
|
private let maskNode: ASImageNode
|
|
let titleNode: TextNode
|
|
let labelNode: TextNode
|
|
let tipMeasurementNode: ImmediateTextNode
|
|
let tipCurrencyNode: ImmediateTextNode
|
|
private let textNode: TextFieldNode
|
|
|
|
private let scrollNode: ASScrollNode
|
|
private var valueNodes: [TipValueNode] = []
|
|
|
|
private var item: BotCheckoutTipItem?
|
|
private var formatter: FormatterImpl?
|
|
|
|
init() {
|
|
self.backgroundNode = ASDisplayNode()
|
|
|
|
self.titleNode = TextNode()
|
|
self.titleNode.isUserInteractionEnabled = false
|
|
|
|
self.labelNode = TextNode()
|
|
self.labelNode.isUserInteractionEnabled = false
|
|
self.labelNode.isHidden = true
|
|
|
|
self.tipMeasurementNode = ImmediateTextNode()
|
|
self.tipCurrencyNode = ImmediateTextNode()
|
|
|
|
self.textNode = TextFieldNode()
|
|
|
|
self.scrollNode = ASScrollNode()
|
|
self.scrollNode.view.disablesInteractiveTransitionGestureRecognizer = true
|
|
self.scrollNode.view.showsVerticalScrollIndicator = false
|
|
self.scrollNode.view.showsHorizontalScrollIndicator = false
|
|
self.scrollNode.view.scrollsToTop = false
|
|
self.scrollNode.view.delaysContentTouches = false
|
|
self.scrollNode.view.canCancelContentTouches = true
|
|
if #available(iOS 11.0, *) {
|
|
self.scrollNode.view.contentInsetAdjustmentBehavior = .never
|
|
}
|
|
|
|
self.maskNode = ASImageNode()
|
|
self.maskNode.isUserInteractionEnabled = false
|
|
|
|
super.init(layerBacked: false, dynamicBounce: false)
|
|
|
|
self.addSubnode(self.backgroundNode)
|
|
|
|
self.addSubnode(self.titleNode)
|
|
self.addSubnode(self.labelNode)
|
|
self.addSubnode(self.textNode)
|
|
self.addSubnode(self.tipCurrencyNode)
|
|
self.addSubnode(self.scrollNode)
|
|
|
|
self.textNode.clipsToBounds = true
|
|
self.textNode.textField.addTarget(self, action: #selector(self.textFieldTextChanged(_:)), for: .editingChanged)
|
|
self.textNode.hitTestSlop = UIEdgeInsets(top: -5.0, left: -5.0, bottom: -5.0, right: -5.0)
|
|
}
|
|
|
|
func asyncLayout() -> (_ item: BotCheckoutTipItem, _ params: ListViewItemLayoutParams, _ insets: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
|
|
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
|
|
let makeLabelLayout = TextNode.asyncLayout(self.labelNode)
|
|
|
|
return { item, params, neighbors in
|
|
//let rightInset: CGFloat = 16.0 + params.rightInset
|
|
|
|
let labelsContentHeight: CGFloat = 34.0
|
|
|
|
var contentSize = CGSize(width: params.width, height: labelsContentHeight)
|
|
if !item.availableVariants.isEmpty {
|
|
contentSize.height += 75.0
|
|
}
|
|
|
|
let insets = priceItemInsets(neighbors)
|
|
|
|
let textFont: UIFont
|
|
let textColor: UIColor
|
|
|
|
textFont = titleFont
|
|
textColor = item.theme.list.itemSecondaryTextColor
|
|
|
|
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.title, font: textFont, textColor: textColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - params.leftInset - params.rightInset - 20.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
|
|
|
let (labelLayout, labelApply) = makeLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.strings.Checkout_OptionalTipItemPlaceholder, font: textFont, textColor: textColor.withMultipliedAlpha(0.8)), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - params.leftInset - params.rightInset - 20.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
|
|
|
return (ListViewItemNodeLayout(contentSize: contentSize, insets: insets), { [weak self] in
|
|
if let strongSelf = self {
|
|
strongSelf.item = item
|
|
|
|
let _ = titleApply()
|
|
let _ = labelApply()
|
|
|
|
let leftInset: CGFloat = 16.0 + params.leftInset
|
|
|
|
strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: leftInset, y: floor((labelsContentHeight - titleLayout.size.height) / 2.0)), size: titleLayout.size)
|
|
strongSelf.labelNode.frame = CGRect(origin: CGPoint(x: params.width - leftInset - labelLayout.size.width, y: floor((labelsContentHeight - labelLayout.size.height) / 2.0)), size: labelLayout.size)
|
|
|
|
if strongSelf.formatter == nil {
|
|
strongSelf.formatter = FormatterImpl(textField: strongSelf.textNode.textField, currency: item.currency, maxNumericalValue: item.maxValue, initialValue: item.value, updated: { value in
|
|
guard let strongSelf = self, let item = strongSelf.item else {
|
|
return
|
|
}
|
|
if item.numericValue != value {
|
|
item.updateValue(value)
|
|
}
|
|
}, focusUpdated: { value in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
if value {
|
|
strongSelf.item?.updatedFocus(true)
|
|
}
|
|
})
|
|
strongSelf.textNode.textField.delegate = strongSelf.formatter
|
|
|
|
/*strongSelf.formatterDelegate = CurrencyUITextFieldDelegate(formatter: CurrencyFormatter(currency: item.currency, { formatter in
|
|
formatter.maxValue = currencyToFractionalAmount(value: item.maxValue, currency: item.currency) ?? 10000.0
|
|
formatter.minValue = 0.0
|
|
formatter.hasDecimals = true
|
|
}))
|
|
strongSelf.formatterDelegate?.passthroughDelegate = strongSelf
|
|
|
|
strongSelf.formatterDelegate?.textUpdated = {
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
strongSelf.textFieldTextChanged(strongSelf.textNode.textField)
|
|
}
|
|
|
|
strongSelf.textNode.textField.delegate = strongSelf.formatterDelegate*/
|
|
|
|
strongSelf.textNode.clipsToBounds = true
|
|
//strongSelf.textNode.textField.delegate = strongSelf
|
|
}
|
|
|
|
strongSelf.textNode.textField.typingAttributes = [NSAttributedString.Key.font: titleFont]
|
|
strongSelf.textNode.textField.font = titleFont
|
|
|
|
strongSelf.textNode.textField.textColor = textColor
|
|
strongSelf.textNode.textField.textAlignment = .right
|
|
strongSelf.textNode.textField.keyboardAppearance = item.theme.rootController.keyboardColor.keyboardAppearance
|
|
strongSelf.textNode.textField.keyboardType = .decimalPad
|
|
strongSelf.textNode.textField.returnKeyType = .next
|
|
strongSelf.textNode.textField.tintColor = item.theme.list.itemAccentColor
|
|
|
|
var textInputFrame = CGRect(origin: CGPoint(x: params.width - leftInset - 150.0, y: -2.0), size: CGSize(width: 150.0, height: labelsContentHeight))
|
|
|
|
let currencyText: (String, String, Bool) = formatCurrencyAmountCustom(item.numericValue, currency: item.currency)
|
|
|
|
let currencySymbolOnTheLeft = currencyText.2
|
|
//let currencySymbolOnTheLeft = true
|
|
|
|
if strongSelf.textNode.textField.text ?? "" != currencyText.0 {
|
|
strongSelf.formatter?.reset(textField: strongSelf.textNode.textField, initialValue: currencyText.0)
|
|
}
|
|
|
|
strongSelf.tipMeasurementNode.attributedText = NSAttributedString(string: currencyText.0, font: titleFont, textColor: textColor)
|
|
let inputTextSize = strongSelf.tipMeasurementNode.updateLayout(textInputFrame.size)
|
|
|
|
let spaceRect = NSAttributedString(string: " ", font: titleFont, textColor: textColor).boundingRect(with: CGSize(width: 100.0, height: 100.0), options: .usesLineFragmentOrigin, context: nil)
|
|
|
|
strongSelf.tipCurrencyNode.attributedText = NSAttributedString(string: "\(currencyText.1)", font: titleFont, textColor: textColor)
|
|
let currencySize = strongSelf.tipCurrencyNode.updateLayout(CGSize(width: 100.0, height: .greatestFiniteMagnitude))
|
|
if currencySymbolOnTheLeft {
|
|
strongSelf.tipCurrencyNode.frame = CGRect(origin: CGPoint(x: textInputFrame.maxX - currencySize.width - inputTextSize.width - spaceRect.width, y: floor((labelsContentHeight - currencySize.height) / 2.0) - 1.0), size: currencySize)
|
|
} else {
|
|
strongSelf.tipCurrencyNode.frame = CGRect(origin: CGPoint(x: textInputFrame.maxX - currencySize.width, y: floor((labelsContentHeight - currencySize.height) / 2.0) - 1.0), size: currencySize)
|
|
textInputFrame.origin.x -= currencySize.width + spaceRect.width
|
|
}
|
|
|
|
strongSelf.textNode.frame = textInputFrame
|
|
|
|
let valueHeight: CGFloat = 52.0
|
|
let valueY: CGFloat = labelsContentHeight + 9.0
|
|
|
|
var index = 0
|
|
var variantLayouts: [(CGFloat, (CGFloat) -> Void)] = []
|
|
var totalMinWidth: CGFloat = 0.0
|
|
for (variantText, variantValue) in item.availableVariants {
|
|
let valueNode: TipValueNode
|
|
if strongSelf.valueNodes.count > index {
|
|
valueNode = strongSelf.valueNodes[index]
|
|
} else {
|
|
valueNode = TipValueNode()
|
|
strongSelf.valueNodes.append(valueNode)
|
|
strongSelf.scrollNode.addSubnode(valueNode)
|
|
}
|
|
let (nodeMinWidth, nodeApply) = valueNode.update(theme: item.theme, text: variantText, isHighlighted: item.value == variantText, height: valueHeight)
|
|
valueNode.action = {
|
|
guard let strongSelf = self, let item = strongSelf.item else {
|
|
return
|
|
}
|
|
if item.numericValue == variantValue {
|
|
item.updateValue(0)
|
|
} else {
|
|
item.updateValue(variantValue)
|
|
}
|
|
}
|
|
totalMinWidth += nodeMinWidth
|
|
variantLayouts.append((nodeMinWidth, nodeApply))
|
|
index += 1
|
|
}
|
|
|
|
let sideInset: CGFloat = params.leftInset + 16.0
|
|
var scaleFactor: CGFloat = 1.0
|
|
let availableWidth = params.width - sideInset * 2.0 - CGFloat(max(0, item.availableVariants.count - 1)) * 12.0
|
|
if totalMinWidth < availableWidth {
|
|
scaleFactor = availableWidth / totalMinWidth
|
|
}
|
|
|
|
var variantsOffset: CGFloat = sideInset
|
|
for index in 0 ..< item.availableVariants.count {
|
|
if index != 0 {
|
|
variantsOffset += 12.0
|
|
}
|
|
|
|
let valueNode: TipValueNode = strongSelf.valueNodes[index]
|
|
let (minWidth, nodeApply) = variantLayouts[index]
|
|
|
|
let nodeWidth = floor(scaleFactor * minWidth)
|
|
|
|
var valueFrame = CGRect(origin: CGPoint(x: variantsOffset, y: 0.0), size: CGSize(width: nodeWidth, height: valueHeight))
|
|
if scaleFactor > 1.0 && index == item.availableVariants.count - 1 {
|
|
valueFrame.size.width = params.width - sideInset - valueFrame.minX
|
|
}
|
|
|
|
valueNode.frame = valueFrame
|
|
nodeApply(nodeWidth)
|
|
variantsOffset += nodeWidth
|
|
}
|
|
|
|
variantsOffset += 16.0
|
|
|
|
strongSelf.scrollNode.frame = CGRect(origin: CGPoint(x: 0.0, y: valueY), size: CGSize(width: params.width, height: max(0.0, contentSize.height - valueY)))
|
|
strongSelf.scrollNode.view.contentSize = CGSize(width: variantsOffset, height: strongSelf.scrollNode.frame.height)
|
|
|
|
strongSelf.backgroundNode.backgroundColor = item.theme.list.itemBlocksBackgroundColor
|
|
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: params.width, height: contentSize.height))
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
@objc private func dismissKeyboard() {
|
|
self.textNode.textField.resignFirstResponder()
|
|
}
|
|
|
|
@objc private func textFieldTextChanged(_ textField: UITextField) {
|
|
let text = textField.text ?? ""
|
|
//self.labelNode.isHidden = !text.isEmpty
|
|
|
|
guard let item = self.item else {
|
|
return
|
|
}
|
|
|
|
if text.isEmpty {
|
|
item.updateValue(0)
|
|
return
|
|
}
|
|
|
|
/*var cleanText = ""
|
|
for c in text {
|
|
if c.isNumber {
|
|
cleanText.append(c)
|
|
} else if c == "," {
|
|
cleanText.append(".")
|
|
}
|
|
}
|
|
|
|
guard let doubleValue = Double(cleanText) else {
|
|
return
|
|
}
|
|
|
|
if var value = fractionalToCurrencyAmount(value: doubleValue, currency: item.currency) {
|
|
if value > item.maxValue {
|
|
value = item.maxValue
|
|
|
|
let currencyText = formatCurrencyAmountCustom(value, currency: item.currency)
|
|
if self.textNode.textField.text ?? "" != currencyText.0 {
|
|
self.textNode.textField.text = currencyText.0
|
|
}
|
|
}
|
|
item.updateValue(value)
|
|
}*/
|
|
}
|
|
|
|
@objc public func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
|
|
return true
|
|
}
|
|
|
|
@objc public func textFieldShouldReturn(_ textField: UITextField) -> Bool {
|
|
return false
|
|
}
|
|
|
|
@objc public func textFieldDidBeginEditing(_ textField: UITextField) {
|
|
textField.selectedTextRange = textField.textRange(from: textField.endOfDocument, to: textField.endOfDocument)
|
|
|
|
self.item?.updatedFocus(true)
|
|
}
|
|
|
|
@objc public func textFieldDidChangeSelection(_ textField: UITextField) {
|
|
textField.selectedTextRange = textField.textRange(from: textField.endOfDocument, to: textField.endOfDocument)
|
|
}
|
|
|
|
@objc public func textFieldDidEndEditing(_ textField: UITextField) {
|
|
}
|
|
|
|
override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) {
|
|
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
|
|
}
|
|
|
|
override func animateAdded(_ currentTimestamp: Double, duration: Double) {
|
|
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
|
}
|
|
|
|
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
|
|
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
|
|
}
|
|
}
|