| /************************************************************** |
| * |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| * |
| *************************************************************/ |
| |
| |
| |
| package complex.sfx2; |
| |
| import com.sun.star.accessibility.XAccessible; |
| import com.sun.star.accessibility.XAccessibleAction; |
| import com.sun.star.awt.Point; |
| import com.sun.star.awt.Size; |
| import com.sun.star.awt.XControl; |
| import com.sun.star.awt.XControlModel; |
| import com.sun.star.beans.NamedValue; |
| import com.sun.star.beans.XPropertySet; |
| import com.sun.star.container.NoSuchElementException; |
| import com.sun.star.container.XChild; |
| import com.sun.star.container.XIndexContainer; |
| import com.sun.star.container.XNameContainer; |
| import com.sun.star.container.XNameReplace; |
| import com.sun.star.container.XSet; |
| import com.sun.star.document.EmptyUndoStackException; |
| import com.sun.star.document.UndoContextNotClosedException; |
| import com.sun.star.document.UndoFailedException; |
| import com.sun.star.document.UndoManagerEvent; |
| import com.sun.star.document.XEmbeddedScripts; |
| import com.sun.star.document.XEventsSupplier; |
| import com.sun.star.document.XUndoAction; |
| import com.sun.star.lang.EventObject; |
| import com.sun.star.lang.IndexOutOfBoundsException; |
| import com.sun.star.lang.XEventListener; |
| import java.lang.reflect.InvocationTargetException; |
| import org.openoffice.test.tools.OfficeDocument; |
| import com.sun.star.document.XUndoManagerSupplier; |
| import com.sun.star.document.XUndoManager; |
| import com.sun.star.document.XUndoManagerListener; |
| import com.sun.star.drawing.XControlShape; |
| import com.sun.star.drawing.XDrawPage; |
| import com.sun.star.drawing.XDrawPageSupplier; |
| import com.sun.star.drawing.XShapes; |
| import com.sun.star.lang.XComponent; |
| import com.sun.star.lang.XMultiServiceFactory; |
| import com.sun.star.lang.XServiceInfo; |
| import com.sun.star.lang.XSingleComponentFactory; |
| import com.sun.star.lang.XTypeProvider; |
| import com.sun.star.script.ScriptEventDescriptor; |
| import com.sun.star.script.XEventAttacherManager; |
| import com.sun.star.script.XLibraryContainer; |
| import com.sun.star.task.XJob; |
| import com.sun.star.uno.Type; |
| import com.sun.star.uno.UnoRuntime; |
| import com.sun.star.uno.XComponentContext; |
| import com.sun.star.util.InvalidStateException; |
| import com.sun.star.util.NotLockedException; |
| import com.sun.star.view.XControlAccess; |
| import complex.sfx2.undo.CalcDocumentTest; |
| import complex.sfx2.undo.ChartDocumentTest; |
| import complex.sfx2.undo.DocumentTest; |
| import complex.sfx2.undo.DrawDocumentTest; |
| import complex.sfx2.undo.ImpressDocumentTest; |
| import complex.sfx2.undo.WriterDocumentTest; |
| import java.lang.reflect.Method; |
| import java.util.ArrayList; |
| import java.util.Iterator; |
| import java.util.Stack; |
| import org.junit.After; |
| import org.junit.AfterClass; |
| import org.junit.Before; |
| import org.junit.BeforeClass; |
| import org.junit.Test; |
| import static org.junit.Assert.*; |
| import org.openoffice.test.OfficeConnection; |
| import org.openoffice.test.tools.DocumentType; |
| import org.openoffice.test.tools.SpreadsheetDocument; |
| |
| /** |
| * Unit test for the UndoManager API |
| * |
| * @author frank.schoenheit@oracle.com |
| */ |
| public class UndoManager |
| { |
| // ----------------------------------------------------------------------------------------------------------------- |
| @Before |
| public void beforeTest() throws com.sun.star.uno.Exception |
| { |
| m_currentTestCase = null; |
| m_currentDocument = null; |
| m_undoListener = null; |
| |
| // at our service factory, insert a new factory for our CallbackComponent |
| // this will allow the Basic code in our test documents to call back into this test case |
| // here, by just instantiating this service |
| final XSet globalFactory = UnoRuntime.queryInterface( XSet.class, getORB() ); |
| m_callbackFactory = new CallbackComponentFactory(); |
| globalFactory.insert( m_callbackFactory ); |
| } |
| |
| // ----------------------------------------------------------------------------------------------------------------- |
| @Test |
| public void checkWriterUndo() throws Exception |
| { |
| m_currentTestCase = new WriterDocumentTest( getORB() ); |
| impl_checkUndo(); |
| } |
| |
| // ----------------------------------------------------------------------------------------------------------------- |
| @Test |
| public void checkCalcUndo() throws Exception |
| { |
| m_currentTestCase = new CalcDocumentTest( getORB() ); |
| impl_checkUndo(); |
| } |
| |
| // ----------------------------------------------------------------------------------------------------------------- |
| @Test |
| public void checkDrawUndo() throws Exception |
| { |
| m_currentTestCase = new DrawDocumentTest( getORB() ); |
| impl_checkUndo(); |
| } |
| |
| // ----------------------------------------------------------------------------------------------------------------- |
| @Test |
| public void checkImpressUndo() throws Exception |
| { |
| m_currentTestCase = new ImpressDocumentTest( getORB() ); |
| impl_checkUndo(); |
| } |
| |
| // ----------------------------------------------------------------------------------------------------------------- |
| @Test |
| public void checkChartUndo() throws Exception |
| { |
| m_currentTestCase = new ChartDocumentTest( getORB() ); |
| impl_checkUndo(); |
| } |
| |
| // ----------------------------------------------------------------------------------------------------------------- |
| @Test |
| public void checkBrokenScripts() throws com.sun.star.uno.Exception, InterruptedException |
| { |
| System.out.println( "testing: broken scripts" ); |
| |
| m_currentDocument = OfficeDocument.blankDocument( getORB(), DocumentType.CALC ); |
| m_undoListener = new UndoListener(); |
| getUndoManager().addUndoManagerListener( m_undoListener ); |
| |
| impl_setupBrokenBasicScript(); |
| final String scriptURI = "vnd.sun.star.script:default.callbacks.brokenScript?language=Basic&location=document"; |
| |
| // ............................................................................................................. |
| // scenario 1: Pressing a button which is bound to execute the script |
| // (This is one of the many cases where SfxObjectShell::CallXScript is invoked) |
| |
| // set up the button |
| final XPropertySet buttonModel = impl_setupButton(); |
| buttonModel.setPropertyValue( "Label", "exec broken script" ); |
| impl_assignScript( buttonModel, "XActionListener", "actionPerformed", |
| scriptURI ); |
| |
| // switch the doc's view to form alive mode (so the button will actually work) |
| m_currentDocument.getCurrentView().dispatch( ".uno:SwitchControlDesignMode" ); |
| |
| // click the button |
| m_callbackCalled = false; |
| impl_clickButton( buttonModel ); |
| // the macro is executed asynchronously by the button, so wait at most 2 seconds for the callback to be |
| // triggered |
| impl_waitFor( m_callbackCondition, 2000 ); |
| // check the callback has actually been called |
| assertTrue( "clicking the test button did not work as expected - basic script not called", m_callbackCalled ); |
| |
| // again, since the script is executed asynchronously, we might arrive here while its execution |
| // is not completely finished. Give OOo another (at most) 2 seconds to finish it. |
| m_undoListener.waitForAllContextsClosed( 20000 ); |
| // assure that the Undo Context Depth of the doc is still "0": The Basic script entered such a |
| // context, and didn't close it (thus it is broken), but the application framework should have |
| // auto-closed the context after the macro finished. |
| assertEquals( "undo context was not auto-closed as expected", 0, m_undoListener.getCurrentUndoContextDepth() ); |
| |
| // ............................................................................................................. |
| // scenario 2: dispatching the script URL. Technically, this is equivalent to configuring the |
| // script into a menu or toolbar, and selecting the respective menu/toolbar item |
| m_callbackCalled = false; |
| m_currentDocument.getCurrentView().dispatch( scriptURI ); |
| assertTrue( "dispatching the Script URL did not work as expected - basic script not called", m_callbackCalled ); |
| // same as above: The script didn't close the context, but the OOo framework should have |
| assertEquals( "undo context was not auto-closed as expected", 0, m_undoListener.getCurrentUndoContextDepth() ); |
| |
| // ............................................................................................................. |
| // scenario 3: assigning the script to some document event, and triggering this event |
| final XEventsSupplier eventSupplier = UnoRuntime.queryInterface( XEventsSupplier.class, m_currentDocument.getDocument() ); |
| final XNameReplace events = UnoRuntime.queryInterface( XNameReplace.class, eventSupplier.getEvents() ); |
| final NamedValue[] scriptDescriptor = new NamedValue[] { |
| new NamedValue( "EventType", "Script" ), |
| new NamedValue( "Script", scriptURI ) |
| }; |
| events.replaceByName( "OnViewCreated", scriptDescriptor ); |
| |
| // The below doesn't work: event notification is broken in m96, see http://www.openoffice.org/issues/show_bug.cgi?id=116313 |
| m_callbackCalled = false; |
| m_currentDocument.getCurrentView().dispatch( ".uno:NewWindow" ); |
| assertTrue( "triggering an event did not work as expected - basic script not called", m_callbackCalled ); |
| // same as above: The script didn't close the context, but the OOo framework should have |
| assertEquals( "undo context was not auto-closed as expected", 0, m_undoListener.getCurrentUndoContextDepth() ); |
| |
| // ............................................................................................................. |
| // scenario 4: let the script enter an Undo context, but not close it, as usual. |
| // Additionally, let the script close the document - the OOo framework code which cares for |
| // auto-closing of Undo contexts should survive this, ideally ... |
| m_closeAfterCallback = true; |
| m_callbackCalled = false; |
| m_currentDocument.getCurrentView().dispatch( scriptURI ); |
| assertTrue( m_callbackCalled ); |
| assertTrue( "The Basic script should have closed the document.", m_undoListener.isDisposed() ); |
| m_currentDocument = null; |
| } |
| |
| // ----------------------------------------------------------------------------------------------------------------- |
| @Test |
| public void checkSerialization() throws com.sun.star.uno.Exception, InterruptedException |
| { |
| System.out.println( "testing: request serialization" ); |
| |
| m_currentDocument = OfficeDocument.blankDocument( getORB(), DocumentType.CALC ); |
| final XUndoManager undoManager = getUndoManager(); |
| |
| final int threadCount = 10; |
| final int actionsPerThread = 10; |
| final int actionCount = threadCount * actionsPerThread; |
| |
| // add some actions to the UndoManager, each knowing its position on the stack |
| final Object lock = new Object(); |
| final Integer actionsUndone[] = new Integer[] { 0 }; |
| for ( int i=actionCount; i>0; ) |
| undoManager.addUndoAction( new CountingUndoAction( --i, lock, actionsUndone ) ); |
| |
| // some concurrent threads which undo the actions |
| Thread[] threads = new Thread[threadCount]; |
| for ( int i=0; i<threadCount; ++i ) |
| { |
| threads[i] = new Thread() |
| { |
| @Override |
| public void run() |
| { |
| for ( int j=0; j<actionsPerThread; ++j ) |
| { |
| try { undoManager.undo(); } |
| catch ( final Exception e ) |
| { |
| fail( "Those dummy actions are not expected to fail." ); |
| return; |
| } |
| } |
| } |
| }; |
| } |
| |
| // start the threads |
| for ( int i=0; i<threadCount; ++i ) |
| threads[i].start(); |
| |
| // wait for them to be finished |
| for ( int i=0; i<threadCount; ++i ) |
| threads[i].join(); |
| |
| // ensure all actions have been undone |
| assertEquals( "not all actions have been undone", actionCount, actionsUndone[0].intValue() ); |
| } |
| |
| // ----------------------------------------------------------------------------------------------------------------- |
| @After |
| public void afterTest() |
| { |
| if ( m_currentTestCase != null ) |
| m_currentTestCase.closeDocument(); |
| else if ( m_currentDocument != null ) |
| m_currentDocument.close(); |
| m_currentTestCase = null; |
| m_currentDocument = null; |
| m_callbackFactory.dispose(); |
| } |
| |
| // ----------------------------------------------------------------------------------------------------------------- |
| /** |
| * @return returns the undo manager belonging to a given document |
| */ |
| private XUndoManager getUndoManager() |
| { |
| final XUndoManagerSupplier suppUndo = UnoRuntime.queryInterface( XUndoManagerSupplier.class, m_currentDocument.getDocument() ); |
| final XUndoManager undoManager = suppUndo.getUndoManager(); |
| assertTrue( UnoRuntime.areSame( undoManager.getParent(), m_currentDocument.getDocument() ) ); |
| return undoManager; |
| } |
| |
| // ----------------------------------------------------------------------------------------------------------------- |
| private void impl_waitFor( final Object i_condition, final int i_milliSeconds ) throws InterruptedException |
| { |
| synchronized( i_condition ) |
| { |
| i_condition.wait( i_milliSeconds ); |
| } |
| } |
| |
| // ----------------------------------------------------------------------------------------------------------------- |
| private void impl_setupBrokenBasicScript() |
| { |
| try |
| { |
| final XEmbeddedScripts embeddedScripts = UnoRuntime.queryInterface( XEmbeddedScripts.class, m_currentDocument.getDocument() ); |
| final XLibraryContainer basicLibs = embeddedScripts.getBasicLibraries(); |
| final XNameContainer basicLib = basicLibs.createLibrary( "default" ); |
| |
| final String brokenScriptCode = |
| "Option Explicit\n" + |
| "\n" + |
| "Sub brokenScript\n" + |
| " Dim callback as Object\n" + |
| " ThisComponent.UndoManager.enterUndoContext( \"" + getCallbackUndoContextTitle() + "\" )\n" + |
| "\n" + |
| " callback = createUnoService( \"" + getCallbackComponentServiceName() + "\" )\n" + |
| " Dim emptyArgs() as new com.sun.star.beans.NamedValue\n" + |
| " Dim result as String\n" + |
| " result = callback.execute( emptyArgs() )\n" + |
| " If result = \"close\" Then\n" + |
| " ThisComponent.close( TRUE )\n" + |
| " End If\n" + |
| "End Sub\n" + |
| "\n"; |
| |
| basicLib.insertByName( "callbacks", brokenScriptCode ); |
| } |
| catch( com.sun.star.uno.Exception e ) |
| { |
| fail( "caught an exception while setting up the script: " + e.toString() ); |
| } |
| } |
| |
| // ----------------------------------------------------------------------------------------------------------------- |
| private XPropertySet impl_setupButton() throws com.sun.star.uno.Exception |
| { |
| // let the document create a shape |
| final XMultiServiceFactory docAsFactory = UnoRuntime.queryInterface( XMultiServiceFactory.class, |
| m_currentDocument.getDocument() ); |
| final XControlShape xShape = UnoRuntime.queryInterface( XControlShape.class, |
| docAsFactory.createInstance( "com.sun.star.drawing.ControlShape" ) ); |
| |
| // position and size of the shape |
| xShape.setSize( new Size( 28 * 100, 10 * 100 ) ); |
| xShape.setPosition( new Point( 10 * 100, 10 * 100 ) ); |
| |
| // create the form component (the model of a form control) |
| final String sQualifiedComponentName = "com.sun.star.form.component.CommandButton"; |
| final XControlModel controlModel = UnoRuntime.queryInterface( XControlModel.class, |
| getORB().createInstance( sQualifiedComponentName ) ); |
| |
| // knitt both |
| xShape.setControl( controlModel ); |
| |
| // add the shape to the shapes collection of the document |
| SpreadsheetDocument spreadsheetDoc = (SpreadsheetDocument)m_currentDocument; |
| final XDrawPageSupplier suppDrawPage = UnoRuntime.queryInterface( XDrawPageSupplier.class, |
| spreadsheetDoc.getSheet( 0 ) ); |
| final XDrawPage insertIntoPage = suppDrawPage.getDrawPage(); |
| |
| final XShapes sheetShapes = UnoRuntime.queryInterface( XShapes.class, insertIntoPage ); |
| sheetShapes.add( xShape ); |
| |
| return UnoRuntime.queryInterface( XPropertySet.class, controlModel ); |
| } |
| |
| // ----------------------------------------------------------------------------------------------------------------- |
| private void impl_assignScript( final XPropertySet i_controlModel, final String i_interfaceName, |
| final String i_interfaceMethod, final String i_scriptURI ) |
| { |
| try |
| { |
| final XChild modelAsChild = UnoRuntime.queryInterface( XChild.class, i_controlModel ); |
| final XIndexContainer parentForm = UnoRuntime.queryInterface( XIndexContainer.class, modelAsChild.getParent() ); |
| |
| final XEventAttacherManager manager = UnoRuntime.queryInterface( XEventAttacherManager.class, parentForm ); |
| |
| int containerPosition = -1; |
| for ( int i = 0; i < parentForm.getCount(); ++i ) |
| { |
| final XPropertySet child = UnoRuntime.queryInterface( XPropertySet.class, parentForm.getByIndex( i ) ); |
| if ( UnoRuntime.areSame( child, i_controlModel ) ) |
| { |
| containerPosition = i; |
| break; |
| } |
| } |
| assertFalse( "could not find the given control model within its parent", containerPosition == -1 ); |
| manager.registerScriptEvent( containerPosition, new ScriptEventDescriptor( |
| i_interfaceName, |
| i_interfaceMethod, |
| "", |
| "Script", |
| i_scriptURI |
| ) ); |
| } |
| catch( com.sun.star.uno.Exception e ) |
| { |
| fail( "caught an exception while assigning the script event to the button: " + e.toString() ); |
| } |
| } |
| |
| // ----------------------------------------------------------------------------------------------------------------- |
| private void impl_clickButton( final XPropertySet i_buttonModel ) throws NoSuchElementException, IndexOutOfBoundsException |
| { |
| final XControlAccess controlAccess = UnoRuntime.queryInterface( XControlAccess.class, |
| m_currentDocument.getCurrentView().getController() ); |
| final XControl control = controlAccess.getControl( UnoRuntime.queryInterface( XControlModel.class, i_buttonModel ) ); |
| final XAccessible accessible = UnoRuntime.queryInterface( XAccessible.class, control ); |
| final XAccessibleAction controlActions = UnoRuntime.queryInterface( XAccessibleAction.class, accessible.getAccessibleContext() ); |
| for ( int i=0; i<controlActions.getAccessibleActionCount(); ++i ) |
| { |
| if ( controlActions.getAccessibleActionDescription(i).equals( "click" ) ) |
| { |
| controlActions.doAccessibleAction(i); |
| return; |
| } |
| } |
| fail( "did not find the accessible action named 'click'" ); |
| } |
| |
| // ----------------------------------------------------------------------------------------------------------------- |
| private static class UndoListener implements XUndoManagerListener |
| { |
| public void undoActionAdded( UndoManagerEvent i_event ) |
| { |
| assertFalse( "|undoActionAdded| called after document was disposed", m_isDisposed ); |
| |
| ++m_undoActionsAdded; |
| m_mostRecentlyAddedAction = i_event.UndoActionTitle; |
| } |
| |
| public void actionUndone( UndoManagerEvent i_event ) |
| { |
| assertFalse( "|actionUndone| called after document was disposed", m_isDisposed ); |
| |
| ++m_undoCount; |
| m_mostRecentlyUndone = i_event.UndoActionTitle; |
| } |
| |
| public void actionRedone( UndoManagerEvent i_event ) |
| { |
| assertFalse( "|actionRedone| called after document was disposed", m_isDisposed ); |
| |
| ++m_redoCount; |
| } |
| |
| public void allActionsCleared( EventObject eo ) |
| { |
| assertFalse( "|allActionsCleared| called after document was disposed", m_isDisposed ); |
| |
| m_wasCleared = true; |
| } |
| |
| public void redoActionsCleared( EventObject eo ) |
| { |
| assertFalse( "|redoActionsCleared| called after document was disposed", m_isDisposed ); |
| |
| m_redoWasCleared = true; |
| } |
| |
| public void resetAll( EventObject i_event ) |
| { |
| assertFalse( "|resetAll| called after document was disposed", m_isDisposed ); |
| |
| m_managerWasReset = true; |
| m_activeUndoContexts.clear(); |
| } |
| |
| public void enteredContext( UndoManagerEvent i_event ) |
| { |
| assertFalse( "|enteredContext| called after document was disposed", m_isDisposed ); |
| |
| m_activeUndoContexts.push( i_event.UndoActionTitle ); |
| assertEquals( "different opinions on the context nesting level (after entering)", |
| m_activeUndoContexts.size(), i_event.UndoContextDepth ); |
| } |
| |
| public void enteredHiddenContext( UndoManagerEvent i_event ) |
| { |
| assertFalse( "|enteredHiddenContext| called after document was disposed", m_isDisposed ); |
| |
| m_activeUndoContexts.push( i_event.UndoActionTitle ); |
| assertEquals( "different opinions on the context nesting level (after entering hidden)", |
| m_activeUndoContexts.size(), i_event.UndoContextDepth ); |
| } |
| |
| public void leftContext( UndoManagerEvent i_event ) |
| { |
| assertFalse( "|leftContext| called after document was disposed", m_isDisposed ); |
| |
| assertEquals( "nested undo context descriptions do not match", m_activeUndoContexts.pop(), i_event.UndoActionTitle ); |
| assertEquals( "different opinions on the context nesting level (after leaving)", |
| m_activeUndoContexts.size(), i_event.UndoContextDepth ); |
| m_leftContext = true; |
| impl_notifyContextDepth(); |
| } |
| |
| public void leftHiddenContext( UndoManagerEvent i_event ) |
| { |
| assertFalse( "|leftHiddenContext| called after document was disposed", m_isDisposed ); |
| assertEquals( "|leftHiddenContext| is not expected to notify an action title", 0, i_event.UndoActionTitle.length() ); |
| |
| m_activeUndoContexts.pop(); |
| assertEquals( "different opinions on the context nesting level (after leaving)", |
| m_activeUndoContexts.size(), i_event.UndoContextDepth ); |
| m_leftHiddenContext = true; |
| impl_notifyContextDepth(); |
| } |
| |
| public void cancelledContext( UndoManagerEvent i_event ) |
| { |
| assertFalse( "|cancelledContext| called after document was disposed", m_isDisposed ); |
| assertEquals( "|cancelledContext| is not expected to notify an action title", 0, i_event.UndoActionTitle.length() ); |
| |
| m_activeUndoContexts.pop(); |
| assertEquals( "different opinions on the context nesting level (after cancelling)", |
| m_activeUndoContexts.size(), i_event.UndoContextDepth ); |
| m_cancelledContext = true; |
| impl_notifyContextDepth(); |
| } |
| |
| public void disposing( EventObject i_event ) |
| { |
| m_isDisposed = true; |
| } |
| |
| public void waitForAllContextsClosed( final int i_milliSeconds ) throws InterruptedException |
| { |
| synchronized ( m_allContextsClosedCondition ) |
| { |
| if ( m_activeUndoContexts.empty() ) |
| return; |
| m_allContextsClosedCondition.wait( i_milliSeconds ); |
| } |
| } |
| |
| private void impl_notifyContextDepth() |
| { |
| synchronized ( m_allContextsClosedCondition ) |
| { |
| if ( m_activeUndoContexts.empty() ) |
| { |
| m_allContextsClosedCondition.notifyAll(); |
| } |
| } |
| } |
| |
| private int getUndoActionsAdded() { return m_undoActionsAdded; } |
| private int getUndoActionCount() { return m_undoCount; } |
| private int getRedoActionCount() { return m_redoCount; } |
| private String getCurrentUndoContextTitle() { return m_activeUndoContexts.peek(); } |
| private String getMostRecentlyAddedActionTitle() { return m_mostRecentlyAddedAction; }; |
| private String getMostRecentlyUndoneTitle() { return m_mostRecentlyUndone; } |
| private int getCurrentUndoContextDepth() { return m_activeUndoContexts.size(); } |
| private boolean isDisposed() { return m_isDisposed; } |
| private boolean wasContextLeft() { return m_leftContext; } |
| private boolean wasHiddenContextLeft() { return m_leftHiddenContext; } |
| private boolean hasContextBeenCancelled() { return m_cancelledContext; } |
| private boolean wereStacksCleared() { return m_wasCleared; } |
| private boolean wasRedoStackCleared() { return m_redoWasCleared; } |
| private boolean wasManagerReset() { return m_managerWasReset; } |
| |
| void reset() |
| { |
| m_undoActionsAdded = m_undoCount = m_redoCount = 0; |
| m_activeUndoContexts.clear(); |
| m_mostRecentlyAddedAction = m_mostRecentlyUndone = null; |
| // m_isDisposed is not cleared, intentionally |
| m_leftContext = m_leftHiddenContext = m_cancelledContext = m_wasCleared = m_redoWasCleared = m_managerWasReset = false; |
| } |
| |
| private int m_undoActionsAdded = 0; |
| private int m_undoCount = 0; |
| private int m_redoCount = 0; |
| private boolean m_isDisposed = false; |
| private boolean m_leftContext = false; |
| private boolean m_leftHiddenContext = false; |
| private boolean m_cancelledContext = false; |
| private boolean m_wasCleared = false; |
| private boolean m_redoWasCleared = false; |
| private boolean m_managerWasReset = false; |
| private Stack< String > |
| m_activeUndoContexts = new Stack<String>(); |
| private String m_mostRecentlyAddedAction = null; |
| private String m_mostRecentlyUndone = null; |
| private final Object m_allContextsClosedCondition = new Object(); |
| }; |
| |
| // ----------------------------------------------------------------------------------------------------------------- |
| private void impl_checkUndo() throws Exception |
| { |
| System.out.println( "testing: " + m_currentTestCase.getDocumentDescription() ); |
| m_currentDocument = m_currentTestCase.getDocument(); |
| m_currentTestCase.initializeDocument(); |
| m_currentTestCase.verifyInitialDocumentState(); |
| |
| final XUndoManager undoManager = getUndoManager(); |
| undoManager.clear(); |
| assertFalse( "clearing the Undo manager should result in the impossibility to undo anything", undoManager.isUndoPossible() ); |
| assertFalse( "clearing the Undo manager should result in the impossibility to redo anything", undoManager.isRedoPossible() ); |
| |
| m_undoListener = new UndoListener(); |
| undoManager.addUndoManagerListener( m_undoListener ); |
| |
| impl_testSingleModification( undoManager ); |
| impl_testMultipleModifications( undoManager ); |
| impl_testCustomUndoActions( undoManager ); |
| impl_testLocking( undoManager ); |
| impl_testNestedContexts( undoManager ); |
| impl_testErrorHandling( undoManager ); |
| impl_testContextHandling( undoManager ); |
| impl_testStackHandling( undoManager ); |
| impl_testClearance( undoManager ); |
| impl_testHiddenContexts( undoManager ); |
| |
| // close the document, ensure the Undo manager listener gets notified |
| m_currentTestCase.closeDocument(); |
| m_currentTestCase = null; |
| m_currentDocument = null; |
| assertTrue( "document is closed, but the UndoManagerListener has not been notified of the disposal", m_undoListener.isDisposed() ); |
| } |
| |
| // ----------------------------------------------------------------------------------------------------------------- |
| private void impl_testSingleModification( final XUndoManager i_undoManager ) throws com.sun.star.uno.Exception |
| { |
| m_currentTestCase.doSingleModification(); |
| m_currentTestCase.verifySingleModificationDocumentState(); |
| |
| // undo the modification, ensure the listener got the proper notifications |
| assertEquals( "We did not yet do a undo!", 0, m_undoListener.getUndoActionCount() ); |
| i_undoManager.undo(); |
| assertEquals( "A simple undo does not result in the proper Undo count.", |
| 1, m_undoListener.getUndoActionCount() ); |
| |
| // verify the document is in its initial state, again |
| m_currentTestCase.verifyInitialDocumentState(); |
| |
| // redo the modification, ensure the listener got the proper notifications |
| assertEquals( "did not yet do a redo!", 0, m_undoListener.getRedoActionCount() ); |
| i_undoManager.redo(); |
| assertEquals( "did a redo, but got no notification of it!", 1, m_undoListener.getRedoActionCount() ); |
| // ensure the document is in the proper state, again |
| m_currentTestCase.verifySingleModificationDocumentState(); |
| |
| // now do an Undo via the UI (aka the dispatch API), and see if this works, and notifies the listener as |
| // expected |
| m_currentTestCase.getDocument().getCurrentView().dispatch( ".uno:Undo" ); |
| m_currentTestCase.verifyInitialDocumentState(); |
| assertEquals( "UI-Undo does not notify the listener", 2, m_undoListener.getUndoActionCount() ); |
| } |
| |
| // ----------------------------------------------------------------------------------------------------------------- |
| private void impl_testMultipleModifications( final XUndoManager i_undoManager ) throws com.sun.star.uno.Exception |
| { |
| m_undoListener.reset(); |
| assertEquals( "unexpected initial undo context depth", 0, m_undoListener.getCurrentUndoContextDepth() ); |
| i_undoManager.enterUndoContext( "Batch Changes" ); |
| assertEquals( "unexpected undo context depth after entering a context", |
| 1, m_undoListener.getCurrentUndoContextDepth() ); |
| assertEquals( "entering an Undo context has not been notified properly", |
| "Batch Changes", m_undoListener.getCurrentUndoContextTitle() ); |
| |
| final int modifications = m_currentTestCase.doMultipleModifications(); |
| assertEquals( "unexpected number of undo actions while doing batch changes to the document", |
| modifications, m_undoListener.getUndoActionsAdded() ); |
| assertEquals( "seems the document operations touched the undo context depth", |
| 1, m_undoListener.getCurrentUndoContextDepth() ); |
| |
| i_undoManager.leaveUndoContext(); |
| assertEquals( "unexpected undo context depth after leaving the last context", |
| 0, m_undoListener.getCurrentUndoContextDepth() ); |
| assertEquals( "no Undo done, yet - still the listener has been notified of an Undo action", |
| 0, m_undoListener.getUndoActionCount() ); |
| |
| i_undoManager.undo(); |
| assertEquals( "Just did an undo - the listener should have been notified", 1, m_undoListener.getUndoActionCount() ); |
| m_currentTestCase.verifyInitialDocumentState(); |
| } |
| |
| // ----------------------------------------------------------------------------------------------------------------- |
| private void impl_testCustomUndoActions( final XUndoManager i_undoManager ) throws com.sun.star.uno.Exception |
| { |
| i_undoManager.clear(); |
| m_undoListener.reset(); |
| assertFalse( "undo stack not empty after clearing the undo manager", i_undoManager.isUndoPossible() ); |
| assertFalse( "redo stack not empty after clearing the undo manager", i_undoManager.isRedoPossible() ); |
| assertArrayEquals( ">0 descriptions for an empty undo stack?", |
| new String[0], i_undoManager.getAllUndoActionTitles() ); |
| assertArrayEquals( ">0 descriptions for an empty redo stack?", |
| new String[0], i_undoManager.getAllRedoActionTitles() ); |
| |
| // add two actions, one directly, one within a context |
| final CustomUndoAction action1 = new CustomUndoAction( "UndoAction1" ); |
| i_undoManager.addUndoAction( action1 ); |
| assertEquals( "Adding an undo action not observed by the listener", 1, m_undoListener.getUndoActionsAdded() ); |
| assertEquals( "Adding an undo action did not notify the proper title", |
| action1.getTitle(), m_undoListener.getMostRecentlyAddedActionTitle() ); |
| final String contextTitle = "Undo Context"; |
| i_undoManager.enterUndoContext( contextTitle ); |
| final CustomUndoAction action2 = new CustomUndoAction( "UndoAction2" ); |
| i_undoManager.addUndoAction( action2 ); |
| assertEquals( "Adding an undo action not observed by the listener", |
| 2, m_undoListener.getUndoActionsAdded() ); |
| assertEquals( "Adding an undo action did not notify the proper title", |
| action2.getTitle(), m_undoListener.getMostRecentlyAddedActionTitle() ); |
| i_undoManager.leaveUndoContext(); |
| |
| // see if the manager has proper descriptions |
| assertArrayEquals( "unexpected Redo descriptions after adding two actions", |
| new String[0], i_undoManager.getAllRedoActionTitles() ); |
| assertArrayEquals( "unexpected Undo descriptions after adding two actions", |
| new String[]{contextTitle, action1.getTitle()}, i_undoManager.getAllUndoActionTitles() ); |
| |
| // undo one action |
| i_undoManager.undo(); |
| assertEquals( "improper action title notified during programmatic Undo", |
| contextTitle, m_undoListener.getMostRecentlyUndoneTitle() ); |
| assertTrue( "nested custom undo action has not been undone as expected", action2.undoCalled() ); |
| assertFalse( "nested custom undo action has not been undone as expected", action1.undoCalled() ); |
| assertArrayEquals( "unexpected Redo descriptions after undoing a nested custom action", |
| new String[]{contextTitle}, i_undoManager.getAllRedoActionTitles() ); |
| assertArrayEquals( "unexpected Undo descriptions after undoing a nested custom action", |
| new String[]{action1.getTitle()}, i_undoManager.getAllUndoActionTitles() ); |
| |
| // undo the second action, via UI dispatches |
| m_currentTestCase.getDocument().getCurrentView().dispatch( ".uno:Undo" ); |
| assertEquals( "improper action title notified during UI Undo", action1.getTitle(), m_undoListener.getMostRecentlyUndoneTitle() ); |
| assertTrue( "nested custom undo action has not been undone as expected", action1.undoCalled() ); |
| assertArrayEquals( "unexpected Redo descriptions after undoing the second custom action", |
| new String[]{action1.getTitle(), contextTitle}, i_undoManager.getAllRedoActionTitles() ); |
| assertArrayEquals( "unexpected Undo descriptions after undoing the second custom action", |
| new String[0], i_undoManager.getAllUndoActionTitles() ); |
| |
| // check the actions are disposed when the stacks are cleared |
| i_undoManager.clear(); |
| assertTrue( action1.disposed() && action2.disposed() ); |
| } |
| |
| // ----------------------------------------------------------------------------------------------------------------- |
| private void impl_testLocking( final XUndoManager i_undoManager ) throws com.sun.star.uno.Exception |
| { |
| i_undoManager.reset(); |
| m_undoListener.reset(); |
| |
| // implicit Undo actions, triggered by changes to the document |
| assertFalse( "unexpected initial locking state", i_undoManager.isLocked() ); |
| i_undoManager.lock(); |
| assertTrue( "just locked the manager, why does it lie?", i_undoManager.isLocked() ); |
| m_currentTestCase.doSingleModification(); |
| assertEquals( "when the Undo manager is locked, no implicit additions should happen", |
| 0, m_undoListener.getUndoActionsAdded() ); |
| i_undoManager.unlock(); |
| assertEquals( "unlock is not expected to add collected actions - they should be discarded", |
| 0, m_undoListener.getUndoActionsAdded() ); |
| assertFalse( "just unlocked the manager, why does it lie?", i_undoManager.isLocked() ); |
| |
| // explicit Undo actions |
| i_undoManager.lock(); |
| i_undoManager.addUndoAction( new CustomUndoAction() ); |
| i_undoManager.unlock(); |
| assertEquals( "explicit Undo actions are expected to be ignored when the manager is locked", |
| 0, m_undoListener.getUndoActionsAdded() ); |
| |
| // Undo contexts while being locked |
| i_undoManager.lock(); |
| i_undoManager.enterUndoContext( "Dummy Context" ); |
| i_undoManager.enterHiddenUndoContext(); |
| assertEquals( "entering Undo contexts should be ignored when the manager is locked", 0, m_undoListener.getCurrentUndoContextDepth() ); |
| i_undoManager.leaveUndoContext(); |
| i_undoManager.leaveUndoContext(); |
| i_undoManager.unlock(); |
| |
| // |unlock| error handling |
| assertFalse( "internal error: manager should not be locked at this point in time", i_undoManager.isLocked() ); |
| boolean caughtExpected = false; |
| try { i_undoManager.unlock(); } catch ( final NotLockedException e ) { caughtExpected = true; } |
| assertTrue( "unlocking the manager when it is not locked should throw", caughtExpected ); |
| } |
| |
| // ----------------------------------------------------------------------------------------------------------------- |
| private void impl_testContextHandling( final XUndoManager i_undoManager ) throws com.sun.star.uno.Exception |
| { |
| // ............................................................................................................. |
| // part I: non-empty contexts |
| i_undoManager.reset(); |
| m_undoListener.reset(); |
| |
| // put one action on the undo and one on the redo stack, as precondition for the following tests |
| final XUndoAction undoAction1 = new CustomUndoAction( "Undo Action 1" ); |
| i_undoManager.addUndoAction( undoAction1 ); |
| final XUndoAction undoAction2 = new CustomUndoAction( "Undo Action 2" ); |
| i_undoManager.addUndoAction( undoAction2 ); |
| i_undoManager.undo(); |
| assertTrue( "precondition for context handling tests not met (1)", i_undoManager.isUndoPossible() ); |
| assertTrue( "precondition for context handling tests not met (2)", i_undoManager.isRedoPossible() ); |
| assertArrayEquals( new String[] { undoAction1.getTitle() }, i_undoManager.getAllUndoActionTitles() ); |
| assertArrayEquals( new String[] { undoAction2.getTitle() }, i_undoManager.getAllRedoActionTitles() ); |
| |
| final String[] expectedRedoActionComments = new String[] { undoAction2.getTitle() }; |
| assertArrayEquals( expectedRedoActionComments, i_undoManager.getAllRedoActionTitles() ); |
| |
| // enter a context |
| i_undoManager.enterUndoContext( "Undo Context" ); |
| // this should not (yet) touch the redo stack |
| assertArrayEquals( expectedRedoActionComments, i_undoManager.getAllRedoActionTitles() ); |
| assertEquals( "unexpected undo context depth after entering a context", 1, m_undoListener.getCurrentUndoContextDepth() ); |
| // add a single action |
| XUndoAction undoAction3 = new CustomUndoAction( "Undo Action 3" ); |
| i_undoManager.addUndoAction( undoAction3 ); |
| // still, the redo stack should be untouched - added at a lower level does not affect it at all |
| assertArrayEquals( expectedRedoActionComments, i_undoManager.getAllRedoActionTitles() ); |
| |
| // while the context is open, its title should already contribute to the stack, ... |
| assertEquals( "Undo Context", i_undoManager.getCurrentUndoActionTitle() ); |
| // ... getAllUndo/RedoActionTitles should operate on the top level, not on the level defined by the open |
| // context, ... |
| assertArrayEquals( new String[] { "Undo Context", undoAction1.getTitle() }, |
| i_undoManager.getAllUndoActionTitles() ); |
| // ... but Undo and Redo should be impossible as long as the context is open |
| assertFalse( i_undoManager.isUndoPossible() ); |
| assertFalse( i_undoManager.isRedoPossible() ); |
| |
| // leave the context, check the listener has been notified properly, and the notified context depth is correct |
| i_undoManager.leaveUndoContext(); |
| assertTrue( m_undoListener.wasContextLeft() ); |
| assertFalse( m_undoListener.wasHiddenContextLeft() ); |
| assertFalse( m_undoListener.hasContextBeenCancelled() ); |
| assertEquals( "unexpected undo context depth leaving a non-empty context", 0, m_undoListener.getCurrentUndoContextDepth() ); |
| // leaving a non-empty context should have cleare the redo stack |
| assertArrayEquals( new String[0], i_undoManager.getAllRedoActionTitles() ); |
| assertTrue( m_undoListener.wasRedoStackCleared() ); |
| |
| // ............................................................................................................. |
| // part II: empty contexts |
| i_undoManager.reset(); |
| m_undoListener.reset(); |
| |
| // enter a context, leave it immediately without adding an action to it |
| i_undoManager.enterUndoContext( "Undo Context" ); |
| i_undoManager.leaveUndoContext(); |
| assertFalse( m_undoListener.wasContextLeft() ); |
| assertFalse( m_undoListener.wasHiddenContextLeft() ); |
| assertTrue( m_undoListener.hasContextBeenCancelled() ); |
| assertFalse( "leaving an empty context should silently remove it, and not contribute to the stack", |
| i_undoManager.isUndoPossible() ); |
| } |
| |
| // ----------------------------------------------------------------------------------------------------------------- |
| private void impl_testNestedContexts( final XUndoManager i_undoManager ) throws com.sun.star.uno.Exception |
| { |
| i_undoManager.reset(); |
| m_undoListener.reset(); |
| i_undoManager.enterUndoContext( "context 1" ); |
| i_undoManager.enterUndoContext( "context 1.1" ); |
| final CustomUndoAction action1 = new CustomUndoAction( "action 1.1.1" ); |
| i_undoManager.addUndoAction( action1 ); |
| i_undoManager.enterUndoContext( "context 1.1.2" ); |
| final CustomUndoAction action2 = new CustomUndoAction( "action 1.1.2.1" ); |
| i_undoManager.addUndoAction( action2 ); |
| i_undoManager.leaveUndoContext(); |
| final CustomUndoAction action3 = new CustomUndoAction( "action 1.1.3" ); |
| i_undoManager.addUndoAction( action3 ); |
| i_undoManager.leaveUndoContext(); |
| i_undoManager.leaveUndoContext(); |
| final CustomUndoAction action4 = new CustomUndoAction( "action 1.2" ); |
| i_undoManager.addUndoAction( action4 ); |
| |
| i_undoManager.undo(); |
| assertEquals( "undoing a single action notifies a wrong title", action4.getTitle(), m_undoListener.getMostRecentlyUndoneTitle() ); |
| assertTrue( "custom Undo not called", action4.undoCalled() ); |
| assertFalse( "too many custom Undos called", action1.undoCalled() || action2.undoCalled() || action3.undoCalled() ); |
| i_undoManager.undo(); |
| assertTrue( "nested actions not properly undone", action1.undoCalled() && action2.undoCalled() && action3.undoCalled() ); |
| } |
| |
| // ----------------------------------------------------------------------------------------------------------------- |
| private void impl_testErrorHandling( final XUndoManager i_undoManager ) throws com.sun.star.uno.Exception |
| { |
| i_undoManager.reset(); |
| m_undoListener.reset(); |
| |
| // try retrieving the comments for the current Undo/Redo - this should fail |
| boolean caughtExpected = false; |
| try { i_undoManager.getCurrentUndoActionTitle(); } |
| catch( final EmptyUndoStackException e ) { caughtExpected = true; } |
| assertTrue( "trying the title of the current Undo action is expected to fail for an empty stack", caughtExpected ); |
| |
| caughtExpected = false; |
| try { i_undoManager.getCurrentRedoActionTitle(); } |
| catch( final EmptyUndoStackException e ) { caughtExpected = true; } |
| assertTrue( "trying the title of the current Redo action is expected to fail for an empty stack", caughtExpected ); |
| |
| caughtExpected = false; |
| try { i_undoManager.undo(); } catch ( final EmptyUndoStackException e ) { caughtExpected = true; } |
| assertTrue( "undo should throw if no Undo action is on the stack", caughtExpected ); |
| |
| caughtExpected = false; |
| try { i_undoManager.redo(); } catch ( final EmptyUndoStackException e ) { caughtExpected = true; } |
| assertTrue( "redo should throw if no Redo action is on the stack", caughtExpected ); |
| |
| caughtExpected = false; |
| try { i_undoManager.leaveUndoContext(); } catch ( final InvalidStateException e ) { caughtExpected = true; } |
| assertTrue( "leaveUndoContext should throw if no context is currently open", caughtExpected ); |
| |
| caughtExpected = false; |
| try { i_undoManager.addUndoAction( null ); } catch ( com.sun.star.lang.IllegalArgumentException e ) { caughtExpected = true; } |
| assertTrue( "adding a NULL action should be rejected", caughtExpected ); |
| |
| i_undoManager.reset(); |
| i_undoManager.addUndoAction( new CustomUndoAction() ); |
| i_undoManager.addUndoAction( new CustomUndoAction() ); |
| i_undoManager.undo(); |
| i_undoManager.enterUndoContext( "Undo Context" ); |
| // those methods should fail when a context is open: |
| final String[] methodNames = new String[] { "undo", "redo", "clear", "clearRedo" }; |
| for ( int i=0; i<methodNames.length; ++i ) |
| { |
| caughtExpected = false; |
| try |
| { |
| Method method = i_undoManager.getClass().getMethod( methodNames[i], new Class[0] ); |
| method.invoke( i_undoManager, new Object[0] ); |
| } |
| catch ( IllegalAccessException ex ) { } |
| catch ( IllegalArgumentException ex ) { } |
| catch ( InvocationTargetException ex ) |
| { |
| Throwable targetException = ex.getTargetException(); |
| caughtExpected = ( targetException instanceof UndoContextNotClosedException ); |
| } |
| catch ( NoSuchMethodException ex ) { } |
| catch ( SecurityException ex ) { } |
| |
| assertTrue( methodNames[i] + " should be rejected when there is an open context", caughtExpected ); |
| } |
| i_undoManager.leaveUndoContext(); |
| |
| // try Undo actions which fail in their Undo/Redo |
| for ( int i=0; i<4; ++i ) |
| { |
| final boolean undo = ( i < 2 ); |
| final boolean doByAPI = ( i % 2 ) == 0; |
| |
| i_undoManager.reset(); |
| i_undoManager.addUndoAction( new CustomUndoAction() ); |
| i_undoManager.addUndoAction( new FailingUndoAction( undo ? FAIL_UNDO : FAIL_REDO ) ); |
| i_undoManager.addUndoAction( new CustomUndoAction() ); |
| i_undoManager.undo(); |
| if ( !undo ) |
| i_undoManager.undo(); |
| // assert preconditions for the below test |
| assertTrue( i_undoManager.isUndoPossible() ); |
| assertTrue( i_undoManager.isRedoPossible() ); |
| |
| boolean caughtUndoFailed = false; |
| try |
| { |
| if ( undo ) |
| if ( doByAPI ) |
| i_undoManager.undo(); |
| else |
| m_currentTestCase.getDocument().getCurrentView().dispatch( ".uno:Undo" ); |
| else |
| if ( doByAPI ) |
| i_undoManager.redo(); |
| else |
| m_currentTestCase.getDocument().getCurrentView().dispatch( ".uno:Redo" ); |
| } |
| catch ( UndoFailedException e ) |
| { |
| caughtUndoFailed = true; |
| } |
| if ( doByAPI ) |
| assertTrue( "Exceptions in XUndoAction.undo should be propagated at the API", caughtUndoFailed ); |
| else |
| assertFalse( "Undo/Redo by UI should not let escape Exceptions", caughtUndoFailed ); |
| if ( undo ) |
| { |
| assertFalse( "a failing Undo should clear the Undo stack", i_undoManager.isUndoPossible() ); |
| assertTrue( "a failing Undo should /not/ clear the Redo stack", i_undoManager.isRedoPossible() ); |
| } |
| else |
| { |
| assertTrue( "a failing Redo should /not/ clear the Undo stack", i_undoManager.isUndoPossible() ); |
| assertFalse( "a failing Redo should clear the Redo stack", i_undoManager.isRedoPossible() ); |
| } |
| } |
| } |
| |
| // ----------------------------------------------------------------------------------------------------------------- |
| private void impl_testStackHandling( final XUndoManager i_undoManager ) throws com.sun.star.uno.Exception |
| { |
| i_undoManager.reset(); |
| m_undoListener.reset(); |
| |
| assertFalse( i_undoManager.isUndoPossible() ); |
| assertFalse( i_undoManager.isRedoPossible() ); |
| |
| i_undoManager.addUndoAction( new CustomUndoAction() ); |
| assertTrue( i_undoManager.isUndoPossible() ); |
| assertFalse( i_undoManager.isRedoPossible() ); |
| i_undoManager.addUndoAction( new CustomUndoAction() ); |
| assertTrue( i_undoManager.isUndoPossible() ); |
| assertFalse( i_undoManager.isRedoPossible() ); |
| i_undoManager.undo(); |
| assertTrue( i_undoManager.isUndoPossible() ); |
| assertTrue( i_undoManager.isRedoPossible() ); |
| i_undoManager.undo(); |
| assertFalse( i_undoManager.isUndoPossible() ); |
| assertTrue( i_undoManager.isRedoPossible() ); |
| i_undoManager.addUndoAction( new CustomUndoAction() ); |
| assertTrue( i_undoManager.isUndoPossible() ); |
| assertFalse( "adding a new action should have cleared the Redo stack", i_undoManager.isRedoPossible() ); |
| } |
| |
| // ----------------------------------------------------------------------------------------------------------------- |
| private void impl_testClearance( final XUndoManager i_undoManager ) throws com.sun.star.uno.Exception |
| { |
| i_undoManager.reset(); |
| m_undoListener.reset(); |
| |
| // add an action, clear the stack, verify the listener has been called |
| i_undoManager.addUndoAction( new CustomUndoAction() ); |
| assertFalse( "clearance listener unexpectedly called", m_undoListener.wereStacksCleared() ); |
| assertFalse( "redo-clearance listener unexpectedly called", m_undoListener.wasRedoStackCleared() ); |
| i_undoManager.clear(); |
| assertTrue( "clearance listener not called as expected", m_undoListener.wereStacksCleared() ); |
| assertFalse( "redo-clearance listener unexpectedly called (2)", m_undoListener.wasRedoStackCleared() ); |
| |
| // ensure the listener is also called if the stack is actually empty at the moment of the call |
| m_undoListener.reset(); |
| assertFalse( i_undoManager.isUndoPossible() ); |
| i_undoManager.clear(); |
| assertTrue( "clearance listener is also expected to be called if the stack was empty before", m_undoListener.wereStacksCleared() ); |
| |
| // ensure the proper listeners are called for clearRedo |
| m_undoListener.reset(); |
| i_undoManager.clearRedo(); |
| assertFalse( m_undoListener.wereStacksCleared() ); |
| assertTrue( m_undoListener.wasRedoStackCleared() ); |
| |
| // ensure the redo listener is also called upon implicit redo stack clearance |
| m_undoListener.reset(); |
| i_undoManager.addUndoAction( new CustomUndoAction() ); |
| i_undoManager.addUndoAction( new CustomUndoAction() ); |
| i_undoManager.undo(); |
| assertTrue( i_undoManager.isUndoPossible() ); |
| assertTrue( i_undoManager.isRedoPossible() ); |
| i_undoManager.addUndoAction( new CustomUndoAction() ); |
| assertFalse( i_undoManager.isRedoPossible() ); |
| assertTrue( "implicit clearance of the Redo stack does not notify listeners", m_undoListener.wasRedoStackCleared() ); |
| |
| // test resetting the manager |
| m_undoListener.reset(); |
| i_undoManager.addUndoAction( new CustomUndoAction() ); |
| i_undoManager.addUndoAction( new CustomUndoAction() ); |
| i_undoManager.undo(); |
| assertTrue( i_undoManager.isUndoPossible() ); |
| assertTrue( i_undoManager.isRedoPossible() ); |
| i_undoManager.reset(); |
| assertFalse( i_undoManager.isUndoPossible() ); |
| assertFalse( i_undoManager.isRedoPossible() ); |
| assertTrue( "|reset| does not properly notify", m_undoListener.wasManagerReset() ); |
| |
| // resetting the manager, with open undo contexts |
| m_undoListener.reset(); |
| i_undoManager.addUndoAction( new CustomUndoAction() ); |
| i_undoManager.enterUndoContext( "Undo Context" ); |
| i_undoManager.addUndoAction( new CustomUndoAction() ); |
| i_undoManager.enterHiddenUndoContext(); |
| i_undoManager.reset(); |
| assertTrue( "|reset| while contexts are open does not properly notify", m_undoListener.wasManagerReset() ); |
| // verify the manager really has the proper context depth now |
| i_undoManager.enterUndoContext( "Undo Context" ); |
| assertEquals( "seems that |reset| did not really close the open contexts", 1, m_undoListener.getCurrentUndoContextDepth() ); |
| } |
| |
| // ----------------------------------------------------------------------------------------------------------------- |
| private void impl_testHiddenContexts( final XUndoManager i_undoManager ) throws com.sun.star.uno.Exception |
| { |
| i_undoManager.reset(); |
| m_undoListener.reset(); |
| assertFalse( "precondition for testing hidden undo contexts not met", i_undoManager.isUndoPossible() ); |
| |
| // entering a hidden context should be rejected if the stack is empty |
| boolean caughtExpected = false; |
| try { i_undoManager.enterHiddenUndoContext(); } |
| catch ( final EmptyUndoStackException e ) { caughtExpected = true; } |
| assertTrue( "entering hidden contexts should be denied on an empty stack", caughtExpected ); |
| |
| // but it should be allowed if the context is not empty |
| final CustomUndoAction undoAction0 = new CustomUndoAction( "Step 0" ); |
| i_undoManager.addUndoAction( undoAction0 ); |
| final CustomUndoAction undoAction1 = new CustomUndoAction( "Step 1" ); |
| i_undoManager.addUndoAction( undoAction1 ); |
| i_undoManager.enterHiddenUndoContext(); |
| final CustomUndoAction hiddenUndoAction = new CustomUndoAction( "hidden context action" ); |
| i_undoManager.addUndoAction( hiddenUndoAction ); |
| i_undoManager.leaveUndoContext(); |
| assertFalse( "leaving a hidden should not call |leftUndocontext|", m_undoListener.wasContextLeft() ); |
| assertTrue( "leaving a hidden does not call |leftHiddenUndocontext|", m_undoListener.wasHiddenContextLeft() ); |
| assertFalse( "leaving a non-empty hidden context claims to have cancelled it", m_undoListener.hasContextBeenCancelled() ); |
| assertEquals( "leaving a hidden context is not properly notified", 0, m_undoListener.getCurrentUndoContextDepth() ); |
| assertArrayEquals( "unexpected Undo stack after leaving a hidden context", |
| new String[] { undoAction1.getTitle(), undoAction0.getTitle() }, |
| i_undoManager.getAllUndoActionTitles() ); |
| |
| // and then calling |undo| once should not only undo everything in the hidden context, but also |
| // the previous action - but not more |
| i_undoManager.undo(); |
| assertTrue( "Undo after leaving a hidden context does not actually undo the context actions", |
| hiddenUndoAction.undoCalled() ); |
| assertTrue( "Undo after leaving a hidden context does not undo the predecessor action", |
| undoAction1.undoCalled() ); |
| assertFalse( "Undo after leaving a hidden context undoes too much", |
| undoAction0.undoCalled() ); |
| |
| // leaving an empty hidden context should call the proper notification method |
| m_undoListener.reset(); |
| i_undoManager.enterHiddenUndoContext(); |
| i_undoManager.leaveUndoContext(); |
| assertFalse( m_undoListener.wasContextLeft() ); |
| assertFalse( m_undoListener.wasHiddenContextLeft() ); |
| assertTrue( m_undoListener.hasContextBeenCancelled() ); |
| |
| // nesting hidden and normal contexts |
| m_undoListener.reset(); |
| i_undoManager.reset(); |
| final CustomUndoAction action0 = new CustomUndoAction( "action 0" ); |
| i_undoManager.addUndoAction( action0 ); |
| i_undoManager.enterUndoContext( "context 1" ); |
| final CustomUndoAction action1 = new CustomUndoAction( "action 1" ); |
| i_undoManager.addUndoAction( action1 ); |
| i_undoManager.enterHiddenUndoContext(); |
| final CustomUndoAction action2 = new CustomUndoAction( "action 2" ); |
| i_undoManager.addUndoAction( action2 ); |
| i_undoManager.enterUndoContext( "context 2" ); |
| // is entering a hidden context rejected even at the nesting level > 0 (the above test was for nesting level == 0)? |
| caughtExpected = false; |
| try { i_undoManager.enterHiddenUndoContext(); } |
| catch( final EmptyUndoStackException e ) { caughtExpected = true; } |
| assertTrue( "at a nesting level > 0, denied hidden contexts does not work as expected", caughtExpected ); |
| final CustomUndoAction action3 = new CustomUndoAction( "action 3" ); |
| i_undoManager.addUndoAction( action3 ); |
| i_undoManager.enterHiddenUndoContext(); |
| assertEquals( "mixed hidden/normal context do are not properly notified", 4, m_undoListener.getCurrentUndoContextDepth() ); |
| i_undoManager.leaveUndoContext(); |
| assertTrue( "the left context was empty - why wasn't 'cancelled' notified?", m_undoListener.hasContextBeenCancelled() ); |
| assertFalse( m_undoListener.wasContextLeft() ); |
| assertFalse( m_undoListener.wasHiddenContextLeft() ); |
| i_undoManager.leaveUndoContext(); |
| i_undoManager.leaveUndoContext(); |
| i_undoManager.leaveUndoContext(); |
| i_undoManager.undo(); |
| assertFalse( "one action too much has been undone", action0.undoCalled() ); |
| assertTrue( action1.undoCalled() ); |
| assertTrue( action2.undoCalled() ); |
| assertTrue( action3.undoCalled() ); |
| } |
| |
| // ----------------------------------------------------------------------------------------------------------------- |
| private XComponentContext getContext() |
| { |
| return m_connection.getComponentContext(); |
| } |
| |
| // ----------------------------------------------------------------------------------------------------------------- |
| private XMultiServiceFactory getORB() |
| { |
| final XMultiServiceFactory xMSF1 = UnoRuntime.queryInterface( |
| XMultiServiceFactory.class, getContext().getServiceManager() ); |
| return xMSF1; |
| } |
| |
| // ----------------------------------------------------------------------------------------------------------------- |
| @BeforeClass |
| public static void setUpConnection() throws Exception |
| { |
| System.out.println( "--------------------------------------------------------------------------------" ); |
| System.out.println( "starting class: " + UndoManager.class.getName() ); |
| System.out.println( "connecting ..." ); |
| m_connection.setUp(); |
| } |
| |
| // ----------------------------------------------------------------------------------------------------------------- |
| @AfterClass |
| public static void tearDownConnection() throws InterruptedException, com.sun.star.uno.Exception |
| { |
| System.out.println(); |
| System.out.println( "tearing down connection" ); |
| m_connection.tearDown(); |
| System.out.println( "finished class: " + UndoManager.class.getName() ); |
| System.out.println( "--------------------------------------------------------------------------------" ); |
| } |
| |
| // ----------------------------------------------------------------------------------------------------------------- |
| private static class CustomUndoAction implements XUndoAction, XComponent |
| { |
| CustomUndoAction() |
| { |
| m_title = "Custom Undo Action"; |
| } |
| |
| CustomUndoAction( final String i_title ) |
| { |
| m_title = i_title; |
| } |
| |
| public String getTitle() |
| { |
| return m_title; |
| } |
| |
| public void undo() throws UndoFailedException |
| { |
| m_undoCalled = true; |
| } |
| |
| public void redo() throws UndoFailedException |
| { |
| m_redoCalled = true; |
| } |
| |
| public void dispose() |
| { |
| m_disposed = true; |
| } |
| |
| public void addEventListener( XEventListener xl ) |
| { |
| fail( "addEventListener is not expected to be called in the course of this test" ); |
| } |
| |
| public void removeEventListener( XEventListener xl ) |
| { |
| fail( "removeEventListener is not expected to be called in the course of this test" ); |
| } |
| |
| boolean undoCalled() { return m_undoCalled; } |
| boolean redoCalled() { return m_redoCalled; } |
| boolean disposed() { return m_disposed; } |
| |
| private final String m_title; |
| private boolean m_undoCalled = false; |
| private boolean m_redoCalled = false; |
| private boolean m_disposed = false; |
| } |
| |
| private static short FAIL_UNDO = 1; |
| private static short FAIL_REDO = 2; |
| |
| private static class FailingUndoAction implements XUndoAction |
| { |
| FailingUndoAction( final short i_failWhich ) |
| { |
| m_failWhich = i_failWhich; |
| } |
| |
| public String getTitle() |
| { |
| return "failing undo"; |
| } |
| |
| public void undo() throws UndoFailedException |
| { |
| if ( m_failWhich != FAIL_REDO ) |
| impl_throw(); |
| } |
| |
| public void redo() throws UndoFailedException |
| { |
| if ( m_failWhich != FAIL_UNDO ) |
| impl_throw(); |
| } |
| |
| private void impl_throw() throws UndoFailedException |
| { |
| throw new UndoFailedException(); |
| } |
| |
| private final short m_failWhich; |
| } |
| |
| // ----------------------------------------------------------------------------------------------------------------- |
| private static class CountingUndoAction implements XUndoAction |
| { |
| CountingUndoAction( final int i_expectedOrder, final Object i_lock, final Integer[] i_actionsUndoneCounter ) |
| { |
| m_expectedOrder = i_expectedOrder; |
| m_lock = i_lock; |
| m_actionsUndoneCounter = i_actionsUndoneCounter; |
| } |
| |
| public String getTitle() |
| { |
| return "Counting Undo Action"; |
| } |
| |
| public void undo() throws UndoFailedException |
| { |
| synchronized( m_lock ) |
| { |
| assertEquals( "Undo action called out of order", m_expectedOrder, m_actionsUndoneCounter[0].intValue() ); |
| ++m_actionsUndoneCounter[0]; |
| } |
| } |
| |
| public void redo() throws UndoFailedException |
| { |
| fail( "CountingUndoAction.redo is not expected to be called in this test." ); |
| } |
| private final int m_expectedOrder; |
| private final Object m_lock; |
| private Integer[] m_actionsUndoneCounter; |
| } |
| |
| // ----------------------------------------------------------------------------------------------------------------- |
| private static String getCallbackUndoContextTitle() |
| { |
| return "Some Unfinished Undo Context"; |
| } |
| |
| // ----------------------------------------------------------------------------------------------------------------- |
| private static String getCallbackComponentServiceName() |
| { |
| return "org.openoffice.complex.sfx2.Callback"; |
| } |
| |
| // ----------------------------------------------------------------------------------------------------------------- |
| /** |
| * a factory for a callback component which, at OOo runtime, is inserted into OOo's "component repository" |
| */ |
| private class CallbackComponentFactory implements XSingleComponentFactory, XServiceInfo, XComponent |
| { |
| public Object createInstanceWithContext( XComponentContext i_context ) throws com.sun.star.uno.Exception |
| { |
| return new CallbackComponent(); |
| } |
| |
| public Object createInstanceWithArgumentsAndContext( Object[] i_arguments, XComponentContext i_context ) throws com.sun.star.uno.Exception |
| { |
| return createInstanceWithContext( i_context ); |
| } |
| |
| public String getImplementationName() |
| { |
| return "org.openoffice.complex.sfx2.CallbackComponent"; |
| } |
| |
| public boolean supportsService( String i_serviceName ) |
| { |
| return i_serviceName.equals( getCallbackComponentServiceName() ); |
| } |
| |
| public String[] getSupportedServiceNames() |
| { |
| return new String[] { getCallbackComponentServiceName() }; |
| } |
| |
| public void dispose() |
| { |
| final EventObject event = new EventObject( this ); |
| |
| final ArrayList eventListenersCopy = (ArrayList)m_eventListeners.clone(); |
| final Iterator iter = eventListenersCopy.iterator(); |
| while ( iter.hasNext() ) |
| { |
| ((XEventListener)iter.next()).disposing( event ); |
| } |
| } |
| |
| public void addEventListener( XEventListener i_listener ) |
| { |
| if ( i_listener != null ) |
| m_eventListeners.add( i_listener ); |
| } |
| |
| public void removeEventListener( XEventListener i_listener ) |
| { |
| m_eventListeners.remove( i_listener ); |
| } |
| |
| private final ArrayList m_eventListeners = new ArrayList(); |
| }; |
| |
| // ----------------------------------------------------------------------------------------------------------------- |
| private class CallbackComponent implements XJob, XTypeProvider |
| { |
| CallbackComponent() |
| { |
| } |
| |
| public Object execute( NamedValue[] i_parameters ) throws com.sun.star.lang.IllegalArgumentException, com.sun.star.uno.Exception |
| { |
| // this method is called from within the Basic script which is to check whether the OOo framework |
| // properly cleans up unfinished Undo contexts. It is called immediately after the context has been |
| // entered, so verify the expected Undo manager state. |
| assertEquals( getCallbackUndoContextTitle(), m_undoListener.getCurrentUndoContextTitle() ); |
| assertEquals( 1, m_undoListener.getCurrentUndoContextDepth() ); |
| |
| synchronized( m_callbackCondition ) |
| { |
| m_callbackCalled = true; |
| m_callbackCondition.notifyAll(); |
| } |
| return m_closeAfterCallback ? "close" : ""; |
| } |
| |
| public Type[] getTypes() |
| { |
| final Class interfaces[] = getClass().getInterfaces(); |
| Type types[] = new Type[ interfaces.length ]; |
| for ( int i = 0; i < interfaces.length; ++i ) |
| types[i] = new Type(interfaces[i]); |
| return types; |
| } |
| |
| public byte[] getImplementationId() |
| { |
| return getClass().toString().getBytes(); |
| } |
| } |
| |
| private static final OfficeConnection m_connection = new OfficeConnection(); |
| private DocumentTest m_currentTestCase; |
| private OfficeDocument m_currentDocument; |
| private UndoListener m_undoListener; |
| private CallbackComponentFactory m_callbackFactory = null; |
| private boolean m_callbackCalled = false; |
| private boolean m_closeAfterCallback = false; |
| private final Object m_callbackCondition = new Object(); |
| } |