Image Editor - Undo button visibility.

This commit is contained in:
Alan Evans 2019-05-17 16:15:27 -03:00 committed by GitHub
parent b5d37702f9
commit 6777b3e0e6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 170 additions and 42 deletions

View File

@ -60,6 +60,9 @@ public final class ImageEditorView extends FrameLayout {
@Nullable @Nullable
private DrawingChangedListener drawingChangedListener; private DrawingChangedListener drawingChangedListener;
@Nullable
private UndoRedoStackListener undoRedoStackListener;
private final Matrix viewMatrix = new Matrix(); private final Matrix viewMatrix = new Matrix();
private final RectF viewPort = Bounds.newFullBounds(); private final RectF viewPort = Bounds.newFullBounds();
private final RectF visibleViewPort = Bounds.newFullBounds(); private final RectF visibleViewPort = Bounds.newFullBounds();
@ -200,9 +203,11 @@ public final class ImageEditorView extends FrameLayout {
if (this.model != model) { if (this.model != model) {
if (this.model != null) { if (this.model != null) {
this.model.setInvalidate(null); this.model.setInvalidate(null);
this.model.setUndoRedoStackListener(null);
} }
this.model = model; this.model = model;
this.model.setInvalidate(this::invalidate); this.model.setInvalidate(this::invalidate);
this.model.setUndoRedoStackListener(this::onUndoRedoAvailabilityChanged);
this.model.setVisibleViewPort(visibleViewPort); this.model.setVisibleViewPort(visibleViewPort);
invalidate(); invalidate();
} }
@ -386,6 +391,10 @@ public final class ImageEditorView extends FrameLayout {
this.drawingChangedListener = drawingChangedListener; this.drawingChangedListener = drawingChangedListener;
} }
public void setUndoRedoStackListener(@Nullable UndoRedoStackListener undoRedoStackListener) {
this.undoRedoStackListener = undoRedoStackListener;
}
public void setTapListener(TapListener tapListener) { public void setTapListener(TapListener tapListener) {
this.tapListener = tapListener; this.tapListener = tapListener;
} }
@ -398,6 +407,12 @@ public final class ImageEditorView extends FrameLayout {
} }
} }
private void onUndoRedoAvailabilityChanged(boolean undoAvailable, boolean redoAvailable) {
if (undoRedoStackListener != null) {
undoRedoStackListener.onAvailabilityChanged(undoAvailable, redoAvailable);
}
}
private final class DoubleTapGestureListener extends GestureDetector.SimpleOnGestureListener { private final class DoubleTapGestureListener extends GestureDetector.SimpleOnGestureListener {
@Override @Override

View File

@ -0,0 +1,6 @@
package org.thoughtcrime.securesms.imageeditor;
public interface UndoRedoStackListener {
void onAvailabilityChanged(boolean undoAvailable, boolean redoAvailable);
}

View File

@ -44,7 +44,7 @@ public final class EditorElement implements Parcelable {
private final Matrix tempMatrix = new Matrix(); private final Matrix tempMatrix = new Matrix();
private final List<EditorElement> children = new LinkedList<>(); private final List<EditorElement> children = new LinkedList<>();
private final List<EditorElement> deletedChildren = new LinkedList<>(); private final List<EditorElement> deletedChildren = new LinkedList<>();
@NonNull @NonNull

View File

@ -17,6 +17,7 @@ import org.thoughtcrime.securesms.imageeditor.Bounds;
import org.thoughtcrime.securesms.imageeditor.ColorableRenderer; import org.thoughtcrime.securesms.imageeditor.ColorableRenderer;
import org.thoughtcrime.securesms.imageeditor.Renderer; import org.thoughtcrime.securesms.imageeditor.Renderer;
import org.thoughtcrime.securesms.imageeditor.RendererContext; import org.thoughtcrime.securesms.imageeditor.RendererContext;
import org.thoughtcrime.securesms.imageeditor.UndoRedoStackListener;
import java.util.HashMap; import java.util.HashMap;
import java.util.LinkedHashSet; import java.util.LinkedHashSet;
@ -42,6 +43,8 @@ public final class EditorModel implements Parcelable, RendererContext.Ready {
@NonNull @NonNull
private Runnable invalidate = NULL_RUNNABLE; private Runnable invalidate = NULL_RUNNABLE;
private UndoRedoStackListener undoRedoStackListener;
private final UndoRedoStacks undoRedoStacks; private final UndoRedoStacks undoRedoStacks;
private final UndoRedoStacks cropUndoRedoStacks; private final UndoRedoStacks cropUndoRedoStacks;
private final InBoundsMemory inBoundsMemory = new InBoundsMemory(); private final InBoundsMemory inBoundsMemory = new InBoundsMemory();
@ -70,6 +73,10 @@ public final class EditorModel implements Parcelable, RendererContext.Ready {
this.invalidate = invalidate != null ? invalidate : NULL_RUNNABLE; this.invalidate = invalidate != null ? invalidate : NULL_RUNNABLE;
} }
public void setUndoRedoStackListener(UndoRedoStackListener undoRedoStackListener) {
this.undoRedoStackListener = undoRedoStackListener;
}
/** /**
* Renders tree with the following matrix: * Renders tree with the following matrix:
* <p> * <p>
@ -117,9 +124,7 @@ public final class EditorModel implements Parcelable, RendererContext.Ready {
UndoRedoStacks stacks = cropping ? cropUndoRedoStacks : undoRedoStacks; UndoRedoStacks stacks = cropping ? cropUndoRedoStacks : undoRedoStacks;
if (stacks.getUndoStack().tryPush(editorElementHierarchy.getRoot())) { stacks.pushState(editorElementHierarchy.getRoot());
stacks.getRedoStack().clear();
}
} }
public void undo() { public void undo() {
@ -127,6 +132,8 @@ public final class EditorModel implements Parcelable, RendererContext.Ready {
UndoRedoStacks stacks = cropping ? cropUndoRedoStacks : undoRedoStacks; UndoRedoStacks stacks = cropping ? cropUndoRedoStacks : undoRedoStacks;
undoRedo(stacks.getUndoStack(), stacks.getRedoStack(), cropping); undoRedo(stacks.getUndoStack(), stacks.getRedoStack(), cropping);
updateUndoRedoAvailableState(stacks);
} }
public void redo() { public void redo() {
@ -134,12 +141,15 @@ public final class EditorModel implements Parcelable, RendererContext.Ready {
UndoRedoStacks stacks = cropping ? cropUndoRedoStacks : undoRedoStacks; UndoRedoStacks stacks = cropping ? cropUndoRedoStacks : undoRedoStacks;
undoRedo(stacks.getRedoStack(), stacks.getUndoStack(), cropping); undoRedo(stacks.getRedoStack(), stacks.getUndoStack(), cropping);
updateUndoRedoAvailableState(stacks);
} }
private void undoRedo(@NonNull ElementStack fromStack, @NonNull ElementStack toStack, boolean keepEditorState) { private void undoRedo(@NonNull ElementStack fromStack, @NonNull ElementStack toStack, boolean keepEditorState) {
final EditorElement popped = fromStack.pop(); final EditorElement oldRootElement = editorElementHierarchy.getRoot();
final EditorElement popped = fromStack.pop(oldRootElement);
if (popped != null) { if (popped != null) {
EditorElement oldRootElement = editorElementHierarchy.getRoot();
editorElementHierarchy = EditorElementHierarchy.create(popped); editorElementHierarchy = EditorElementHierarchy.create(popped);
toStack.tryPush(oldRootElement); toStack.tryPush(oldRootElement);
@ -187,6 +197,14 @@ public final class EditorModel implements Parcelable, RendererContext.Ready {
} }
} }
private void updateUndoRedoAvailableState(UndoRedoStacks currentStack) {
if (undoRedoStackListener == null) return;
EditorElement root = editorElementHierarchy.getRoot();
undoRedoStackListener.onAvailabilityChanged(currentStack.canUndo(root), currentStack.canRedo(root));
}
private static Map<UUID, EditorElement> getElementMap(@NonNull EditorElement element) { private static Map<UUID, EditorElement> getElementMap(@NonNull EditorElement element) {
final Map<UUID, EditorElement> result = new HashMap<>(); final Map<UUID, EditorElement> result = new HashMap<>();
element.buildMap(result); element.buildMap(result);
@ -195,14 +213,15 @@ public final class EditorModel implements Parcelable, RendererContext.Ready {
public void startCrop() { public void startCrop() {
pushUndoPoint(); pushUndoPoint();
cropUndoRedoStacks.getUndoStack().clear(); cropUndoRedoStacks.clear(editorElementHierarchy.getRoot());
cropUndoRedoStacks.getUndoStack().clear();
editorElementHierarchy.startCrop(invalidate); editorElementHierarchy.startCrop(invalidate);
inBoundsMemory.push(editorElementHierarchy.getMainImage(), editorElementHierarchy.getCropEditorElement()); inBoundsMemory.push(editorElementHierarchy.getMainImage(), editorElementHierarchy.getCropEditorElement());
updateUndoRedoAvailableState(cropUndoRedoStacks);
} }
public void doneCrop() { public void doneCrop() {
editorElementHierarchy.doneCrop(visibleViewPort, invalidate); editorElementHierarchy.doneCrop(visibleViewPort, invalidate);
updateUndoRedoAvailableState(undoRedoStacks);
} }
public void setCropAspectLock(boolean locked) { public void setCropAspectLock(boolean locked) {
@ -223,6 +242,9 @@ public final class EditorModel implements Parcelable, RendererContext.Ready {
if (isCropping()) { if (isCropping()) {
ensureFitsBounds(allowScaleToRepairCrop); ensureFitsBounds(allowScaleToRepairCrop);
} }
UndoRedoStacks stacks = isCropping() ? cropUndoRedoStacks : undoRedoStacks;
updateUndoRedoAvailableState(stacks);
} }
private void ensureFitsBounds(boolean allowScaleToRepairCrop) { private void ensureFitsBounds(boolean allowScaleToRepairCrop) {
@ -467,13 +489,14 @@ public final class EditorModel implements Parcelable, RendererContext.Ready {
parent.addElement(element); parent.addElement(element);
if (parent != mainImage) { if (parent != mainImage) {
undoRedoStacks.getUndoStack().clear(); undoRedoStacks.clear(editorElementHierarchy.getRoot());
} }
updateUndoRedoAvailableState(undoRedoStacks);
} }
public boolean isChanged() { public boolean isChanged() {
ElementStack undoStack = undoRedoStacks.getUndoStack(); return undoRedoStacks.isChanged(editorElementHierarchy.getRoot());
return !undoStack.isEmpty() || undoStack.isOverflowed();
} }
public RectF findCropRelativeToRoot() { public RectF findCropRelativeToRoot() {
@ -578,4 +601,5 @@ public final class EditorModel implements Parcelable, RendererContext.Ready {
public boolean isCropping() { public boolean isCropping() {
return editorElementHierarchy.getCropEditorElement().getFlags().isVisible(); return editorElementHierarchy.getCropEditorElement().getFlags().isVisible();
} }
} }

View File

@ -13,14 +13,12 @@ import java.util.Stack;
* <p> * <p>
* Elements are mutable, so this stack serializes the element and keeps a stack of serialized data. * Elements are mutable, so this stack serializes the element and keeps a stack of serialized data.
* <p> * <p>
* The stack has a {@link #limit} and if it exceeds that limit the {@link #overflowed} flag is set. * The stack has a {@link #limit} and if it exceeds that limit during a push the earliest item is removed.
* So that when used as an undo stack, {@link #isEmpty()} and {@link #isOverflowed()} tell you if the image has ever changed.
*/ */
final class ElementStack implements Parcelable { final class ElementStack implements Parcelable {
private final int limit; private final int limit;
private final Stack<byte[]> stack = new Stack<>(); private final Stack<byte[]> stack = new Stack<>();
private boolean overflowed;
ElementStack(int limit) { ElementStack(int limit) {
this.limit = limit; this.limit = limit;
@ -28,7 +26,6 @@ final class ElementStack implements Parcelable {
private ElementStack(@NonNull Parcel in) { private ElementStack(@NonNull Parcel in) {
this(in.readInt()); this(in.readInt());
overflowed = in.readInt() != 0;
final int count = in.readInt(); final int count = in.readInt();
for (int i = 0; i < count; i++) { for (int i = 0; i < count; i++) {
stack.add(i, in.createByteArray()); stack.add(i, in.createByteArray());
@ -43,32 +40,52 @@ final class ElementStack implements Parcelable {
* @return true iff the pushed item was different to the top item. * @return true iff the pushed item was different to the top item.
*/ */
boolean tryPush(@NonNull EditorElement element) { boolean tryPush(@NonNull EditorElement element) {
Parcel parcel = Parcel.obtain(); byte[] bytes = getBytes(element);
byte[] bytes; boolean push = stack.isEmpty() || !Arrays.equals(bytes, stack.peek());
try {
parcel.writeParcelable(element, 0);
bytes = parcel.marshall();
} finally {
parcel.recycle();
}
boolean push = stack.isEmpty() || !Arrays.equals(bytes, stack.peek());
if (push) { if (push) {
stack.push(bytes); stack.push(bytes);
if (stack.size() > limit) { if (stack.size() > limit) {
stack.remove(0); stack.remove(0);
overflowed = true;
} }
} }
return push; return push;
} }
@Nullable EditorElement pop() { static byte[] getBytes(@NonNull Parcelable parcelable) {
Parcel parcel = Parcel.obtain();
byte[] bytes;
try {
parcel.writeParcelable(parcelable, 0);
bytes = parcel.marshall();
} finally {
parcel.recycle();
}
return bytes;
}
/**
* Pops the first different state from the supplied element.
*/
@Nullable EditorElement pop(@NonNull EditorElement element) {
if (stack.empty()) return null; if (stack.empty()) return null;
byte[] data = stack.pop(); byte[] elementBytes = getBytes(element);
byte[] stackData = null;
while (!stack.empty() && stackData == null) {
byte[] topData = stack.pop();
if (!Arrays.equals(topData, elementBytes)) {
stackData = topData;
}
}
if (stackData == null) return null;
Parcel parcel = Parcel.obtain(); Parcel parcel = Parcel.obtain();
try { try {
parcel.unmarshall(data, 0, data.length); parcel.unmarshall(stackData, 0, stackData.length);
parcel.setDataPosition(0); parcel.setDataPosition(0);
return parcel.readParcelable(EditorElement.class.getClassLoader()); return parcel.readParcelable(EditorElement.class.getClassLoader());
} finally { } finally {
@ -100,7 +117,6 @@ final class ElementStack implements Parcelable {
@Override @Override
public void writeToParcel(Parcel dest, int flags) { public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(limit); dest.writeInt(limit);
dest.writeInt(overflowed ? 1 : 0);
final int count = stack.size(); final int count = stack.size();
dest.writeInt(count); dest.writeInt(count);
for (int i = 0; i < count; i++) { for (int i = 0; i < count; i++) {
@ -108,11 +124,17 @@ final class ElementStack implements Parcelable {
} }
} }
boolean isEmpty() { boolean stackContainsStateDifferentFrom(@NonNull EditorElement element) {
return stack.isEmpty(); if (stack.isEmpty()) return false;
}
boolean isOverflowed() { byte[] currentStateBytes = getBytes(element);
return overflowed;
for (byte[] item : stack) {
if (!Arrays.equals(item, currentStateBytes)) {
return true;
}
}
return false;
} }
} }

View File

@ -2,19 +2,27 @@ package org.thoughtcrime.securesms.imageeditor.model;
import android.os.Parcel; import android.os.Parcel;
import android.os.Parcelable; import android.os.Parcelable;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import java.util.Arrays;
final class UndoRedoStacks implements Parcelable { final class UndoRedoStacks implements Parcelable {
private final ElementStack undoStack; private final ElementStack undoStack;
private final ElementStack redoStack; private final ElementStack redoStack;
public UndoRedoStacks(int limit) { @NonNull
this(new ElementStack(limit), new ElementStack(limit)); private byte[] unchangedState;
UndoRedoStacks(int limit) {
this(new ElementStack(limit), new ElementStack(limit), null);
} }
private UndoRedoStacks(ElementStack undoStack, ElementStack redoStack) { private UndoRedoStacks(ElementStack undoStack, ElementStack redoStack, @Nullable byte[] unchangedState) {
this.undoStack = undoStack; this.undoStack = undoStack;
this.redoStack = redoStack; this.redoStack = redoStack;
this.unchangedState = unchangedState != null ? unchangedState : new byte[0];
} }
public static final Creator<UndoRedoStacks> CREATOR = new Creator<UndoRedoStacks>() { public static final Creator<UndoRedoStacks> CREATOR = new Creator<UndoRedoStacks>() {
@ -22,7 +30,8 @@ final class UndoRedoStacks implements Parcelable {
public UndoRedoStacks createFromParcel(Parcel in) { public UndoRedoStacks createFromParcel(Parcel in) {
return new UndoRedoStacks( return new UndoRedoStacks(
in.readParcelable(ElementStack.class.getClassLoader()), in.readParcelable(ElementStack.class.getClassLoader()),
in.readParcelable(ElementStack.class.getClassLoader()) in.readParcelable(ElementStack.class.getClassLoader()),
in.createByteArray()
); );
} }
@ -36,6 +45,7 @@ final class UndoRedoStacks implements Parcelable {
public void writeToParcel(Parcel dest, int flags) { public void writeToParcel(Parcel dest, int flags) {
dest.writeParcelable(undoStack, flags); dest.writeParcelable(undoStack, flags);
dest.writeParcelable(redoStack, flags); dest.writeParcelable(redoStack, flags);
dest.writeByteArray(unchangedState);
} }
@Override @Override
@ -50,4 +60,34 @@ final class UndoRedoStacks implements Parcelable {
ElementStack getRedoStack() { ElementStack getRedoStack() {
return redoStack; return redoStack;
} }
void pushState(@NonNull EditorElement element) {
if (undoStack.tryPush(element)) {
redoStack.clear();
}
}
void clear(@NonNull EditorElement element) {
undoStack.clear();
redoStack.clear();
unchangedState = ElementStack.getBytes(element);
}
boolean isChanged(@NonNull EditorElement element) {
return !Arrays.equals(ElementStack.getBytes(element), unchangedState);
}
/**
* As long as there is something different in the stack somewhere, then we can undo.
*/
boolean canUndo(@NonNull EditorElement currentState) {
return undoStack.stackContainsStateDifferentFrom(currentState);
}
/**
* As long as there is something different in the stack somewhere, then we can redo.
*/
boolean canRedo(@NonNull EditorElement currentState) {
return redoStack.stackContainsStateDifferentFrom(currentState);
}
} }

View File

@ -118,13 +118,14 @@ public final class ImageEditorFragment extends Fragment implements ImageEditorHu
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState); super.onViewCreated(view, savedInstanceState);
imageEditorHud = view.findViewById(R.id.scribble_hud); imageEditorHud = view.findViewById(R.id.scribble_hud);
imageEditorView = view.findViewById(R.id.image_editor_view); imageEditorView = view.findViewById(R.id.image_editor_view);
imageEditorHud.setEventListener(this); imageEditorHud.setEventListener(this);
imageEditorView.setTapListener(selectionListener); imageEditorView.setTapListener(selectionListener);
imageEditorView.setDrawingChangedListener(this::refreshUniqueColors); imageEditorView.setDrawingChangedListener(this::refreshUniqueColors);
imageEditorView.setUndoRedoStackListener(this::onUndoRedoAvailabilityChanged);
EditorModel editorModel = null; EditorModel editorModel = null;
@ -321,6 +322,10 @@ public final class ImageEditorFragment extends Fragment implements ImageEditorHu
imageEditorHud.setColorPalette(imageEditorView.getModel().getUniqueColorsIgnoringAlpha()); imageEditorHud.setColorPalette(imageEditorView.getModel().getUniqueColorsIgnoringAlpha());
} }
private void onUndoRedoAvailabilityChanged(boolean undoAvailable, boolean redoAvailable) {
imageEditorHud.setUndoAvailability(undoAvailable);
}
private final ImageEditorView.TapListener selectionListener = new ImageEditorView.TapListener() { private final ImageEditorView.TapListener selectionListener = new ImageEditorView.TapListener() {
@Override @Override

View File

@ -47,7 +47,10 @@ public final class ImageEditorHud extends LinearLayout {
private ColorPaletteAdapter colorPaletteAdapter; private ColorPaletteAdapter colorPaletteAdapter;
private final Map<Mode, Set<View>> visibilityModeMap = new HashMap<>(); private final Map<Mode, Set<View>> visibilityModeMap = new HashMap<>();
private final Set<View> allViews = new HashSet<>(); private final Set<View> allViews = new HashSet<>();
private Mode currentMode;
private boolean undoAvailable;
public ImageEditorHud(@NonNull Context context) { public ImageEditorHud(@NonNull Context context) {
super(context); super(context);
@ -171,9 +174,10 @@ public final class ImageEditorHud extends LinearLayout {
} }
private void setMode(@NonNull Mode mode, boolean notify) { private void setMode(@NonNull Mode mode, boolean notify) {
this.currentMode = mode;
Set<View> visibleButtons = visibilityModeMap.get(mode); Set<View> visibleButtons = visibilityModeMap.get(mode);
for (View button : allViews) { for (View button : allViews) {
button.setVisibility(visibleButtons != null && visibleButtons.contains(button) ? VISIBLE : GONE); button.setVisibility(buttonIsVisible(visibleButtons, button) ? VISIBLE : GONE);
} }
switch (mode) { switch (mode) {
@ -189,6 +193,12 @@ public final class ImageEditorHud extends LinearLayout {
eventListener.onRequestFullScreen(mode != Mode.NONE); eventListener.onRequestFullScreen(mode != Mode.NONE);
} }
private boolean buttonIsVisible(@Nullable Set<View> visibleButtons, @NonNull View button) {
return visibleButtons != null &&
visibleButtons.contains(button) &&
(button != undoButton || undoAvailable);
}
private void presentModeCrop() { private void presentModeCrop() {
updateCropAspectLockImage(eventListener.isCropAspectLocked()); updateCropAspectLockImage(eventListener.isCropAspectLocked());
} }
@ -216,6 +226,12 @@ public final class ImageEditorHud extends LinearLayout {
return color & ~0xff000000 | 0x80000000; return color & ~0xff000000 | 0x80000000;
} }
public void setUndoAvailability(boolean undoAvailable) {
this.undoAvailable = undoAvailable;
undoButton.setVisibility(buttonIsVisible(visibilityModeMap.get(currentMode), undoButton) ? VISIBLE : GONE);
}
public enum Mode { public enum Mode {
NONE, DRAW, HIGHLIGHT, TEXT, MOVE_DELETE, CROP NONE, DRAW, HIGHLIGHT, TEXT, MOVE_DELETE, CROP
} }