@@ -51,6 +51,21 @@ data class RecognitionFeedback(
5151 val words : List <Word > = emptyList()
5252)
5353
54+ enum class EditorHistoryAction {
55+ ADD ,
56+ REMOVE
57+ }
58+
59+ data class EditorHistoryItem (
60+ val editorHistoryAction : EditorHistoryAction ,
61+ val strokes : List <InkView .Brush >
62+ )
63+
64+ data class EditorHistoryState (
65+ val canUndo : Boolean = false ,
66+ val canRedo : Boolean = false
67+ )
68+
5469/* *
5570 * ViewModel responsible for maintaining the state of the OffScreenInteractivity demo application.
5671 *
@@ -72,6 +87,12 @@ class InkViewModel(
7287 val strokes: LiveData <List <InkView .Brush >>
7388 get() = _strokes
7489
90+ private val undoRedoStack = mutableListOf<List <EditorHistoryItem >>()
91+ private var undoRedoIndex = 0
92+ private val _editorHistoryState : MutableLiveData <EditorHistoryState > = MutableLiveData (EditorHistoryState ())
93+ val editorHistoryState: LiveData <EditorHistoryState >
94+ get() = _editorHistoryState
95+
7596 // The iinkModel and recognitionFeedback are straightforward methods for debugging and showcasing, providing visual representation for easier understanding.
7697 // While this is not the method your app should use to display recognition, it can provide a starting point or guide on how to accomplish this.
7798 private val _recognitionFeedback : MutableLiveData <RecognitionFeedback > = MutableLiveData (RecognitionFeedback ())
@@ -164,10 +185,24 @@ class InkViewModel(
164185 // or when tasks have side-effects that must be isolated to a single thread.
165186 withContext(workDispatcher) {
166187 // ItemIds may refer to partial strokes, retrieve the corresponding full strokes ids
167- val fullStrokeIds = itemIds.map(itemIdHelper::getFullItemId) + gestureStrokeId
188+ val fullStrokeIds = itemIds.map(itemIdHelper::getFullItemId)
168189
169- // Erase the gesture stroke (gestureStrokeId) and the erased strokes (fullItemIds) in your application
190+ val strokesToRemove = mutableListOf< InkView . Brush >()
170191 fullStrokeIds.forEach { strokeId ->
192+ val appStrokeId = strokeIdsMapping[strokeId]
193+ remainingStrokes.firstOrNull { it.id == appStrokeId }?.let { strokeBrush ->
194+ strokesToRemove.add(strokeBrush)
195+ }
196+ }
197+
198+ removeLastFromUndoRedoStack()
199+ val newHistory = addToUndoRedoStack(EditorHistoryAction .REMOVE , strokesToRemove)
200+ withContext(uiDispatcher) {
201+ _editorHistoryState .value = newHistory
202+ }
203+
204+ // Erase the gesture stroke (gestureStrokeId) and the erased strokes (fullItemIds) in your application
205+ (fullStrokeIds + gestureStrokeId).forEach { strokeId ->
171206 val appStrokeId = strokeIdsMapping[strokeId]
172207 strokeIdsMapping.remove(strokeId)
173208 val strokeBrush = remainingStrokes.firstOrNull { it.id == appStrokeId }
@@ -222,6 +257,19 @@ class InkViewModel(
222257 emptyArray()
223258 }
224259
260+ val strokesToRemove = mutableListOf<InkView .Brush >()
261+ fullItemIds.forEach { strokeId ->
262+ val appStrokeId = strokeIdsMapping[strokeId]
263+ remainingStrokes.firstOrNull { it.id == appStrokeId }?.let {
264+ strokesToRemove.add(it)
265+ }
266+ }
267+ removeLastFromUndoRedoStack()
268+ val newHistory = addToUndoRedoStack(EditorHistoryAction .REMOVE , strokesToRemove)
269+ withContext(uiDispatcher) {
270+ _editorHistoryState .value = newHistory
271+ }
272+
225273 // Erase the erased strokes and gesture strokes in your application
226274 (fullItemIds + gestureStrokeId).forEach { strokeId ->
227275 val appStrokeId = strokeIdsMapping[strokeId]
@@ -325,16 +373,145 @@ class InkViewModel(
325373 }
326374 }
327375
376+ private fun addToUndoRedoStack (action : EditorHistoryAction , strokes : List <InkView .Brush >): EditorHistoryState {
377+ return addToUndoRedoStack(listOf (EditorHistoryItem (action, strokes)))
378+ }
379+
380+ private fun addToUndoRedoStack (editorHistoryItems : List <EditorHistoryItem >): EditorHistoryState {
381+ synchronized(undoRedoStack) {
382+ if (undoRedoStack.isNotEmpty()) {
383+ for (i in (undoRedoStack.size - 1 ).downTo(undoRedoIndex)) {
384+ undoRedoStack.removeAt(i)
385+ }
386+ }
387+ undoRedoStack.add(undoRedoIndex++ , editorHistoryItems)
388+
389+ return EditorHistoryState (
390+ canUndo = undoRedoIndex > 0 ,
391+ canRedo = undoRedoIndex < undoRedoStack.size
392+ )
393+ }
394+ }
395+
396+ private fun addStrokesForUndoRedo (initialStrokes : List <InkView .Brush >, strokesToAdd : List <InkView .Brush >): List <InkView .Brush > {
397+ val strokes = initialStrokes.toMutableList()
398+ strokes.addAll(strokesToAdd)
399+
400+ val pointerEvents = strokesToAdd.flatMap { brush ->
401+ brush.stroke.toPointerEvents().map { pointerEvent ->
402+ pointerEvent.convertPointerEvent(converter)
403+ }
404+ }.toTypedArray()
405+
406+ if (pointerEvents.isNotEmpty()) {
407+ val addedStrokes = offscreenEditor?.addStrokes(pointerEvents, false )
408+
409+ if (addedStrokes != null ) {
410+ strokesToAdd.forEachIndexed { index, brush ->
411+ if (index in addedStrokes.indices) {
412+ strokeIdsMapping[addedStrokes[index]] = brush.id
413+ }
414+ }
415+ }
416+ }
417+
418+ return strokes
419+ }
420+
421+ private fun removeStrokesForUndoRedo (initialStrokes : List <InkView .Brush >, strokesToRemove : List <InkView .Brush >): List <InkView .Brush > {
422+ val updatedStrokes = initialStrokes.filter {
423+ it.id !in strokesToRemove.map { strokeToRemove -> strokeToRemove.id }
424+ }
425+
426+ val strokeToUndoMapping = strokeIdsMapping.filter { (_, appStrokeId) ->
427+ appStrokeId in strokesToRemove.map { strokeToRemove -> strokeToRemove.id }
428+ }
429+ offscreenEditor?.erase(strokeToUndoMapping.keys.toTypedArray())
430+ strokeToUndoMapping.forEach {
431+ strokeIdsMapping.remove(it.key)
432+ }
433+
434+ return updatedStrokes
435+ }
436+
437+ private fun clearUndoRedoStack (): EditorHistoryState {
438+ synchronized(undoRedoStack) {
439+ undoRedoStack.clear()
440+ undoRedoIndex = 0
441+ return EditorHistoryState ()
442+ }
443+ }
444+
445+ private fun removeLastFromUndoRedoStack (): EditorHistoryState {
446+ synchronized(undoRedoStack) {
447+ undoRedoStack.removeAt(-- undoRedoIndex)
448+
449+ return EditorHistoryState (
450+ canUndo = undoRedoIndex > 0 ,
451+ canRedo = undoRedoIndex < undoRedoStack.size
452+ )
453+ }
454+ }
455+
456+ fun undo () {
457+ viewModelScope.launch(uiDispatcher) {
458+ if (undoRedoIndex == 0 || undoRedoStack.isEmpty()) return @launch
459+
460+ val undoItems = synchronized(undoRedoStack){
461+ undoRedoStack[-- undoRedoIndex]
462+ }
463+ undoItems.forEach { item ->
464+ val initialStrokes = strokes.value ? : emptyList()
465+ _strokes .value = when (item.editorHistoryAction) {
466+ EditorHistoryAction .ADD -> removeStrokesForUndoRedo(initialStrokes, item.strokes)
467+ EditorHistoryAction .REMOVE -> addStrokesForUndoRedo(initialStrokes, item.strokes)
468+ }
469+ }
470+
471+ _editorHistoryState .value = EditorHistoryState (
472+ canUndo = undoRedoIndex > 0 ,
473+ canRedo = undoRedoIndex < undoRedoStack.size
474+ )
475+ }
476+ }
477+
478+ fun redo () {
479+ viewModelScope.launch(uiDispatcher) {
480+ if (undoRedoIndex == undoRedoStack.size || undoRedoStack.isEmpty()) return @launch
481+
482+ val redoItems = synchronized(undoRedoStack) {
483+ undoRedoStack[undoRedoIndex++ ]
484+ }
485+ redoItems.forEach { item ->
486+ val initialStrokes = strokes.value ? : emptyList()
487+ _strokes .value = when (item.editorHistoryAction) {
488+ EditorHistoryAction .ADD -> addStrokesForUndoRedo(initialStrokes, item.strokes)
489+ EditorHistoryAction .REMOVE -> removeStrokesForUndoRedo(initialStrokes, item.strokes)
490+ }
491+ }
492+
493+ _editorHistoryState .value = EditorHistoryState (
494+ canUndo = undoRedoIndex > 0 ,
495+ canRedo = undoRedoIndex < undoRedoStack.size
496+ )
497+ }
498+ }
499+
328500 fun clearInk () {
329501 viewModelScope.launch(uiDispatcher) {
330502 offscreenEditor?.clear()
331503 strokeIdsMapping.clear()
504+
505+ _editorHistoryState .value = addToUndoRedoStack(EditorHistoryAction .REMOVE , _strokes .value ? : emptyList())
506+
332507 _strokes .value = emptyList()
333508 }
334509 }
335510
336511 fun loadInk () {
337512 viewModelScope.launch(uiDispatcher) {
513+ _editorHistoryState .value = clearUndoRedoStack()
514+
338515 val jsonString = withContext(ioDispatcher) {
339516 repository.readInkFromFile()
340517 }
@@ -402,6 +579,8 @@ class InkViewModel(
402579 offscreenEditor?.addStrokes(pointerEvents, true )?.firstNotNullOf { strokeId ->
403580 strokeIdsMapping[strokeId] = brush.id
404581 }
582+
583+ _editorHistoryState .value = addToUndoRedoStack(EditorHistoryAction .ADD , listOf (brush))
405584 }
406585 }
407586
0 commit comments