Sketch out image editor undo/redo.

pull/1/head
Matthew Chen 6 years ago
parent f95526bff7
commit 57232683fc

@ -35,5 +35,78 @@ class ImageEditorTest: SignalBaseTest {
XCTAssertEqual(1, contents.itemIds.count)
XCTAssertEqual(0, contentsCopy.itemMap.count)
XCTAssertEqual(0, contentsCopy.itemIds.count)
let modifiedItem = ImageEditorItem(itemId: item.itemId)
contents.replace(item: modifiedItem)
XCTAssertEqual(1, contents.itemMap.count)
XCTAssertEqual(1, contents.itemIds.count)
XCTAssertEqual(0, contentsCopy.itemMap.count)
XCTAssertEqual(0, contentsCopy.itemIds.count)
}
private func writeDummyImage() -> String {
let image = UIImage.init(color: .red, size: CGSize(width: 1, height: 1))
guard let data = UIImagePNGRepresentation(image) else {
owsFail("Couldn't export dummy image.")
}
let filePath = OWSFileSystem.temporaryFilePath(withFileExtension: "png")
do {
try data.write(to: URL(fileURLWithPath: filePath))
} catch {
owsFail("Couldn't write dummy image.")
}
return filePath
}
func testImageEditor() {
let imagePath = writeDummyImage()
let imageEditor: ImageEditorModel
do {
imageEditor = try ImageEditorModel(srcImagePath: imagePath)
} catch {
owsFail("Couldn't create ImageEditorModel.")
}
XCTAssertFalse(imageEditor.canUndo())
XCTAssertFalse(imageEditor.canRedo())
XCTAssertEqual(0, imageEditor.itemCount())
let itemA = ImageEditorItem()
imageEditor.append(item: itemA)
XCTAssertTrue(imageEditor.canUndo())
XCTAssertFalse(imageEditor.canRedo())
XCTAssertEqual(1, imageEditor.itemCount())
imageEditor.undo()
XCTAssertFalse(imageEditor.canUndo())
XCTAssertTrue(imageEditor.canRedo())
XCTAssertEqual(0, imageEditor.itemCount())
imageEditor.redo()
XCTAssertTrue(imageEditor.canUndo())
XCTAssertFalse(imageEditor.canRedo())
XCTAssertEqual(1, imageEditor.itemCount())
imageEditor.undo()
XCTAssertFalse(imageEditor.canUndo())
XCTAssertTrue(imageEditor.canRedo())
XCTAssertEqual(0, imageEditor.itemCount())
let itemB = ImageEditorItem()
imageEditor.append(item: itemB)
XCTAssertTrue(imageEditor.canUndo())
XCTAssertFalse(imageEditor.canRedo())
XCTAssertEqual(1, imageEditor.itemCount())
let itemC = ImageEditorItem()
imageEditor.append(item: itemC)
XCTAssertTrue(imageEditor.canUndo())
XCTAssertFalse(imageEditor.canRedo())
XCTAssertEqual(2, imageEditor.itemCount())
imageEditor.undo()
XCTAssertTrue(imageEditor.canUndo())
XCTAssertTrue(imageEditor.canRedo())
XCTAssertEqual(1, imageEditor.itemCount())
}
}

@ -9,6 +9,8 @@ import UIKit
case invalidInput
}
// MARK: -
@objc
public class ImageEditorItem: NSObject {
@objc
@ -20,9 +22,17 @@ public class ImageEditorItem: NSObject {
super.init()
}
@objc
public init(itemId: String) {
self.itemId = itemId
super.init()
}
}
@objc
// MARK: -
public class ImageEditorContents: NSObject {
var itemMap = [String: ImageEditorItem]()
@ -30,7 +40,6 @@ public class ImageEditorContents: NSObject {
@objc
public override init() {
}
@objc
@ -58,6 +67,26 @@ public class ImageEditorContents: NSObject {
} else {
itemIds.append(item.itemId)
}
if itemIds.count != itemMap.count {
owsFailDebug("Invalid contents.")
}
}
@objc
public func replace(item: ImageEditorItem) {
if itemMap[item.itemId] == nil {
owsFail("Missing item in item map: \(item.itemId)")
}
itemMap[item.itemId] = item
if !itemIds.contains(item.itemId) {
owsFail("Missing item in item list: \(item.itemId)")
}
if itemIds.count != itemMap.count {
owsFailDebug("Invalid contents.")
}
}
@objc
@ -78,9 +107,35 @@ public class ImageEditorContents: NSObject {
} else {
itemIds = itemIds.filter { $0 != itemId }
}
if itemIds.count != itemMap.count {
owsFailDebug("Invalid contents.")
}
}
@objc
public func itemCount() -> Int {
if itemIds.count != itemMap.count {
owsFailDebug("Invalid contents.")
}
return itemIds.count
}
}
// MARK: -
// Used to represent undo/redo operations.
private class ImageEditorOperation: NSObject {
let contents: ImageEditorContents
required init(contents: ImageEditorContents) {
self.contents = contents
}
}
// MARK: -
@objc
public class ImageEditorModel: NSObject {
@objc
@ -91,6 +146,9 @@ public class ImageEditorModel: NSObject {
private var contents = ImageEditorContents()
private var undoStack = [ImageEditorOperation]()
private var redoStack = [ImageEditorOperation]()
@objc
public required init(srcImagePath: String) throws {
self.srcImagePath = srcImagePath
@ -115,4 +173,76 @@ public class ImageEditorModel: NSObject {
super.init()
}
@objc
public func itemCount() -> Int {
return contents.itemCount()
}
@objc
public func canUndo() -> Bool {
return !undoStack.isEmpty
}
@objc
public func canRedo() -> Bool {
return !redoStack.isEmpty
}
@objc
public func undo() {
guard let undoOperation = undoStack.popLast() else {
owsFailDebug("Cannot undo.")
return
}
let redoOperation = ImageEditorOperation(contents: contents)
redoStack.append(redoOperation)
self.contents = undoOperation.contents
}
@objc
public func redo() {
guard let redoOperation = redoStack.popLast() else {
owsFailDebug("Cannot redo.")
return
}
let undoOperation = ImageEditorOperation(contents: contents)
undoStack.append(undoOperation)
self.contents = redoOperation.contents
}
@objc
public func append(item: ImageEditorItem) {
performAction { (newContents) in
newContents.append(item: item)
}
}
@objc
public func replace(item: ImageEditorItem) {
performAction { (newContents) in
newContents.replace(item: item)
}
}
@objc
public func remove(item: ImageEditorItem) {
performAction { (newContents) in
newContents.remove(item: item)
}
}
private func performAction(action: (ImageEditorContents) -> Void) {
let undoOperation = ImageEditorOperation(contents: contents)
undoStack.append(undoOperation)
redoStack.removeAll()
let newContents = contents.clone()
action(newContents)
contents = newContents
}
}

Loading…
Cancel
Save