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
private DrawingChangedListener drawingChangedListener;
@Nullable
private UndoRedoStackListener undoRedoStackListener;
private final Matrix viewMatrix = new Matrix();
private final RectF viewPort = 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 != null) {
this.model.setInvalidate(null);
this.model.setUndoRedoStackListener(null);
}
this.model = model;
this.model.setInvalidate(this::invalidate);
this.model.setUndoRedoStackListener(this::onUndoRedoAvailabilityChanged);
this.model.setVisibleViewPort(visibleViewPort);
invalidate();
}
@ -386,6 +391,10 @@ public final class ImageEditorView extends FrameLayout {
this.drawingChangedListener = drawingChangedListener;
}
public void setUndoRedoStackListener(@Nullable UndoRedoStackListener undoRedoStackListener) {
this.undoRedoStackListener = undoRedoStackListener;
}
public void setTapListener(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 {
@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 List<EditorElement> children = new LinkedList<>();
private final List<EditorElement> children = new LinkedList<>();
private final List<EditorElement> deletedChildren = new LinkedList<>();
@NonNull

View File

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

View File

@ -13,14 +13,12 @@ import java.util.Stack;
* <p>
* Elements are mutable, so this stack serializes the element and keeps a stack of serialized data.
* <p>
* The stack has a {@link #limit} and if it exceeds that limit the {@link #overflowed} flag is set.
* So that when used as an undo stack, {@link #isEmpty()} and {@link #isOverflowed()} tell you if the image has ever changed.
* The stack has a {@link #limit} and if it exceeds that limit during a push the earliest item is removed.
*/
final class ElementStack implements Parcelable {
private final int limit;
private final Stack<byte[]> stack = new Stack<>();
private boolean overflowed;
ElementStack(int limit) {
this.limit = limit;
@ -28,7 +26,6 @@ final class ElementStack implements Parcelable {
private ElementStack(@NonNull Parcel in) {
this(in.readInt());
overflowed = in.readInt() != 0;
final int count = in.readInt();
for (int i = 0; i < count; i++) {
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.
*/
boolean tryPush(@NonNull EditorElement element) {
Parcel parcel = Parcel.obtain();
byte[] bytes;
try {
parcel.writeParcelable(element, 0);
bytes = parcel.marshall();
} finally {
parcel.recycle();
}
boolean push = stack.isEmpty() || !Arrays.equals(bytes, stack.peek());
byte[] bytes = getBytes(element);
boolean push = stack.isEmpty() || !Arrays.equals(bytes, stack.peek());
if (push) {
stack.push(bytes);
if (stack.size() > limit) {
stack.remove(0);
overflowed = true;
}
}
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;
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();
try {
parcel.unmarshall(data, 0, data.length);
parcel.unmarshall(stackData, 0, stackData.length);
parcel.setDataPosition(0);
return parcel.readParcelable(EditorElement.class.getClassLoader());
} finally {
@ -100,7 +117,6 @@ final class ElementStack implements Parcelable {
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(limit);
dest.writeInt(overflowed ? 1 : 0);
final int count = stack.size();
dest.writeInt(count);
for (int i = 0; i < count; i++) {
@ -108,11 +124,17 @@ final class ElementStack implements Parcelable {
}
}
boolean isEmpty() {
return stack.isEmpty();
}
boolean stackContainsStateDifferentFrom(@NonNull EditorElement element) {
if (stack.isEmpty()) return false;
boolean isOverflowed() {
return overflowed;
byte[] currentStateBytes = getBytes(element);
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.Parcelable;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import java.util.Arrays;
final class UndoRedoStacks implements Parcelable {
private final ElementStack undoStack;
private final ElementStack redoStack;
public UndoRedoStacks(int limit) {
this(new ElementStack(limit), new ElementStack(limit));
@NonNull
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.redoStack = redoStack;
this.unchangedState = unchangedState != null ? unchangedState : new byte[0];
}
public static final Creator<UndoRedoStacks> CREATOR = new Creator<UndoRedoStacks>() {
@ -22,7 +30,8 @@ final class UndoRedoStacks implements Parcelable {
public UndoRedoStacks createFromParcel(Parcel in) {
return new UndoRedoStacks(
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) {
dest.writeParcelable(undoStack, flags);
dest.writeParcelable(redoStack, flags);
dest.writeByteArray(unchangedState);
}
@Override
@ -50,4 +60,34 @@ final class UndoRedoStacks implements Parcelable {
ElementStack getRedoStack() {
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) {
super.onViewCreated(view, savedInstanceState);
imageEditorHud = view.findViewById(R.id.scribble_hud);
imageEditorView = view.findViewById(R.id.image_editor_view);
imageEditorHud = view.findViewById(R.id.scribble_hud);
imageEditorView = view.findViewById(R.id.image_editor_view);
imageEditorHud.setEventListener(this);
imageEditorView.setTapListener(selectionListener);
imageEditorView.setDrawingChangedListener(this::refreshUniqueColors);
imageEditorView.setUndoRedoStackListener(this::onUndoRedoAvailabilityChanged);
EditorModel editorModel = null;
@ -321,6 +322,10 @@ public final class ImageEditorFragment extends Fragment implements ImageEditorHu
imageEditorHud.setColorPalette(imageEditorView.getModel().getUniqueColorsIgnoringAlpha());
}
private void onUndoRedoAvailabilityChanged(boolean undoAvailable, boolean redoAvailable) {
imageEditorHud.setUndoAvailability(undoAvailable);
}
private final ImageEditorView.TapListener selectionListener = new ImageEditorView.TapListener() {
@Override

View File

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