Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added capability to attachment enabling selection without display in … #332

Merged
merged 4 commits into from
Aug 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 42 additions & 5 deletions Proton/Sources/Swift/Attachment/Attachment.swift
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ open class Attachment: NSTextAttachment, BoundsObserving {
private(set) var cachedContainerSize: CGSize?
private var indexInContainer: Int?
private let backgroundColor: UIColor?
private var showSelectionViewWhenSelected = true

var cachedBounds: CGRect?

Expand Down Expand Up @@ -182,6 +183,19 @@ open class Attachment: NSTextAttachment, BoundsObserving {
isRenderingAsync && isAsyncRendered == false
}

/// Returns `true` if any of the child views is first Responder, else false
public var isFocussed: Bool {
contentView?.firstResponderChildView() != nil
}

/// Returns the child view that is first Responder, else nil
public var firstResponderChildView: UIView? {
contentView?.firstResponderChildView()
}

/// Determines if attachment is in selected range in the container `EditorView`
public var isInSelectedRange: Bool { isSelected }

var isImageBasedAttachment: Bool {
self.view == nil
}
Expand All @@ -190,12 +204,10 @@ open class Attachment: NSTextAttachment, BoundsObserving {
return view?.superview != nil
}

/// Determines if attachment is in selected range in the container `EditorView`
public var isInSelectedRange: Bool { isSelected }

var isSelected: Bool = false {
didSet {
guard let view = self.view else { return }
guard let view = self.view,
showSelectionViewWhenSelected else { return }
if isSelected {
selectionView.addTo(parent: view)
} else {
Expand Down Expand Up @@ -398,9 +410,30 @@ open class Attachment: NSTextAttachment, BoundsObserving {
view?.removeFromSuperview()
containerEditorView = nil
}

/// Selects attachment range in container `EditorView`. This does not shows attachment itself as selected nor does it makes
/// any changes to first responder. To show attachment as selected, see `setSelected`
/// - Returns: `true` if attachment range was selected, else `false`
@discardableResult
public func selectRangeInContainer() -> Bool {
guard let container = containerEditorView,
let range = rangeInContainer(),
range.isValidIn(container.textInput) else { return false }
selectContainerWithoutSelfSelection {
container.selectedRange = range
}
return true
}

private func selectContainerWithoutSelfSelection(_ closure: () -> Void) {
showSelectionViewWhenSelected = false
closure()
showSelectionViewWhenSelected = true
}

/// Selects the attachment in Editor.
/// - Parameter isSelected: `true` to set selected, else `false`
/// - Parameters:
/// - isSelected: `true` to set selected, else `false`
public func setSelected(_ isSelected: Bool) {
guard let containerEditor = containerEditorView,
let range = rangeInContainer() else { return }
Expand Down Expand Up @@ -666,6 +699,10 @@ extension UIView {
containerAttachmentFor(view: self)
}

func firstResponderChildView() -> UIView? {
return isFirstResponder ? self : subviews.compactMap { $0.firstResponderChildView() }.first
}

private func containerAttachmentFor(view: UIView?) -> AttachmentContentView? {
guard view != nil else { return nil }
guard let attachmentView = view as? AttachmentContentView else {
Expand Down
113 changes: 112 additions & 1 deletion Proton/Tests/Attachments/ViewAttachmentSnapshotTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,13 @@ import SnapshotTesting
@testable import Proton

class ViewAttachmentSnapshotTests: SnapshotTestCase {
var attachmentOffset = CGPoint(x: 0, y: -3)

override func setUp() {
super.setUp()
attachmentOffset = CGPoint(x: 0, y: -3)
}

func testMatchContentRendering() {
let viewController = EditorTestViewController()
let textView = viewController.editor
Expand Down Expand Up @@ -110,7 +117,100 @@ class ViewAttachmentSnapshotTests: SnapshotTestCase {
assertSnapshot(matching: viewController.view, as: .image, record: recordMode)
}

func testSetsSelectionWithDisplay() {
let viewController = EditorTestViewController()
let textView = viewController.editor

let attachment1 = makeTextFieldAttachment(text: NSAttributedString(string: "Test text"))

textView.replaceCharacters(in: .zero, with: "Short text ")
textView.insertAttachment(in: textView.textEndRange, attachment: attachment1)

textView.selectedRange = .zero

attachment1.setSelected(true)
viewController.render(size: CGSize(width: 300, height: 120))

XCTAssertNotNil(attachment1.rangeInContainer())
XCTAssertTrue(attachment1.isSelected)
XCTAssertEqual(attachment1.containerEditorView?.selectedRange, attachment1.rangeInContainer())
assertSnapshot(matching: viewController.view, as: .image, record: recordMode)
}

func testSetsSelectionWithoutDisplay() {
let viewController = EditorTestViewController()
let textView = viewController.editor

let attachment1 = makeTextFieldAttachment(text: NSAttributedString(string: "Test text"))

textView.replaceCharacters(in: .zero, with: "Short text ")
textView.insertAttachment(in: textView.textEndRange, attachment: attachment1)

textView.selectedRange = .zero

XCTAssertTrue(attachment1.selectRangeInContainer())
viewController.render(size: CGSize(width: 300, height: 120))

XCTAssertNotNil(attachment1.rangeInContainer())
XCTAssertTrue(attachment1.isSelected)
XCTAssertEqual(attachment1.containerEditorView?.selectedRange, attachment1.rangeInContainer())
assertSnapshot(matching: viewController.view, as: .image, record: recordMode)
}

func testGetsFocussedChildView() {
let window = UIWindow(frame: CGRect(origin: .zero, size: CGSize(width: 300, height: 800)))
window.makeKeyAndVisible()

let viewController = EditorTestViewController()
window.rootViewController = viewController

let textView = viewController.editor
let attachment1 = makeTextFieldAttachment(text: NSAttributedString(string: "Test text"))

textView.replaceCharacters(in: .zero, with: "Short text ")
textView.insertAttachment(in: textView.textEndRange, attachment: attachment1)

textView.selectedRange = .zero

viewController.render(size: CGSize(width: 300, height: 120))

XCTAssertTrue((attachment1.contentView as? AutogrowingTextField)?.becomeFirstResponder() ?? false)
XCTAssertTrue(attachment1.isFocussed)

XCTAssertTrue(attachment1.firstResponderChildView is AutogrowingTextField)

assertSnapshot(matching: viewController.view, as: .image, record: recordMode)
}

func testReturnsNilForNonFocussedChildView() {
let window = UIWindow(frame: CGRect(origin: .zero, size: CGSize(width: 300, height: 800)))
window.makeKeyAndVisible()

let viewController = EditorTestViewController()
window.rootViewController = viewController

let textView = viewController.editor
let attachment1 = makeTextFieldAttachment(text: NSAttributedString(string: "Test text"))

textView.replaceCharacters(in: .zero, with: "Short text ")
textView.insertAttachment(in: textView.textEndRange, attachment: attachment1)

textView.selectedRange = .zero

viewController.render(size: CGSize(width: 300, height: 120))

XCTAssertTrue((attachment1.contentView as? AutogrowingTextField)?.becomeFirstResponder() ?? false)
XCTAssertTrue(attachment1.isFocussed)
XCTAssertTrue((attachment1.contentView as? AutogrowingTextField)?.resignFirstResponder() ?? false)

XCTAssertFalse(attachment1.isFocussed)
XCTAssertNil(attachment1.firstResponderChildView)

assertSnapshot(matching: viewController.view, as: .image, record: recordMode)
}

private func makeDummyAttachment(text: String, size: AttachmentSize) -> Attachment {
attachmentOffset = CGPoint(x: 0, y: -3)
let textView = RichTextAttachmentView(context: RichTextViewContext())
textView.textContainerInset = .zero
textView.layoutMargins = .zero
Expand All @@ -120,10 +220,21 @@ class ViewAttachmentSnapshotTests: SnapshotTestCase {
attachment.offsetProvider = self
return attachment
}

private func makeTextFieldAttachment(text: NSAttributedString) -> Attachment {
attachmentOffset = CGPoint(x: 0, y: -4.5)
let textField = AutogrowingTextField()
let textFieldAttachment = Attachment(textField, size: .matchContent)
textFieldAttachment.offsetProvider = self
textField.attributedText = text
textField.layer.borderColor = UIColor.black.cgColor
textField.layer.borderWidth = 1
return textFieldAttachment
}
}

extension ViewAttachmentSnapshotTests: AttachmentOffsetProviding {
func offset(for attachment: Attachment, in textContainer: NSTextContainer, proposedLineFragment lineFrag: CGRect, glyphPosition position: CGPoint, characterIndex charIndex: Int) -> CGPoint {
return CGPoint(x: 0, y: -3)
return attachmentOffset
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading