mirror of https://github.com/oxen-io/session-ios
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
748 lines
27 KiB
Swift
748 lines
27 KiB
Swift
//
|
|
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
|
|
//
|
|
|
|
import UIKit
|
|
|
|
@objc
|
|
public protocol ImageEditorViewDelegate: class {
|
|
func imageEditor(presentFullScreenOverlay viewController: UIViewController,
|
|
withNavigation: Bool)
|
|
func imageEditorPresentCaptionView()
|
|
func imageEditorUpdateNavigationBar()
|
|
}
|
|
|
|
// MARK: -
|
|
|
|
// A view for editing outgoing image attachments.
|
|
// It can also be used to render the final output.
|
|
@objc
|
|
public class ImageEditorView: UIView {
|
|
|
|
weak var delegate: ImageEditorViewDelegate?
|
|
|
|
private let model: ImageEditorModel
|
|
|
|
private let canvasView: ImageEditorCanvasView
|
|
|
|
private let paletteView = ImageEditorPaletteView()
|
|
|
|
enum EditorMode: String {
|
|
// This is the default mode. It is used for interacting with text items.
|
|
case none
|
|
case brush
|
|
case text
|
|
}
|
|
|
|
private var editorMode = EditorMode.none {
|
|
didSet {
|
|
AssertIsOnMainThread()
|
|
|
|
updateButtons()
|
|
updateGestureState()
|
|
delegate?.imageEditorUpdateNavigationBar()
|
|
}
|
|
}
|
|
|
|
private var currentColor: UIColor {
|
|
get {
|
|
return paletteView.selectedColor
|
|
}
|
|
}
|
|
|
|
@objc
|
|
public required init(model: ImageEditorModel, delegate: ImageEditorViewDelegate) {
|
|
self.model = model
|
|
self.delegate = delegate
|
|
self.canvasView = ImageEditorCanvasView(model: model)
|
|
|
|
super.init(frame: .zero)
|
|
|
|
model.add(observer: self)
|
|
}
|
|
|
|
@available(*, unavailable, message: "use other init() instead.")
|
|
required public init?(coder aDecoder: NSCoder) {
|
|
notImplemented()
|
|
}
|
|
|
|
// MARK: - Views
|
|
|
|
private var moveTextGestureRecognizer: ImageEditorPanGestureRecognizer?
|
|
private var brushGestureRecognizer: ImageEditorPanGestureRecognizer?
|
|
private var tapGestureRecognizer: UITapGestureRecognizer?
|
|
private var pinchGestureRecognizer: ImageEditorPinchGestureRecognizer?
|
|
|
|
@objc
|
|
public func configureSubviews() -> Bool {
|
|
canvasView.configureSubviews()
|
|
self.addSubview(canvasView)
|
|
canvasView.autoPinEdgesToSuperviewEdges()
|
|
|
|
paletteView.delegate = self
|
|
|
|
self.isUserInteractionEnabled = true
|
|
|
|
let moveTextGestureRecognizer = ImageEditorPanGestureRecognizer(target: self, action: #selector(handleMoveTextGesture(_:)))
|
|
moveTextGestureRecognizer.maximumNumberOfTouches = 1
|
|
moveTextGestureRecognizer.referenceView = canvasView.gestureReferenceView
|
|
moveTextGestureRecognizer.delegate = self
|
|
self.addGestureRecognizer(moveTextGestureRecognizer)
|
|
self.moveTextGestureRecognizer = moveTextGestureRecognizer
|
|
|
|
let brushGestureRecognizer = ImageEditorPanGestureRecognizer(target: self, action: #selector(handleBrushGesture(_:)))
|
|
brushGestureRecognizer.maximumNumberOfTouches = 1
|
|
brushGestureRecognizer.referenceView = canvasView.gestureReferenceView
|
|
self.addGestureRecognizer(brushGestureRecognizer)
|
|
self.brushGestureRecognizer = brushGestureRecognizer
|
|
|
|
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTapGesture(_:)))
|
|
self.addGestureRecognizer(tapGestureRecognizer)
|
|
self.tapGestureRecognizer = tapGestureRecognizer
|
|
|
|
let pinchGestureRecognizer = ImageEditorPinchGestureRecognizer(target: self, action: #selector(handlePinchGesture(_:)))
|
|
pinchGestureRecognizer.referenceView = canvasView.gestureReferenceView
|
|
self.addGestureRecognizer(pinchGestureRecognizer)
|
|
self.pinchGestureRecognizer = pinchGestureRecognizer
|
|
|
|
// De-conflict the GRs.
|
|
// editorGestureRecognizer.require(toFail: tapGestureRecognizer)
|
|
// editorGestureRecognizer.require(toFail: pinchGestureRecognizer)
|
|
|
|
updateGestureState()
|
|
|
|
return true
|
|
}
|
|
|
|
private func commitTextEditingChanges(textItem: ImageEditorTextItem, textView: UITextView) {
|
|
AssertIsOnMainThread()
|
|
|
|
guard let text = textView.text?.ows_stripped(),
|
|
text.count > 0 else {
|
|
model.remove(item: textItem)
|
|
return
|
|
}
|
|
|
|
// Model items are immutable; we _replace_ the item rather than modify it.
|
|
let newItem = textItem.copy(withText: text)
|
|
if model.has(itemForId: textItem.itemId) {
|
|
model.replace(item: newItem, suppressUndo: false)
|
|
} else {
|
|
model.append(item: newItem)
|
|
}
|
|
}
|
|
|
|
// The model supports redo if we ever want to add it.
|
|
private let undoButton = OWSButton()
|
|
private let brushButton = OWSButton()
|
|
private let cropButton = OWSButton()
|
|
private let newTextButton = OWSButton()
|
|
private let captionButton = OWSButton()
|
|
private let doneButton = OWSButton()
|
|
private let buttonStackView = UIStackView()
|
|
|
|
// TODO: Should this method be private?
|
|
@objc
|
|
public func addControls(to containerView: UIView,
|
|
viewController: UIViewController) {
|
|
configure(button: undoButton,
|
|
imageName: "image_editor_undo",
|
|
selector: #selector(didTapUndo(sender:)))
|
|
|
|
configure(button: brushButton,
|
|
imageName: "image_editor_brush",
|
|
selector: #selector(didTapBrush(sender:)))
|
|
|
|
configure(button: cropButton,
|
|
imageName: "image_editor_crop",
|
|
selector: #selector(didTapCrop(sender:)))
|
|
|
|
configure(button: newTextButton,
|
|
imageName: "image_editor_text",
|
|
selector: #selector(didTapNewText(sender:)))
|
|
|
|
configure(button: captionButton,
|
|
imageName: "image_editor_caption",
|
|
selector: #selector(didTapCaption(sender:)))
|
|
|
|
configure(button: doneButton,
|
|
imageName: "image_editor_checkmark_full",
|
|
selector: #selector(didTapDone(sender:)))
|
|
|
|
buttonStackView.axis = .horizontal
|
|
buttonStackView.alignment = .center
|
|
buttonStackView.spacing = 20
|
|
|
|
containerView.addSubview(buttonStackView)
|
|
buttonStackView.autoPin(toTopLayoutGuideOf: viewController, withInset: 0)
|
|
buttonStackView.autoPinTrailingToSuperviewMargin(withInset: 18)
|
|
|
|
containerView.addSubview(paletteView)
|
|
paletteView.autoVCenterInSuperview()
|
|
paletteView.autoPinLeadingToSuperviewMargin(withInset: 10)
|
|
|
|
updateButtons()
|
|
|
|
delegate?.imageEditorUpdateNavigationBar()
|
|
}
|
|
|
|
private func configure(button: UIButton,
|
|
imageName: String,
|
|
selector: Selector) {
|
|
if let image = UIImage(named: imageName) {
|
|
button.setImage(image.withRenderingMode(.alwaysTemplate), for: .normal)
|
|
} else {
|
|
owsFailDebug("Missing asset: \(imageName)")
|
|
}
|
|
button.tintColor = .white
|
|
button.addTarget(self, action: selector, for: .touchUpInside)
|
|
button.layer.shadowColor = UIColor.black.cgColor
|
|
button.layer.shadowRadius = 4
|
|
button.layer.shadowOpacity = 0.66
|
|
}
|
|
|
|
private func updateButtons() {
|
|
var buttons = [OWSButton]()
|
|
|
|
var hasPalette = false
|
|
switch editorMode {
|
|
case .text:
|
|
// TODO:
|
|
hasPalette = true
|
|
break
|
|
case .brush:
|
|
hasPalette = true
|
|
|
|
if model.canUndo() {
|
|
buttons = [undoButton, doneButton]
|
|
} else {
|
|
buttons = [doneButton]
|
|
}
|
|
case .none:
|
|
if model.canUndo() {
|
|
buttons = [undoButton, newTextButton, brushButton, cropButton, captionButton]
|
|
} else {
|
|
buttons = [newTextButton, brushButton, cropButton, captionButton]
|
|
}
|
|
}
|
|
|
|
for subview in buttonStackView.subviews {
|
|
subview.removeFromSuperview()
|
|
}
|
|
buttonStackView.addArrangedSubview(UIView.hStretchingSpacer())
|
|
for button in buttons {
|
|
buttonStackView.addArrangedSubview(button)
|
|
}
|
|
|
|
paletteView.isHidden = !hasPalette
|
|
}
|
|
|
|
// MARK: - Navigation Bar
|
|
|
|
public func navigationBarItems() -> [UIView] {
|
|
let undoButton = navigationBarButton(imageName: "image_editor_undo",
|
|
selector: #selector(didTapUndo(sender:)))
|
|
let brushButton = navigationBarButton(imageName: "image_editor_brush",
|
|
selector: #selector(didTapBrush(sender:)))
|
|
let cropButton = navigationBarButton(imageName: "image_editor_crop",
|
|
selector: #selector(didTapCrop(sender:)))
|
|
let newTextButton = navigationBarButton(imageName: "image_editor_text",
|
|
selector: #selector(didTapNewText(sender:)))
|
|
let captionButton = navigationBarButton(imageName: "image_editor_caption",
|
|
selector: #selector(didTapCaption(sender:)))
|
|
|
|
switch editorMode {
|
|
case .text:
|
|
return []
|
|
case .brush:
|
|
return []
|
|
case .none:
|
|
if model.canUndo() {
|
|
return [undoButton, newTextButton, brushButton, cropButton, captionButton]
|
|
} else {
|
|
return [newTextButton, brushButton, cropButton, captionButton]
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Actions
|
|
|
|
@objc func didTapUndo(sender: UIButton) {
|
|
Logger.verbose("")
|
|
guard model.canUndo() else {
|
|
owsFailDebug("Can't undo.")
|
|
return
|
|
}
|
|
model.undo()
|
|
}
|
|
|
|
@objc func didTapBrush(sender: UIButton) {
|
|
Logger.verbose("")
|
|
|
|
self.editorMode = .brush
|
|
|
|
let brushView = ImageEditorBrushViewController(delegate: self, model: model)
|
|
self.delegate?.imageEditor(presentFullScreenOverlay: brushView,
|
|
withNavigation: true)
|
|
}
|
|
|
|
@objc func didTapCrop(sender: UIButton) {
|
|
Logger.verbose("")
|
|
|
|
presentCropTool()
|
|
}
|
|
|
|
@objc func didTapNewText(sender: UIButton) {
|
|
Logger.verbose("")
|
|
|
|
let viewSize = canvasView.gestureReferenceView.bounds.size
|
|
let imageSize = model.srcImageSizePixels
|
|
let imageFrame = ImageEditorCanvasView.imageFrame(forViewSize: viewSize, imageSize: imageSize,
|
|
transform: model.currentTransform())
|
|
|
|
let textWidthPoints = viewSize.width * ImageEditorTextItem.kDefaultUnitWidth
|
|
let textWidthUnit = textWidthPoints / imageFrame.size.width
|
|
|
|
let textItem = ImageEditorTextItem.empty(withColor: currentColor,
|
|
unitWidth: textWidthUnit,
|
|
fontReferenceImageWidth: imageFrame.size.width)
|
|
|
|
edit(textItem: textItem)
|
|
}
|
|
|
|
@objc func didTapCaption(sender: UIButton) {
|
|
Logger.verbose("")
|
|
|
|
delegate?.imageEditorPresentCaptionView()
|
|
|
|
// // TODO:
|
|
// let maxTextWidthPoints = model.srcImageSizePixels.width * ImageEditorTextItem.kDefaultUnitWidth
|
|
// // let maxTextWidthPoints = canvasView.imageView.width() * ImageEditorTextItem.kDefaultUnitWidth
|
|
//
|
|
// let textEditor = ImageEditorTextViewController(delegate: self, textItem: textItem, maxTextWidthPoints: maxTextWidthPoints)
|
|
// self.delegate?.imageEditor(presentFullScreenOverlay: textEditor,
|
|
// withNavigation: true)
|
|
|
|
// TODO:
|
|
}
|
|
|
|
@objc func didTapDone(sender: UIButton) {
|
|
Logger.verbose("")
|
|
|
|
self.editorMode = .none
|
|
}
|
|
|
|
// MARK: - Gestures
|
|
|
|
private func updateGestureState() {
|
|
AssertIsOnMainThread()
|
|
|
|
switch editorMode {
|
|
case .none:
|
|
moveTextGestureRecognizer?.isEnabled = true
|
|
brushGestureRecognizer?.isEnabled = false
|
|
tapGestureRecognizer?.isEnabled = true
|
|
pinchGestureRecognizer?.isEnabled = true
|
|
case .brush:
|
|
// Brush strokes can start and end (and return from) outside the view.
|
|
moveTextGestureRecognizer?.isEnabled = false
|
|
brushGestureRecognizer?.isEnabled = true
|
|
tapGestureRecognizer?.isEnabled = false
|
|
pinchGestureRecognizer?.isEnabled = false
|
|
case .text:
|
|
moveTextGestureRecognizer?.isEnabled = false
|
|
brushGestureRecognizer?.isEnabled = false
|
|
tapGestureRecognizer?.isEnabled = false
|
|
pinchGestureRecognizer?.isEnabled = false
|
|
}
|
|
}
|
|
|
|
// MARK: - Tap Gesture
|
|
|
|
@objc
|
|
public func handleTapGesture(_ gestureRecognizer: UIGestureRecognizer) {
|
|
AssertIsOnMainThread()
|
|
|
|
guard gestureRecognizer.state == .recognized else {
|
|
owsFailDebug("Unexpected state.")
|
|
return
|
|
}
|
|
|
|
let location = gestureRecognizer.location(in: canvasView.gestureReferenceView)
|
|
guard let textLayer = self.textLayer(forLocation: location) else {
|
|
return
|
|
}
|
|
|
|
guard let textItem = model.item(forId: textLayer.itemId) as? ImageEditorTextItem else {
|
|
owsFailDebug("Missing or invalid text item.")
|
|
return
|
|
}
|
|
|
|
edit(textItem: textItem)
|
|
}
|
|
|
|
// MARK: - Pinch Gesture
|
|
|
|
// These properties are valid while moving a text item.
|
|
private var pinchingTextItem: ImageEditorTextItem?
|
|
private var pinchHasChanged = false
|
|
|
|
@objc
|
|
public func handlePinchGesture(_ gestureRecognizer: ImageEditorPinchGestureRecognizer) {
|
|
AssertIsOnMainThread()
|
|
|
|
// We could undo an in-progress pinch if the gesture is cancelled, but it seems gratuitous.
|
|
|
|
switch gestureRecognizer.state {
|
|
case .began:
|
|
let pinchState = gestureRecognizer.pinchStateStart
|
|
guard let textLayer = self.textLayer(forLocation: pinchState.centroid) else {
|
|
// The pinch needs to start centered on a text item.
|
|
return
|
|
}
|
|
guard let textItem = model.item(forId: textLayer.itemId) as? ImageEditorTextItem else {
|
|
owsFailDebug("Missing or invalid text item.")
|
|
return
|
|
}
|
|
pinchingTextItem = textItem
|
|
pinchHasChanged = false
|
|
case .changed, .ended:
|
|
guard let textItem = pinchingTextItem else {
|
|
return
|
|
}
|
|
|
|
let view = self.canvasView.gestureReferenceView
|
|
let viewBounds = view.bounds
|
|
let locationStart = gestureRecognizer.pinchStateStart.centroid
|
|
let locationNow = gestureRecognizer.pinchStateLast.centroid
|
|
let gestureStartImageUnit = ImageEditorCanvasView.locationImageUnit(forLocationInView: locationStart,
|
|
viewBounds: viewBounds,
|
|
model: self.model,
|
|
transform: self.model.currentTransform())
|
|
let gestureNowImageUnit = ImageEditorCanvasView.locationImageUnit(forLocationInView: locationNow,
|
|
viewBounds: viewBounds,
|
|
model: self.model,
|
|
transform: self.model.currentTransform())
|
|
let gestureDeltaImageUnit = gestureNowImageUnit.minus(gestureStartImageUnit)
|
|
let unitCenter = CGPointClamp01(textItem.unitCenter.plus(gestureDeltaImageUnit))
|
|
|
|
// NOTE: We use max(1, ...) to avoid divide-by-zero.
|
|
let newScaling = CGFloatClamp(textItem.scaling * gestureRecognizer.pinchStateLast.distance / max(1.0, gestureRecognizer.pinchStateStart.distance),
|
|
ImageEditorTextItem.kMinScaling,
|
|
ImageEditorTextItem.kMaxScaling)
|
|
|
|
let newRotationRadians = textItem.rotationRadians + gestureRecognizer.pinchStateLast.angleRadians - gestureRecognizer.pinchStateStart.angleRadians
|
|
|
|
let newItem = textItem.copy(withUnitCenter: unitCenter,
|
|
scaling: newScaling,
|
|
rotationRadians: newRotationRadians)
|
|
|
|
if pinchHasChanged {
|
|
model.replace(item: newItem, suppressUndo: true)
|
|
} else {
|
|
model.replace(item: newItem, suppressUndo: false)
|
|
pinchHasChanged = true
|
|
}
|
|
|
|
if gestureRecognizer.state == .ended {
|
|
pinchingTextItem = nil
|
|
}
|
|
default:
|
|
pinchingTextItem = nil
|
|
}
|
|
}
|
|
|
|
// MARK: - Editor Gesture
|
|
|
|
// These properties are valid while moving a text item.
|
|
private var movingTextItem: ImageEditorTextItem?
|
|
private var movingTextStartUnitCenter: CGPoint?
|
|
private var movingTextHasMoved = false
|
|
|
|
private func textLayer(forLocation locationInView: CGPoint) -> EditorTextLayer? {
|
|
let viewBounds = self.canvasView.gestureReferenceView.bounds
|
|
let affineTransform = self.model.currentTransform().affineTransform(viewSize: viewBounds.size)
|
|
let locationInCanvas = locationInView.minus(viewBounds.center).applyingInverse(affineTransform).plus(viewBounds.center)
|
|
return canvasView.textLayer(forLocation: locationInCanvas)
|
|
}
|
|
|
|
@objc
|
|
public func handleMoveTextGesture(_ gestureRecognizer: ImageEditorPanGestureRecognizer) {
|
|
AssertIsOnMainThread()
|
|
|
|
// We could undo an in-progress move if the gesture is cancelled, but it seems gratuitous.
|
|
|
|
switch gestureRecognizer.state {
|
|
case .began:
|
|
guard let locationStart = gestureRecognizer.locationStart else {
|
|
owsFailDebug("Missing locationStart.")
|
|
return
|
|
}
|
|
guard let textLayer = self.textLayer(forLocation: locationStart) else {
|
|
owsFailDebug("No text layer")
|
|
return
|
|
}
|
|
guard let textItem = model.item(forId: textLayer.itemId) as? ImageEditorTextItem else {
|
|
owsFailDebug("Missing or invalid text item.")
|
|
return
|
|
}
|
|
movingTextItem = textItem
|
|
movingTextStartUnitCenter = textItem.unitCenter
|
|
movingTextHasMoved = false
|
|
|
|
case .changed, .ended:
|
|
guard let textItem = movingTextItem else {
|
|
return
|
|
}
|
|
guard let locationStart = gestureRecognizer.locationStart else {
|
|
owsFailDebug("Missing locationStart.")
|
|
return
|
|
}
|
|
guard let movingTextStartUnitCenter = movingTextStartUnitCenter else {
|
|
owsFailDebug("Missing movingTextStartUnitCenter.")
|
|
return
|
|
}
|
|
|
|
let view = self.canvasView.gestureReferenceView
|
|
let viewBounds = view.bounds
|
|
let locationInView = gestureRecognizer.location(in: view)
|
|
let gestureStartImageUnit = ImageEditorCanvasView.locationImageUnit(forLocationInView: locationStart,
|
|
viewBounds: viewBounds,
|
|
model: self.model,
|
|
transform: self.model.currentTransform())
|
|
let gestureNowImageUnit = ImageEditorCanvasView.locationImageUnit(forLocationInView: locationInView,
|
|
viewBounds: viewBounds,
|
|
model: self.model,
|
|
transform: self.model.currentTransform())
|
|
let gestureDeltaImageUnit = gestureNowImageUnit.minus(gestureStartImageUnit)
|
|
let unitCenter = CGPointClamp01(movingTextStartUnitCenter.plus(gestureDeltaImageUnit))
|
|
let newItem = textItem.copy(withUnitCenter: unitCenter)
|
|
|
|
if movingTextHasMoved {
|
|
model.replace(item: newItem, suppressUndo: true)
|
|
} else {
|
|
model.replace(item: newItem, suppressUndo: false)
|
|
movingTextHasMoved = true
|
|
}
|
|
|
|
if gestureRecognizer.state == .ended {
|
|
movingTextItem = nil
|
|
}
|
|
default:
|
|
movingTextItem = nil
|
|
}
|
|
}
|
|
|
|
// MARK: - Brush
|
|
|
|
// These properties are non-empty while drawing a stroke.
|
|
private var currentStroke: ImageEditorStrokeItem?
|
|
private var currentStrokeSamples = [ImageEditorStrokeItem.StrokeSample]()
|
|
|
|
@objc
|
|
public func handleBrushGesture(_ gestureRecognizer: UIGestureRecognizer) {
|
|
AssertIsOnMainThread()
|
|
|
|
let removeCurrentStroke = {
|
|
if let stroke = self.currentStroke {
|
|
self.model.remove(item: stroke)
|
|
}
|
|
self.currentStroke = nil
|
|
self.currentStrokeSamples.removeAll()
|
|
}
|
|
let tryToAppendStrokeSample = {
|
|
let view = self.canvasView.gestureReferenceView
|
|
let viewBounds = view.bounds
|
|
let locationInView = gestureRecognizer.location(in: view)
|
|
let newSample = ImageEditorCanvasView.locationImageUnit(forLocationInView: locationInView,
|
|
viewBounds: viewBounds,
|
|
model: self.model,
|
|
transform: self.model.currentTransform())
|
|
|
|
if let prevSample = self.currentStrokeSamples.last,
|
|
prevSample == newSample {
|
|
// Ignore duplicate samples.
|
|
return
|
|
}
|
|
self.currentStrokeSamples.append(newSample)
|
|
}
|
|
|
|
let strokeColor = currentColor
|
|
// TODO: Tune stroke width.
|
|
let unitStrokeWidth = ImageEditorStrokeItem.defaultUnitStrokeWidth()
|
|
|
|
switch gestureRecognizer.state {
|
|
case .began:
|
|
removeCurrentStroke()
|
|
|
|
tryToAppendStrokeSample()
|
|
|
|
let stroke = ImageEditorStrokeItem(color: strokeColor, unitSamples: currentStrokeSamples, unitStrokeWidth: unitStrokeWidth)
|
|
model.append(item: stroke)
|
|
currentStroke = stroke
|
|
|
|
case .changed, .ended:
|
|
tryToAppendStrokeSample()
|
|
|
|
guard let lastStroke = self.currentStroke else {
|
|
owsFailDebug("Missing last stroke.")
|
|
removeCurrentStroke()
|
|
return
|
|
}
|
|
|
|
// Model items are immutable; we _replace_ the
|
|
// stroke item rather than modify it.
|
|
let stroke = ImageEditorStrokeItem(itemId: lastStroke.itemId, color: strokeColor, unitSamples: currentStrokeSamples, unitStrokeWidth: unitStrokeWidth)
|
|
model.replace(item: stroke, suppressUndo: true)
|
|
|
|
if gestureRecognizer.state == .ended {
|
|
currentStroke = nil
|
|
currentStrokeSamples.removeAll()
|
|
} else {
|
|
currentStroke = stroke
|
|
}
|
|
default:
|
|
removeCurrentStroke()
|
|
}
|
|
}
|
|
|
|
// MARK: - Edit Text Tool
|
|
|
|
private func edit(textItem: ImageEditorTextItem) {
|
|
Logger.verbose("")
|
|
|
|
self.editorMode = .text
|
|
|
|
// TODO:
|
|
let maxTextWidthPoints = model.srcImageSizePixels.width * ImageEditorTextItem.kDefaultUnitWidth
|
|
// let maxTextWidthPoints = canvasView.imageView.width() * ImageEditorTextItem.kDefaultUnitWidth
|
|
|
|
let textEditor = ImageEditorTextViewController(delegate: self, textItem: textItem, maxTextWidthPoints: maxTextWidthPoints)
|
|
self.delegate?.imageEditor(presentFullScreenOverlay: textEditor,
|
|
withNavigation: true)
|
|
}
|
|
|
|
// MARK: - Crop Tool
|
|
|
|
private func presentCropTool() {
|
|
Logger.verbose("")
|
|
|
|
self.editorMode = .none
|
|
|
|
guard let srcImage = canvasView.loadSrcImage() else {
|
|
owsFailDebug("Couldn't load src image.")
|
|
return
|
|
}
|
|
|
|
// We want to render a preview image that "flattens" all of the brush strokes, text items,
|
|
// into the background image without applying the transform (e.g. rotating, etc.), so we
|
|
// use a default transform.
|
|
let previewTransform = ImageEditorTransform.defaultTransform(srcImageSizePixels: model.srcImageSizePixels)
|
|
guard let previewImage = ImageEditorCanvasView.renderForOutput(model: model, transform: previewTransform) else {
|
|
owsFailDebug("Couldn't generate preview image.")
|
|
return
|
|
}
|
|
|
|
let cropTool = ImageEditorCropViewController(delegate: self, model: model, srcImage: srcImage, previewImage: previewImage)
|
|
self.delegate?.imageEditor(presentFullScreenOverlay: cropTool,
|
|
withNavigation: false)
|
|
}}
|
|
|
|
// MARK: -
|
|
|
|
extension ImageEditorView: UIGestureRecognizerDelegate {
|
|
|
|
@objc public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
|
|
guard moveTextGestureRecognizer == gestureRecognizer else {
|
|
owsFailDebug("Unexpected gesture.")
|
|
return false
|
|
}
|
|
guard editorMode == .none else {
|
|
// We only filter touches when in default mode.
|
|
return true
|
|
}
|
|
|
|
let location = touch.location(in: canvasView.gestureReferenceView)
|
|
let isInTextArea = self.textLayer(forLocation: location) != nil
|
|
return isInTextArea
|
|
}
|
|
}
|
|
|
|
// MARK: -
|
|
|
|
extension ImageEditorView: ImageEditorModelObserver {
|
|
|
|
public func imageEditorModelDidChange(before: ImageEditorContents,
|
|
after: ImageEditorContents) {
|
|
updateButtons()
|
|
|
|
delegate?.imageEditorUpdateNavigationBar()
|
|
}
|
|
|
|
public func imageEditorModelDidChange(changedItemIds: [String]) {
|
|
updateButtons()
|
|
|
|
delegate?.imageEditorUpdateNavigationBar()
|
|
}
|
|
}
|
|
|
|
// MARK: -
|
|
|
|
extension ImageEditorView: ImageEditorTextViewControllerDelegate {
|
|
|
|
public func textEditDidComplete(textItem: ImageEditorTextItem, text: String?) {
|
|
AssertIsOnMainThread()
|
|
|
|
self.editorMode = .none
|
|
|
|
guard let text = text?.ows_stripped(),
|
|
text.count > 0 else {
|
|
if model.has(itemForId: textItem.itemId) {
|
|
model.remove(item: textItem)
|
|
}
|
|
return
|
|
}
|
|
|
|
// Model items are immutable; we _replace_ the item rather than modify it.
|
|
let newItem = textItem.copy(withText: text)
|
|
if model.has(itemForId: textItem.itemId) {
|
|
model.replace(item: newItem, suppressUndo: false)
|
|
} else {
|
|
model.append(item: newItem)
|
|
}
|
|
}
|
|
|
|
public func textEditDidCancel() {
|
|
self.editorMode = .none
|
|
}
|
|
}
|
|
|
|
// MARK: -
|
|
|
|
extension ImageEditorView: ImageEditorCropViewControllerDelegate {
|
|
public func cropDidComplete(transform: ImageEditorTransform) {
|
|
// TODO: Ignore no-change updates.
|
|
model.replace(transform: transform)
|
|
}
|
|
|
|
public func cropDidCancel() {
|
|
// TODO:
|
|
}
|
|
}
|
|
|
|
// MARK: -
|
|
|
|
extension ImageEditorView: ImageEditorPaletteViewDelegate {
|
|
public func selectedColorDidChange() {
|
|
// TODO:
|
|
}
|
|
}
|
|
|
|
// MARK: -
|
|
|
|
extension ImageEditorView: ImageEditorBrushViewControllerDelegate {
|
|
public func brushDidComplete() {
|
|
self.editorMode = .none
|
|
}
|
|
}
|