xref: /trunk/main/sfx2/qa/complex/sfx2/UndoManager.java (revision e6b649b5)
1 /**************************************************************
2  *
3  * Licensed to the Apache Software Foundation (ASF) under one
4  * or more contributor license agreements.  See the NOTICE file
5  * distributed with this work for additional information
6  * regarding copyright ownership.  The ASF licenses this file
7  * to you under the Apache License, Version 2.0 (the
8  * "License"); you may not use this file except in compliance
9  * with the License.  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,
14  * software distributed under the License is distributed on an
15  * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16  * KIND, either express or implied.  See the License for the
17  * specific language governing permissions and limitations
18  * under the License.
19  *
20  *************************************************************/
21 
22 
23 
24 package complex.sfx2;
25 
26 import com.sun.star.accessibility.XAccessible;
27 import com.sun.star.accessibility.XAccessibleAction;
28 import com.sun.star.awt.Point;
29 import com.sun.star.awt.Size;
30 import com.sun.star.awt.XControl;
31 import com.sun.star.awt.XControlModel;
32 import com.sun.star.beans.NamedValue;
33 import com.sun.star.beans.XPropertySet;
34 import com.sun.star.container.NoSuchElementException;
35 import com.sun.star.container.XChild;
36 import com.sun.star.container.XIndexContainer;
37 import com.sun.star.container.XNameContainer;
38 import com.sun.star.container.XNameReplace;
39 import com.sun.star.container.XSet;
40 import com.sun.star.document.EmptyUndoStackException;
41 import com.sun.star.document.UndoContextNotClosedException;
42 import com.sun.star.document.UndoFailedException;
43 import com.sun.star.document.UndoManagerEvent;
44 import com.sun.star.document.XEmbeddedScripts;
45 import com.sun.star.document.XEventsSupplier;
46 import com.sun.star.document.XUndoAction;
47 import com.sun.star.lang.EventObject;
48 import com.sun.star.lang.IndexOutOfBoundsException;
49 import com.sun.star.lang.XEventListener;
50 import java.lang.reflect.InvocationTargetException;
51 import org.openoffice.test.tools.OfficeDocument;
52 import com.sun.star.document.XUndoManagerSupplier;
53 import com.sun.star.document.XUndoManager;
54 import com.sun.star.document.XUndoManagerListener;
55 import com.sun.star.drawing.XControlShape;
56 import com.sun.star.drawing.XDrawPage;
57 import com.sun.star.drawing.XDrawPageSupplier;
58 import com.sun.star.drawing.XShapes;
59 import com.sun.star.lang.XComponent;
60 import com.sun.star.lang.XMultiServiceFactory;
61 import com.sun.star.lang.XServiceInfo;
62 import com.sun.star.lang.XSingleComponentFactory;
63 import com.sun.star.lang.XTypeProvider;
64 import com.sun.star.script.ScriptEventDescriptor;
65 import com.sun.star.script.XEventAttacherManager;
66 import com.sun.star.script.XLibraryContainer;
67 import com.sun.star.task.XJob;
68 import com.sun.star.uno.Type;
69 import com.sun.star.uno.UnoRuntime;
70 import com.sun.star.uno.XComponentContext;
71 import com.sun.star.util.InvalidStateException;
72 import com.sun.star.util.NotLockedException;
73 import com.sun.star.view.XControlAccess;
74 import complex.sfx2.undo.CalcDocumentTest;
75 import complex.sfx2.undo.ChartDocumentTest;
76 import complex.sfx2.undo.DocumentTest;
77 import complex.sfx2.undo.DrawDocumentTest;
78 import complex.sfx2.undo.ImpressDocumentTest;
79 import complex.sfx2.undo.WriterDocumentTest;
80 import java.lang.reflect.Method;
81 import java.util.ArrayList;
82 import java.util.Iterator;
83 import java.util.Stack;
84 import org.junit.After;
85 import org.junit.AfterClass;
86 import org.junit.Before;
87 import org.junit.BeforeClass;
88 import org.junit.Test;
89 import static org.junit.Assert.*;
90 import org.openoffice.test.OfficeConnection;
91 import org.openoffice.test.tools.DocumentType;
92 import org.openoffice.test.tools.SpreadsheetDocument;
93 
94 /**
95  * Unit test for the UndoManager API
96  *
97  * @author frank.schoenheit@oracle.com
98  */
99 public class UndoManager
100 {
101     // -----------------------------------------------------------------------------------------------------------------
102     @Before
beforeTest()103     public void beforeTest() throws com.sun.star.uno.Exception
104     {
105         m_currentTestCase = null;
106         m_currentDocument = null;
107         m_undoListener = null;
108 
109         // at our service factory, insert a new factory for our CallbackComponent
110         // this will allow the Basic code in our test documents to call back into this test case
111         // here, by just instantiating this service
112         final XSet globalFactory = UnoRuntime.queryInterface( XSet.class, getORB() );
113         m_callbackFactory = new CallbackComponentFactory();
114         globalFactory.insert( m_callbackFactory );
115     }
116 
117     // -----------------------------------------------------------------------------------------------------------------
118     @Test
checkWriterUndo()119     public void checkWriterUndo() throws Exception
120     {
121         m_currentTestCase = new WriterDocumentTest( getORB() );
122         impl_checkUndo();
123     }
124 
125     // -----------------------------------------------------------------------------------------------------------------
126     @Test
checkCalcUndo()127     public void checkCalcUndo() throws Exception
128     {
129         m_currentTestCase = new CalcDocumentTest( getORB() );
130         impl_checkUndo();
131     }
132 
133     // -----------------------------------------------------------------------------------------------------------------
134     @Test
checkDrawUndo()135     public void checkDrawUndo() throws Exception
136     {
137         m_currentTestCase = new DrawDocumentTest( getORB() );
138         impl_checkUndo();
139     }
140 
141     // -----------------------------------------------------------------------------------------------------------------
142     @Test
checkImpressUndo()143     public void checkImpressUndo() throws Exception
144     {
145         m_currentTestCase = new ImpressDocumentTest( getORB() );
146         impl_checkUndo();
147     }
148 
149     // -----------------------------------------------------------------------------------------------------------------
150     @Test
checkChartUndo()151     public void checkChartUndo() throws Exception
152     {
153         m_currentTestCase = new ChartDocumentTest( getORB() );
154         impl_checkUndo();
155     }
156 
157     // -----------------------------------------------------------------------------------------------------------------
158     @Test
checkBrokenScripts()159     public void checkBrokenScripts() throws com.sun.star.uno.Exception, InterruptedException
160     {
161         System.out.println( "testing: broken scripts" );
162 
163         m_currentDocument = OfficeDocument.blankDocument( getORB(), DocumentType.CALC );
164         m_undoListener = new UndoListener();
165         getUndoManager().addUndoManagerListener( m_undoListener );
166 
167         impl_setupBrokenBasicScript();
168         final String scriptURI = "vnd.sun.star.script:default.callbacks.brokenScript?language=Basic&location=document";
169 
170         // .............................................................................................................
171         // scenario 1: Pressing a button which is bound to execute the script
172         // (This is one of the many cases where SfxObjectShell::CallXScript is invoked)
173 
174         // set up the button
175         final XPropertySet buttonModel = impl_setupButton();
176         buttonModel.setPropertyValue( "Label", "exec broken script" );
177         impl_assignScript( buttonModel, "XActionListener", "actionPerformed",
178             scriptURI );
179 
180         // switch the doc's view to form alive mode (so the button will actually work)
181         m_currentDocument.getCurrentView().dispatch( ".uno:SwitchControlDesignMode" );
182 
183         // click the button
184         m_callbackCalled = false;
185         impl_clickButton( buttonModel );
186         // the macro is executed asynchronously by the button, so wait at most 2 seconds for the callback to be
187         // triggered
188         impl_waitFor( m_callbackCondition, 2000 );
189         // check the callback has actually been called
190         assertTrue( "clicking the test button did not work as expected - basic script not called", m_callbackCalled );
191 
192         // again, since the script is executed asynchronously, we might arrive here while its execution
193         // is not completely finished. Give OOo another (at most) 2 seconds to finish it.
194         m_undoListener.waitForAllContextsClosed( 20000 );
195         // assure that the Undo Context Depth of the doc is still "0": The Basic script entered such a
196         // context, and didn't close it (thus it is broken), but the application framework should have
197         // auto-closed the context after the macro finished.
198         assertEquals( "undo context was not auto-closed as expected", 0, m_undoListener.getCurrentUndoContextDepth() );
199 
200         // .............................................................................................................
201         // scenario 2: dispatching the script URL. Technically, this is equivalent to configuring the
202         // script into a menu or toolbar, and selecting the respective menu/toolbar item
203         m_callbackCalled = false;
204         m_currentDocument.getCurrentView().dispatch( scriptURI );
205         assertTrue( "dispatching the Script URL did not work as expected - basic script not called", m_callbackCalled );
206         // same as above: The script didn't close the context, but the OOo framework should have
207         assertEquals( "undo context was not auto-closed as expected", 0, m_undoListener.getCurrentUndoContextDepth() );
208 
209         // .............................................................................................................
210         // scenario 3: assigning the script to some document event, and triggering this event
211         final XEventsSupplier eventSupplier = UnoRuntime.queryInterface( XEventsSupplier.class, m_currentDocument.getDocument() );
212         final XNameReplace events = UnoRuntime.queryInterface( XNameReplace.class, eventSupplier.getEvents() );
213         final NamedValue[] scriptDescriptor = new NamedValue[] {
214             new NamedValue( "EventType", "Script" ),
215             new NamedValue( "Script", scriptURI )
216         };
217         events.replaceByName( "OnViewCreated", scriptDescriptor );
218 
219         // The below doesn't work: event notification is broken in m96, see http://www.openoffice.org/issues/show_bug.cgi?id=116313
220         m_callbackCalled = false;
221         m_currentDocument.getCurrentView().dispatch( ".uno:NewWindow" );
222         assertTrue( "triggering an event did not work as expected - basic script not called", m_callbackCalled );
223         // same as above: The script didn't close the context, but the OOo framework should have
224         assertEquals( "undo context was not auto-closed as expected", 0, m_undoListener.getCurrentUndoContextDepth() );
225 
226         // .............................................................................................................
227         // scenario 4: let the script enter an Undo context, but not close it, as usual.
228         // Additionally, let the script close the document - the OOo framework code which cares for
229         // auto-closing of Undo contexts should survive this, ideally ...
230         m_closeAfterCallback = true;
231         m_callbackCalled = false;
232         m_currentDocument.getCurrentView().dispatch( scriptURI );
233         assertTrue( m_callbackCalled );
234         assertTrue( "The Basic script should have closed the document.", m_undoListener.isDisposed() );
235         m_currentDocument = null;
236     }
237 
238     // -----------------------------------------------------------------------------------------------------------------
239     @Test
checkSerialization()240     public void checkSerialization() throws com.sun.star.uno.Exception, InterruptedException
241     {
242         System.out.println( "testing: request serialization" );
243 
244         m_currentDocument = OfficeDocument.blankDocument( getORB(), DocumentType.CALC );
245         final XUndoManager undoManager = getUndoManager();
246 
247         final int threadCount = 10;
248         final int actionsPerThread = 10;
249         final int actionCount = threadCount * actionsPerThread;
250 
251         // add some actions to the UndoManager, each knowing its position on the stack
252         final Object lock = new Object();
253         final Integer actionsUndone[] = new Integer[] { 0 };
254         for ( int i=actionCount; i>0; )
255             undoManager.addUndoAction( new CountingUndoAction( --i, lock, actionsUndone ) );
256 
257         // some concurrent threads which undo the actions
258         Thread[] threads = new Thread[threadCount];
259         for ( int i=0; i<threadCount; ++i )
260         {
261             threads[i] = new Thread()
262             {
263                 @Override
264                 public void run()
265                 {
266                     for ( int j=0; j<actionsPerThread; ++j )
267                     {
268                         try { undoManager.undo(); }
269                         catch ( final Exception e )
270                         {
271                             fail( "Those dummy actions are not expected to fail." );
272                             return;
273                         }
274                     }
275                 }
276             };
277         }
278 
279         // start the threads
280         for ( int i=0; i<threadCount; ++i )
281             threads[i].start();
282 
283         // wait for them to be finished
284         for ( int i=0; i<threadCount; ++i )
285             threads[i].join();
286 
287         // ensure all actions have been undone
288         assertEquals( "not all actions have been undone", actionCount, actionsUndone[0].intValue() );
289     }
290 
291     // -----------------------------------------------------------------------------------------------------------------
292     @After
afterTest()293     public void afterTest()
294     {
295         if ( m_currentTestCase != null )
296             m_currentTestCase.closeDocument();
297         else if ( m_currentDocument != null )
298             m_currentDocument.close();
299         m_currentTestCase = null;
300         m_currentDocument = null;
301         m_callbackFactory.dispose();
302     }
303 
304     // -----------------------------------------------------------------------------------------------------------------
305     /**
306      * @return returns the undo manager belonging to a given document
307      */
getUndoManager()308     private XUndoManager getUndoManager()
309     {
310         final XUndoManagerSupplier suppUndo = UnoRuntime.queryInterface( XUndoManagerSupplier.class, m_currentDocument.getDocument() );
311         final XUndoManager undoManager = suppUndo.getUndoManager();
312         assertTrue( UnoRuntime.areSame( undoManager.getParent(), m_currentDocument.getDocument() ) );
313         return undoManager;
314     }
315 
316     // -----------------------------------------------------------------------------------------------------------------
impl_waitFor( final Object i_condition, final int i_milliSeconds )317     private void impl_waitFor( final Object i_condition, final int i_milliSeconds ) throws InterruptedException
318     {
319         synchronized( i_condition )
320         {
321             i_condition.wait( i_milliSeconds );
322         }
323     }
324 
325     // -----------------------------------------------------------------------------------------------------------------
impl_setupBrokenBasicScript()326     private void impl_setupBrokenBasicScript()
327     {
328         try
329         {
330             final XEmbeddedScripts embeddedScripts = UnoRuntime.queryInterface( XEmbeddedScripts.class, m_currentDocument.getDocument() );
331             final XLibraryContainer basicLibs = embeddedScripts.getBasicLibraries();
332             final XNameContainer basicLib = basicLibs.createLibrary( "default" );
333 
334             final String brokenScriptCode =
335                 "Option Explicit\n" +
336                 "\n" +
337                 "Sub brokenScript\n" +
338                 "    Dim callback as Object\n" +
339                 "    ThisComponent.UndoManager.enterUndoContext( \"" + getCallbackUndoContextTitle() + "\" )\n" +
340                 "\n" +
341                 "    callback = createUnoService( \"" + getCallbackComponentServiceName() + "\" )\n" +
342                 "    Dim emptyArgs() as new com.sun.star.beans.NamedValue\n" +
343                 "    Dim result as String\n" +
344                 "    result = callback.execute( emptyArgs() )\n" +
345                 "    If result = \"close\" Then\n" +
346                 "        ThisComponent.close( TRUE )\n" +
347                 "    End If\n" +
348                 "End Sub\n" +
349                 "\n";
350 
351             basicLib.insertByName( "callbacks", brokenScriptCode );
352         }
353         catch( com.sun.star.uno.Exception e )
354         {
355             fail( "caught an exception while setting up the script: " + e.toString() );
356         }
357     }
358 
359     // -----------------------------------------------------------------------------------------------------------------
impl_setupButton()360     private XPropertySet impl_setupButton() throws com.sun.star.uno.Exception
361     {
362         // let the document create a shape
363         final XMultiServiceFactory docAsFactory = UnoRuntime.queryInterface( XMultiServiceFactory.class,
364             m_currentDocument.getDocument() );
365         final XControlShape xShape = UnoRuntime.queryInterface( XControlShape.class,
366             docAsFactory.createInstance( "com.sun.star.drawing.ControlShape" ) );
367 
368         // position and size of the shape
369         xShape.setSize( new Size( 28 * 100, 10 * 100 ) );
370         xShape.setPosition( new Point( 10 * 100, 10 * 100 ) );
371 
372         // create the form component (the model of a form control)
373         final String sQualifiedComponentName = "com.sun.star.form.component.CommandButton";
374         final XControlModel controlModel = UnoRuntime.queryInterface( XControlModel.class,
375             getORB().createInstance( sQualifiedComponentName ) );
376 
377         // knitt both
378         xShape.setControl( controlModel );
379 
380         // add the shape to the shapes collection of the document
381         SpreadsheetDocument spreadsheetDoc = (SpreadsheetDocument)m_currentDocument;
382         final XDrawPageSupplier suppDrawPage = UnoRuntime.queryInterface( XDrawPageSupplier.class,
383             spreadsheetDoc.getSheet( 0 ) );
384         final XDrawPage insertIntoPage = suppDrawPage.getDrawPage();
385 
386         final XShapes sheetShapes = UnoRuntime.queryInterface( XShapes.class, insertIntoPage );
387         sheetShapes.add( xShape );
388 
389         return UnoRuntime.queryInterface( XPropertySet.class, controlModel );
390     }
391 
392     // -----------------------------------------------------------------------------------------------------------------
impl_assignScript( final XPropertySet i_controlModel, final String i_interfaceName, final String i_interfaceMethod, final String i_scriptURI )393     private void impl_assignScript( final XPropertySet i_controlModel, final String i_interfaceName,
394         final String i_interfaceMethod, final String i_scriptURI )
395     {
396         try
397         {
398             final XChild modelAsChild = UnoRuntime.queryInterface( XChild.class, i_controlModel );
399             final XIndexContainer parentForm = UnoRuntime.queryInterface( XIndexContainer.class, modelAsChild.getParent() );
400 
401             final XEventAttacherManager manager = UnoRuntime.queryInterface( XEventAttacherManager.class, parentForm );
402 
403             int containerPosition = -1;
404             for ( int i = 0; i < parentForm.getCount(); ++i )
405             {
406                 final XPropertySet child = UnoRuntime.queryInterface( XPropertySet.class, parentForm.getByIndex( i ) );
407                 if ( UnoRuntime.areSame( child, i_controlModel ) )
408                 {
409                     containerPosition = i;
410                     break;
411                 }
412             }
413             assertFalse( "could not find the given control model within its parent", containerPosition == -1 );
414             manager.registerScriptEvent( containerPosition, new ScriptEventDescriptor(
415                 i_interfaceName,
416                 i_interfaceMethod,
417                 "",
418                 "Script",
419                 i_scriptURI
420             ) );
421         }
422         catch( com.sun.star.uno.Exception e )
423         {
424             fail( "caught an exception while assigning the script event to the button: " + e.toString() );
425         }
426     }
427 
428     // -----------------------------------------------------------------------------------------------------------------
impl_clickButton( final XPropertySet i_buttonModel )429     private void impl_clickButton( final XPropertySet i_buttonModel ) throws NoSuchElementException, IndexOutOfBoundsException
430     {
431         final XControlAccess controlAccess = UnoRuntime.queryInterface( XControlAccess.class,
432             m_currentDocument.getCurrentView().getController() );
433         final XControl control = controlAccess.getControl( UnoRuntime.queryInterface( XControlModel.class, i_buttonModel ) );
434         final XAccessible accessible = UnoRuntime.queryInterface( XAccessible.class, control );
435         final XAccessibleAction controlActions = UnoRuntime.queryInterface( XAccessibleAction.class, accessible.getAccessibleContext() );
436         for ( int i=0; i<controlActions.getAccessibleActionCount(); ++i )
437         {
438             if ( controlActions.getAccessibleActionDescription(i).equals( "click" ) )
439             {
440                 controlActions.doAccessibleAction(i);
441                 return;
442             }
443         }
444         fail( "did not find the accessible action named 'click'" );
445     }
446 
447     // -----------------------------------------------------------------------------------------------------------------
448     private static class UndoListener implements XUndoManagerListener
449     {
undoActionAdded( UndoManagerEvent i_event )450         public void undoActionAdded( UndoManagerEvent i_event )
451         {
452             assertFalse( "|undoActionAdded| called after document was disposed", m_isDisposed );
453 
454             ++m_undoActionsAdded;
455             m_mostRecentlyAddedAction = i_event.UndoActionTitle;
456         }
457 
actionUndone( UndoManagerEvent i_event )458         public void actionUndone( UndoManagerEvent i_event )
459         {
460             assertFalse( "|actionUndone| called after document was disposed", m_isDisposed );
461 
462             ++m_undoCount;
463             m_mostRecentlyUndone = i_event.UndoActionTitle;
464         }
465 
actionRedone( UndoManagerEvent i_event )466         public void actionRedone( UndoManagerEvent i_event )
467         {
468             assertFalse( "|actionRedone| called after document was disposed", m_isDisposed );
469 
470             ++m_redoCount;
471         }
472 
allActionsCleared( EventObject eo )473         public void allActionsCleared( EventObject eo )
474         {
475             assertFalse( "|allActionsCleared| called after document was disposed", m_isDisposed );
476 
477             m_wasCleared = true;
478         }
479 
redoActionsCleared( EventObject eo )480         public void redoActionsCleared( EventObject eo )
481         {
482             assertFalse( "|redoActionsCleared| called after document was disposed", m_isDisposed );
483 
484             m_redoWasCleared = true;
485         }
486 
resetAll( EventObject i_event )487         public void resetAll( EventObject i_event )
488         {
489             assertFalse( "|resetAll| called after document was disposed", m_isDisposed );
490 
491             m_managerWasReset = true;
492             m_activeUndoContexts.clear();
493         }
494 
enteredContext( UndoManagerEvent i_event )495         public void enteredContext( UndoManagerEvent i_event )
496         {
497             assertFalse( "|enteredContext| called after document was disposed", m_isDisposed );
498 
499             m_activeUndoContexts.push( i_event.UndoActionTitle );
500             assertEquals( "different opinions on the context nesting level (after entering)",
501                 m_activeUndoContexts.size(), i_event.UndoContextDepth );
502         }
503 
enteredHiddenContext( UndoManagerEvent i_event )504         public void enteredHiddenContext( UndoManagerEvent i_event )
505         {
506             assertFalse( "|enteredHiddenContext| called after document was disposed", m_isDisposed );
507 
508             m_activeUndoContexts.push( i_event.UndoActionTitle );
509             assertEquals( "different opinions on the context nesting level (after entering hidden)",
510                 m_activeUndoContexts.size(), i_event.UndoContextDepth );
511         }
512 
leftContext( UndoManagerEvent i_event )513         public void leftContext( UndoManagerEvent i_event )
514         {
515             assertFalse( "|leftContext| called after document was disposed", m_isDisposed );
516 
517             assertEquals( "nested undo context descriptions do not match", m_activeUndoContexts.pop(), i_event.UndoActionTitle );
518             assertEquals( "different opinions on the context nesting level (after leaving)",
519                 m_activeUndoContexts.size(), i_event.UndoContextDepth );
520             m_leftContext = true;
521             impl_notifyContextDepth();
522         }
523 
leftHiddenContext( UndoManagerEvent i_event )524         public void leftHiddenContext( UndoManagerEvent i_event )
525         {
526             assertFalse( "|leftHiddenContext| called after document was disposed", m_isDisposed );
527             assertEquals( "|leftHiddenContext| is not expected to notify an action title", 0, i_event.UndoActionTitle.length() );
528 
529             m_activeUndoContexts.pop();
530             assertEquals( "different opinions on the context nesting level (after leaving)",
531                 m_activeUndoContexts.size(), i_event.UndoContextDepth );
532             m_leftHiddenContext = true;
533             impl_notifyContextDepth();
534         }
535 
cancelledContext( UndoManagerEvent i_event )536         public void cancelledContext( UndoManagerEvent i_event )
537         {
538             assertFalse( "|cancelledContext| called after document was disposed", m_isDisposed );
539             assertEquals( "|cancelledContext| is not expected to notify an action title", 0, i_event.UndoActionTitle.length() );
540 
541             m_activeUndoContexts.pop();
542             assertEquals( "different opinions on the context nesting level (after cancelling)",
543                 m_activeUndoContexts.size(), i_event.UndoContextDepth );
544             m_cancelledContext = true;
545             impl_notifyContextDepth();
546         }
547 
disposing( EventObject i_event )548         public void disposing( EventObject i_event )
549         {
550             m_isDisposed = true;
551         }
552 
waitForAllContextsClosed( final int i_milliSeconds )553         public void waitForAllContextsClosed( final int i_milliSeconds ) throws InterruptedException
554         {
555             synchronized ( m_allContextsClosedCondition )
556             {
557                 if ( m_activeUndoContexts.empty() )
558                     return;
559                 m_allContextsClosedCondition.wait( i_milliSeconds );
560             }
561         }
562 
impl_notifyContextDepth()563         private void impl_notifyContextDepth()
564         {
565             synchronized ( m_allContextsClosedCondition )
566             {
567                 if ( m_activeUndoContexts.empty() )
568                 {
569                     m_allContextsClosedCondition.notifyAll();
570                 }
571             }
572         }
573 
getUndoActionsAdded()574         private int     getUndoActionsAdded() { return m_undoActionsAdded; }
getUndoActionCount()575         private int     getUndoActionCount() { return m_undoCount; }
getRedoActionCount()576         private int     getRedoActionCount() { return m_redoCount; }
getCurrentUndoContextTitle()577         private String  getCurrentUndoContextTitle() { return m_activeUndoContexts.peek(); }
getMostRecentlyAddedActionTitle()578         private String  getMostRecentlyAddedActionTitle() { return m_mostRecentlyAddedAction; };
getMostRecentlyUndoneTitle()579         private String  getMostRecentlyUndoneTitle() { return m_mostRecentlyUndone; }
getCurrentUndoContextDepth()580         private int     getCurrentUndoContextDepth() { return m_activeUndoContexts.size(); }
isDisposed()581         private boolean isDisposed() { return m_isDisposed; }
wasContextLeft()582         private boolean wasContextLeft() { return m_leftContext; }
wasHiddenContextLeft()583         private boolean wasHiddenContextLeft() { return m_leftHiddenContext; }
hasContextBeenCancelled()584         private boolean hasContextBeenCancelled() { return m_cancelledContext; }
wereStacksCleared()585         private boolean wereStacksCleared() { return m_wasCleared; }
wasRedoStackCleared()586         private boolean wasRedoStackCleared() { return m_redoWasCleared; }
wasManagerReset()587         private boolean wasManagerReset() { return m_managerWasReset; }
588 
reset()589         void reset()
590         {
591             m_undoActionsAdded = m_undoCount = m_redoCount = 0;
592             m_activeUndoContexts.clear();
593             m_mostRecentlyAddedAction = m_mostRecentlyUndone = null;
594             // m_isDisposed is not cleared, intentionally
595             m_leftContext = m_leftHiddenContext = m_cancelledContext = m_wasCleared = m_redoWasCleared = m_managerWasReset = false;
596         }
597 
598         private int     m_undoActionsAdded = 0;
599         private int     m_undoCount = 0;
600         private int     m_redoCount = 0;
601         private boolean m_isDisposed = false;
602         private boolean m_leftContext = false;
603         private boolean m_leftHiddenContext = false;
604         private boolean m_cancelledContext = false;
605         private boolean m_wasCleared = false;
606         private boolean m_redoWasCleared = false;
607         private boolean m_managerWasReset = false;
608         private Stack< String >
609                         m_activeUndoContexts = new Stack<String>();
610         private String  m_mostRecentlyAddedAction = null;
611         private String  m_mostRecentlyUndone = null;
612         private final Object    m_allContextsClosedCondition = new Object();
613     };
614 
615     // -----------------------------------------------------------------------------------------------------------------
impl_checkUndo()616     private void impl_checkUndo() throws Exception
617     {
618         System.out.println( "testing: " + m_currentTestCase.getDocumentDescription() );
619         m_currentDocument = m_currentTestCase.getDocument();
620         m_currentTestCase.initializeDocument();
621         m_currentTestCase.verifyInitialDocumentState();
622 
623         final XUndoManager undoManager = getUndoManager();
624         undoManager.clear();
625         assertFalse( "clearing the Undo manager should result in the impossibility to undo anything", undoManager.isUndoPossible() );
626         assertFalse( "clearing the Undo manager should result in the impossibility to redo anything", undoManager.isRedoPossible() );
627 
628         m_undoListener = new UndoListener();
629         undoManager.addUndoManagerListener( m_undoListener );
630 
631         impl_testSingleModification( undoManager );
632         impl_testMultipleModifications( undoManager );
633         impl_testCustomUndoActions( undoManager );
634         impl_testLocking( undoManager );
635         impl_testNestedContexts( undoManager );
636         impl_testErrorHandling( undoManager );
637         impl_testContextHandling( undoManager );
638         impl_testStackHandling( undoManager );
639         impl_testClearance( undoManager );
640         impl_testHiddenContexts( undoManager );
641 
642         // close the document, ensure the Undo manager listener gets notified
643         m_currentTestCase.closeDocument();
644         m_currentTestCase = null;
645         m_currentDocument = null;
646         assertTrue( "document is closed, but the UndoManagerListener has not been notified of the disposal", m_undoListener.isDisposed() );
647     }
648 
649     // -----------------------------------------------------------------------------------------------------------------
impl_testSingleModification( final XUndoManager i_undoManager )650     private void impl_testSingleModification( final XUndoManager i_undoManager ) throws com.sun.star.uno.Exception
651     {
652         m_currentTestCase.doSingleModification();
653         m_currentTestCase.verifySingleModificationDocumentState();
654 
655         // undo the modification, ensure the listener got the proper notifications
656         assertEquals( "We did not yet do a undo!", 0, m_undoListener.getUndoActionCount() );
657         i_undoManager.undo();
658         assertEquals( "A simple undo does not result in the proper Undo count.",
659             1, m_undoListener.getUndoActionCount() );
660 
661         // verify the document is in its initial state, again
662         m_currentTestCase.verifyInitialDocumentState();
663 
664         // redo the modification, ensure the listener got the proper notifications
665         assertEquals( "did not yet do a redo!", 0, m_undoListener.getRedoActionCount() );
666         i_undoManager.redo();
667         assertEquals( "did a redo, but got no notification of it!", 1, m_undoListener.getRedoActionCount() );
668         // ensure the document is in the proper state, again
669         m_currentTestCase.verifySingleModificationDocumentState();
670 
671         // now do an Undo via the UI (aka the dispatch API), and see if this works, and notifies the listener as
672         // expected
673         m_currentTestCase.getDocument().getCurrentView().dispatch( ".uno:Undo" );
674         m_currentTestCase.verifyInitialDocumentState();
675         assertEquals( "UI-Undo does not notify the listener", 2, m_undoListener.getUndoActionCount() );
676     }
677 
678     // -----------------------------------------------------------------------------------------------------------------
impl_testMultipleModifications( final XUndoManager i_undoManager )679     private void impl_testMultipleModifications( final XUndoManager i_undoManager ) throws com.sun.star.uno.Exception
680     {
681         m_undoListener.reset();
682         assertEquals( "unexpected initial undo context depth", 0, m_undoListener.getCurrentUndoContextDepth() );
683         i_undoManager.enterUndoContext( "Batch Changes" );
684         assertEquals( "unexpected undo context depth after entering a context",
685             1, m_undoListener.getCurrentUndoContextDepth() );
686         assertEquals( "entering an Undo context has not been notified properly",
687             "Batch Changes", m_undoListener.getCurrentUndoContextTitle() );
688 
689         final int modifications = m_currentTestCase.doMultipleModifications();
690         assertEquals( "unexpected number of undo actions while doing batch changes to the document",
691             modifications, m_undoListener.getUndoActionsAdded() );
692         assertEquals( "seems the document operations touched the undo context depth",
693             1, m_undoListener.getCurrentUndoContextDepth() );
694 
695         i_undoManager.leaveUndoContext();
696         assertEquals( "unexpected undo context depth after leaving the last context",
697             0, m_undoListener.getCurrentUndoContextDepth() );
698         assertEquals( "no Undo done, yet - still the listener has been notified of an Undo action",
699             0, m_undoListener.getUndoActionCount() );
700 
701         i_undoManager.undo();
702         assertEquals( "Just did an undo - the listener should have been notified", 1, m_undoListener.getUndoActionCount() );
703         m_currentTestCase.verifyInitialDocumentState();
704     }
705 
706     // -----------------------------------------------------------------------------------------------------------------
impl_testCustomUndoActions( final XUndoManager i_undoManager )707     private void impl_testCustomUndoActions( final XUndoManager i_undoManager ) throws com.sun.star.uno.Exception
708     {
709         i_undoManager.clear();
710         m_undoListener.reset();
711         assertFalse( "undo stack not empty after clearing the undo manager", i_undoManager.isUndoPossible() );
712         assertFalse( "redo stack not empty after clearing the undo manager", i_undoManager.isRedoPossible() );
713         assertArrayEquals( ">0 descriptions for an empty undo stack?",
714             new String[0], i_undoManager.getAllUndoActionTitles() );
715         assertArrayEquals( ">0 descriptions for an empty redo stack?",
716             new String[0], i_undoManager.getAllRedoActionTitles() );
717 
718         // add two actions, one directly, one within a context
719         final CustomUndoAction action1 = new CustomUndoAction( "UndoAction1" );
720         i_undoManager.addUndoAction( action1 );
721         assertEquals( "Adding an undo action not observed by the listener", 1, m_undoListener.getUndoActionsAdded() );
722         assertEquals( "Adding an undo action did not notify the proper title",
723             action1.getTitle(), m_undoListener.getMostRecentlyAddedActionTitle() );
724         final String contextTitle = "Undo Context";
725         i_undoManager.enterUndoContext( contextTitle );
726         final CustomUndoAction action2 = new CustomUndoAction( "UndoAction2" );
727         i_undoManager.addUndoAction( action2 );
728         assertEquals( "Adding an undo action not observed by the listener",
729             2, m_undoListener.getUndoActionsAdded() );
730         assertEquals( "Adding an undo action did not notify the proper title",
731             action2.getTitle(), m_undoListener.getMostRecentlyAddedActionTitle() );
732         i_undoManager.leaveUndoContext();
733 
734         // see if the manager has proper descriptions
735         assertArrayEquals( "unexpected Redo descriptions after adding two actions",
736             new String[0], i_undoManager.getAllRedoActionTitles() );
737         assertArrayEquals( "unexpected Undo descriptions after adding two actions",
738             new String[]{contextTitle, action1.getTitle()}, i_undoManager.getAllUndoActionTitles() );
739 
740         // undo one action
741         i_undoManager.undo();
742         assertEquals( "improper action title notified during programmatic Undo",
743             contextTitle, m_undoListener.getMostRecentlyUndoneTitle() );
744         assertTrue( "nested custom undo action has not been undone as expected", action2.undoCalled() );
745         assertFalse( "nested custom undo action has not been undone as expected", action1.undoCalled() );
746         assertArrayEquals( "unexpected Redo descriptions after undoing a nested custom action",
747             new String[]{contextTitle}, i_undoManager.getAllRedoActionTitles() );
748         assertArrayEquals( "unexpected Undo descriptions after undoing a nested custom action",
749             new String[]{action1.getTitle()}, i_undoManager.getAllUndoActionTitles() );
750 
751         // undo the second action, via UI dispatches
752         m_currentTestCase.getDocument().getCurrentView().dispatch( ".uno:Undo" );
753         assertEquals( "improper action title notified during UI Undo", action1.getTitle(), m_undoListener.getMostRecentlyUndoneTitle() );
754         assertTrue( "nested custom undo action has not been undone as expected", action1.undoCalled() );
755         assertArrayEquals( "unexpected Redo descriptions after undoing the second custom action",
756             new String[]{action1.getTitle(), contextTitle}, i_undoManager.getAllRedoActionTitles() );
757         assertArrayEquals( "unexpected Undo descriptions after undoing the second custom action",
758             new String[0], i_undoManager.getAllUndoActionTitles() );
759 
760         // check the actions are disposed when the stacks are cleared
761         i_undoManager.clear();
762         assertTrue( action1.disposed() && action2.disposed() );
763     }
764 
765     // -----------------------------------------------------------------------------------------------------------------
impl_testLocking( final XUndoManager i_undoManager )766     private void impl_testLocking( final XUndoManager i_undoManager ) throws com.sun.star.uno.Exception
767     {
768         i_undoManager.reset();
769         m_undoListener.reset();
770 
771         // implicit Undo actions, triggered by changes to the document
772         assertFalse( "unexpected initial locking state", i_undoManager.isLocked() );
773         i_undoManager.lock();
774         assertTrue( "just locked the manager, why does it lie?", i_undoManager.isLocked() );
775         m_currentTestCase.doSingleModification();
776         assertEquals( "when the Undo manager is locked, no implicit additions should happen",
777             0, m_undoListener.getUndoActionsAdded() );
778         i_undoManager.unlock();
779         assertEquals( "unlock is not expected to add collected actions - they should be discarded",
780             0, m_undoListener.getUndoActionsAdded() );
781         assertFalse( "just unlocked the manager, why does it lie?", i_undoManager.isLocked() );
782 
783         // explicit Undo actions
784         i_undoManager.lock();
785         i_undoManager.addUndoAction( new CustomUndoAction() );
786         i_undoManager.unlock();
787         assertEquals( "explicit Undo actions are expected to be ignored when the manager is locked",
788             0, m_undoListener.getUndoActionsAdded() );
789 
790         // Undo contexts while being locked
791         i_undoManager.lock();
792         i_undoManager.enterUndoContext( "Dummy Context" );
793         i_undoManager.enterHiddenUndoContext();
794         assertEquals( "entering Undo contexts should be ignored when the manager is locked", 0, m_undoListener.getCurrentUndoContextDepth() );
795         i_undoManager.leaveUndoContext();
796         i_undoManager.leaveUndoContext();
797         i_undoManager.unlock();
798 
799         // |unlock| error handling
800         assertFalse( "internal error: manager should not be locked at this point in time", i_undoManager.isLocked() );
801         boolean caughtExpected = false;
802         try { i_undoManager.unlock(); } catch ( final NotLockedException e ) { caughtExpected = true; }
803         assertTrue( "unlocking the manager when it is not locked should throw", caughtExpected );
804     }
805 
806     // -----------------------------------------------------------------------------------------------------------------
impl_testContextHandling( final XUndoManager i_undoManager )807     private void impl_testContextHandling( final XUndoManager i_undoManager ) throws com.sun.star.uno.Exception
808     {
809         // .............................................................................................................
810         // part I: non-empty contexts
811         i_undoManager.reset();
812         m_undoListener.reset();
813 
814         // put one action on the undo and one on the redo stack, as precondition for the following tests
815         final XUndoAction undoAction1 = new CustomUndoAction( "Undo Action 1" );
816         i_undoManager.addUndoAction( undoAction1 );
817         final XUndoAction undoAction2 = new CustomUndoAction( "Undo Action 2" );
818         i_undoManager.addUndoAction( undoAction2 );
819         i_undoManager.undo();
820         assertTrue( "precondition for context handling tests not met (1)", i_undoManager.isUndoPossible() );
821         assertTrue( "precondition for context handling tests not met (2)", i_undoManager.isRedoPossible() );
822         assertArrayEquals( new String[] { undoAction1.getTitle() }, i_undoManager.getAllUndoActionTitles() );
823         assertArrayEquals( new String[] { undoAction2.getTitle() }, i_undoManager.getAllRedoActionTitles() );
824 
825         final String[] expectedRedoActionComments = new String[] { undoAction2.getTitle() };
826         assertArrayEquals( expectedRedoActionComments, i_undoManager.getAllRedoActionTitles() );
827 
828         // enter a context
829         i_undoManager.enterUndoContext( "Undo Context" );
830         // this should not (yet) touch the redo stack
831         assertArrayEquals( expectedRedoActionComments, i_undoManager.getAllRedoActionTitles() );
832         assertEquals( "unexpected undo context depth after entering a context", 1, m_undoListener.getCurrentUndoContextDepth() );
833         // add a single action
834         XUndoAction undoAction3 = new CustomUndoAction( "Undo Action 3" );
835         i_undoManager.addUndoAction( undoAction3 );
836         // still, the redo stack should be untouched - added at a lower level does not affect it at all
837         assertArrayEquals( expectedRedoActionComments, i_undoManager.getAllRedoActionTitles() );
838 
839         // while the context is open, its title should already contribute to the stack, ...
840         assertEquals( "Undo Context", i_undoManager.getCurrentUndoActionTitle() );
841         // ... getAllUndo/RedoActionTitles should operate on the top level, not on the level defined by the open
842         // context,  ...
843         assertArrayEquals( new String[] { "Undo Context", undoAction1.getTitle() },
844             i_undoManager.getAllUndoActionTitles() );
845         // ... but Undo and Redo should be impossible as long as the context is open
846         assertFalse( i_undoManager.isUndoPossible() );
847         assertFalse( i_undoManager.isRedoPossible() );
848 
849         // leave the context, check the listener has been notified properly, and the notified context depth is correct
850         i_undoManager.leaveUndoContext();
851         assertTrue( m_undoListener.wasContextLeft() );
852         assertFalse( m_undoListener.wasHiddenContextLeft() );
853         assertFalse( m_undoListener.hasContextBeenCancelled() );
854         assertEquals( "unexpected undo context depth leaving a non-empty context", 0, m_undoListener.getCurrentUndoContextDepth() );
855         // leaving a non-empty context should have cleare the redo stack
856         assertArrayEquals( new String[0], i_undoManager.getAllRedoActionTitles() );
857         assertTrue( m_undoListener.wasRedoStackCleared() );
858 
859         // .............................................................................................................
860         // part II: empty contexts
861         i_undoManager.reset();
862         m_undoListener.reset();
863 
864         // enter a context, leave it immediately without adding an action to it
865         i_undoManager.enterUndoContext( "Undo Context" );
866         i_undoManager.leaveUndoContext();
867         assertFalse( m_undoListener.wasContextLeft() );
868         assertFalse( m_undoListener.wasHiddenContextLeft() );
869         assertTrue( m_undoListener.hasContextBeenCancelled() );
870         assertFalse( "leaving an empty context should silently remove it, and not contribute to the stack",
871             i_undoManager.isUndoPossible() );
872     }
873 
874     // -----------------------------------------------------------------------------------------------------------------
impl_testNestedContexts( final XUndoManager i_undoManager )875     private void impl_testNestedContexts( final XUndoManager i_undoManager ) throws com.sun.star.uno.Exception
876     {
877         i_undoManager.reset();
878         m_undoListener.reset();
879         i_undoManager.enterUndoContext( "context 1" );
880         i_undoManager.enterUndoContext( "context 1.1" );
881         final CustomUndoAction action1 = new CustomUndoAction( "action 1.1.1" );
882         i_undoManager.addUndoAction( action1 );
883         i_undoManager.enterUndoContext( "context 1.1.2" );
884         final CustomUndoAction action2 = new CustomUndoAction( "action 1.1.2.1" );
885         i_undoManager.addUndoAction( action2 );
886         i_undoManager.leaveUndoContext();
887         final CustomUndoAction action3 = new CustomUndoAction( "action 1.1.3" );
888         i_undoManager.addUndoAction( action3 );
889         i_undoManager.leaveUndoContext();
890         i_undoManager.leaveUndoContext();
891         final CustomUndoAction action4 = new CustomUndoAction( "action 1.2" );
892         i_undoManager.addUndoAction( action4 );
893 
894         i_undoManager.undo();
895         assertEquals( "undoing a single action notifies a wrong title", action4.getTitle(), m_undoListener.getMostRecentlyUndoneTitle() );
896         assertTrue( "custom Undo not called", action4.undoCalled() );
897         assertFalse( "too many custom Undos called", action1.undoCalled() || action2.undoCalled() || action3.undoCalled() );
898         i_undoManager.undo();
899         assertTrue( "nested actions not properly undone", action1.undoCalled() && action2.undoCalled() && action3.undoCalled() );
900     }
901 
902     // -----------------------------------------------------------------------------------------------------------------
impl_testErrorHandling( final XUndoManager i_undoManager )903     private void impl_testErrorHandling( final XUndoManager i_undoManager ) throws com.sun.star.uno.Exception
904     {
905         i_undoManager.reset();
906         m_undoListener.reset();
907 
908         // try retrieving the comments for the current Undo/Redo - this should fail
909         boolean caughtExpected = false;
910         try { i_undoManager.getCurrentUndoActionTitle(); }
911         catch( final EmptyUndoStackException e ) { caughtExpected = true; }
912         assertTrue( "trying the title of the current Undo action is expected to fail for an empty stack", caughtExpected );
913 
914         caughtExpected = false;
915         try { i_undoManager.getCurrentRedoActionTitle(); }
916         catch( final EmptyUndoStackException e ) { caughtExpected = true; }
917         assertTrue( "trying the title of the current Redo action is expected to fail for an empty stack", caughtExpected );
918 
919         caughtExpected = false;
920         try { i_undoManager.undo(); } catch ( final EmptyUndoStackException e ) { caughtExpected = true; }
921         assertTrue( "undo should throw if no Undo action is on the stack", caughtExpected );
922 
923         caughtExpected = false;
924         try { i_undoManager.redo(); } catch ( final EmptyUndoStackException e ) { caughtExpected = true; }
925         assertTrue( "redo should throw if no Redo action is on the stack", caughtExpected );
926 
927         caughtExpected = false;
928         try { i_undoManager.leaveUndoContext(); } catch ( final InvalidStateException e ) { caughtExpected = true; }
929         assertTrue( "leaveUndoContext should throw if no context is currently open", caughtExpected );
930 
931         caughtExpected = false;
932         try { i_undoManager.addUndoAction( null ); } catch ( com.sun.star.lang.IllegalArgumentException e ) { caughtExpected = true; }
933         assertTrue( "adding a NULL action should be rejected", caughtExpected );
934 
935         i_undoManager.reset();
936         i_undoManager.addUndoAction( new CustomUndoAction() );
937         i_undoManager.addUndoAction( new CustomUndoAction() );
938         i_undoManager.undo();
939         i_undoManager.enterUndoContext( "Undo Context" );
940         // those methods should fail when a context is open:
941         final String[] methodNames = new String[] { "undo", "redo", "clear", "clearRedo" };
942         for ( int i=0; i<methodNames.length; ++i )
943         {
944             caughtExpected = false;
945             try
946             {
947                 Method method = i_undoManager.getClass().getMethod( methodNames[i], new Class[0] );
948                 method.invoke( i_undoManager, new Object[0] );
949             }
950             catch ( IllegalAccessException ex ) { }
951             catch ( IllegalArgumentException ex ) { }
952             catch ( InvocationTargetException ex )
953             {
954                 Throwable targetException = ex.getTargetException();
955                 caughtExpected = ( targetException instanceof UndoContextNotClosedException );
956             }
957             catch ( NoSuchMethodException ex ) { }
958             catch ( SecurityException ex ) { }
959 
960             assertTrue( methodNames[i] + " should be rejected when there is an open context", caughtExpected );
961         }
962         i_undoManager.leaveUndoContext();
963 
964         // try Undo actions which fail in their Undo/Redo
965         for ( int i=0; i<4; ++i )
966         {
967             final boolean undo = ( i < 2 );
968             final boolean doByAPI = ( i % 2 ) == 0;
969 
970             i_undoManager.reset();
971             i_undoManager.addUndoAction( new CustomUndoAction() );
972             i_undoManager.addUndoAction( new FailingUndoAction( undo ? FAIL_UNDO : FAIL_REDO ) );
973             i_undoManager.addUndoAction( new CustomUndoAction() );
974             i_undoManager.undo();
975             if ( !undo )
976                 i_undoManager.undo();
977             // assert preconditions for the below test
978             assertTrue( i_undoManager.isUndoPossible() );
979             assertTrue( i_undoManager.isRedoPossible() );
980 
981             boolean caughtUndoFailed = false;
982             try
983             {
984                 if ( undo )
985                     if ( doByAPI )
986                         i_undoManager.undo();
987                     else
988                         m_currentTestCase.getDocument().getCurrentView().dispatch( ".uno:Undo" );
989                 else
990                     if ( doByAPI )
991                         i_undoManager.redo();
992                     else
993                         m_currentTestCase.getDocument().getCurrentView().dispatch( ".uno:Redo" );
994             }
995             catch ( UndoFailedException e )
996             {
997                 caughtUndoFailed = true;
998             }
999             if ( doByAPI )
1000                 assertTrue( "Exceptions in XUndoAction.undo should be propagated at the API", caughtUndoFailed );
1001             else
1002                 assertFalse( "Undo/Redo by UI should not let escape Exceptions", caughtUndoFailed );
1003             if ( undo )
1004             {
1005                 assertFalse( "a failing Undo should clear the Undo stack", i_undoManager.isUndoPossible() );
1006                 assertTrue( "a failing Undo should /not/ clear the Redo stack", i_undoManager.isRedoPossible() );
1007             }
1008             else
1009             {
1010                 assertTrue( "a failing Redo should /not/ clear the Undo stack", i_undoManager.isUndoPossible() );
1011                 assertFalse( "a failing Redo should clear the Redo stack", i_undoManager.isRedoPossible() );
1012             }
1013         }
1014     }
1015 
1016     // -----------------------------------------------------------------------------------------------------------------
impl_testStackHandling( final XUndoManager i_undoManager )1017     private void impl_testStackHandling( final XUndoManager i_undoManager ) throws com.sun.star.uno.Exception
1018     {
1019         i_undoManager.reset();
1020         m_undoListener.reset();
1021 
1022         assertFalse( i_undoManager.isUndoPossible() );
1023         assertFalse( i_undoManager.isRedoPossible() );
1024 
1025         i_undoManager.addUndoAction( new CustomUndoAction() );
1026         assertTrue( i_undoManager.isUndoPossible() );
1027         assertFalse( i_undoManager.isRedoPossible() );
1028         i_undoManager.addUndoAction( new CustomUndoAction() );
1029         assertTrue( i_undoManager.isUndoPossible() );
1030         assertFalse( i_undoManager.isRedoPossible() );
1031         i_undoManager.undo();
1032         assertTrue( i_undoManager.isUndoPossible() );
1033         assertTrue( i_undoManager.isRedoPossible() );
1034         i_undoManager.undo();
1035         assertFalse( i_undoManager.isUndoPossible() );
1036         assertTrue( i_undoManager.isRedoPossible() );
1037         i_undoManager.addUndoAction( new CustomUndoAction() );
1038         assertTrue( i_undoManager.isUndoPossible() );
1039         assertFalse( "adding a new action should have cleared the Redo stack", i_undoManager.isRedoPossible() );
1040     }
1041 
1042     // -----------------------------------------------------------------------------------------------------------------
impl_testClearance( final XUndoManager i_undoManager )1043     private void impl_testClearance( final XUndoManager i_undoManager ) throws com.sun.star.uno.Exception
1044     {
1045         i_undoManager.reset();
1046         m_undoListener.reset();
1047 
1048         // add an action, clear the stack, verify the listener has been called
1049         i_undoManager.addUndoAction( new CustomUndoAction() );
1050         assertFalse( "clearance listener unexpectedly called", m_undoListener.wereStacksCleared() );
1051         assertFalse( "redo-clearance listener unexpectedly called", m_undoListener.wasRedoStackCleared() );
1052         i_undoManager.clear();
1053         assertTrue( "clearance listener not called as expected", m_undoListener.wereStacksCleared() );
1054         assertFalse( "redo-clearance listener unexpectedly called (2)", m_undoListener.wasRedoStackCleared() );
1055 
1056         // ensure the listener is also called if the stack is actually empty at the moment of the call
1057         m_undoListener.reset();
1058         assertFalse( i_undoManager.isUndoPossible() );
1059         i_undoManager.clear();
1060         assertTrue( "clearance listener is also expected to be called if the stack was empty before", m_undoListener.wereStacksCleared() );
1061 
1062         // ensure the proper listeners are called for clearRedo
1063         m_undoListener.reset();
1064         i_undoManager.clearRedo();
1065         assertFalse( m_undoListener.wereStacksCleared() );
1066         assertTrue( m_undoListener.wasRedoStackCleared() );
1067 
1068         // ensure the redo listener is also called upon implicit redo stack clearance
1069         m_undoListener.reset();
1070         i_undoManager.addUndoAction( new CustomUndoAction() );
1071         i_undoManager.addUndoAction( new CustomUndoAction() );
1072         i_undoManager.undo();
1073         assertTrue( i_undoManager.isUndoPossible() );
1074         assertTrue( i_undoManager.isRedoPossible() );
1075         i_undoManager.addUndoAction( new CustomUndoAction() );
1076         assertFalse( i_undoManager.isRedoPossible() );
1077         assertTrue( "implicit clearance of the Redo stack does not notify listeners", m_undoListener.wasRedoStackCleared() );
1078 
1079         // test resetting the manager
1080         m_undoListener.reset();
1081         i_undoManager.addUndoAction( new CustomUndoAction() );
1082         i_undoManager.addUndoAction( new CustomUndoAction() );
1083         i_undoManager.undo();
1084         assertTrue( i_undoManager.isUndoPossible() );
1085         assertTrue( i_undoManager.isRedoPossible() );
1086         i_undoManager.reset();
1087         assertFalse( i_undoManager.isUndoPossible() );
1088         assertFalse( i_undoManager.isRedoPossible() );
1089         assertTrue( "|reset| does not properly notify", m_undoListener.wasManagerReset() );
1090 
1091         // resetting the manager, with open undo contexts
1092         m_undoListener.reset();
1093         i_undoManager.addUndoAction( new CustomUndoAction() );
1094         i_undoManager.enterUndoContext( "Undo Context" );
1095         i_undoManager.addUndoAction( new CustomUndoAction() );
1096         i_undoManager.enterHiddenUndoContext();
1097         i_undoManager.reset();
1098         assertTrue( "|reset| while contexts are open does not properly notify", m_undoListener.wasManagerReset() );
1099         // verify the manager really has the proper context depth now
1100         i_undoManager.enterUndoContext( "Undo Context" );
1101         assertEquals( "seems that |reset| did not really close the open contexts", 1, m_undoListener.getCurrentUndoContextDepth() );
1102     }
1103 
1104     // -----------------------------------------------------------------------------------------------------------------
impl_testHiddenContexts( final XUndoManager i_undoManager )1105     private void impl_testHiddenContexts( final XUndoManager i_undoManager ) throws com.sun.star.uno.Exception
1106     {
1107         i_undoManager.reset();
1108         m_undoListener.reset();
1109         assertFalse( "precondition for testing hidden undo contexts not met", i_undoManager.isUndoPossible() );
1110 
1111         // entering a hidden context should be rejected if the stack is empty
1112         boolean caughtExpected = false;
1113         try { i_undoManager.enterHiddenUndoContext(); }
1114         catch ( final EmptyUndoStackException e ) { caughtExpected = true; }
1115         assertTrue( "entering hidden contexts should be denied on an empty stack", caughtExpected );
1116 
1117         // but it should be allowed if the context is not empty
1118         final CustomUndoAction undoAction0 = new CustomUndoAction( "Step 0" );
1119         i_undoManager.addUndoAction( undoAction0 );
1120         final CustomUndoAction undoAction1 = new CustomUndoAction( "Step 1" );
1121         i_undoManager.addUndoAction( undoAction1 );
1122         i_undoManager.enterHiddenUndoContext();
1123         final CustomUndoAction hiddenUndoAction = new CustomUndoAction( "hidden context action" );
1124         i_undoManager.addUndoAction( hiddenUndoAction );
1125         i_undoManager.leaveUndoContext();
1126         assertFalse( "leaving a hidden should not call |leftUndocontext|", m_undoListener.wasContextLeft() );
1127         assertTrue( "leaving a hidden does not call |leftHiddenUndocontext|", m_undoListener.wasHiddenContextLeft() );
1128         assertFalse( "leaving a non-empty hidden context claims to have cancelled it", m_undoListener.hasContextBeenCancelled() );
1129         assertEquals( "leaving a hidden context is not properly notified", 0, m_undoListener.getCurrentUndoContextDepth() );
1130         assertArrayEquals( "unexpected Undo stack after leaving a hidden context",
1131             new String[] { undoAction1.getTitle(), undoAction0.getTitle() },
1132             i_undoManager.getAllUndoActionTitles() );
1133 
1134         // and then calling |undo| once should not only undo everything in the hidden context, but also
1135         // the previous action - but not more
1136         i_undoManager.undo();
1137         assertTrue( "Undo after leaving a hidden context does not actually undo the context actions",
1138             hiddenUndoAction.undoCalled() );
1139         assertTrue( "Undo after leaving a hidden context does not undo the predecessor action",
1140             undoAction1.undoCalled() );
1141         assertFalse( "Undo after leaving a hidden context undoes too much",
1142             undoAction0.undoCalled() );
1143 
1144         // leaving an empty hidden context should call the proper notification method
1145         m_undoListener.reset();
1146         i_undoManager.enterHiddenUndoContext();
1147         i_undoManager.leaveUndoContext();
1148         assertFalse( m_undoListener.wasContextLeft() );
1149         assertFalse( m_undoListener.wasHiddenContextLeft() );
1150         assertTrue( m_undoListener.hasContextBeenCancelled() );
1151 
1152         // nesting hidden and normal contexts
1153         m_undoListener.reset();
1154         i_undoManager.reset();
1155         final CustomUndoAction action0 = new CustomUndoAction( "action 0" );
1156         i_undoManager.addUndoAction( action0 );
1157         i_undoManager.enterUndoContext( "context 1" );
1158         final CustomUndoAction action1 = new CustomUndoAction( "action 1" );
1159         i_undoManager.addUndoAction( action1 );
1160         i_undoManager.enterHiddenUndoContext();
1161         final CustomUndoAction action2 = new CustomUndoAction( "action 2" );
1162         i_undoManager.addUndoAction( action2 );
1163         i_undoManager.enterUndoContext( "context 2" );
1164         // is entering a hidden context rejected even at the nesting level > 0 (the above test was for nesting level == 0)?
1165         caughtExpected = false;
1166         try { i_undoManager.enterHiddenUndoContext(); }
1167         catch( final EmptyUndoStackException e ) { caughtExpected = true; }
1168         assertTrue( "at a nesting level > 0, denied hidden contexts does not work as expected", caughtExpected );
1169         final CustomUndoAction action3 = new CustomUndoAction( "action 3" );
1170         i_undoManager.addUndoAction( action3 );
1171         i_undoManager.enterHiddenUndoContext();
1172         assertEquals( "mixed hidden/normal context do are not properly notified", 4, m_undoListener.getCurrentUndoContextDepth() );
1173         i_undoManager.leaveUndoContext();
1174         assertTrue( "the left context was empty - why wasn't 'cancelled' notified?", m_undoListener.hasContextBeenCancelled() );
1175         assertFalse( m_undoListener.wasContextLeft() );
1176         assertFalse( m_undoListener.wasHiddenContextLeft() );
1177         i_undoManager.leaveUndoContext();
1178         i_undoManager.leaveUndoContext();
1179         i_undoManager.leaveUndoContext();
1180         i_undoManager.undo();
1181         assertFalse( "one action too much has been undone", action0.undoCalled() );
1182         assertTrue( action1.undoCalled() );
1183         assertTrue( action2.undoCalled() );
1184         assertTrue( action3.undoCalled() );
1185     }
1186 
1187     // -----------------------------------------------------------------------------------------------------------------
getContext()1188     private XComponentContext getContext()
1189     {
1190         return m_connection.getComponentContext();
1191     }
1192 
1193     // -----------------------------------------------------------------------------------------------------------------
getORB()1194     private XMultiServiceFactory getORB()
1195     {
1196         final XMultiServiceFactory xMSF1 = UnoRuntime.queryInterface(
1197             XMultiServiceFactory.class, getContext().getServiceManager() );
1198         return xMSF1;
1199     }
1200 
1201     // -----------------------------------------------------------------------------------------------------------------
1202     @BeforeClass
setUpConnection()1203     public static void setUpConnection() throws Exception
1204     {
1205         System.out.println( "--------------------------------------------------------------------------------" );
1206         System.out.println( "starting class: " + UndoManager.class.getName() );
1207         System.out.println( "connecting ..." );
1208         m_connection.setUp();
1209     }
1210 
1211     // -----------------------------------------------------------------------------------------------------------------
1212     @AfterClass
tearDownConnection()1213     public static void tearDownConnection() throws InterruptedException, com.sun.star.uno.Exception
1214     {
1215         System.out.println();
1216         System.out.println( "tearing down connection" );
1217         m_connection.tearDown();
1218         System.out.println( "finished class: " + UndoManager.class.getName() );
1219         System.out.println( "--------------------------------------------------------------------------------" );
1220     }
1221 
1222     // -----------------------------------------------------------------------------------------------------------------
1223     private static class CustomUndoAction implements XUndoAction, XComponent
1224     {
CustomUndoAction()1225         CustomUndoAction()
1226         {
1227             m_title = "Custom Undo Action";
1228         }
1229 
CustomUndoAction( final String i_title )1230         CustomUndoAction( final String i_title )
1231         {
1232             m_title = i_title;
1233         }
1234 
getTitle()1235         public String getTitle()
1236         {
1237             return m_title;
1238         }
1239 
undo()1240         public void undo() throws UndoFailedException
1241         {
1242             m_undoCalled = true;
1243         }
1244 
redo()1245         public void redo() throws UndoFailedException
1246         {
1247             m_redoCalled = true;
1248         }
1249 
dispose()1250         public void dispose()
1251         {
1252             m_disposed = true;
1253         }
1254 
addEventListener( XEventListener xl )1255         public void addEventListener( XEventListener xl )
1256         {
1257             fail( "addEventListener is not expected to be called in the course of this test" );
1258         }
1259 
removeEventListener( XEventListener xl )1260         public void removeEventListener( XEventListener xl )
1261         {
1262             fail( "removeEventListener is not expected to be called in the course of this test" );
1263         }
1264 
undoCalled()1265         boolean undoCalled() { return m_undoCalled; }
redoCalled()1266         boolean redoCalled() { return m_redoCalled; }
disposed()1267         boolean disposed() { return m_disposed; }
1268 
1269         private final String    m_title;
1270         private boolean         m_undoCalled = false;
1271         private boolean         m_redoCalled = false;
1272         private boolean         m_disposed = false;
1273     }
1274 
1275     private static short FAIL_UNDO = 1;
1276     private static short FAIL_REDO = 2;
1277 
1278     private static class FailingUndoAction implements XUndoAction
1279     {
FailingUndoAction( final short i_failWhich )1280         FailingUndoAction( final short i_failWhich )
1281         {
1282             m_failWhich = i_failWhich;
1283         }
1284 
getTitle()1285         public String getTitle()
1286         {
1287             return "failing undo";
1288         }
1289 
undo()1290         public void undo() throws UndoFailedException
1291         {
1292             if ( m_failWhich != FAIL_REDO )
1293                 impl_throw();
1294         }
1295 
redo()1296         public void redo() throws UndoFailedException
1297         {
1298             if ( m_failWhich != FAIL_UNDO )
1299                 impl_throw();
1300         }
1301 
impl_throw()1302         private void impl_throw() throws UndoFailedException
1303         {
1304             throw new UndoFailedException();
1305         }
1306 
1307         private final short m_failWhich;
1308     }
1309 
1310     // -----------------------------------------------------------------------------------------------------------------
1311     private static class CountingUndoAction implements XUndoAction
1312     {
CountingUndoAction( final int i_expectedOrder, final Object i_lock, final Integer[] i_actionsUndoneCounter )1313         CountingUndoAction( final int i_expectedOrder, final Object i_lock, final Integer[] i_actionsUndoneCounter )
1314         {
1315             m_expectedOrder = i_expectedOrder;
1316             m_lock = i_lock;
1317             m_actionsUndoneCounter = i_actionsUndoneCounter;
1318         }
1319 
getTitle()1320         public String getTitle()
1321         {
1322             return "Counting Undo Action";
1323         }
1324 
undo()1325         public void undo() throws UndoFailedException
1326         {
1327             synchronized( m_lock )
1328             {
1329                 assertEquals( "Undo action called out of order", m_expectedOrder, m_actionsUndoneCounter[0].intValue() );
1330                 ++m_actionsUndoneCounter[0];
1331             }
1332         }
1333 
redo()1334         public void redo() throws UndoFailedException
1335         {
1336             fail( "CountingUndoAction.redo is not expected to be called in this test." );
1337         }
1338         private final int       m_expectedOrder;
1339         private final Object    m_lock;
1340         private Integer[]       m_actionsUndoneCounter;
1341     }
1342 
1343     // -----------------------------------------------------------------------------------------------------------------
getCallbackUndoContextTitle()1344     private static String getCallbackUndoContextTitle()
1345     {
1346         return "Some Unfinished Undo Context";
1347     }
1348 
1349     // -----------------------------------------------------------------------------------------------------------------
getCallbackComponentServiceName()1350     private static String getCallbackComponentServiceName()
1351     {
1352         return "org.openoffice.complex.sfx2.Callback";
1353     }
1354 
1355     // -----------------------------------------------------------------------------------------------------------------
1356     /**
1357      * a factory for a callback component which, at OOo runtime, is inserted into OOo's "component repository"
1358      */
1359     private class CallbackComponentFactory implements XSingleComponentFactory, XServiceInfo, XComponent
1360     {
createInstanceWithContext( XComponentContext i_context )1361         public Object createInstanceWithContext( XComponentContext i_context ) throws com.sun.star.uno.Exception
1362         {
1363             return new CallbackComponent();
1364         }
1365 
createInstanceWithArgumentsAndContext( Object[] i_arguments, XComponentContext i_context )1366         public Object createInstanceWithArgumentsAndContext( Object[] i_arguments, XComponentContext i_context ) throws com.sun.star.uno.Exception
1367         {
1368             return createInstanceWithContext( i_context );
1369         }
1370 
getImplementationName()1371         public String getImplementationName()
1372         {
1373             return "org.openoffice.complex.sfx2.CallbackComponent";
1374         }
1375 
supportsService( String i_serviceName )1376         public boolean supportsService( String i_serviceName )
1377         {
1378             return i_serviceName.equals( getCallbackComponentServiceName() );
1379         }
1380 
getSupportedServiceNames()1381         public String[] getSupportedServiceNames()
1382         {
1383             return new String[] { getCallbackComponentServiceName() };
1384         }
1385 
dispose()1386         public void dispose()
1387         {
1388             final EventObject event = new EventObject( this );
1389 
1390             final ArrayList eventListenersCopy = (ArrayList)m_eventListeners.clone();
1391             final Iterator iter = eventListenersCopy.iterator();
1392             while ( iter.hasNext() )
1393             {
1394                 ((XEventListener)iter.next()).disposing( event );
1395             }
1396         }
1397 
addEventListener( XEventListener i_listener )1398         public void addEventListener( XEventListener i_listener )
1399         {
1400             if ( i_listener != null )
1401                 m_eventListeners.add( i_listener );
1402         }
1403 
removeEventListener( XEventListener i_listener )1404         public void removeEventListener( XEventListener i_listener )
1405         {
1406             m_eventListeners.remove( i_listener );
1407         }
1408 
1409         private final ArrayList m_eventListeners = new ArrayList();
1410     };
1411 
1412     // -----------------------------------------------------------------------------------------------------------------
1413     private class CallbackComponent implements XJob, XTypeProvider
1414     {
CallbackComponent()1415         CallbackComponent()
1416         {
1417         }
1418 
execute( NamedValue[] i_parameters )1419         public Object execute( NamedValue[] i_parameters ) throws com.sun.star.lang.IllegalArgumentException, com.sun.star.uno.Exception
1420         {
1421             // this method is called from within the Basic script which is to check whether the OOo framework
1422             // properly cleans up unfinished Undo contexts. It is called immediately after the context has been
1423             // entered, so verify the expected Undo manager state.
1424             assertEquals( getCallbackUndoContextTitle(), m_undoListener.getCurrentUndoContextTitle() );
1425             assertEquals( 1, m_undoListener.getCurrentUndoContextDepth() );
1426 
1427             synchronized( m_callbackCondition )
1428             {
1429                 m_callbackCalled = true;
1430                 m_callbackCondition.notifyAll();
1431             }
1432             return m_closeAfterCallback ? "close" : "";
1433         }
1434 
getTypes()1435         public Type[] getTypes()
1436         {
1437             final Class interfaces[] = getClass().getInterfaces();
1438             Type types[] = new Type[ interfaces.length ];
1439             for ( int i = 0; i < interfaces.length; ++i )
1440                 types[i] = new Type(interfaces[i]);
1441             return types;
1442         }
1443 
getImplementationId()1444         public byte[] getImplementationId()
1445         {
1446             return getClass().toString().getBytes();
1447         }
1448     }
1449 
1450     private static final OfficeConnection   m_connection = new OfficeConnection();
1451     private DocumentTest                    m_currentTestCase;
1452     private OfficeDocument                  m_currentDocument;
1453     private UndoListener                    m_undoListener;
1454     private CallbackComponentFactory        m_callbackFactory = null;
1455     private boolean                         m_callbackCalled = false;
1456     private boolean                         m_closeAfterCallback = false;
1457     private final Object                    m_callbackCondition = new Object();
1458 }
1459