diff --git a/SignalMessaging/Views/ImageEditor/ImageEditorModel.swift b/SignalMessaging/Views/ImageEditor/ImageEditorModel.swift index 9aa1d5896..81fdb756e 100644 --- a/SignalMessaging/Views/ImageEditor/ImageEditorModel.swift +++ b/SignalMessaging/Views/ImageEditor/ImageEditorModel.swift @@ -320,6 +320,13 @@ public class ImageEditorModel: NSObject { private var undoStack = [ImageEditorOperation]() private var redoStack = [ImageEditorOperation]() + // In some cases, we want to suppress changes to undo state. + // e.g. drawing a stroke will modify the model many times (once + // for each touch event/stroke sample), but we only want that + // to add a single undo operation. + private var isUndoSuppressed = false + private var suppressedUndoContents: ImageEditorContents? + // We don't want to allow editing of images if: // // * They are invalid. @@ -365,6 +372,28 @@ public class ImageEditorModel: NSObject { return !undoStack.isEmpty } + @objc + public func setIsUndoSuppressed(isUndoSuppressed: Bool) { + if isUndoSuppressed { + if self.isUndoSuppressed { + owsFailDebug("Undo already suppressed.") + } + if suppressedUndoContents != nil { + owsFailDebug("Unexpected suppressed undo contents.") + } + suppressedUndoContents = contents.clone() + } else { + if self.isUndoSuppressed { + if suppressedUndoContents == nil { + owsFailDebug("Missing suppressed undo contents.") + } + } + suppressedUndoContents = nil + } + + self.isUndoSuppressed = isUndoSuppressed + } + @objc public func canRedo() -> Bool { return !redoStack.isEmpty @@ -402,29 +431,47 @@ public class ImageEditorModel: NSObject { @objc public func append(item: ImageEditorItem) { - performAction { (newContents) in + performAction({ (newContents) in newContents.append(item: item) - } + }) } @objc - public func replace(item: ImageEditorItem) { - performAction { (newContents) in + public func replace(item: ImageEditorItem, + shouldRemoveUndoSuppression: Bool = false) { + performAction({ (newContents) in newContents.replace(item: item) - } + }, shouldRemoveUndoSuppression: shouldRemoveUndoSuppression) } @objc public func remove(item: ImageEditorItem) { - performAction { (newContents) in + performAction({ (newContents) in newContents.remove(item: item) - } + }) } - private func performAction(action: (ImageEditorContents) -> Void) { - let undoOperation = ImageEditorOperation(contents: contents) - undoStack.append(undoOperation) - redoStack.removeAll() + private func performAction(_ action: (ImageEditorContents) -> Void, + shouldRemoveUndoSuppression: Bool = false) { + if shouldRemoveUndoSuppression { + if !isUndoSuppressed { + owsFailDebug("Can't remove undo suppression, not suppressed.") + } + if let suppressedUndoContents = self.suppressedUndoContents { + let undoOperation = ImageEditorOperation(contents: suppressedUndoContents) + undoStack.append(undoOperation) + redoStack.removeAll() + } else { + owsFailDebug("Missing suppressed undo contents.") + } + self.isUndoSuppressed = false + + setIsUndoSuppressed(isUndoSuppressed: false) + } else if !isUndoSuppressed { + let undoOperation = ImageEditorOperation(contents: contents) + undoStack.append(undoOperation) + redoStack.removeAll() + } let newContents = contents.clone() action(newContents) diff --git a/SignalMessaging/Views/ImageEditor/ImageEditorView.swift b/SignalMessaging/Views/ImageEditor/ImageEditorView.swift index 9eeec7745..d460c8834 100644 --- a/SignalMessaging/Views/ImageEditor/ImageEditorView.swift +++ b/SignalMessaging/Views/ImageEditor/ImageEditorView.swift @@ -107,6 +107,7 @@ public class ImageEditorView: UIView, ImageEditorModelDelegate { if let stroke = self.currentStroke { self.model.remove(item: stroke) } + self.model.setIsUndoSuppressed(isUndoSuppressed: false) self.currentStroke = nil self.currentStrokeSamples.removeAll() } @@ -131,9 +132,11 @@ public class ImageEditorView: UIView, ImageEditorModelDelegate { currentStrokeSamples.append(unitSampleForGestureLocation()) - let stroke = ImageEditorStrokeItem(color: strokeColor, unitSamples: self.currentStrokeSamples, unitStrokeWidth: unitStrokeWidth) - self.model.append(item: stroke) - self.currentStroke = stroke + model.setIsUndoSuppressed(isUndoSuppressed: true) + + let stroke = ImageEditorStrokeItem(color: strokeColor, unitSamples: currentStrokeSamples, unitStrokeWidth: unitStrokeWidth) + model.append(item: stroke) + currentStroke = stroke case .changed, .ended: currentStrokeSamples.append(unitSampleForGestureLocation()) @@ -146,13 +149,15 @@ public class ImageEditorView: UIView, ImageEditorModelDelegate { // Model items are immutable; we _replace_ the // stroke item rather than modify it. - let stroke = ImageEditorStrokeItem(itemId: lastStroke.itemId, color: strokeColor, unitSamples: self.currentStrokeSamples, unitStrokeWidth: unitStrokeWidth) - self.model.replace(item: stroke) - self.currentStroke = stroke + let stroke = ImageEditorStrokeItem(itemId: lastStroke.itemId, color: strokeColor, unitSamples: currentStrokeSamples, unitStrokeWidth: unitStrokeWidth) if gestureRecognizer.state == .ended { - self.currentStroke = nil - self.currentStrokeSamples.removeAll() + model.replace(item: stroke, shouldRemoveUndoSuppression: true) + currentStroke = nil + currentStrokeSamples.removeAll() + } else { + model.replace(item: stroke) + currentStroke = stroke } default: removeCurrentStroke()