001/* 002 * Copyright 2010-2015 Institut Pasteur. 003 * 004 * This file is part of Icy. 005 * 006 * Icy is free software: you can redistribute it and/or modify 007 * it under the terms of the GNU General Public License as published by 008 * the Free Software Foundation, either version 3 of the License, or 009 * (at your option) any later version. 010 * 011 * Icy is distributed in the hope that it will be useful, 012 * but WITHOUT ANY WARRANTY; without even the implied warranty of 013 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 014 * GNU General Public License for more details. 015 * 016 * You should have received a copy of the GNU General Public License 017 * along with Icy. If not, see <http://www.gnu.org/licenses/>. 018 */ 019package icy.undo; 020 021import java.util.ArrayList; 022import java.util.List; 023 024import javax.swing.UIManager; 025import javax.swing.event.EventListenerList; 026import javax.swing.event.UndoableEditEvent; 027import javax.swing.event.UndoableEditListener; 028import javax.swing.undo.AbstractUndoableEdit; 029import javax.swing.undo.CannotRedoException; 030import javax.swing.undo.CannotUndoException; 031import javax.swing.undo.CompoundEdit; 032import javax.swing.undo.UndoableEdit; 033 034/** 035 * Custom UndoManager for Icy. 036 * 037 * @author Stephane 038 */ 039// public class IcyUndoManager extends UndoManager implements IcyUndoableEditListener 040public class IcyUndoManager extends AbstractUndoableEdit implements UndoableEditListener 041{ 042 /** 043 * 044 */ 045 private static final long serialVersionUID = 3080107472163005941L; 046 047 private static final int INITIAL_LIMIT = 64; 048 049 /** 050 * owner of UndoManager 051 */ 052 protected final Object owner; 053 054 /** 055 * The collection of <code>AbstractIcyUndoableEdit</code>s 056 * undone/redone "en masse" by this <code>CompoundEdit</code>. 057 */ 058 protected List<AbstractIcyUndoableEdit> edits; 059 060 /** 061 * listeners 062 */ 063 protected final EventListenerList listeners; 064 065 /** 066 * internals 067 */ 068 protected int indexOfNextAdd; 069 protected int limit; 070 071 public IcyUndoManager(Object owner, int limit) 072 { 073 super(); 074 075 edits = new ArrayList<AbstractIcyUndoableEdit>(INITIAL_LIMIT / 2); 076 this.owner = owner; 077 listeners = new EventListenerList(); 078 indexOfNextAdd = 0; 079 this.limit = limit; 080 } 081 082 public IcyUndoManager(Object owner) 083 { 084 this(owner, INITIAL_LIMIT); 085 } 086 087 /** 088 * @return the owner 089 */ 090 public Object getOwner() 091 { 092 return owner; 093 } 094 095 /** 096 * Returns the maximum number of edits this {@code UndoManager} holds. A value less than 0 097 * indicates the number of edits is not limited. 098 * 099 * @return the maximum number of edits this {@code UndoManager} holds 100 * @see #addEdit 101 * @see #setLimit 102 */ 103 public synchronized int getLimit() 104 { 105 return limit; 106 } 107 108 /** 109 * Empties the undo manager sending each edit a <code>die</code> message 110 * in the process. 111 * 112 * @see AbstractUndoableEdit#die 113 */ 114 public void discardAllEdits() 115 { 116 if (trimEdits(0, edits.size() - 1)) 117 fireChangeEvent(); 118 } 119 120 /** 121 * Remove future edits (the ones to redo) from the undo manager sending each edit a 122 * <code>die</code> message in the process. 123 * 124 * @param keep 125 * number of future edits to keep (1 means we keep only the next edit) 126 * @see AbstractUndoableEdit#die 127 */ 128 public synchronized void discardFutureEdits(int keep) 129 { 130 if (trimEdits(indexOfNextAdd + keep, edits.size() - 1)) 131 fireChangeEvent(); 132 } 133 134 /** 135 * Remove future edits (the ones to redo) from the undo manager sending each edit a 136 * <code>die</code> message in the process. 137 * 138 * @see AbstractUndoableEdit#die 139 */ 140 public synchronized void discardFutureEdits() 141 { 142 discardFutureEdits(0); 143 } 144 145 /** 146 * Remove previous edits (the ones to undo) from the undo manager sending each edit a 147 * <code>die</code> message 148 * in the process. 149 * 150 * @param keep 151 * number of previous edits to keep (1 mean we keep only the last edit) 152 * @see AbstractUndoableEdit#die 153 */ 154 public synchronized void discardOldEdits(int keep) 155 { 156 if (trimEdits(0, indexOfNextAdd - (1 + keep))) 157 fireChangeEvent(); 158 } 159 160 /** 161 * Remove old edits (the ones to undo) from the undo manager sending each edit a 162 * <code>die</code> message in the process. 163 * 164 * @see AbstractUndoableEdit#die 165 */ 166 public synchronized void discardOldEdits() 167 { 168 discardOldEdits(0); 169 } 170 171 /** 172 * Reduces the number of queued edits to a range of size limit, 173 * centered on the index of the next edit. 174 */ 175 protected boolean trimForLimit() 176 { 177 boolean result = false; 178 179 if (limit >= 0) 180 { 181 synchronized (edits) 182 { 183 final int size = edits.size(); 184 185 if (size > limit) 186 { 187 final int halfLimit = limit / 2; 188 int keepFrom = indexOfNextAdd - 1 - halfLimit; 189 int keepTo = indexOfNextAdd - 1 + halfLimit; 190 191 // These are ints we're playing with, so dividing by two 192 // rounds down for odd numbers, so make sure the limit was 193 // honored properly. Note that the keep range is 194 // inclusive. 195 if (keepTo - keepFrom + 1 > limit) 196 keepFrom++; 197 198 // The keep range is centered on indexOfNextAdd, 199 // but odds are good that the actual edits Vector 200 // isn't. Move the keep range to keep it legal. 201 if (keepFrom < 0) 202 { 203 keepTo -= keepFrom; 204 keepFrom = 0; 205 } 206 if (keepTo >= size) 207 { 208 int delta = size - keepTo - 1; 209 keepTo += delta; 210 keepFrom += delta; 211 } 212 213 if (trimEdits(keepTo + 1, size - 1)) 214 result = true; 215 if (trimEdits(0, keepFrom - 1)) 216 result = true; 217 } 218 } 219 } 220 221 return result; 222 } 223 224 /** 225 * Removes edits in the specified range. 226 * All edits in the given range (inclusive, and in reverse order) 227 * will have <code>die</code> invoked on them and are removed from 228 * the list of edits. This has no effect if <code>from</code> > <code>to</code>. 229 * 230 * @param from 231 * the minimum index to remove 232 * @param to 233 * the maximum index to remove 234 * @return <code>true</code> if some edits has been removed. 235 */ 236 protected boolean trimEdits(int from, int to) 237 { 238 if (from <= to) 239 { 240 synchronized (edits) 241 { 242 for (int i = to; i >= from; i--) 243 { 244 edits.get(i).die(); 245 edits.remove(i); 246 } 247 } 248 249 if (indexOfNextAdd > to) 250 indexOfNextAdd -= to - from + 1; 251 else if (indexOfNextAdd >= from) 252 indexOfNextAdd = from; 253 254 return true; 255 } 256 257 return false; 258 } 259 260 /** 261 * Sets the maximum number of edits this <code>UndoManager</code> holds. A value less than 0 262 * indicates the number of edits is not limited. If edits need to be discarded 263 * to shrink the limit, <code>die</code> will be invoked on them in the reverse 264 * order they were added. The default is 100. 265 * 266 * @param l 267 * the new limit 268 * @throws RuntimeException 269 * if this {@code UndoManager} is not in progress 270 * ({@code end} has been invoked) 271 * @see #addEdit 272 * @see #getLimit 273 */ 274 public synchronized void setLimit(int l) 275 { 276 limit = l; 277 278 if (trimForLimit()) 279 fireChangeEvent(); 280 } 281 282 /** 283 * Returns the the next significant edit to be undone if <code>undo</code> is invoked. This 284 * returns <code>null</code> if there are no edits to be undone. 285 * 286 * @return the next significant edit to be undone 287 */ 288 protected AbstractIcyUndoableEdit editToBeUndone() 289 { 290 synchronized (edits) 291 { 292 int i = indexOfNextAdd; 293 294 while (i > 0) 295 { 296 final AbstractIcyUndoableEdit edit = edits.get(--i); 297 298 if (edit.isSignificant()) 299 return edit; 300 } 301 } 302 return null; 303 } 304 305 /** 306 * Returns the the next significant edit to be redone if <code>redo</code> is invoked. This 307 * returns <code>null</code> if there are no edits to be redone. 308 * 309 * @return the next significant edit to be redone 310 */ 311 protected AbstractIcyUndoableEdit editToBeRedone() 312 { 313 synchronized (edits) 314 { 315 final int count = edits.size(); 316 int i = indexOfNextAdd; 317 318 while (i < count) 319 { 320 final AbstractIcyUndoableEdit edit = edits.get(i++); 321 322 if (edit.isSignificant()) 323 return edit; 324 } 325 } 326 327 return null; 328 } 329 330 /** 331 * Undoes all changes. 332 * 333 * @throws CannotUndoException 334 * if one of the edits throws <code>CannotUndoException</code> 335 */ 336 public void undoAll() throws CannotUndoException 337 { 338 while (indexOfNextAdd > 0) 339 { 340 final AbstractIcyUndoableEdit next = edits.get(--indexOfNextAdd); 341 next.undo(); 342 } 343 344 // automatically remove useless edits 345 if (!canRedo()) 346 discardFutureEdits(); 347 348 fireChangeEvent(); 349 } 350 351 /** 352 * Undoes all changes from the index of the next edit to <code>edit</code>, updating the index 353 * of the next edit appropriately. 354 * 355 * @throws CannotUndoException 356 * if one of the edits throws <code>CannotUndoException</code> 357 */ 358 protected void undoTo(AbstractIcyUndoableEdit edit) throws CannotUndoException 359 { 360 boolean done = false; 361 362 while (!done) 363 { 364 final AbstractIcyUndoableEdit next = edits.get(--indexOfNextAdd); 365 next.undo(); 366 done = (next == edit); 367 } 368 } 369 370 /** 371 * Undoes the appropriate edits. This invokes <code>undo</code> on all edits between the 372 * index of the next edit and the last significant edit, updating 373 * the index of the next edit appropriately. 374 * 375 * @throws CannotUndoException 376 * if one of the edits throws <code>CannotUndoException</code> or there are no edits 377 * to be undone 378 * @see #canUndo 379 * @see #editToBeUndone 380 */ 381 @Override 382 public synchronized void undo() throws CannotUndoException 383 { 384 final AbstractIcyUndoableEdit edit = editToBeUndone(); 385 386 if (edit == null) 387 throw new CannotUndoException(); 388 389 undoTo(edit); 390 391 // automatically remove useless edits 392 if (!canRedo()) 393 discardFutureEdits(); 394 395 fireChangeEvent(); 396 } 397 398 /** 399 * Returns true if edits may be undone. This returns true if there are any edits to be undone 400 * (<code>editToBeUndone</code> returns non-<code>null</code>). 401 * 402 * @return true if there are edits to be undone 403 * @see #editToBeUndone 404 */ 405 @Override 406 public synchronized boolean canUndo() 407 { 408 final AbstractIcyUndoableEdit edit = editToBeUndone(); 409 return (edit != null) && edit.canUndo(); 410 } 411 412 /** 413 * Redoes all changes from the index of the next edit to <code>edit</code>, updating the index 414 * of the next edit appropriately. 415 * 416 * @throws CannotRedoException 417 * if one of the edits throws <code>CannotRedoException</code> 418 */ 419 protected void redoTo(AbstractIcyUndoableEdit edit) throws CannotRedoException 420 { 421 boolean done = false; 422 423 while (!done) 424 { 425 final AbstractIcyUndoableEdit next = edits.get(indexOfNextAdd++); 426 next.redo(); 427 done = (next == edit); 428 } 429 } 430 431 /** 432 * Redo the appropriate edits. This invokes <code>redo</code> on 433 * all edits between the index of the next edit and the next 434 * significant edit, updating the index of the next edit appropriately. 435 * 436 * @throws CannotRedoException 437 * if one of the edits throws <code>CannotRedoException</code> or there are no edits 438 * to be redone 439 * @see CompoundEdit#end 440 * @see #canRedo 441 * @see #editToBeRedone 442 */ 443 @Override 444 public synchronized void redo() throws CannotRedoException 445 { 446 AbstractIcyUndoableEdit edit = editToBeRedone(); 447 448 if (edit == null) 449 throw new CannotRedoException(); 450 451 redoTo(edit); 452 453 // automatically remove useless edits 454 if (!canUndo()) 455 discardOldEdits(); 456 457 fireChangeEvent(); 458 } 459 460 /** 461 * Returns true if edits may be redone. If <code>end</code> has 462 * been invoked, this returns the value from super. Otherwise, 463 * this returns true if there are any edits to be redone 464 * (<code>editToBeRedone</code> returns non-<code>null</code>). 465 * 466 * @return true if there are edits to be redone 467 * @see CompoundEdit#canRedo 468 * @see #editToBeRedone 469 */ 470 @Override 471 public synchronized boolean canRedo() 472 { 473 final AbstractIcyUndoableEdit edit = editToBeRedone(); 474 return (edit != null) && edit.canRedo(); 475 } 476 477 /** 478 * Undo or redo all changes until the specified edit.<br> 479 * The specified edit should be in "done" state after the operation.<br> 480 * That means redo operation is inclusive while undo is exclusive.<br> 481 * To undo all operations just use undoAll(). 482 */ 483 public synchronized void undoOrRedoTo(AbstractIcyUndoableEdit edit) throws CannotRedoException, CannotUndoException 484 { 485 final int index = getIndex(edit); 486 487 // can undo or redo ? 488 if (index != -1) 489 { 490 // we want indexOfNextAdd to change to (index + 1) 491 while ((indexOfNextAdd - 1) > index) 492 { 493 // process undo 494 final AbstractIcyUndoableEdit next = edits.get(--indexOfNextAdd); 495 next.undo(); 496 } 497 while (indexOfNextAdd <= index) 498 { 499 // process undo 500 AbstractIcyUndoableEdit next = edits.get(indexOfNextAdd++); 501 next.redo(); 502 } 503 504 // automatically remove useless edits 505 if (!canUndo()) 506 discardOldEdits(); 507 if (!canRedo()) 508 discardFutureEdits(); 509 510 // notify change 511 fireChangeEvent(); 512 } 513 } 514 515 /** 516 * Returns the last <code>AbstractIcyUndoableEdit</code> in <code>edits</code>, or 517 * <code>null</code> if <code>edits</code> is empty. 518 */ 519 protected AbstractIcyUndoableEdit lastEdit() 520 { 521 synchronized (edits) 522 { 523 int count = edits.size(); 524 525 if (count > 0) 526 return edits.get(count - 1); 527 } 528 529 return null; 530 } 531 532 @Override 533 public boolean addEdit(UndoableEdit anEdit) 534 { 535 if (anEdit instanceof AbstractIcyUndoableEdit) 536 addEdit((AbstractIcyUndoableEdit) anEdit); 537 538 return super.addEdit(anEdit); 539 } 540 541 /** 542 * Adds an <code>AbstractIcyUndoableEdit</code> to this <code>UndoManager</code>, if it's 543 * possible. This 544 * removes all edits from the index of the next edit to the end of the edits 545 * list. 546 * 547 * @param anEdit 548 * the edit to be added 549 * @see CompoundEdit#addEdit 550 */ 551 public synchronized void addEdit(AbstractIcyUndoableEdit anEdit) 552 { 553 synchronized (edits) 554 { 555 // Trim from the indexOfNextAdd to the end, as we'll 556 // never reach these edits once the new one is added. 557 trimEdits(indexOfNextAdd, edits.size() - 1); 558 559 final AbstractIcyUndoableEdit last = lastEdit(); 560 561 // If this is the first edit received, just add it. 562 // Otherwise, give the last one a chance to absorb the new 563 // one. If it won't, give the new one a chance to absorb 564 // the last one. 565 if (last == null) 566 edits.add(anEdit); 567 else if (!last.addEdit(anEdit)) 568 { 569 // try to replace current edit 570 if (anEdit.replaceEdit(last)) 571 edits.set(edits.size() - 1, anEdit); 572 else 573 // simply add the new edit 574 edits.add(anEdit); 575 } 576 577 // make sure the indexOfNextAdd is pointed at the right place 578 indexOfNextAdd = edits.size(); 579 580 // enforce the limit 581 trimForLimit(); 582 } 583 584 // notify change 585 fireChangeEvent(); 586 } 587 588 /** 589 * Prevent the last edit inserted in the UndoManager to be merged with the next inserted edit 590 * (even if they are compatible) 591 */ 592 public void noMergeForNextEdit() 593 { 594 // set last edit to un-mergeable 595 if (indexOfNextAdd > 0) 596 edits.get(indexOfNextAdd - 1).setMergeable(false); 597 } 598 599 /** 600 * Returns a description of the undoable form of this edit. 601 * If there are edits to be undone, this returns 602 * the value from the next significant edit that will be undone. 603 * If there are no edits to be undone this returns the value from 604 * the <code>UIManager</code> property "AbstractUndoableEdit.undoText". 605 * 606 * @return a description of the undoable form of this edit 607 * @see #undo 608 * @see CompoundEdit#getUndoPresentationName 609 */ 610 @Override 611 public synchronized String getUndoPresentationName() 612 { 613 if (canUndo()) 614 return editToBeUndone().getUndoPresentationName(); 615 616 return UIManager.getString("AbstractUndoableEdit.undoText"); 617 } 618 619 /** 620 * Returns a description of the redoable form of this edit. 621 * If there are edits to be redone, this returns 622 * the value from the next significant edit that will be redone. 623 * If there are no edits to be redone this returns the value from 624 * the <code>UIManager</code> property "AbstractUndoableEdit.redoText". 625 * 626 * @return a description of the redoable form of this edit 627 * @see #redo 628 * @see CompoundEdit#getRedoPresentationName 629 */ 630 @Override 631 public synchronized String getRedoPresentationName() 632 { 633 if (canRedo()) 634 return editToBeRedone().getRedoPresentationName(); 635 636 return UIManager.getString("AbstractUndoableEdit.redoText"); 637 } 638 639 /** 640 * Add the specified listener to listeners list 641 */ 642 public void addListener(IcyUndoManagerListener listener) 643 { 644 listeners.add(IcyUndoManagerListener.class, listener); 645 } 646 647 /** 648 * Remove the specified listener from listeners list 649 */ 650 public void removeListener(IcyUndoManagerListener listener) 651 { 652 listeners.remove(IcyUndoManagerListener.class, listener); 653 } 654 655 /** 656 * Get listeners list 657 */ 658 public IcyUndoManagerListener[] getListeners() 659 { 660 return listeners.getListeners(IcyUndoManagerListener.class); 661 } 662 663 /** 664 * fire change event 665 */ 666 private void fireChangeEvent() 667 { 668 for (IcyUndoManagerListener listener : listeners.getListeners(IcyUndoManagerListener.class)) 669 listener.undoManagerChanged(this); 670 } 671 672 /** 673 * Retrieve all edits in the UndoManager 674 */ 675 public List<AbstractIcyUndoableEdit> getAllEdits() 676 { 677 synchronized (edits) 678 { 679 return new ArrayList<AbstractIcyUndoableEdit>(edits); 680 } 681 } 682 683 /** 684 * Get number of edit in UndoManager 685 */ 686 public int getEditsCount() 687 { 688 return edits.size(); 689 } 690 691 /** 692 * Get the index in list of specified edit 693 */ 694 public int getIndex(AbstractIcyUndoableEdit e) 695 { 696 return edits.indexOf(e); 697 } 698 699 /** 700 * Get the index in list of specified significant edit 701 */ 702 public int getSignificantIndex(AbstractIcyUndoableEdit e) 703 { 704 int result = 0; 705 706 synchronized (edits) 707 { 708 for (AbstractIcyUndoableEdit edit : edits) 709 { 710 if (edit.isSignificant()) 711 { 712 if (edit == e) 713 return result; 714 result++; 715 } 716 } 717 } 718 719 return -1; 720 } 721 722 /** 723 * Get number of significant edit before the specified position in UndoManager 724 */ 725 public int getSignificantEditsCountBefore(int index) 726 { 727 int result = 0; 728 729 synchronized (edits) 730 { 731 for (int i = 0; i < index; i++) 732 if (edits.get(i).isSignificant()) 733 result++; 734 } 735 736 return result; 737 } 738 739 /** 740 * Get number of significant edit in UndoManager 741 */ 742 public int getSignificantEditsCount() 743 { 744 int result = 0; 745 746 synchronized (edits) 747 { 748 for (AbstractIcyUndoableEdit edit : edits) 749 if (edit.isSignificant()) 750 result++; 751 } 752 753 return result; 754 } 755 756 /** 757 * Get edit of specified index 758 */ 759 public AbstractIcyUndoableEdit getEdit(int index) 760 { 761 synchronized (edits) 762 { 763 if (index < edits.size()) 764 return edits.get(index); 765 } 766 767 return null; 768 } 769 770 /** 771 * Get significant edit of specified index 772 */ 773 public AbstractIcyUndoableEdit getSignificantEdit(int index) 774 { 775 int i = 0; 776 777 synchronized (edits) 778 { 779 for (AbstractIcyUndoableEdit edit : edits) 780 { 781 if (edit.isSignificant()) 782 { 783 if (i == index) 784 return edit; 785 i++; 786 } 787 } 788 } 789 790 return null; 791 } 792 793 /** 794 * Return the next insert index. 795 */ 796 public int getNextAddIndex() 797 { 798 return indexOfNextAdd; 799 } 800 801 /** 802 * Get index of first edit from specified source 803 */ 804 public int getFirstEditIndex(Object source) 805 { 806 if (source == null) 807 return -1; 808 809 synchronized (edits) 810 { 811 for (int i = 0; i < edits.size(); i++) 812 if ((edits.get(i)).getSource() == source) 813 return i; 814 } 815 816 return -1; 817 } 818 819 /** 820 * Get index of last edit from specified source 821 */ 822 public int getLastEditIndex(Object source) 823 { 824 if (source == null) 825 return -1; 826 827 synchronized (edits) 828 { 829 for (int i = edits.size() - 1; i >= 0; i--) 830 if ((edits.get(i)).getSource() == source) 831 return i; 832 } 833 834 return -1; 835 } 836 837 /** 838 * Discard edits from specified source by sending each edit a <code>die</code> message 839 * in the process. 840 */ 841 public void discardEdits(Object source) 842 { 843 synchronized (edits) 844 { 845 final int lastIndex = getLastEditIndex(source); 846 847 if (lastIndex != -1) 848 { 849 final List<AbstractIcyUndoableEdit> validEdits = new ArrayList<AbstractIcyUndoableEdit>(); 850 851 // keep valid edits 852 for (int i = lastIndex + 1; i < edits.size(); i++) 853 validEdits.add(edits.get(i)); 854 855 // remove all edits 856 edits.clear(); 857 indexOfNextAdd = 0; 858 859 // add valid edits 860 for (AbstractIcyUndoableEdit edit : validEdits) 861 edits.add(edit); 862 863 // make sure the indexOfNextAdd is pointed at the right place 864 indexOfNextAdd = edits.size(); 865 866 // notify we removed some edits 867 fireChangeEvent(); 868 } 869 } 870 } 871 872 @Override 873 public void undoableEditHappened(UndoableEditEvent e) 874 { 875 addEdit(e.getEdit()); 876 } 877 878}