Limit context menu previews in ContentTextView to link's text line rects

This commit is contained in:
Shadowfacts 2020-06-25 10:42:46 -04:00
parent e70a84274e
commit fdcdbced38
1 changed files with 73 additions and 1 deletions

View File

@ -27,6 +27,9 @@ class ContentTextView: LinkTextView {
delegate = self delegate = self
// Disable layer masking, otherwise the context menu opening animation
// may be clipped if it's at an edge of the text view
layer.masksToBounds = false
addInteraction(UIContextMenuInteraction(delegate: self)) addInteraction(UIContextMenuInteraction(delegate: self))
textDragInteraction?.isEnabled = false textDragInteraction?.isEnabled = false
@ -253,6 +256,12 @@ extension ContentTextView: MenuPreviewProvider {
extension ContentTextView: UIContextMenuInteractionDelegate { extension ContentTextView: UIContextMenuInteractionDelegate {
func contextMenuInteraction(_ interaction: UIContextMenuInteraction, configurationForMenuAtLocation location: CGPoint) -> UIContextMenuConfiguration? { func contextMenuInteraction(_ interaction: UIContextMenuInteraction, configurationForMenuAtLocation location: CGPoint) -> UIContextMenuConfiguration? {
if let (link, range) = getLinkAtPoint(location) { if let (link, range) = getLinkAtPoint(location) {
// Determine the line rects that the link takes up in the coordinate space of this view
var rects = [CGRect]()
layoutManager.enumerateEnclosingRects(forGlyphRange: range, withinSelectedGlyphRange: NSRange(location: NSNotFound, length: 0), in: textContainer) { (rect, stop) in
rects.append(rect)
}
let preview: UIContextMenuContentPreviewProvider = { let preview: UIContextMenuContentPreviewProvider = {
self.getViewController(forLink: link, inRange: range) self.getViewController(forLink: link, inRange: range)
} }
@ -268,11 +277,68 @@ extension ContentTextView: UIContextMenuInteractionDelegate {
} }
return UIMenu(title: "", image: nil, identifier: nil, options: [], children: actions) return UIMenu(title: "", image: nil, identifier: nil, options: [], children: actions)
} }
return UIContextMenuConfiguration(identifier: nil, previewProvider: preview, actionProvider: actions)
// Use a custom UIContentMenuConfiguration subclass to pass the text line rect information
// to the `contextMenuInteraction(_:previewForHighlightingMenuWithConfiguration:)` method.
let configuration = ContextMenuConfiguration(identifier: nil, previewProvider: preview, actionProvider: actions)
configuration.textLineRects = rects
return configuration
} else { } else {
return nil return nil
} }
} }
func contextMenuInteraction(_ interaction: UIContextMenuInteraction, previewForHighlightingMenuWithConfiguration configuration: UIContextMenuConfiguration) -> UITargetedPreview? {
// If there isn't custom text line rect data, use the default system-generated preview.
guard let config = configuration as? ContextMenuConfiguration,
let rects = config.textLineRects,
rects.count > 0 else {
return nil
}
// Calculate the smallest rect enclosing all of the text line rects, in the coordinate space of this view.
var minX: CGFloat = .greatestFiniteMagnitude, maxX: CGFloat = .leastNonzeroMagnitude, minY: CGFloat = .greatestFiniteMagnitude, maxY: CGFloat = .leastNonzeroMagnitude
for rect in rects {
minX = min(rect.minX, minX)
maxX = max(rect.maxX, maxX)
minY = min(rect.minY, minY)
maxY = max(rect.maxY, maxY)
}
let rectEnclosingTextLineRects = CGRect(x: minX, y: minY, width: maxX - minX, height: maxY - minY)
// Try to create a snapshot view of this view that only shows the minimum
// rectangle necessary to fully display the link text (reduces the likelihood that
// other text will be displayed alongside it).
// If a snapshot view cannot be created, we bail and use the system-provided preview.
guard let snapshot = self.resizableSnapshotView(from: rectEnclosingTextLineRects, afterScreenUpdates: false, withCapInsets: .zero) else {
return nil
}
// Convert the textLineRects from the context menu configuration to be in the
// coordinate space of the snapshot view. The snapshot view is created from
// rectEnclosingTextLineRects, which means that, while its size is the same as the
// enclosing rect, its coordinate space is relative to this text views by rectEnclosingTextLineRects.origin.
// Since the text line rects passed to UIPreviewParameters need to be in the coordinate space of
// the preview view, we subtract the origin position from each rect to convert to the snapshot view's
// coordinate space.
let rectsInCoordinateSpaceOfEnclosingRect = rects.map {
$0.offsetBy(dx: -rectEnclosingTextLineRects.minX, dy: -rectEnclosingTextLineRects.minY)
}
// The preview parameters describe how the preview view is shown inside the prev.
let parameters = UIPreviewParameters(textLineRects: rectsInCoordinateSpaceOfEnclosingRect as [NSValue])
// todo: parameters.visiblePath around text
// The center point of the the minimum enclosing rect in our coordinate space is the point where the
// center of the preview should be, since that's also in this view's coordinate space.
let rectsCenter = CGPoint(x: rectEnclosingTextLineRects.midX, y: rectEnclosingTextLineRects.midY)
// The preview target describes how the preview is positioned.
let target = UIPreviewTarget(container: self, center: rectsCenter)
return UITargetedPreview(view: snapshot, parameters: parameters, target: target)
}
func contextMenuInteraction(_ interaction: UIContextMenuInteraction, willPerformPreviewActionForMenuWith configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionCommitAnimating) { func contextMenuInteraction(_ interaction: UIContextMenuInteraction, willPerformPreviewActionForMenuWith configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionCommitAnimating) {
if let viewController = animator.previewViewController { if let viewController = animator.previewViewController {
animator.preferredCommitStyle = .pop animator.preferredCommitStyle = .pop
@ -281,4 +347,10 @@ extension ContentTextView: UIContextMenuInteractionDelegate {
} }
} }
} }
/// Used to pass text line rect data between `contextMenuInteraction(_:configurationForMenuAtLocation:)` and `contextMenuInteraction(_:previewForHighlightingMenuWithConfiguration:)`
fileprivate class ContextMenuConfiguration: UIContextMenuConfiguration {
/// The line rects of the source of this context menu configuration in the coordinate space of the preview target view.
var textLineRects: [CGRect]?
}
} }