() {
59 | public ParcelableParcel createFromParcel(Parcel in) {
60 | return new ParcelableParcel(in, null);
61 | }
62 | public ParcelableParcel createFromParcel(Parcel in, ClassLoader loader) {
63 | return new ParcelableParcel(in, loader);
64 | }
65 | public ParcelableParcel[] newArray(int size) {
66 | return new ParcelableParcel[size];
67 | }
68 | };
69 | }
--------------------------------------------------------------------------------
/library/src/main/java/com/cardinalblue/android/piccollage/UndoManager.java:
--------------------------------------------------------------------------------
1 | package com.cardinalblue.android.piccollage;
2 |
3 |
4 | /*
5 | * Copyright (C) 2013 The Android Open Source Project
6 | *
7 | * Licensed under the Apache License, Version 2.0 (the "License");
8 | * you may not use this file except in compliance with the License.
9 | * You may obtain a copy of the License at
10 | *
11 | * http://www.apache.org/licenses/LICENSE-2.0
12 | *
13 | * Unless required by applicable law or agreed to in writing, software
14 | * distributed under the License is distributed on an "AS IS" BASIS,
15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 | * See the License for the specific language governing permissions and
17 | * limitations under the License.
18 | */
19 | import android.os.Parcel;
20 | import android.os.Parcelable;
21 | import android.text.TextUtils;
22 | import java.util.ArrayList;
23 |
24 | /**
25 | * Top-level class for managing and interacting with the global undo state for
26 | * a document or application. This class supports both undo and redo and has
27 | * helpers for merging undoable operations together as they are performed.
28 | *
29 | * A single undoable operation is represented by {@link UndoOperation} which
30 | * apps implement to define their undo/redo behavior. The UndoManager keeps
31 | * a stack of undo states; each state can have one or more undo operations
32 | * inside of it.
33 | *
34 | * Updates to the stack must be done inside of a {@link #beginUpdate}/{@link #endUpdate()}
35 | * pair. During this time you can add new operations to the stack with
36 | * {@link #addOperation}, retrieve and modify existing operations with
37 | * {@link #getLastOperation}, control the label shown to the user for this operation
38 | * with {@link #setUndoLabel} and {@link #suggestUndoLabel}, etc.
39 | *
40 | * For example, you may have a document with multiple embedded objects. If the
41 | * document itself and each embedded object use different owners, then you
42 | * can provide undo semantics appropriate to the user's context: while within
43 | * an embedded object, only edits to that object are seen and the user can
44 | * undo/redo them without needing to impact edits in other objects; while
45 | * within the larger document, all edits can be seen and the user must
46 | * undo/redo them as a single stream.
47 | *
48 | * @hide
49 | */
50 | public class UndoManager {
51 | private final ArrayList mUndos = new ArrayList<>();
52 | private final ArrayList mRedos = new ArrayList<>();
53 | private int mUpdateCount;
54 | private int mHistorySize = 20;
55 | private UndoState mWorking;
56 | private int mCommitId = 1;
57 | private boolean mInUndo;
58 | private boolean mMerged;
59 | private int mStateSeq;
60 |
61 | /**
62 | * Never merge with the last undo state.
63 | */
64 | public static final int MERGE_MODE_NONE = 0;
65 | /**
66 | * Allow merge with the last undo state only if it contains
67 | * operations with the caller's owner.
68 | */
69 | public static final int MERGE_MODE_UNIQUE = 1;
70 | /**
71 | * Always allow merge with the last undo state, if possible.
72 | */
73 | public static final int MERGE_MODE_ANY = 2;
74 | /**
75 | * Flatten the current undo state into a Parcelable object, which can later be restored
76 | * with {@link #restoreInstanceState(android.os.Parcelable)}.
77 | */
78 | public Parcelable saveInstanceState() {
79 | if (mUpdateCount > 0) {
80 | throw new IllegalStateException("Can't save state while updating");
81 | }
82 | ParcelableParcel pp = new ParcelableParcel(getClass().getClassLoader());
83 | Parcel p = pp.getParcel();
84 | mStateSeq++;
85 | if (mStateSeq <= 0) {
86 | mStateSeq = 0;
87 | }
88 | p.writeInt(mHistorySize);
89 | // XXX eventually we need to be smart here about limiting the
90 | // number of undo states we write to not exceed X bytes.
91 | int i = mUndos.size();
92 | while (i > 0) {
93 | p.writeInt(1);
94 | i--;
95 | mUndos.get(i).writeToParcel(p);
96 | }
97 | i = mRedos.size();
98 | p.writeInt(i);
99 | while (i > 0) {
100 | p.writeInt(2);
101 | i--;
102 | mRedos.get(i).writeToParcel(p);
103 | }
104 | p.writeInt(0);
105 | return pp;
106 | }
107 | /**
108 | * Restore an undo state previously created with {@link #saveInstanceState()}. This will
109 | * restore the UndoManager's state to almost exactly what it was at the point it had
110 | * been previously saved; the only information not restored is the data object
111 | * associated with each {@link UndoOperation}
112 | */
113 | public void restoreInstanceState(Parcelable state) {
114 | if (mUpdateCount > 0) {
115 | throw new IllegalStateException("Can't save state while updating");
116 | }
117 | forgetUndos(-1);
118 | forgetRedos(-1);
119 | ParcelableParcel pp = (ParcelableParcel)state;
120 | Parcel p = pp.getParcel();
121 | mHistorySize = p.readInt();
122 | int stype;
123 | while ((stype=p.readInt()) != 0) {
124 | UndoState ustate = new UndoState(p, pp.getClassLoader());
125 | if (stype == 1) {
126 | mUndos.add(0, ustate);
127 | } else {
128 | mRedos.add(0, ustate);
129 | }
130 | }
131 | }
132 | /**
133 | * Set the maximum number of undo states that will be retained.
134 | */
135 | public void setHistorySize(int size) {
136 | mHistorySize = size;
137 | if (mHistorySize >= 0 && countUndos() > mHistorySize) {
138 | forgetUndos(countUndos() - mHistorySize);
139 | }
140 | }
141 | /**
142 | * Return the current maximum number of undo states.
143 | */
144 | public int getHistorySize() {
145 | return mHistorySize;
146 | }
147 |
148 | /**
149 | * Perform undo of last/top count undo states. The states impacted
150 | * by this can be limited through owners.
151 | * @param count Number of undo states to pop.
152 | * @return Returns the number of undo states that were actually popped.
153 | */
154 | public int undo(int count) {
155 | if (mWorking != null) {
156 | throw new IllegalStateException("Can't be called during an update");
157 | }
158 | int num = 0;
159 | int i = -1;
160 | mInUndo = true;
161 | UndoState us = getTopUndo();
162 | if (us != null) {
163 | us.makeExecuted();
164 | }
165 | while (count > 0 && (i=findPrevState(mUndos, i)) >= 0) {
166 | UndoState state = mUndos.remove(i);
167 | state.undo();
168 | mRedos.add(state);
169 | count--;
170 | num++;
171 | }
172 | mInUndo = false;
173 | return num;
174 | }
175 |
176 | /**
177 | * Perform redo of last/top count undo states in the transient redo stack.
178 | * The states impacted by this can be limited through owners.
179 | * @param count Number of undo states to pop.
180 | * @return Returns the number of undo states that were actually redone.
181 | */
182 | public int redo(int count) {
183 | if (mWorking != null) {
184 | throw new IllegalStateException("Can't be called during an update");
185 | }
186 | int num = 0;
187 | int i = -1;
188 | mInUndo = true;
189 | while (count > 0 && (i=findPrevState(mRedos, i)) >= 0) {
190 | UndoState state = mRedos.remove(i);
191 | state.redo();
192 | mUndos.add(state);
193 | count--;
194 | num++;
195 | }
196 | mInUndo = false;
197 | return num;
198 | }
199 | /**
200 | * Returns true if we are currently inside of an undo/redo operation. This is
201 | * useful for editors to know whether they should be generating new undo state
202 | * when they see edit operations happening.
203 | */
204 | public boolean isInUndo() {
205 | return mInUndo;
206 | }
207 | public int forgetUndos(int count) {
208 | if (count < 0) {
209 | count = mUndos.size();
210 | }
211 | int removed = 0;
212 | for (int i=0; i 0) {
215 | state.destroy();
216 | mUndos.remove(i);
217 | removed++;
218 | }
219 | }
220 | return removed;
221 | }
222 | public int forgetRedos(int count) {
223 | if (count < 0) {
224 | count = mRedos.size();
225 | }
226 | int removed = 0;
227 | for (int i=0; i 0) {
230 | state.destroy();
231 | mRedos.remove(i);
232 | removed++;
233 | }
234 | }
235 | return removed;
236 | }
237 | /**
238 | * Return the number of undo states on the undo stack.
239 | */
240 | public int countUndos() {
241 | return mUndos.size();
242 | }
243 | /**
244 | * Return the number of redo states on the undo stack.
245 | */
246 | public int countRedos() {
247 | return mRedos.size();
248 | }
249 | /**
250 | * Return the user-visible label for the top undo state on the stack.
251 | */
252 | public CharSequence getUndoLabel() {
253 | UndoState state = getTopUndo();
254 | return state != null ? state.getLabel() : null;
255 | }
256 | /**
257 | * Return the user-visible label for the top redo state on the stack.
258 | */
259 | public CharSequence getRedoLabel() {
260 | UndoState state = getTopRedo();
261 | return state != null ? state.getLabel() : null;
262 | }
263 | /**
264 | * Start creating a new undo state. Multiple calls to this function will nest until
265 | * they are all matched by a later call to {@link #endUpdate}.
266 | * @param label Optional user-visible label for this new undo state.
267 | */
268 | public void beginUpdate(CharSequence label) {
269 | if (mInUndo) {
270 | throw new IllegalStateException("Can't being update while performing undo/redo");
271 | }
272 | if (mUpdateCount <= 0) {
273 | createWorkingState();
274 | mMerged = false;
275 | mUpdateCount = 0;
276 | }
277 | mWorking.updateLabel(label);
278 | mUpdateCount++;
279 | }
280 | private void createWorkingState() {
281 | mWorking = new UndoState(mCommitId++);
282 | if (mCommitId < 0) {
283 | mCommitId = 1;
284 | }
285 | }
286 | /**
287 | * Returns true if currently inside of a {@link #beginUpdate}.
288 | */
289 | public boolean isInUpdate() {
290 | return mUpdateCount > 0;
291 | }
292 | /**
293 | * Forcibly set a new for the new undo state being built within a {@link #beginUpdate}.
294 | * Any existing label will be replaced with this one.
295 | */
296 | public void setUndoLabel(CharSequence label) {
297 | if (mWorking == null) {
298 | throw new IllegalStateException("Must be called during an update");
299 | }
300 | mWorking.setLabel(label);
301 | }
302 | /**
303 | * Set a new for the new undo state being built within a {@link #beginUpdate}, but
304 | * only if there is not a label currently set for it.
305 | */
306 | public void suggestUndoLabel(CharSequence label) {
307 | if (mWorking == null) {
308 | throw new IllegalStateException("Must be called during an update");
309 | }
310 | mWorking.updateLabel(label);
311 | }
312 | /**
313 | * Return the number of times {@link #beginUpdate} has been called without a matching
314 | * {@link #endUpdate} call.
315 | */
316 | public int getUpdateNestingLevel() {
317 | return mUpdateCount;
318 | }
319 | /**
320 | * Check whether there is an {@link UndoOperation} in the current {@link #beginUpdate}
321 | * undo state.
322 | * @return Returns true if there is a matching operation in the current undo state.
323 | */
324 | public boolean hasOperation() {
325 | if (mWorking == null) {
326 | throw new IllegalStateException("Must be called during an update");
327 | }
328 | return mWorking.hasOperation();
329 | }
330 | /**
331 | * Return the most recent {@link UndoOperation} that was added to the update.
332 | * @param mergeMode May be either {@link #MERGE_MODE_NONE} or {@link #MERGE_MODE_ANY}.
333 | */
334 | public UndoOperation> getLastOperation(int mergeMode) {
335 | return getLastOperation(null, mergeMode);
336 | }
337 | /**
338 | * Return the most recent {@link UndoOperation} that was added to the update and
339 | * has the given owner.
340 | * @param clazz Optional class of the last operation to retrieve. If null, the
341 | * last operation regardless of class will be retrieved; if non-null, the last
342 | * operation whose class is the same as the given class will be retrieved.
343 | * @param mergeMode May be either {@link #MERGE_MODE_NONE}, {@link #MERGE_MODE_UNIQUE},
344 | * or {@link #MERGE_MODE_ANY}.
345 | */
346 | public T getLastOperation(Class clazz, int mergeMode) {
347 | if (mWorking == null) {
348 | throw new IllegalStateException("Must be called during an update");
349 | }
350 | if (mergeMode != MERGE_MODE_NONE && !mMerged && !mWorking.hasData()) {
351 | UndoState state = getTopUndo();
352 | UndoOperation> last;
353 | if (state != null && (mergeMode == MERGE_MODE_ANY)
354 | && state.canMerge() && (last=state.getLastOperation(clazz)) != null) {
355 | if (last.allowMerge()) {
356 | mWorking.destroy();
357 | mWorking = state;
358 | mUndos.remove(state);
359 | mMerged = true;
360 | return (T)last;
361 | }
362 | }
363 | }
364 | return mWorking.getLastOperation(clazz);
365 | }
366 | public void addOperation(UndoOperation> op) {
367 | addOperation(op, MERGE_MODE_NONE);
368 | }
369 | /**
370 | * Add a new UndoOperation to the current update.
371 | * @param op The new operation to add.
372 | * @param mergeMode May be either {@link #MERGE_MODE_NONE}, {@link #MERGE_MODE_UNIQUE},
373 | * or {@link #MERGE_MODE_ANY}.
374 | */
375 | private void addOperation(UndoOperation> op, int mergeMode) {
376 | if (mWorking == null) {
377 | throw new IllegalStateException("Must be called during an update");
378 | }
379 | if (mergeMode != MERGE_MODE_NONE && !mMerged && !mWorking.hasData()) {
380 | UndoState state = getTopUndo();
381 | if (state != null && (mergeMode == MERGE_MODE_ANY)
382 | && state.canMerge() && state.hasOperation()) {
383 | mWorking.destroy();
384 | mWorking = state;
385 | mUndos.remove(state);
386 | mMerged = true;
387 | }
388 | }
389 | mWorking.addOperation(op);
390 | }
391 | /**
392 | * Finish the creation of an undo state, matching a previous call to
393 | * {@link #beginUpdate}.
394 | */
395 | public void endUpdate() {
396 | if (mWorking == null) {
397 | throw new IllegalStateException("Must be called during an update");
398 | }
399 | mUpdateCount--;
400 | if (mUpdateCount == 0) {
401 | pushWorkingState();
402 | }
403 | }
404 | private void pushWorkingState() {
405 | int N = mUndos.size() + 1;
406 | if (mWorking.hasData()) {
407 | mUndos.add(mWorking);
408 | forgetRedos(-1);
409 | mWorking.commit();
410 | if (N >= 2) {
411 | // The state before this one can no longer be merged, ever.
412 | // The only way to get back to it is for the user to perform
413 | // an undo.
414 | mUndos.get(N-2).makeExecuted();
415 | }
416 | } else {
417 | mWorking.destroy();
418 | }
419 | mWorking = null;
420 | if (mHistorySize >= 0 && N > mHistorySize) {
421 | forgetUndos(N - mHistorySize);
422 | }
423 | }
424 | /**
425 | * Commit the last finished undo state. This undo state can no longer be
426 | * modified with further {@link #MERGE_MODE_UNIQUE} or
427 | * {@link #MERGE_MODE_ANY} merge modes. If called while inside of an update,
428 | * this will push any changes in the current update on to the undo stack
429 | * and result with a fresh undo state, behaving as if {@link #endUpdate()}
430 | * had been called enough to unwind the current update, then the last state
431 | * committed, and {@link #beginUpdate} called to restore the update nesting.
432 | * @return Returns an integer identifier for the committed undo state, which
433 | * can later be used to try to uncommit the state to perform further edits on it.
434 | */
435 | public int commitState() {
436 | if (mWorking != null && mWorking.hasData()) {
437 | if (mWorking.hasOperation()) {
438 | mWorking.setCanMerge(false);
439 | int commitId = mWorking.getCommitId();
440 | pushWorkingState();
441 | createWorkingState();
442 | mMerged = true;
443 | return commitId;
444 | }
445 | } else {
446 | UndoState state = getTopUndo();
447 | if (state != null) {
448 | state.setCanMerge(false);
449 | return state.getCommitId();
450 | }
451 | }
452 | return -1;
453 | }
454 | /**
455 | * Attempt to undo a previous call to {@link #commitState}. This will work
456 | * if the undo state at the top of the stack has the given id, and has not been
457 | * involved in an undo operation. Otherwise false is returned.
458 | * @param commitId The identifier for the state to be uncommitted, as returned
459 | * by {@link #commitState}.
460 | * @return Returns true if the uncommit is successful, else false.
461 | */
462 | public boolean uncommitState(int commitId) {
463 | if (mWorking != null && mWorking.getCommitId() == commitId) {
464 | if (mWorking.hasOperation()) {
465 | return mWorking.setCanMerge(true);
466 | }
467 | } else {
468 | UndoState state = getTopUndo();
469 | if (state != null) {
470 | if (state.getCommitId() == commitId) {
471 | return state.setCanMerge(true);
472 | }
473 | }
474 | }
475 | return false;
476 | }
477 | public boolean canUndo() {
478 | return !mUndos.isEmpty();
479 | }
480 | public boolean canRedo() {
481 | return !mRedos.isEmpty();
482 | }
483 | UndoState getTopUndo() {
484 | if (mUndos.size() <= 0) {
485 | return null;
486 | }
487 | int i = findPrevState(mUndos, -1);
488 | return i >= 0 ? mUndos.get(i) : null;
489 | }
490 | UndoState getTopRedo() {
491 | if (mRedos.size() <= 0) {
492 | return null;
493 | }
494 | int i = findPrevState(mRedos, -1);
495 | return i >= 0 ? mRedos.get(i) : null;
496 | }
497 | int findPrevState(ArrayList states, int from) {
498 | final int N = states.size();
499 | if (from == -1) {
500 | from = N-1;
501 | }
502 | if (from >= N) {
503 | return -1;
504 | }
505 | return from;
506 | }
507 |
508 | final static class UndoState {
509 | private final int mCommitId;
510 | private final ArrayList> mOperations = new ArrayList>();
511 | private ArrayList> mRecent;
512 | private CharSequence mLabel;
513 | private boolean mCanMerge = true;
514 | private boolean mExecuted;
515 | UndoState(int commitId) {
516 | mCommitId = commitId;
517 | }
518 | UndoState(Parcel p, ClassLoader loader) {
519 | mCommitId = p.readInt();
520 | mCanMerge = p.readInt() != 0;
521 | mExecuted = p.readInt() != 0;
522 | mLabel = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(p);
523 | final int N = p.readInt();
524 | for (int i=0; i op) {
580 | if (mOperations.contains(op)) {
581 | throw new IllegalStateException("Already holds " + op);
582 | }
583 | mOperations.add(op);
584 | if (mRecent == null) {
585 | mRecent = new ArrayList<>();
586 | mRecent.add(op);
587 | }
588 | }
589 | T getLastOperation(Class clazz) {
590 | final int N = mOperations.size();
591 | if (clazz == null) {
592 | return N > 0 ? (T)mOperations.get(N-1) : null;
593 | }
594 | // First look for the top-most operation with the same owner.
595 | for (int i=N-1; i>=0; i--) {
596 | UndoOperation> op = mOperations.get(i);
597 | // Return this operation if it has the same class that the caller wants.
598 | // Note that we don't search deeper for the class, because we don't want
599 | // to end up with a different order of operations for the same owner.
600 | if (clazz != null && op.getClass() != clazz) {
601 | return null;
602 | }
603 | return (T)op;
604 | }
605 | return null;
606 | }
607 | boolean hasData() {
608 | for (int i=mOperations.size()-1; i>=0; i--) {
609 | if (mOperations.get(i).hasData()) {
610 | return true;
611 | }
612 | }
613 | return false;
614 | }
615 | void commit() {
616 | final int N = mRecent != null ? mRecent.size() : 0;
617 | for (int i=0; i=0; i--) {
624 | mOperations.get(i).undo();
625 | }
626 | }
627 | void redo() {
628 | final int N = mOperations.size();
629 | for (int i=0; i implements Parcelable {
646 | protected UndoOperation() {
647 | }
648 | /**
649 | * Construct from a Parcel.
650 | */
651 | protected UndoOperation(Parcel src, ClassLoader loader) {
652 | }
653 | /**
654 | * Return true if this operation actually contains modification data. The
655 | * default implementation always returns true. If you return false, the
656 | * operation will be dropped when the final undo state is being built.
657 | */
658 | public boolean hasData() {
659 | return true;
660 | }
661 | /**
662 | * Return true if this operation can be merged with a later operation.
663 | * The default implementation always returns true.
664 | */
665 | public boolean allowMerge() {
666 | return true;
667 | }
668 | /**
669 | * Called when this undo state is being committed to the undo stack.
670 | * The implementation should perform the initial edits and save any state that
671 | * may be needed to undo them.
672 | */
673 | public abstract void commit();
674 | /**
675 | * Called when this undo state is being popped off the undo stack (in to
676 | * the temporary redo stack). The implementation should remove the original
677 | * edits and thus restore the target object to its prior value.
678 | */
679 | public abstract void undo();
680 | /**
681 | * Called when this undo state is being pushed back from the transient
682 | * redo stack to the main undo stack. The implementation should re-apply
683 | * the edits that were previously removed by {@link #undo}.
684 | */
685 | public abstract void redo();
686 | public int describeContents() {
687 | return 0;
688 | }
689 | }
690 | }
--------------------------------------------------------------------------------
/library/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | library
3 |
4 |
--------------------------------------------------------------------------------
/library/src/test/java/com/cardinalblue/android/piccollage/ExampleUnitTest.java:
--------------------------------------------------------------------------------
1 | package com.cardinalblue.android.piccollage;
2 |
3 | import org.junit.Test;
4 |
5 | import static org.junit.Assert.*;
6 |
7 | /**
8 | * To work on unit tests, switch the Test Artifact in the Build Variants view.
9 | */
10 | public class ExampleUnitTest {
11 | @Test
12 | public void addition_isCorrect() throws Exception {
13 | assertEquals(4, 2 + 2);
14 | }
15 | }
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app', ':library'
2 |
--------------------------------------------------------------------------------