mirror of
https://github.com/oxen-io/session-android.git
synced 2024-12-28 18:57:43 +00:00
Image Editor - Undo button visibility.
This commit is contained in:
parent
b5d37702f9
commit
6777b3e0e6
@ -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
|
||||
|
@ -0,0 +1,6 @@
|
||||
package org.thoughtcrime.securesms.imageeditor;
|
||||
|
||||
public interface UndoRedoStackListener {
|
||||
|
||||
void onAvailabilityChanged(boolean undoAvailable, boolean redoAvailable);
|
||||
}
|
@ -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
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user