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