mirror of
https://github.com/TelegramMessenger/Telegram-iOS.git
synced 2026-07-05 19:28:46 +02:00
Text selection
This commit is contained in:
parent
3920eb1507
commit
93bf472472
17 changed files with 266 additions and 60 deletions
|
|
@ -190,7 +190,7 @@ public final class MultilineTextWithEntitiesComponent: Component {
|
|||
|
||||
public final class View: UIView {
|
||||
var spoilerTextNode: ImmediateTextNodeWithEntities?
|
||||
let textNode: ImmediateTextNodeWithEntities
|
||||
public let textNode: ImmediateTextNodeWithEntities
|
||||
|
||||
public override init(frame: CGRect) {
|
||||
self.textNode = ImmediateTextNodeWithEntities()
|
||||
|
|
|
|||
|
|
@ -466,7 +466,7 @@ public final class ResizableSheetComponent<ChildEnvironmentType: Sendable & Equa
|
|||
topOffsetFraction = 1.0
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
#if DEBUG && false
|
||||
if "".isEmpty {
|
||||
topOffsetFraction = 1.0
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,13 +3,23 @@ import UIKit
|
|||
import AsyncDisplayKit
|
||||
|
||||
public final class ContextMenuControllerPresentationArguments {
|
||||
public let sourceNodeAndRect: () -> (ASDisplayNode, CGRect, ASDisplayNode, CGRect)?
|
||||
public let sourceViewAndRect: () -> (UIView, CGRect, UIView, CGRect)?
|
||||
public let bounce: Bool
|
||||
|
||||
public init(sourceNodeAndRect: @escaping () -> (ASDisplayNode, CGRect, ASDisplayNode, CGRect)?, bounce: Bool = true) {
|
||||
self.sourceNodeAndRect = sourceNodeAndRect
|
||||
public init(sourceViewAndRect: @escaping () -> (UIView, CGRect, UIView, CGRect)?, bounce: Bool = true) {
|
||||
self.sourceViewAndRect = sourceViewAndRect
|
||||
self.bounce = bounce
|
||||
}
|
||||
|
||||
public convenience init(sourceNodeAndRect: @escaping () -> (ASDisplayNode, CGRect, ASDisplayNode, CGRect)?, bounce: Bool = true) {
|
||||
self.init(sourceViewAndRect: {
|
||||
if let (view1, rect1, view2, rect2) = sourceNodeAndRect() {
|
||||
return (view1.view, rect1, view2.view, rect2)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}, bounce: bounce)
|
||||
}
|
||||
}
|
||||
|
||||
public protocol ContextMenuController: ViewController, StandalonePresentableController {
|
||||
|
|
|
|||
|
|
@ -459,18 +459,18 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll
|
|||
)
|
||||
self.textNode.visibility = true
|
||||
|
||||
let textSelectionNode = TextSelectionNode(theme: TextSelectionTheme(selection: defaultDarkPresentationTheme.list.itemAccentColor.withMultipliedAlpha(0.5), knob: defaultDarkPresentationTheme.list.itemAccentColor, isDark: true), strings: presentationData.strings, textNode: self.textNode, updateIsActive: { [weak self] value in
|
||||
let textSelectionNode = TextSelectionNode(theme: TextSelectionTheme(selection: defaultDarkPresentationTheme.list.itemAccentColor.withMultipliedAlpha(0.5), knob: defaultDarkPresentationTheme.list.itemAccentColor, isDark: true), strings: presentationData.strings, textNodeOrView: .node(self.textNode), updateIsActive: { [weak self] value in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
let _ = self
|
||||
}, present: { c, a in
|
||||
present(c, a)
|
||||
}, rootNode: { [weak self] in
|
||||
}, rootView: { [weak self] in
|
||||
guard let self else {
|
||||
return nil
|
||||
}
|
||||
return self.controllerInteraction?.controller()?.displayNode
|
||||
return self.controllerInteraction?.controller()?.displayNode.view
|
||||
}, externalKnobSurface: self.textSelectionKnobSurface, performAction: { [weak self] text, action in
|
||||
guard let self else {
|
||||
return
|
||||
|
|
|
|||
|
|
@ -156,12 +156,12 @@ public class ChatMessageFactCheckBubbleContentNode: ChatMessageBubbleContentNode
|
|||
let selectionColor: UIColor = item.presentationData.theme.theme.chat.message.incoming.textSelectionColor
|
||||
let knobColor: UIColor = item.presentationData.theme.theme.chat.message.incoming.textSelectionKnobColor
|
||||
|
||||
let textSelectionNode = TextSelectionNode(theme: TextSelectionTheme(selection: selectionColor, knob: knobColor, isDark: item.presentationData.theme.theme.overallDarkAppearance), strings: item.presentationData.strings, textNode: self.textNode, updateIsActive: { [weak self] value in
|
||||
let textSelectionNode = TextSelectionNode(theme: TextSelectionTheme(selection: selectionColor, knob: knobColor, isDark: item.presentationData.theme.theme.overallDarkAppearance), strings: item.presentationData.strings, textNodeOrView: .node(self.textNode), updateIsActive: { [weak self] value in
|
||||
self?.updateIsTextSelectionActive?(value)
|
||||
}, present: { [weak self] c, a in
|
||||
self?.item?.controllerInteraction.presentGlobalOverlayController(c, a)
|
||||
}, rootNode: { [weak rootNode] in
|
||||
return rootNode
|
||||
}, rootView: { [weak rootNode] in
|
||||
return rootNode?.view
|
||||
}, performAction: { [weak self] text, action in
|
||||
guard let strongSelf = self, let item = strongSelf.item else {
|
||||
return
|
||||
|
|
|
|||
|
|
@ -2049,12 +2049,12 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
|||
knobColor = item.presentationData.theme.theme.chat.message.outgoing.textSelectionKnobColor
|
||||
}
|
||||
|
||||
let textSelectionNode = TextSelectionNode(theme: TextSelectionTheme(selection: selectionColor, knob: knobColor, isDark: item.presentationData.theme.theme.overallDarkAppearance), strings: item.presentationData.strings, textNode: self.textNode, updateIsActive: { [weak self] value in
|
||||
let textSelectionNode = TextSelectionNode(theme: TextSelectionTheme(selection: selectionColor, knob: knobColor, isDark: item.presentationData.theme.theme.overallDarkAppearance), strings: item.presentationData.strings, textNodeOrView: .node(self.textNode), updateIsActive: { [weak self] value in
|
||||
self?.updateIsTextSelectionActive?(value)
|
||||
}, present: { [weak self] c, a in
|
||||
self?.arguments?.controllerInteraction.presentGlobalOverlayController(c, a)
|
||||
}, rootNode: { [weak rootNode] in
|
||||
return rootNode
|
||||
}, rootView: { [weak rootNode] in
|
||||
return rootNode?.view
|
||||
}, performAction: { [weak self] text, action in
|
||||
guard let strongSelf = self, let item = strongSelf.arguments else {
|
||||
return
|
||||
|
|
|
|||
|
|
@ -1622,7 +1622,7 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
|
|||
knobColor = item.presentationData.theme.theme.chat.message.outgoing.textSelectionKnobColor
|
||||
}
|
||||
|
||||
let textSelectionNode = TextSelectionNode(theme: TextSelectionTheme(selection: selectionColor, knob: knobColor, isDark: item.presentationData.theme.theme.overallDarkAppearance), strings: item.presentationData.strings, textNode: self.textNode.textNode, updateIsActive: { [weak self] value in
|
||||
let textSelectionNode = TextSelectionNode(theme: TextSelectionTheme(selection: selectionColor, knob: knobColor, isDark: item.presentationData.theme.theme.overallDarkAppearance), strings: item.presentationData.strings, textNodeOrView: .node(self.textNode.textNode), updateIsActive: { [weak self] value in
|
||||
self?.updateIsTextSelectionActive?(value)
|
||||
}, present: { [weak self] c, a in
|
||||
guard let self, let item = self.item else {
|
||||
|
|
@ -1634,8 +1634,8 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
|
|||
} else {
|
||||
item.controllerInteraction.presentGlobalOverlayController(c, a)
|
||||
}
|
||||
}, rootNode: { [weak rootNode] in
|
||||
return rootNode
|
||||
}, rootView: { [weak rootNode] in
|
||||
return rootNode?.view
|
||||
}, performAction: { [weak self] text, action in
|
||||
guard let strongSelf = self, let item = strongSelf.item else {
|
||||
return
|
||||
|
|
|
|||
|
|
@ -3562,14 +3562,14 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
|
|||
var aiButtonAlpha: CGFloat = actualTextFieldFrame.height >= 70.0 ? 1.0 : 0.0
|
||||
self.heightDependentAiButtonAlpha = aiButtonAlpha
|
||||
var inputHasText = false
|
||||
if let textInputNode = self.textInputNode, let attributedText = textInputNode.attributedText, attributedText.length != 0 {
|
||||
if let textInputNode = self.textInputNode, let attributedText = textInputNode.attributedText, !attributedText.string.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
||||
inputHasText = true
|
||||
}
|
||||
if !inputHasText {
|
||||
aiButtonAlpha = 0.0
|
||||
}
|
||||
transition.updateAlpha(layer: aiButton.button.layer, alpha: aiButtonAlpha)
|
||||
transition.updateAlpha(layer: aiButton.icon.layer, alpha: aiButtonAlpha)
|
||||
ComponentTransition(transition).setAlpha(view: aiButton.button, alpha: aiButtonAlpha)
|
||||
ComponentTransition(transition).setAlpha(view: aiButton.icon, alpha: aiButtonAlpha)
|
||||
} else if let aiButton = self.aiButton {
|
||||
self.aiButton = nil
|
||||
let aiButtonView = aiButton.button
|
||||
|
|
@ -4398,11 +4398,11 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
|
|||
if let aiButton = self.aiButton {
|
||||
let transition: ContainedViewLayoutTransition = .immediate
|
||||
var aiButtonAlpha: CGFloat = self.heightDependentAiButtonAlpha
|
||||
if !inputHasText {
|
||||
if let textInputNode = self.textInputNode, let attributedText = textInputNode.attributedText, !attributedText.string.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
||||
aiButtonAlpha = 0.0
|
||||
}
|
||||
transition.updateAlpha(layer: aiButton.button.layer, alpha: aiButtonAlpha)
|
||||
transition.updateAlpha(layer: aiButton.icon.layer, alpha: aiButtonAlpha)
|
||||
ComponentTransition(transition).setAlpha(view: aiButton.button, alpha: aiButtonAlpha)
|
||||
ComponentTransition(transition).setAlpha(view: aiButton.icon, alpha: aiButtonAlpha)
|
||||
}
|
||||
|
||||
self.updateTextHeight(animated: animated)
|
||||
|
|
|
|||
|
|
@ -438,10 +438,10 @@ final class InnerTextSelectionTipContainerNode: ASDisplayNode {
|
|||
|
||||
super.init()
|
||||
|
||||
let textSelectionNode = TextSelectionNode(theme: TextSelectionTheme(selection: presentationData.theme.contextMenu.primaryColor.withAlphaComponent(0.15), knob: presentationData.theme.contextMenu.primaryColor, knobDiameter: 8.0, isDark: presentationData.theme.overallDarkAppearance), strings: presentationData.strings, textNode: self.textNode.textNode, updateIsActive: { _ in
|
||||
let textSelectionNode = TextSelectionNode(theme: TextSelectionTheme(selection: presentationData.theme.contextMenu.primaryColor.withAlphaComponent(0.15), knob: presentationData.theme.contextMenu.primaryColor, knobDiameter: 8.0, isDark: presentationData.theme.overallDarkAppearance), strings: presentationData.strings, textNodeOrView: .node(self.textNode.textNode), updateIsActive: { _ in
|
||||
}, present: { _, _ in
|
||||
}, rootNode: { [weak self] in
|
||||
return self
|
||||
}, rootView: { [weak self] in
|
||||
return self?.view
|
||||
}, performAction: { _, _ in
|
||||
})
|
||||
self.textSelectionNode = textSelectionNode
|
||||
|
|
|
|||
|
|
@ -85,13 +85,13 @@ public final class ContextMenuControllerImpl: ViewController, KeyShortcutRespond
|
|||
} else {
|
||||
self.layout = layout
|
||||
|
||||
if let presentationArguments = self.presentationArguments as? ContextMenuControllerPresentationArguments, let (sourceNode, sourceRect, containerNode, containerRect) = presentationArguments.sourceNodeAndRect() {
|
||||
if let presentationArguments = self.presentationArguments as? ContextMenuControllerPresentationArguments, let (sourceView, sourceRect, containerView, containerRect) = presentationArguments.sourceViewAndRect() {
|
||||
if self.skipCoordnateConversion {
|
||||
self.contextMenuNode.sourceRect = sourceRect
|
||||
self.contextMenuNode.containerRect = containerRect
|
||||
} else {
|
||||
self.contextMenuNode.sourceRect = sourceNode.view.convert(sourceRect, to: nil)
|
||||
self.contextMenuNode.containerRect = containerNode.view.convert(containerRect, to: nil)
|
||||
self.contextMenuNode.sourceRect = sourceView.convert(sourceRect, to: nil)
|
||||
self.contextMenuNode.containerRect = containerView.convert(containerRect, to: nil)
|
||||
}
|
||||
} else {
|
||||
self.contextMenuNode.sourceRect = nil
|
||||
|
|
|
|||
|
|
@ -402,7 +402,7 @@ final class CreateBotContentComponent: Component {
|
|||
theme: environment.theme,
|
||||
strings: environment.strings,
|
||||
action: { [weak self] in
|
||||
guard let self, let itemView = self.nameSection.findTaggedView(tag: self.usernameInputTag) as? ListMultilineTextFieldItemComponent.View else {
|
||||
guard let self, let itemView = self.usernameSection.findTaggedView(tag: self.usernameInputTag) as? ListMultilineTextFieldItemComponent.View else {
|
||||
return
|
||||
}
|
||||
itemView.activateInput()
|
||||
|
|
|
|||
|
|
@ -896,7 +896,7 @@ final class StoryContentCaptionComponent: Component {
|
|||
self.textSelectionKnobContainer.addSubview(textSelectionKnobSurface)
|
||||
}
|
||||
|
||||
let textSelectionNode = TextSelectionNode(theme: TextSelectionTheme(selection: selectionColor, knob: component.theme.list.itemAccentColor, isDark: true), strings: component.strings, textNode: textNode, updateIsActive: { [weak self] value in
|
||||
let textSelectionNode = TextSelectionNode(theme: TextSelectionTheme(selection: selectionColor, knob: component.theme.list.itemAccentColor, isDark: true), strings: component.strings, textNodeOrView: .node(textNode), updateIsActive: { [weak self] value in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
|
|
@ -912,8 +912,8 @@ final class StoryContentCaptionComponent: Component {
|
|||
return
|
||||
}
|
||||
component.controller()?.presentInGlobalOverlay(c, with: a)
|
||||
}, rootNode: { [weak controller] in
|
||||
return controller?.displayNode
|
||||
}, rootView: { [weak controller] in
|
||||
return controller?.displayNode.view
|
||||
}, externalKnobSurface: self.textSelectionKnobSurface, performAction: { [weak self] text, action in
|
||||
guard let self, let component = self.component else {
|
||||
return
|
||||
|
|
|
|||
|
|
@ -41,6 +41,9 @@ swift_library(
|
|||
"//submodules/Markdown",
|
||||
"//submodules/TelegramUIPreferences",
|
||||
"//submodules/ChatSendMessageActionUI",
|
||||
"//submodules/TextSelectionNode",
|
||||
"//submodules/Pasteboard",
|
||||
"//submodules/Speak",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
|
|
|||
|
|
@ -84,6 +84,7 @@ final class TextProcessingContentComponent: Component {
|
|||
|
||||
final class View: UIView {
|
||||
private var component: TextProcessingContentComponent?
|
||||
private var environment: ViewControllerComponentContainer.Environment?
|
||||
private weak var state: EmptyComponentState?
|
||||
private var isUpdating: Bool = false
|
||||
|
||||
|
|
@ -213,6 +214,7 @@ final class TextProcessingContentComponent: Component {
|
|||
}
|
||||
|
||||
self.component = component
|
||||
self.environment = environment
|
||||
self.state = state
|
||||
|
||||
let sideInset: CGFloat = 16.0
|
||||
|
|
@ -351,7 +353,13 @@ final class TextProcessingContentComponent: Component {
|
|||
inputText: component.inputText,
|
||||
mode: .translate,
|
||||
copyAction: component.copyCurrentResult,
|
||||
displayLanguageSelectionMenu: component.displayLanguageSelectionMenu
|
||||
displayLanguageSelectionMenu: component.displayLanguageSelectionMenu,
|
||||
present: { [weak self] c, a in
|
||||
self?.environment?.controller()?.present(c, in: .window(.root), with: a)
|
||||
},
|
||||
rootViewForTextSelection: { [weak self] in
|
||||
return self?.environment?.controller()?.view
|
||||
}
|
||||
))
|
||||
case .stylize:
|
||||
contentComponent = AnyComponent(TextProcessingTranslateContentComponent(
|
||||
|
|
@ -363,7 +371,13 @@ final class TextProcessingContentComponent: Component {
|
|||
inputText: component.inputText,
|
||||
mode: .stylize,
|
||||
copyAction: component.copyCurrentResult,
|
||||
displayLanguageSelectionMenu: component.displayLanguageSelectionMenu
|
||||
displayLanguageSelectionMenu: component.displayLanguageSelectionMenu,
|
||||
present: { [weak self] c, a in
|
||||
self?.environment?.controller()?.present(c, in: .window(.root), with: a)
|
||||
},
|
||||
rootViewForTextSelection: { [weak self] in
|
||||
return self?.environment?.controller()?.view
|
||||
}
|
||||
))
|
||||
case .fix:
|
||||
contentComponent = AnyComponent(TextProcessingTranslateContentComponent(
|
||||
|
|
@ -375,7 +389,13 @@ final class TextProcessingContentComponent: Component {
|
|||
inputText: component.inputText,
|
||||
mode: .fix,
|
||||
copyAction: component.copyCurrentResult,
|
||||
displayLanguageSelectionMenu: component.displayLanguageSelectionMenu
|
||||
displayLanguageSelectionMenu: component.displayLanguageSelectionMenu,
|
||||
present: { [weak self] c, a in
|
||||
self?.environment?.controller()?.present(c, in: .window(.root), with: a)
|
||||
},
|
||||
rootViewForTextSelection: { [weak self] in
|
||||
return self?.environment?.controller()?.view
|
||||
}
|
||||
))
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -13,10 +13,14 @@ import TextFormat
|
|||
import PlainButtonComponent
|
||||
import CheckComponent
|
||||
import ShimmerEffect
|
||||
import TextSelectionNode
|
||||
import Pasteboard
|
||||
import Speak
|
||||
|
||||
final class TextProcessingTextAreaComponent: Component {
|
||||
let context: AccountContext
|
||||
let theme: PresentationTheme
|
||||
let strings: PresentationStrings
|
||||
let titlePrefix: String
|
||||
let title: String
|
||||
let titleAction: ((UIView) -> Void)?
|
||||
|
|
@ -26,10 +30,13 @@ final class TextProcessingTextAreaComponent: Component {
|
|||
let text: TextWithEntities?
|
||||
let loadingStateMeasuringText: String?
|
||||
let textCorrectionRanges: [Range<Int>]
|
||||
let present: (ViewController, Any?) -> Void
|
||||
let rootViewForTextSelection: () -> UIView?
|
||||
|
||||
init(
|
||||
context: AccountContext,
|
||||
theme: PresentationTheme,
|
||||
strings: PresentationStrings,
|
||||
titlePrefix: String,
|
||||
title: String,
|
||||
titleAction: ((UIView) -> Void)?,
|
||||
|
|
@ -38,10 +45,13 @@ final class TextProcessingTextAreaComponent: Component {
|
|||
emojify: (value: Bool, toggle: () -> Void)?,
|
||||
text: TextWithEntities?,
|
||||
loadingStateMeasuringText: String?,
|
||||
textCorrectionRanges: [Range<Int>]
|
||||
textCorrectionRanges: [Range<Int>],
|
||||
present: @escaping (ViewController, Any?) -> Void,
|
||||
rootViewForTextSelection: @escaping () -> UIView?
|
||||
) {
|
||||
self.context = context
|
||||
self.theme = theme
|
||||
self.strings = strings
|
||||
self.titlePrefix = titlePrefix
|
||||
self.isExpanded = isExpanded
|
||||
self.copyAction = copyAction
|
||||
|
|
@ -51,6 +61,8 @@ final class TextProcessingTextAreaComponent: Component {
|
|||
self.text = text
|
||||
self.loadingStateMeasuringText = loadingStateMeasuringText
|
||||
self.textCorrectionRanges = textCorrectionRanges
|
||||
self.present = present
|
||||
self.rootViewForTextSelection = rootViewForTextSelection
|
||||
}
|
||||
|
||||
static func ==(lhs: TextProcessingTextAreaComponent, rhs: TextProcessingTextAreaComponent) -> Bool {
|
||||
|
|
@ -60,6 +72,9 @@ final class TextProcessingTextAreaComponent: Component {
|
|||
if lhs.theme !== rhs.theme {
|
||||
return false
|
||||
}
|
||||
if lhs.strings !== rhs.strings {
|
||||
return false
|
||||
}
|
||||
if lhs.titlePrefix != rhs.titlePrefix {
|
||||
return false
|
||||
}
|
||||
|
|
@ -115,15 +130,29 @@ final class TextProcessingTextAreaComponent: Component {
|
|||
private let measureLoadingText = ComponentView<Empty>()
|
||||
private var shimmerEffectNode: ShimmerEffectNode?
|
||||
|
||||
private var textSelectionNode: TextSelectionNode?
|
||||
private let textSelectionContainer: UIView
|
||||
private let textSelectionKnobContainer: UIView
|
||||
private let textSelectionKnobSurface: UIView
|
||||
|
||||
private var currentSpeechHolder: SpeechSynthesizerHolder?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.textContainer = UIView()
|
||||
self.textContainer.clipsToBounds = true
|
||||
|
||||
self.textSelectionContainer = UIView()
|
||||
self.textSelectionContainer.isUserInteractionEnabled = false
|
||||
self.textSelectionKnobContainer = UIView()
|
||||
self.textSelectionKnobSurface = UIView()
|
||||
self.textSelectionKnobContainer.addSubview(self.textSelectionKnobSurface)
|
||||
|
||||
self.titleButton = HighlightTrackingButton()
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.addSubview(self.textContainer)
|
||||
self.addSubview(self.textSelectionContainer)
|
||||
self.addSubview(self.titleButton)
|
||||
|
||||
self.titleButton.highligthedChanged = { [weak self] highighed in
|
||||
|
|
@ -459,6 +488,96 @@ final class TextProcessingTextAreaComponent: Component {
|
|||
}
|
||||
}
|
||||
|
||||
if component.text != nil, component.isExpanded?.value ?? true, let textView = self.text.view as? MultilineTextWithEntitiesComponent.View {
|
||||
let textSelectionNode: TextSelectionNode
|
||||
if let current = self.textSelectionNode {
|
||||
textSelectionNode = current
|
||||
} else {
|
||||
textSelectionNode = TextSelectionNode(theme: TextSelectionTheme(selection: component.theme.list.itemAccentColor.withMultipliedAlpha(0.5), knob: component.theme.list.itemAccentColor, isDark: component.theme.overallDarkAppearance), strings: component.strings, textNodeOrView: .node(textView.textNode), updateIsActive: { [weak self] value in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
let _ = self
|
||||
}, present: { [weak self] c, a in
|
||||
guard let self, let component = self.component else {
|
||||
return
|
||||
}
|
||||
component.present(c, a)
|
||||
}, rootView: { [weak self] in
|
||||
guard let self, let component = self.component else {
|
||||
return nil
|
||||
}
|
||||
return component.rootViewForTextSelection()
|
||||
}, externalKnobSurface: self.textSelectionKnobSurface, performAction: { [weak self] text, action in
|
||||
guard let self, let component = self.component else {
|
||||
return
|
||||
}
|
||||
|
||||
switch action {
|
||||
case .copy:
|
||||
storeAttributedTextInPasteboard(text)
|
||||
case .share:
|
||||
let shareController = component.context.sharedContext.makeShareController(context: component.context, subject: .text(text.string), forceExternal: true, shareStory: nil, enqueued: nil, actionCompleted: nil)
|
||||
component.present(shareController, nil)
|
||||
case .lookup:
|
||||
let controller = UIReferenceLibraryViewController(term: text.string)
|
||||
if let window = self.window {
|
||||
controller.popoverPresentationController?.sourceView = window
|
||||
controller.popoverPresentationController?.sourceRect = CGRect(origin: CGPoint(x: window.bounds.width / 2.0, y: window.bounds.size.height - 1.0), size: CGSize(width: 1.0, height: 1.0))
|
||||
window.rootViewController?.present(controller, animated: true)
|
||||
}
|
||||
case .speak:
|
||||
if let speechHolder = speakText(context: component.context, text: text.string) {
|
||||
speechHolder.completion = { [weak self, weak speechHolder] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if self.currentSpeechHolder === speechHolder {
|
||||
self.currentSpeechHolder = nil
|
||||
}
|
||||
}
|
||||
self.currentSpeechHolder = speechHolder
|
||||
}
|
||||
case .translate:
|
||||
break
|
||||
case .quote:
|
||||
break
|
||||
}
|
||||
})
|
||||
|
||||
textSelectionNode.enableLookup = true
|
||||
textSelectionNode.enableTranslate = false
|
||||
textSelectionNode.menuSkipCoordnateConversion = false
|
||||
textSelectionNode.canBeginSelection = { _ in
|
||||
return true
|
||||
}
|
||||
self.textSelectionNode = textSelectionNode
|
||||
|
||||
self.textSelectionContainer.insertSubview(self.textSelectionKnobContainer, at: 0)
|
||||
self.textContainer.insertSubview(textSelectionNode.highlightAreaNode.view, belowSubview: textView)
|
||||
self.textContainer.insertSubview(textSelectionNode.view, aboveSubview: textView)
|
||||
}
|
||||
|
||||
textSelectionNode.frame = textView.frame
|
||||
textSelectionNode.highlightAreaNode.frame = textView.frame
|
||||
self.textSelectionKnobSurface.frame = textView.frame
|
||||
} else {
|
||||
for subview in Array(self.textSelectionContainer.subviews) {
|
||||
subview.removeFromSuperview()
|
||||
}
|
||||
if self.textSelectionKnobContainer.superview != nil {
|
||||
self.textSelectionKnobContainer.removeFromSuperview()
|
||||
}
|
||||
if let textSelectionNode = self.textSelectionNode {
|
||||
self.textSelectionNode = nil
|
||||
textSelectionNode.view.removeFromSuperview()
|
||||
|
||||
if textSelectionNode.highlightAreaNode.view.superview != nil {
|
||||
textSelectionNode.highlightAreaNode.view.removeFromSuperview()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if component.text == nil {
|
||||
let shimmerEffectNode: ShimmerEffectNode
|
||||
if let current = self.shimmerEffectNode {
|
||||
|
|
@ -568,6 +687,7 @@ final class TextProcessingTextAreaComponent: Component {
|
|||
}
|
||||
|
||||
transition.setFrame(view: self.textContainer, frame: textContainerFrame)
|
||||
transition.setFrame(view: self.textSelectionContainer, frame: textContainerFrame)
|
||||
contentHeight += textContainerFrame.height
|
||||
|
||||
contentHeight += bottomInset
|
||||
|
|
|
|||
|
|
@ -83,6 +83,8 @@ final class TextProcessingTranslateContentComponent: Component {
|
|||
let mode: Mode
|
||||
let copyAction: (() -> Void)?
|
||||
let displayLanguageSelectionMenu: (UIView, String, TelegramComposeAIMessageMode.StyleId, Bool, @escaping (String, TelegramComposeAIMessageMode.StyleId) -> Void) -> Void
|
||||
let present: (ViewController, Any?) -> Void
|
||||
let rootViewForTextSelection: () -> UIView?
|
||||
|
||||
init(
|
||||
context: AccountContext,
|
||||
|
|
@ -93,7 +95,9 @@ final class TextProcessingTranslateContentComponent: Component {
|
|||
inputText: TextWithEntities,
|
||||
mode: Mode,
|
||||
copyAction: (() -> Void)?,
|
||||
displayLanguageSelectionMenu: @escaping (UIView, String, TelegramComposeAIMessageMode.StyleId, Bool, @escaping (String, TelegramComposeAIMessageMode.StyleId) -> Void) -> Void
|
||||
displayLanguageSelectionMenu: @escaping (UIView, String, TelegramComposeAIMessageMode.StyleId, Bool, @escaping (String, TelegramComposeAIMessageMode.StyleId) -> Void) -> Void,
|
||||
present: @escaping (ViewController, Any?) -> Void,
|
||||
rootViewForTextSelection: @escaping () -> UIView?
|
||||
) {
|
||||
self.context = context
|
||||
self.theme = theme
|
||||
|
|
@ -104,6 +108,8 @@ final class TextProcessingTranslateContentComponent: Component {
|
|||
self.mode = mode
|
||||
self.copyAction = copyAction
|
||||
self.displayLanguageSelectionMenu = displayLanguageSelectionMenu
|
||||
self.present = present
|
||||
self.rootViewForTextSelection = rootViewForTextSelection
|
||||
}
|
||||
|
||||
static func ==(lhs: TextProcessingTranslateContentComponent, rhs: TextProcessingTranslateContentComponent) -> Bool {
|
||||
|
|
@ -354,6 +360,7 @@ final class TextProcessingTranslateContentComponent: Component {
|
|||
component: AnyComponent(TextProcessingTextAreaComponent(
|
||||
context: component.context,
|
||||
theme: component.theme,
|
||||
strings: component.strings,
|
||||
titlePrefix: fromPrefix,
|
||||
title: localizedLanguageName(strings: component.strings, language: component.externalState.sourceLanguage ?? ""),
|
||||
titleAction: nil,
|
||||
|
|
@ -373,7 +380,9 @@ final class TextProcessingTranslateContentComponent: Component {
|
|||
emojify: nil,
|
||||
text: component.inputText,
|
||||
loadingStateMeasuringText: nil,
|
||||
textCorrectionRanges: []
|
||||
textCorrectionRanges: [],
|
||||
present: component.present,
|
||||
rootViewForTextSelection: component.rootViewForTextSelection
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: availableSize.height)
|
||||
|
|
@ -396,6 +405,7 @@ final class TextProcessingTranslateContentComponent: Component {
|
|||
component: AnyComponent(TextProcessingTextAreaComponent(
|
||||
context: component.context,
|
||||
theme: component.theme,
|
||||
strings: component.strings,
|
||||
titlePrefix: toPrefix,
|
||||
title: toTitle,
|
||||
titleAction: component.mode == .translate ? { [weak self] sourceView in
|
||||
|
|
@ -441,7 +451,9 @@ final class TextProcessingTranslateContentComponent: Component {
|
|||
) : nil,
|
||||
text: component.externalState.result?.text,
|
||||
loadingStateMeasuringText: component.inputText.text,
|
||||
textCorrectionRanges: component.mode == .fix ? (component.externalState.result?.textCorrectionRanges ?? []) : []
|
||||
textCorrectionRanges: component.mode == .fix ? (component.externalState.result?.textCorrectionRanges ?? []) : [],
|
||||
present: component.present,
|
||||
rootViewForTextSelection: component.rootViewForTextSelection
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: availableSize.height)
|
||||
|
|
|
|||
|
|
@ -216,15 +216,56 @@ public enum TextSelectionAction: Equatable {
|
|||
}
|
||||
|
||||
public final class TextSelectionNode: ASDisplayNode {
|
||||
public enum TextNodeOrView {
|
||||
case node(TextNodeProtocol)
|
||||
case view(TextView)
|
||||
|
||||
var view: UIView {
|
||||
switch self {
|
||||
case let .node(node):
|
||||
return node.view
|
||||
case let .view(view):
|
||||
return view
|
||||
}
|
||||
}
|
||||
|
||||
var currentText: NSAttributedString? {
|
||||
switch self {
|
||||
case let .node(node):
|
||||
return node.currentText
|
||||
case let .view(view):
|
||||
return view.cachedLayout?.attributedString
|
||||
}
|
||||
}
|
||||
|
||||
func attributesAtPoint(_ point: CGPoint, orNearest: Bool = false) -> (Int, [NSAttributedString.Key: Any])? {
|
||||
switch self {
|
||||
case let .node(node):
|
||||
return node.attributesAtPoint(point, orNearest: orNearest)
|
||||
case let .view(view):
|
||||
return view.attributesAtPoint(point, orNearest: orNearest)
|
||||
}
|
||||
}
|
||||
|
||||
func textRangeRects(in range: NSRange) -> (rects: [CGRect], start: TextRangeRectEdge, end: TextRangeRectEdge)? {
|
||||
switch self {
|
||||
case let .node(node):
|
||||
return node.textRangeRects(in: range)
|
||||
case let .view(view):
|
||||
return view.cachedLayout?.rangeRects(in: range)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private let theme: TextSelectionTheme
|
||||
private let strings: PresentationStrings
|
||||
private let textNode: TextNodeProtocol
|
||||
private let textNodeOrView: TextNodeOrView
|
||||
private let updateIsActive: (Bool) -> Void
|
||||
public var canBeginSelection: (CGPoint) -> Bool = { _ in true }
|
||||
public var updateRange: ((NSRange?) -> Void)?
|
||||
public var presentMenu: ((UIView, CGPoint, [ContextMenuAction]) -> Void)?
|
||||
private let present: (ViewController, Any?) -> Void
|
||||
private let rootNode: () -> ASDisplayNode?
|
||||
private let rootView: () -> UIView?
|
||||
private let performAction: (NSAttributedString, TextSelectionAction) -> Void
|
||||
private var highlightOverlay: LinkHighlightingNode?
|
||||
private let leftKnob: ASImageNode
|
||||
|
|
@ -252,13 +293,13 @@ public final class TextSelectionNode: ASDisplayNode {
|
|||
|
||||
private weak var contextMenu: ContextMenuController?
|
||||
|
||||
public init(theme: TextSelectionTheme, strings: PresentationStrings, textNode: TextNodeProtocol, updateIsActive: @escaping (Bool) -> Void, present: @escaping (ViewController, Any?) -> Void, rootNode: @escaping () -> ASDisplayNode?, externalKnobSurface: UIView? = nil, performAction: @escaping (NSAttributedString, TextSelectionAction) -> Void) {
|
||||
public init(theme: TextSelectionTheme, strings: PresentationStrings, textNodeOrView: TextNodeOrView, updateIsActive: @escaping (Bool) -> Void, present: @escaping (ViewController, Any?) -> Void, rootView: @escaping () -> UIView?, externalKnobSurface: UIView? = nil, performAction: @escaping (NSAttributedString, TextSelectionAction) -> Void) {
|
||||
self.theme = theme
|
||||
self.strings = strings
|
||||
self.textNode = textNode
|
||||
self.textNodeOrView = textNodeOrView
|
||||
self.updateIsActive = updateIsActive
|
||||
self.present = present
|
||||
self.rootNode = rootNode
|
||||
self.rootView = rootView
|
||||
self.performAction = performAction
|
||||
self.leftKnob = ASImageNode()
|
||||
self.leftKnob.isUserInteractionEnabled = false
|
||||
|
|
@ -306,8 +347,8 @@ public final class TextSelectionNode: ASDisplayNode {
|
|||
return
|
||||
}
|
||||
|
||||
let mappedPoint = strongSelf.view.convert(point, to: strongSelf.textNode.view)
|
||||
if let stringIndex = strongSelf.textNode.attributesAtPoint(mappedPoint, orNearest: true)?.0 {
|
||||
let mappedPoint = strongSelf.view.convert(point, to: strongSelf.textNodeOrView.view)
|
||||
if let stringIndex = strongSelf.textNodeOrView.attributesAtPoint(mappedPoint, orNearest: true)?.0 {
|
||||
var updatedLeft = currentRange.0
|
||||
var updatedRight = currentRange.1
|
||||
switch knob {
|
||||
|
|
@ -335,15 +376,15 @@ public final class TextSelectionNode: ASDisplayNode {
|
|||
strongSelf.displayMenu()
|
||||
}
|
||||
recognizer.beginSelection = { [weak self] point in
|
||||
guard let strongSelf = self, let attributedString = strongSelf.textNode.currentText else {
|
||||
guard let strongSelf = self, let attributedString = strongSelf.textNodeOrView.currentText else {
|
||||
return
|
||||
}
|
||||
|
||||
strongSelf.dismissSelection()
|
||||
|
||||
let mappedPoint = strongSelf.view.convert(point, to: strongSelf.textNode.view)
|
||||
let mappedPoint = strongSelf.view.convert(point, to: strongSelf.textNodeOrView.view)
|
||||
var resultRange: NSRange?
|
||||
if let stringIndex = strongSelf.textNode.attributesAtPoint(mappedPoint, orNearest: false)?.0 {
|
||||
if let stringIndex = strongSelf.textNodeOrView.attributesAtPoint(mappedPoint, orNearest: false)?.0 {
|
||||
let string = attributedString.string as NSString
|
||||
|
||||
let inputRange = CFRangeMake(0, string.length)
|
||||
|
|
@ -398,7 +439,7 @@ public final class TextSelectionNode: ASDisplayNode {
|
|||
}
|
||||
|
||||
public func pretendInitiateSelection() {
|
||||
guard let attributedString = self.textNode.currentText else {
|
||||
guard let attributedString = self.textNodeOrView.currentText else {
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -432,7 +473,7 @@ public final class TextSelectionNode: ASDisplayNode {
|
|||
}
|
||||
|
||||
public func pretendExtendSelection(to index: Int) {
|
||||
guard let endRangeRect = self.textNode.textRangeRects(in: NSRange(location: index, length: 1))?.rects.first else {
|
||||
guard let endRangeRect = self.textNodeOrView.textRangeRects(in: NSRange(location: index, length: 1))?.rects.first else {
|
||||
return
|
||||
}
|
||||
let startPoint = self.rightKnob.frame.center
|
||||
|
|
@ -449,7 +490,7 @@ public final class TextSelectionNode: ASDisplayNode {
|
|||
}
|
||||
|
||||
public func setSelection(range: NSRange, displayMenu: Bool) {
|
||||
guard let attributedString = self.textNode.currentText else {
|
||||
guard let attributedString = self.textNodeOrView.currentText else {
|
||||
return
|
||||
}
|
||||
let range = self.convertSelectionFromOriginalText(attributedString: attributedString, range: range)
|
||||
|
|
@ -561,7 +602,7 @@ public final class TextSelectionNode: ASDisplayNode {
|
|||
}
|
||||
|
||||
public func getSelection() -> NSRange? {
|
||||
guard let currentRange = self.currentRange, let attributedString = self.textNode.currentText else {
|
||||
guard let currentRange = self.currentRange, let attributedString = self.textNodeOrView.currentText else {
|
||||
return nil
|
||||
}
|
||||
let range = NSRange(location: min(currentRange.0, currentRange.1), length: max(currentRange.0, currentRange.1) - min(currentRange.0, currentRange.1))
|
||||
|
|
@ -574,7 +615,7 @@ public final class TextSelectionNode: ASDisplayNode {
|
|||
var rects: (rects: [CGRect], start: TextRangeRectEdge, end: TextRangeRectEdge)?
|
||||
|
||||
if let range {
|
||||
if var rectsValue = self.textNode.textRangeRects(in: range) {
|
||||
if var rectsValue = self.textNodeOrView.textRangeRects(in: range) {
|
||||
var rectList = rectsValue.rects
|
||||
if rectList.count > 1 {
|
||||
for i in 0 ..< rectList.count - 1 {
|
||||
|
|
@ -683,7 +724,7 @@ public final class TextSelectionNode: ASDisplayNode {
|
|||
}
|
||||
|
||||
private func displayMenu() {
|
||||
guard let currentRects = self.currentRects, !currentRects.isEmpty, let currentRange = self.currentRange, let attributedString = self.textNode.currentText else {
|
||||
guard let currentRects = self.currentRects, !currentRects.isEmpty, let currentRange = self.currentRange, let attributedString = self.textNodeOrView.currentText else {
|
||||
return
|
||||
}
|
||||
let range = NSRange(location: min(currentRange.0, currentRange.1), length: max(currentRange.0, currentRange.1) - min(currentRange.0, currentRange.1))
|
||||
|
|
@ -778,15 +819,15 @@ public final class TextSelectionNode: ASDisplayNode {
|
|||
return true
|
||||
}
|
||||
self.contextMenu = contextMenu
|
||||
self.present(contextMenu, ContextMenuControllerPresentationArguments(sourceNodeAndRect: { [weak self] in
|
||||
guard let strongSelf = self, let rootNode = strongSelf.rootNode() else {
|
||||
self.present(contextMenu, ContextMenuControllerPresentationArguments(sourceViewAndRect: { [weak self] in
|
||||
guard let strongSelf = self, let rootView = strongSelf.rootView() else {
|
||||
return nil
|
||||
}
|
||||
|
||||
if strongSelf.menuSkipCoordnateConversion {
|
||||
return (strongSelf, strongSelf.view.convert(completeRect, to: rootNode.view), rootNode, rootNode.bounds)
|
||||
return (strongSelf.view, strongSelf.view.convert(completeRect, to: rootView), rootView, rootView.bounds)
|
||||
} else {
|
||||
return (strongSelf, completeRect, rootNode, rootNode.bounds)
|
||||
return (strongSelf.view, completeRect, rootView, rootView.bounds)
|
||||
}
|
||||
}, bounce: false))
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue