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.
		
		
		
		
		
			
		
			
				
	
	
		
			562 lines
		
	
	
		
			21 KiB
		
	
	
	
		
			Swift
		
	
			
		
		
	
	
			562 lines
		
	
	
		
			21 KiB
		
	
	
	
		
			Swift
		
	
| //
 | |
| //  Copyright (c) 2019 Open Whisper Systems. All rights reserved.
 | |
| //
 | |
| 
 | |
| import UIKit
 | |
| 
 | |
| @objc
 | |
| public protocol ImageEditorViewDelegate: class {
 | |
|     func imageEditor(presentFullScreenView viewController: UIViewController,
 | |
|                      isTransparent: Bool)
 | |
|     func imageEditorUpdateNavigationBar()
 | |
|     func imageEditorUpdateControls()
 | |
| }
 | |
| 
 | |
| // 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
 | |
| 
 | |
|     // TODO: We could hang this on the model or make this static
 | |
|     //       if we wanted more color continuity.
 | |
|     private var currentColor = ImageEditorColor.defaultColor()
 | |
| 
 | |
|     @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 tapGestureRecognizer: UITapGestureRecognizer?
 | |
|     private var pinchGestureRecognizer: ImageEditorPinchGestureRecognizer?
 | |
| 
 | |
|     @objc
 | |
|     public func configureSubviews() -> Bool {
 | |
|         canvasView.configureSubviews()
 | |
|         self.addSubview(canvasView)
 | |
|         canvasView.autoPinEdgesToSuperviewEdges()
 | |
| 
 | |
|         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 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)
 | |
| 
 | |
|         return true
 | |
|     }
 | |
| 
 | |
|     // MARK: - Navigation Bar
 | |
| 
 | |
|     private func updateNavigationBar() {
 | |
|         delegate?.imageEditorUpdateNavigationBar()
 | |
|     }
 | |
| 
 | |
|     public func navigationBarItems() -> [UIView] {
 | |
|         guard !shouldHideControls else {
 | |
|             return []
 | |
|         }
 | |
| 
 | |
|         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:)))
 | |
| 
 | |
|         var buttons: [UIView]
 | |
|         if model.canUndo() {
 | |
|             buttons = [undoButton, newTextButton, brushButton, cropButton]
 | |
|         } else {
 | |
|             buttons = [newTextButton, brushButton, cropButton]
 | |
|         }
 | |
| 
 | |
|         return buttons
 | |
|     }
 | |
| 
 | |
|     private func updateControls() {
 | |
|         delegate?.imageEditorUpdateControls()
 | |
|     }
 | |
| 
 | |
|     public var shouldHideControls: Bool {
 | |
|         // Hide controls during "text item move".
 | |
|         return movingTextItem != nil
 | |
|     }
 | |
| 
 | |
|     // 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("")
 | |
| 
 | |
|         let brushView = ImageEditorBrushViewController(delegate: self, model: model, currentColor: currentColor)
 | |
|         self.delegate?.imageEditor(presentFullScreenView: brushView,
 | |
|                                    isTransparent: false)
 | |
|     }
 | |
| 
 | |
|     @objc func didTapCrop(sender: UIButton) {
 | |
|         Logger.verbose("")
 | |
| 
 | |
|         presentCropTool()
 | |
|     }
 | |
| 
 | |
|     @objc func didTapNewText(sender: UIButton) {
 | |
|         Logger.verbose("")
 | |
| 
 | |
|         createNewTextItem()
 | |
|     }
 | |
| 
 | |
|     private func createNewTextItem() {
 | |
|         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
 | |
| 
 | |
|         // New items should be aligned "upright", so they should have the _opposite_
 | |
|         // of the current transform rotation.
 | |
|         let rotationRadians = -model.currentTransform().rotationRadians
 | |
|         // Similarly, the size of the text item shuo
 | |
|         let scaling = 1 / model.currentTransform().scaling
 | |
| 
 | |
|         let textItem = ImageEditorTextItem.empty(withColor: currentColor,
 | |
|                                                  unitWidth: textWidthUnit,
 | |
|                                                  fontReferenceImageWidth: imageFrame.size.width,
 | |
|                                                  scaling: scaling,
 | |
|                                                  rotationRadians: rotationRadians)
 | |
| 
 | |
|         edit(textItem: textItem, isNewItem: true)
 | |
|     }
 | |
| 
 | |
|     @objc func didTapDone(sender: UIButton) {
 | |
|         Logger.verbose("")
 | |
|     }
 | |
| 
 | |
|     // 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 {
 | |
|             // If there is no text item under the "tap", start a new one.
 | |
|             createNewTextItem()
 | |
|             return
 | |
|         }
 | |
| 
 | |
|         guard let textItem = model.item(forId: textLayer.itemId) as? ImageEditorTextItem else {
 | |
|             owsFailDebug("Missing or invalid text item.")
 | |
|             return
 | |
|         }
 | |
| 
 | |
|         edit(textItem: textItem, isNewItem: false)
 | |
|     }
 | |
| 
 | |
|     // 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(unitCenter: unitCenter).copy(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? {
 | |
|         didSet {
 | |
|             updateNavigationBar()
 | |
|             updateControls()
 | |
|         }
 | |
|     }
 | |
|     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.locationFirst 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.locationFirst 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(unitCenter: 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.color
 | |
|         // 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, isNewItem: Bool) {
 | |
|         Logger.verbose("")
 | |
| 
 | |
|         // TODO:
 | |
|         let maxTextWidthPoints = model.srcImageSizePixels.width * ImageEditorTextItem.kDefaultUnitWidth
 | |
|         //        let maxTextWidthPoints = canvasView.imageView.width() * ImageEditorTextItem.kDefaultUnitWidth
 | |
| 
 | |
|         let textEditor = ImageEditorTextViewController(delegate: self,
 | |
|                                                        model: model,
 | |
|                                                        textItem: textItem,
 | |
|                                                        isNewItem: isNewItem,
 | |
|                                                        maxTextWidthPoints: maxTextWidthPoints)
 | |
|         self.delegate?.imageEditor(presentFullScreenView: textEditor,
 | |
|                                    isTransparent: false)
 | |
|     }
 | |
| 
 | |
|     // MARK: - Crop Tool
 | |
| 
 | |
|     private func presentCropTool() {
 | |
|         Logger.verbose("")
 | |
| 
 | |
|         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(presentFullScreenView: cropTool,
 | |
|                                    isTransparent: false)
 | |
|     }
 | |
| }
 | |
| 
 | |
| // MARK: -
 | |
| 
 | |
| extension ImageEditorView: UIGestureRecognizerDelegate {
 | |
| 
 | |
|     @objc public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
 | |
|         guard moveTextGestureRecognizer == gestureRecognizer else {
 | |
|             owsFailDebug("Unexpected gesture.")
 | |
|             return false
 | |
|         }
 | |
| 
 | |
|         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) {
 | |
|         updateNavigationBar()
 | |
|     }
 | |
| 
 | |
|     public func imageEditorModelDidChange(changedItemIds: [String]) {
 | |
|         updateNavigationBar()
 | |
|     }
 | |
| }
 | |
| 
 | |
| // MARK: -
 | |
| 
 | |
| extension ImageEditorView: ImageEditorTextViewControllerDelegate {
 | |
| 
 | |
|     public func textEditDidComplete(textItem: ImageEditorTextItem) {
 | |
|         AssertIsOnMainThread()
 | |
| 
 | |
|         // Model items are immutable; we _replace_ the item rather than modify it.
 | |
|         if model.has(itemForId: textItem.itemId) {
 | |
|             model.replace(item: textItem, suppressUndo: false)
 | |
|         } else {
 | |
|             model.append(item: textItem)
 | |
|         }
 | |
| 
 | |
|         self.currentColor = textItem.color
 | |
|     }
 | |
| 
 | |
|     public func textEditDidDelete(textItem: ImageEditorTextItem) {
 | |
|         AssertIsOnMainThread()
 | |
| 
 | |
|         if model.has(itemForId: textItem.itemId) {
 | |
|             model.remove(item: textItem)
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     public func textEditDidCancel() {
 | |
|     }
 | |
| }
 | |
| 
 | |
| // 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: ImageEditorBrushViewControllerDelegate {
 | |
|     public func brushDidComplete(currentColor: ImageEditorColor) {
 | |
|         self.currentColor = currentColor
 | |
|     }
 | |
| }
 |