session-ios/SignalUtilitiesKit/Media Viewing & Editing/Image Editing/ImageEditorTextViewControll...

368 lines
12 KiB
Swift

//
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
//
import UIKit
import SessionUIKit
@objc
public protocol VAlignTextViewDelegate: class {
func textViewDidComplete()
}
// MARK: -
private class VAlignTextView: UITextView {
fileprivate weak var textViewDelegate: VAlignTextViewDelegate?
enum Alignment: String {
case top
case center
case bottom
}
private let alignment: Alignment
@objc public override var bounds: CGRect {
didSet {
if oldValue != bounds {
updateInsets()
}
}
}
@objc public override var frame: CGRect {
didSet {
if oldValue != frame {
updateInsets()
}
}
}
public init(alignment: Alignment) {
self.alignment = alignment
super.init(frame: .zero, textContainer: nil)
self.addObserver(self, forKeyPath: "contentSize", options: .new, context: nil)
}
@available(*, unavailable, message: "use other init() instead.")
required public init?(coder aDecoder: NSCoder) {
notImplemented()
}
deinit {
self.removeObserver(self, forKeyPath: "contentSize")
}
private func updateInsets() {
let topOffset: CGFloat
switch alignment {
case .top:
topOffset = 0
case .center:
topOffset = max(0, (self.height() - contentSize.height) * 0.5)
case .bottom:
topOffset = max(0, self.height() - contentSize.height)
}
contentInset = UIEdgeInsets(top: topOffset, leading: 0, bottom: 0, trailing: 0)
}
open override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey: Any]?, context: UnsafeMutableRawPointer?) {
updateInsets()
}
// MARK: - Key Commands
override var keyCommands: [UIKeyCommand]? {
return [
UIKeyCommand(input: "\r", modifierFlags: .command, action: #selector(self.modifiedReturnPressed(sender:)), discoverabilityTitle: "Add Text"),
UIKeyCommand(input: "\r", modifierFlags: .alternate, action: #selector(self.modifiedReturnPressed(sender:)), discoverabilityTitle: "Add Text")
]
}
@objc
public func modifiedReturnPressed(sender: UIKeyCommand) {
Logger.verbose("")
self.textViewDelegate?.textViewDidComplete()
}
}
// MARK: -
@objc
public protocol ImageEditorTextViewControllerDelegate: class {
func textEditDidComplete(textItem: ImageEditorTextItem)
func textEditDidDelete(textItem: ImageEditorTextItem)
func textEditDidCancel()
}
// MARK: -
// A view for editing text item in image editor.
public class ImageEditorTextViewController: OWSViewController, VAlignTextViewDelegate {
private weak var delegate: ImageEditorTextViewControllerDelegate?
private let textItem: ImageEditorTextItem
private let isNewItem: Bool
private let maxTextWidthPoints: CGFloat
private let textView = VAlignTextView(alignment: .center)
private let model: ImageEditorModel
private let canvasView: ImageEditorCanvasView
private let paletteView: ImageEditorPaletteView
init(delegate: ImageEditorTextViewControllerDelegate,
model: ImageEditorModel,
textItem: ImageEditorTextItem,
isNewItem: Bool,
maxTextWidthPoints: CGFloat) {
self.delegate = delegate
self.model = model
self.textItem = textItem
self.isNewItem = isNewItem
self.maxTextWidthPoints = maxTextWidthPoints
self.canvasView = ImageEditorCanvasView(model: model,
itemIdsToIgnore: [textItem.itemId])
self.paletteView = ImageEditorPaletteView(currentColor: textItem.color)
super.init(nibName: nil, bundle: nil)
self.textView.textViewDelegate = self
}
@available(*, unavailable, message: "use other init() instead.")
required public init?(coder aDecoder: NSCoder) {
notImplemented()
}
// MARK: - View Lifecycle
public override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
textView.becomeFirstResponder()
self.view.layoutSubviews()
}
public override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
textView.becomeFirstResponder()
self.view.layoutSubviews()
}
public override func loadView() {
self.view = UIView()
self.view.backgroundColor = Colors.navigationBarBackground
self.view.isOpaque = true
canvasView.configureSubviews()
self.view.addSubview(canvasView)
canvasView.autoPinEdgesToSuperviewEdges()
let tintView = UIView()
tintView.backgroundColor = .clear
tintView.isOpaque = false
self.view.addSubview(tintView)
tintView.autoPinEdgesToSuperviewEdges()
tintView.layer.opacity = 0
UIView.animate(withDuration: 0.25, animations: {
tintView.layer.opacity = 1
}, completion: { (_) in
tintView.layer.opacity = 1
})
configureTextView()
self.view.layoutMargins = UIEdgeInsets(top: 16, left: 20, bottom: 16, right: 20)
self.view.addSubview(textView)
textView.autoPinTopToSuperviewMargin()
textView.autoHCenterInSuperview()
self.autoPinView(toBottomOfViewControllerOrKeyboard: textView, avoidNotch: true)
paletteView.delegate = self
self.view.addSubview(paletteView)
paletteView.autoAlignAxis(.horizontal, toSameAxisOf: textView)
paletteView.autoPinEdge(toSuperviewEdge: .trailing, withInset: 0)
// This will determine the text view's size.
paletteView.autoPinEdge(.leading, to: .trailing, of: textView, withOffset: 0)
let pinchGestureRecognizer = ImageEditorPinchGestureRecognizer(target: self, action: #selector(handlePinchGesture(_:)))
pinchGestureRecognizer.referenceView = view
view.addGestureRecognizer(pinchGestureRecognizer)
updateNavigationBar()
}
private func configureTextView() {
textView.text = textItem.text
textView.font = textItem.font
textView.textColor = textItem.color.color
textView.isEditable = true
textView.backgroundColor = .clear
textView.isOpaque = false
// We use a white cursor since we use a dark background.
textView.tintColor = .white
// TODO: Limit the size of the text?
// textView.delegate = self
textView.isScrollEnabled = true
textView.scrollsToTop = false
textView.isUserInteractionEnabled = true
textView.textAlignment = .center
textView.textContainerInset = .zero
textView.textContainer.lineFragmentPadding = 0
textView.contentInset = .zero
}
private func updateNavigationBar() {
let undoButton = navigationBarButton(imageName: "image_editor_undo",
selector: #selector(didTapUndo(sender:)))
let doneButton = navigationBarButton(imageName: "image_editor_checkmark_full",
selector: #selector(didTapDone(sender:)))
let navigationBarItems = [undoButton, doneButton]
updateNavigationBar(navigationBarItems: navigationBarItems)
}
@objc
public override var prefersStatusBarHidden: Bool {
return true
}
@objc
override public var canBecomeFirstResponder: Bool {
return true
}
// MARK: - Pinch Gesture
private var pinchFontStart: UIFont?
@objc
public func handlePinchGesture(_ gestureRecognizer: ImageEditorPinchGestureRecognizer) {
AssertIsOnMainThread()
switch gestureRecognizer.state {
case .began:
pinchFontStart = textView.font
case .changed, .ended:
guard let pinchFontStart = pinchFontStart else {
return
}
var pointSize: CGFloat = pinchFontStart.pointSize
if gestureRecognizer.pinchStateLast.distance > 0 {
pointSize *= gestureRecognizer.pinchStateLast.distance / gestureRecognizer.pinchStateStart.distance
}
let minPointSize: CGFloat = 12
let maxPointSize: CGFloat = 64
pointSize = max(minPointSize, min(maxPointSize, pointSize))
let font = pinchFontStart.withSize(pointSize)
textView.font = font
default:
pinchFontStart = nil
}
}
// MARK: - Events
@objc func didTapUndo(sender: UIButton) {
Logger.verbose("")
self.delegate?.textEditDidCancel()
self.dismiss(animated: false) {
// Do nothing.
}
}
@objc func didTapDone(sender: UIButton) {
Logger.verbose("")
completeAndDismiss()
}
private func completeAndDismiss() {
var newTextItem = textItem
if isNewItem {
let view = self.canvasView.gestureReferenceView
let viewBounds = view.bounds
// Ensure continuity of the new text item's location
// with its apparent location in this text editor.
let locationInView = view.convert(textView.bounds.center, from: textView).clamp(view.bounds)
let textCenterImageUnit = ImageEditorCanvasView.locationImageUnit(forLocationInView: locationInView,
viewBounds: viewBounds,
model: model,
transform: model.currentTransform())
// Same, but for size.
let imageFrame = ImageEditorCanvasView.imageFrame(forViewSize: viewBounds.size,
imageSize: model.srcImageSizePixels,
transform: model.currentTransform())
let unitWidth = textView.width() / imageFrame.width
newTextItem = textItem.copy(unitCenter: textCenterImageUnit).copy(unitWidth: unitWidth)
}
var font = textItem.font
if let newFont = textView.font {
font = newFont
} else {
owsFailDebug("Missing font.")
}
newTextItem = newTextItem.copy(font: font)
guard let text = textView.text?.ows_stripped(),
text.count > 0 else {
self.delegate?.textEditDidDelete(textItem: textItem)
self.dismiss(animated: false) {
// Do nothing.
}
return
}
newTextItem = newTextItem.copy(withText: text, color: paletteView.selectedValue)
// Hide the text view immediately to avoid animation glitches in the dismiss transition.
textView.isHidden = true
if textItem == newTextItem {
// No changes were made. Cancel to avoid dirtying the undo stack.
self.delegate?.textEditDidCancel()
} else {
self.delegate?.textEditDidComplete(textItem: newTextItem)
}
self.dismiss(animated: false) {
// Do nothing.
}
}
// MARK: - VAlignTextViewDelegate
public func textViewDidComplete() {
completeAndDismiss()
}
}
// MARK: -
extension ImageEditorTextViewController: ImageEditorPaletteViewDelegate {
public func selectedColorDidChange() {
self.textView.textColor = self.paletteView.selectedValue.color
}
}