001 /* 002 * Copyright 2002-2005 the original author or authors. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); you may not 005 * use this file except in compliance with the License. You may obtain a copy of 006 * the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 012 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 013 * License for the specific language governing permissions and limitations under 014 * the License. 015 */ 016 package org.springframework.richclient.form; 017 018 import java.beans.PropertyChangeEvent; 019 import java.beans.PropertyChangeListener; 020 import java.util.List; 021 022 import javax.swing.JButton; 023 import javax.swing.JComponent; 024 import javax.swing.JPopupMenu; 025 import javax.swing.ListSelectionModel; 026 import javax.swing.event.ListSelectionListener; 027 028 import org.springframework.beans.BeanUtils; 029 import org.springframework.binding.form.HierarchicalFormModel; 030 import org.springframework.binding.form.ValidatingFormModel; 031 import org.springframework.binding.validation.ValidationResultsModel; 032 import org.springframework.binding.validation.support.DefaultValidationResultsModel; 033 import org.springframework.binding.value.ValueModel; 034 import org.springframework.binding.value.support.DeepCopyBufferedCollectionValueModel; 035 import org.springframework.binding.value.support.DirtyTrackingValueModel; 036 import org.springframework.binding.value.support.ObservableEventList; 037 import org.springframework.binding.value.support.ObservableList; 038 import org.springframework.binding.value.support.ValueHolder; 039 import org.springframework.richclient.command.AbstractCommand; 040 import org.springframework.richclient.command.ActionCommand; 041 import org.springframework.richclient.command.CommandGroup; 042 import org.springframework.richclient.dialog.ConfirmationDialog; 043 import org.springframework.richclient.dialog.Messagable; 044 import org.springframework.richclient.table.ListSelectionListenerSupport; 045 import org.springframework.richclient.util.GuiStandardUtils; 046 import org.springframework.util.Assert; 047 import org.springframework.util.ClassUtils; 048 import org.springframework.util.StringUtils; 049 050 import ca.odell.glazedlists.BasicEventList; 051 import ca.odell.glazedlists.EventList; 052 053 /** 054 * Abstract base for the Master form of a Master/Detail pair. Derived types must implement 055 * two methods: 056 * <dt>{@link #createDetailForm}</dt> 057 * <dd>To construct the detail half of this master/detail pair</dd> 058 * <dt>{@link #getSelectionModel()}</dt> 059 * <dd>To return the selection model of the object rendering the list</dd> 060 * <p> 061 * <strong>Important note:</strong> Any subclass that implements 062 * {@link AbstractForm#createControl()} <strong>MUST</strong> call {@link #configure()} 063 * prior to its work in order to have the detail form properly prepared. 064 * 065 * @author Larry Streepy 066 * @see AbstractDetailForm 067 * @see #creatingNewObject() 068 */ 069 public abstract class AbstractMasterForm extends AbstractForm { 070 071 /** Property name for indicating changes in our selected index. */ 072 public static final String SELECTION_INDEX_PROPERTY = "selectionIndex"; 073 074 /** 075 * Property name for indicating changes in our "is creating new object" state. 076 */ 077 public static final String IS_CREATING_PROPERTY = "isCreating"; 078 079 private DirtyTrackingDCBCVM collectionVM; 080 private EventList rootEventList; 081 private ObservableEventList masterEventList; 082 private AbstractDetailForm detailForm; 083 private Class detailType; 084 private ActionCommand newFormObjectCommand; 085 private ActionCommand deleteCommand; 086 private CommandGroup commandGroup; 087 private boolean confirmDelete = true; 088 private ListSelectionHandler selectionHandler = new ListSelectionHandler(); 089 private PropertyChangeListener parentFormPropertyChangeHandler = new ParentFormPropertyChangeHandler(); 090 091 /** 092 * Construct a new AbstractMasterForm using the given parent form model and property 093 * path. The form model for this class will be constructed by getting the value model 094 * of the specified property from the parent form model and constructing a 095 * DeepCopyBufferedCollectionValueModel on top of it. 096 * 097 * @param parentFormModel Parent form model to access for this form's data 098 * @param property containing this forms data (must be a collection or an array) 099 * @param formId Id of this form 100 * @param detailType Type of detail object managed by this master form 101 */ 102 protected AbstractMasterForm(HierarchicalFormModel parentFormModel, String property, String formId, Class detailType) { 103 super( formId ); 104 this.detailType = detailType; 105 106 ValueModel propertyVM = parentFormModel.getValueModel( property ); 107 108 // Now construct the dirty tracking model 109 Class collectionType = getMasterCollectionType( propertyVM ); 110 111 collectionVM = new DirtyTrackingDCBCVM( propertyVM, collectionType ); 112 ValidatingFormModel formModel = FormModelHelper.createChildPageFormModel( parentFormModel, formId, 113 collectionVM); 114 setFormModel( formModel ); 115 116 // Install a handler to detect when the parents form model changes 117 propertyVM.addValueChangeListener(parentFormPropertyChangeHandler); 118 } 119 120 /** 121 * Get the value model representing the collection we are managing. This value model 122 * can be used to register vlue change listeners and to update the collection 123 * contents. 124 * <p> 125 * You must use this method to get the value model since calling getValueModel on the 126 * parent form model will not get you what you want. 127 * 128 * @return collection value model 129 */ 130 public ValueModel getCollectionValueModel() { 131 return collectionVM; 132 } 133 134 /** 135 * Determine the type of the collection holding the detail items. This will be used to 136 * create the value model for the collection. 137 * <p> 138 * <b>Note to Hibernate users:</b> You will most likely need to override this method 139 * in order to force the use of a simple <code>List</code> class instead of the 140 * default implementation that would return <code>PersistentList</code>. Creating a 141 * new instance of this type would result in a somewhat misleading error regarding 142 * lazy instantiation since the new PersistentList instance would not have been 143 * properly initialized by Hibernate. 144 * 145 * @param collectionPropertyVM ValueModel holding the master collection 146 * @return Type of collection to use 147 */ 148 protected Class getMasterCollectionType(ValueModel collectionPropertyVM) { 149 return collectionPropertyVM.getValue().getClass(); 150 } 151 152 /** 153 * Configure this master form's data and prepare the detail form. 154 */ 155 protected void configure() { 156 // Just configure a basic event list to handle our data 157 installEventList( getRootEventList() ); 158 159 // Now we need to construct a subform and value model to handle the 160 // detail elements of this master table 161 162 Object detailObject = BeanUtils.instantiateClass(detailType); 163 ValueModel valueHolder = new ValueHolder( detailObject ); 164 detailForm = createDetailForm( getFormModel(), valueHolder, masterEventList); 165 166 // Start the form disabled and not validating until the form is actually in use. 167 detailForm.setEnabled( false ); 168 detailForm.getFormModel().setValidating( false ); 169 170 // Wire up the monitor to track the selected index and edit state so we 171 // can keep the delete and add button states up to date 172 detailForm.getEditingIndexHolder().addValueChangeListener( new EditingIndexMonitor() ); 173 detailForm.addPropertyChangeListener( new EditStateMonitor() ); 174 } 175 176 /** 177 * Construct the detail half of this master/detail pair. 178 * 179 * @param parentFormModel 180 * @param valueHolder BufferedValueModel holding an object of the type configured for 181 * this master form. 182 * @param masterList The ObservableList of data to from the master form (this will 183 * constitute the editable object list for the detail form). 184 */ 185 protected abstract AbstractDetailForm createDetailForm(HierarchicalFormModel parentFormModel, 186 ValueModel valueHolder, ObservableList masterList); 187 188 /** 189 * Install an EventList for use as our master list data. The event list must have been 190 * created on top of the event list obtained from {@link #getRootEventList()} so that 191 * all changes are propery proxied onto the actual form data. The event list provided 192 * will be wrapped in a {@link ObservableEventList}. 193 * 194 * @param eventList new EventList to install 195 */ 196 protected void installEventList(EventList eventList) { 197 masterEventList = new ObservableEventList( eventList ); 198 199 // Propogate this down to the detail form 200 if( detailForm != null ) { 201 detailForm.setMasterList(masterEventList); 202 } 203 } 204 205 /** 206 * Get the root event list for this model. This event list will be constructed from 207 * the form objects value (assumed to be an EventList). Any subclasses that are 208 * installing additional transformed lists should use this method to obtain the 209 * original event list on top of which all the other lists are constructed. 210 */ 211 protected EventList getRootEventList() { 212 if( rootEventList == null ) { 213 rootEventList = (EventList) getFormModel().getFormObjectHolder().getValue(); 214 } 215 return rootEventList; 216 } 217 218 /** 219 * Handle the root event list being changed externally. This is normally invoked 220 * when the value model holding this forms source property has changed (which can 221 * occur when the parent form object is changed). This method is normally invoked from 222 * the parent form object listener. 223 * 224 * @see #parentFormPropertyChangeHandler 225 */ 226 protected void handleExternalRootEventListChange() { 227 if( masterEventList != null ) { 228 // While we do this, we need to disable our normal list listener since it's 229 // too late to interact with the user due to unsaved changes (the underlying 230 // value model has already changed). 231 uninstallSelectionHandler(); 232 233 // Clean up the detail form 234 if( detailForm != null ) { 235 detailForm.reset(); 236 detailForm.setSelectedIndex( -1 ); 237 } 238 239 if( isControlCreated() ) { 240 updateControlsForState(); // Ensure our controls are properly updated 241 } 242 243 installSelectionHandler(); // Reinstate the handler 244 } 245 } 246 247 /** 248 * Get the form data we are operating upon. The form object must be castable to List 249 * for this to work. 250 * 251 * @return List The form object's value 252 */ 253 public List getFormData() { 254 return (List) getFormModel().getFormObjectHolder().getValue(); 255 } 256 257 /** 258 * Get the master EventList (which proxies the real form data). 259 * 260 * @return EventList 261 */ 262 public ObservableEventList getMasterEventList() { 263 return masterEventList; 264 } 265 266 /** 267 * Get the selection model for the master list representation. 268 * 269 * @return selection model 270 */ 271 protected abstract ListSelectionModel getSelectionModel(); 272 273 /** 274 * Install our selection handler. 275 */ 276 protected void installSelectionHandler() { 277 ListSelectionModel lsm = getSelectionModel(); 278 if( lsm != null ) { 279 lsm.addListSelectionListener( getSelectionHandler() ); 280 } 281 } 282 283 /** 284 * Uninstall our selection handler. 285 */ 286 protected void uninstallSelectionHandler() { 287 ListSelectionModel lsm = getSelectionModel(); 288 if( lsm != null ) { 289 lsm.removeListSelectionListener( getSelectionHandler() ); 290 } 291 } 292 293 /** 294 * Indicates that we are creating a new detail object. Default behavior is to just 295 * clear the selection on the master set. 296 */ 297 public void creatingNewObject() { 298 getSelectionModel().clearSelection(); 299 } 300 301 /** 302 * Get the selection handler for the master list. Default implementation. 303 * 304 * @return listener to handle master table selection events 305 */ 306 protected ListSelectionListener getSelectionHandler() { 307 return selectionHandler; 308 } 309 310 /** 311 * Get the command group for the master table (the add and delete commands). 312 * 313 * @return command group 314 */ 315 protected CommandGroup getCommandGroup() { 316 if( commandGroup == null ) { 317 commandGroup = CommandGroup.createCommandGroup( null, new AbstractCommand[] { getDeleteCommand(), 318 getNewFormObjectCommand() } ); 319 } 320 return commandGroup; 321 } 322 323 /** 324 * Return a standardized row of command buttons, right-justified and all of the same 325 * size, with OK as the default button, and no mnemonics used, as per the Java Look 326 * and Feel guidelines. 327 */ 328 protected JComponent createButtonBar() { 329 JComponent buttonBar = getCommandGroup().createButtonBar(); 330 GuiStandardUtils.attachDialogBorder( buttonBar ); 331 return buttonBar; 332 } 333 334 /** 335 * Get the popup menu for the master table. This is built from the command group 336 * returned from {@link #getCommandGroup()}. 337 * 338 * @return popup menu 339 */ 340 protected JPopupMenu getPopupMenu() { 341 return getCommandGroup().createPopupMenu(); 342 } 343 344 /** 345 * Get the action command to creating a new detail object. Note that we have to 346 * override this method in order to call our own createNewDetailObjectCommand method 347 * since the AbstractForm's implementation of createNewFormObjectCommand is private! 348 */ 349 public ActionCommand getNewFormObjectCommand() { 350 if( newFormObjectCommand == null ) { 351 newFormObjectCommand = createNewFormObjectCommand(); 352 } 353 return newFormObjectCommand; 354 } 355 356 /** 357 * Create the "new detail object" command. This will encapsulate the action from our 358 * detail forms {@link AbstractForm#getNewFormObjectCommand} as well as controlling 359 * the state of the detail form. 360 * 361 * @return command 362 */ 363 protected ActionCommand createNewFormObjectCommand() { 364 String commandId = getNewFormObjectCommandId(); 365 if( !StringUtils.hasText( commandId ) ) { 366 return null; 367 } 368 369 ActionCommand newDetailObjectCommand = new ActionCommand( commandId ) { 370 protected void doExecuteCommand() { 371 maybeCreateNewObject(); // Avoid losing user edits 372 } 373 }; 374 String scid = constructSecurityControllerId( commandId ); 375 newDetailObjectCommand.setSecurityControllerId( scid ); 376 return (ActionCommand) getCommandConfigurer().configure( newDetailObjectCommand ); 377 } 378 379 /** 380 * Construct the "new detail object" command id as 381 * <code>new[detailTypeName]Command</code> 382 * 383 * @return constructed command id 384 */ 385 protected String getNewFormObjectCommandId() { 386 return "new" + StringUtils.capitalize( ClassUtils.getShortName( detailType + "Command" ) ); 387 } 388 389 /** 390 * Return the command to delete the currently selected item in the master set. 391 * 392 * @return command, created on demand 393 */ 394 public ActionCommand getDeleteCommand() { 395 if( deleteCommand == null ) { 396 deleteCommand = createDeleteCommand(); 397 } 398 return deleteCommand; 399 } 400 401 /** 402 * Create the "delete object" command. 403 * 404 * @return command 405 */ 406 protected ActionCommand createDeleteCommand() { 407 String commandId = getDeleteCommandId(); 408 if( !StringUtils.hasText( commandId ) ) { 409 return null; 410 } 411 412 final ActionCommand deleteCommand = new ActionCommand( commandId ) { 413 protected void doExecuteCommand() { 414 maybeDeleteSelectedItems(); 415 } 416 }; 417 418 String scid = constructSecurityControllerId( commandId ); 419 deleteCommand.setSecurityControllerId( scid ); 420 return (ActionCommand) getCommandConfigurer().configure( deleteCommand ); 421 } 422 423 /** 424 * Get the message to present to the user when confirming the delete of selected 425 * detail items. This default implementation just obtains the message with key 426 * <code><formId>.confirmDelete.message</code> or 427 * <code>masterForm.confirmDelete.message</code>. Subclasses can use the selected 428 * item(s) to construct a more meaningful message. 429 */ 430 protected String getConfirmDeleteMessage() { 431 return getMessage(new String[] { getId() + ".confirmDelete.message", "masterForm.confirmDelete.message" }); 432 } 433 434 /** 435 * Maybe delete the selected items. If we are configured to confirm the delete, then 436 * do so. If the user confirms, then delete the selected items. 437 */ 438 protected void maybeDeleteSelectedItems() { 439 440 // If configured, have the user confirm the delete operation 441 if( isConfirmDelete() ) { 442 String title = getMessage( new String[] { getId() + ".confirmDelete.title", "masterForm.confirmDelete.title" } ); 443 String message = getConfirmDeleteMessage(); 444 ConfirmationDialog dlg = new ConfirmationDialog( title, message ) { 445 446 protected void onConfirm() { 447 deleteSelectedItems(); 448 getSelectionModel().clearSelection(); 449 } 450 }; 451 dlg.showDialog(); 452 } else { 453 deleteSelectedItems(); 454 getSelectionModel().clearSelection(); 455 } 456 } 457 458 /** 459 * Delete the detail item at the specified index. 460 */ 461 protected void deleteSelectedItems() { 462 ListSelectionModel sm = getSelectionModel(); 463 464 if( sm.isSelectionEmpty() ) { 465 return; 466 } 467 468 detailForm.reset(); 469 470 int min = sm.getMinSelectionIndex(); 471 int max = sm.getMaxSelectionIndex(); 472 473 // Loop backwards and delete each selected item in the interval 474 for( int index = max; index >= min; index-- ) { 475 if( sm.isSelectedIndex( index ) ) { 476 getMasterEventList().remove( index ); 477 } 478 } 479 } 480 481 /** 482 * Construct the button to invoke the delete command. 483 * 484 * @return button 485 */ 486 protected JButton createDeleteButton() { 487 Assert.state( deleteCommand != null, "Delete command has not been created!" ); 488 return (JButton) deleteCommand.createButton(); 489 } 490 491 /** 492 * Construct the "delete detail object" command id as 493 * <code>delete[DetailTypeName]Command</code> 494 * 495 * @return constructed command id 496 */ 497 protected String getDeleteCommandId() { 498 return "delete" + StringUtils.capitalize( ClassUtils.getShortName( detailType + "Command" ) ); 499 } 500 501 /** 502 * @return Returns the detailForm. 503 */ 504 protected AbstractDetailForm getDetailForm() { 505 return detailForm; 506 } 507 508 /** 509 * @param form The detailForm to set. 510 */ 511 protected void setDetailForm(AbstractDetailForm form) { 512 detailForm = form; 513 } 514 515 /** 516 * @return Returns the detailFormModel. 517 */ 518 protected ValidatingFormModel getDetailFormModel() { 519 return detailForm.getFormModel(); 520 } 521 522 /** 523 * @return Returns the detailType. 524 */ 525 protected Class getDetailType() { 526 return detailType; 527 } 528 529 /** 530 * @param type The detailType to set. 531 */ 532 protected void setDetailType(Class type) { 533 detailType = type; 534 } 535 536 /** 537 * Return confirm delete setting. 538 * @return confirm delete setting. 539 */ 540 public boolean isConfirmDelete() { 541 return confirmDelete; 542 } 543 544 /** 545 * Set confirm delete. If this is <code>true</code> then the master form will 546 * confirm with the user prior to deleting a detail item. 547 * @param confirmDelete 548 */ 549 public void setConfirmDelete(boolean confirmDelete) { 550 this.confirmDelete = confirmDelete; 551 } 552 553 /** 554 * Deal with the user invoking a "new object" command. If we have unsaved changes, 555 * then we need to query the user to ensure they want to really make the change. 556 */ 557 protected void maybeCreateNewObject() { 558 if( getDetailForm().isEditingNewFormObject() ) { 559 return; // Already creating a new object, just bail 560 } 561 562 final ActionCommand detailNewObjectCommand = detailForm.getNewFormObjectCommand(); 563 564 if( getDetailForm().isDirty() ) { 565 String title = getMessage( new String[] { getId() + ".dirtyNew.title", "masterForm.dirtyNew.title" } ); 566 String message = getMessage( new String[] { getId() + ".dirtyNew.message", "masterForm.dirtyNew.message" } ); 567 ConfirmationDialog dlg = new ConfirmationDialog( title, message ) { 568 protected void onConfirm() { 569 // Tell both forms that we are creating a new object 570 detailNewObjectCommand.execute(); // Do subform action first 571 creatingNewObject(); 572 detailForm.creatingNewObject(); 573 } 574 }; 575 dlg.showDialog(); 576 } else { 577 // Tell both forms that we are creating a new object 578 detailNewObjectCommand.execute(); // Do subform action first 579 creatingNewObject(); 580 detailForm.creatingNewObject(); 581 } 582 } 583 584 /** 585 * Update our controls based on our state. 586 */ 587 protected void updateControlsForState() { 588 int state = getDetailForm().getEditState(); 589 boolean isCreating = state == AbstractDetailForm.STATE_CREATE; 590 591 getDeleteCommand().setEnabled( getDetailForm().getEditingFormObjectIndex() >= 0 ); 592 getNewFormObjectCommand().setEnabled( !isCreating ); 593 594 // If we are in the CLEAR state, then we need to disable validations on the form 595 getDetailFormModel().setValidating( state != AbstractDetailForm.STATE_CLEAR ); 596 } 597 598 /** 599 * When the results reporter is setup on the master form, we need to capture it and 600 * forward it on to the detail form as well. 601 */ 602 public ValidationResultsReporter newSingleLineResultsReporter(Messagable messageReceiver) { 603 // create a resultsModel container which receives events from detail and master 604 ValidationResultsModel validationResultsModel = new DefaultValidationResultsModel(); 605 validationResultsModel.add(getFormModel().getValidationResults()); 606 validationResultsModel.add(getDetailFormModel().getValidationResults()); 607 ValidationResultsReporter reporter = new SimpleValidationResultsReporter(validationResultsModel, messageReceiver); 608 return reporter; 609 } 610 611 /** 612 * Inner class to handle the list selection and installing the selection into the 613 * detail form. 614 */ 615 protected class ListSelectionHandler extends ListSelectionListenerSupport { 616 /** 617 * Called when nothing gets selected. Override this method to handle empty 618 * selection 619 */ 620 protected void onNoSelection() { 621 maybeChangeSelection( -1 ); 622 } 623 624 /** 625 * Called when the user selects a single row. Override this method to handle 626 * single selection 627 * 628 * @param index the selected row 629 */ 630 protected void onSingleSelection(final int index) { 631 maybeChangeSelection( index ); 632 } 633 634 /** 635 * Deal with a change in the selected index. If we have unsaved changes, then we 636 * need to query the user to ensure they want to really make the change. 637 * 638 * @param newIndex The new selection index, may be -1 to clear the selection 639 */ 640 protected void maybeChangeSelection(final int newIndex) { 641 if( newIndex == getDetailForm().getSelectedIndex() ) { 642 return; 643 } 644 if( getDetailForm().isDirty() ) { 645 String title = getMessage( new String[] { getId() + ".dirtyChange.title", "masterForm.dirtyChange.title" } ); 646 String message = getMessage( new String[] { getId() + ".dirtyChange.message", "masterForm.dirtyChange.message" } ); 647 ConfirmationDialog dlg = new ConfirmationDialog( title, message ) { 648 649 protected void onConfirm() { 650 getDetailForm().setSelectedIndex( newIndex ); 651 } 652 653 protected void onCancel() { 654 // Force the selction back 655 super.onCancel(); 656 if( getDetailForm().isEditingNewFormObject() ) { 657 // Since they were editing a new object, we just 658 // need to clear the selection 659 getSelectionModel().clearSelection(); 660 } else { 661 int index = getDetailForm().getSelectedIndex(); 662 getSelectionModel().setSelectionInterval( index, index ); 663 } 664 } 665 666 }; 667 dlg.showDialog(); 668 } else { 669 getDetailForm().setSelectedIndex( newIndex ); 670 } 671 } 672 } 673 674 /** 675 * Inner class to monitor the editing index on the detail form and update the state of 676 * the delete command whenever it changes. 677 */ 678 private class EditingIndexMonitor implements PropertyChangeListener { 679 public void propertyChange(PropertyChangeEvent evt) { 680 updateControlsForState(); 681 } 682 } 683 684 /** 685 * Inner class to monitor the edit state of the detail form and update the state of 686 * the commands whenever it changes. 687 */ 688 private class EditStateMonitor implements PropertyChangeListener { 689 public void propertyChange(PropertyChangeEvent evt) { 690 updateControlsForState(); 691 } 692 } 693 694 /** 695 * This class handles changes in the property that this master form is editing. This 696 * can occur when the parent form's form object is changed. When that occurs, our 697 * collection data will change automatically and we then need to update our event list 698 * accordingly. 699 */ 700 private class ParentFormPropertyChangeHandler implements PropertyChangeListener { 701 public void propertyChange(PropertyChangeEvent evt) { 702 handleExternalRootEventListChange(); 703 } 704 705 } 706 707 /** 708 * Specialized DCBCVM to provide dirty tracking semantics. This will allow the form 709 * model built on this value model to properly track our dirty status. 710 */ 711 private class DirtyTrackingDCBCVM extends DeepCopyBufferedCollectionValueModel implements DirtyTrackingValueModel { 712 713 private boolean dirty = false; 714 private boolean oldDirty = false; 715 716 /** 717 * Constructs a new DirtyTrackingDCBCVM. 718 * 719 * @param wrappedModel the value model to wrap 720 * @param wrappedType the class of the value contained by wrappedModel; this must 721 * be assignable to <code>java.util.Collection</code> or 722 * <code>Object[]</code>. 723 */ 724 public DirtyTrackingDCBCVM(ValueModel wrappedModel, Class wrappedType) { 725 super( wrappedModel, wrappedType ); 726 // FIXME: make DCBCVM do dirty tracking on its own 727 dirty = false; // We should never start life as dirty 728 } 729 730 /** 731 * Create the buffered list model. We want to use an ObservableEventList so that 732 * it can be used as the root event list of the master form model. 733 * @return ObservableList to use 734 */ 735 protected ObservableList createBufferedListModel() { 736 return new ObservableEventList( new BasicEventList() ); 737 } 738 739 /** 740 * Set the value. If this is our original value, then clear dirty. 741 * @param value New value 742 */ 743 public void setValue(Object value) { 744 super.setValue( value ); 745 if( value == getWrappedValueModel().getValue() ) { 746 // this is a revert 747 dirty = false; 748 valueUpdated(); 749 } 750 } 751 752 /** 753 * Our underlying list has changed, we are now dirty. 754 */ 755 protected void fireListModelChanged() { 756 super.fireListModelChanged(); 757 dirty = true; 758 valueUpdated(); 759 } 760 761 /** 762 * Return our dirty status. 763 * @return dirty 764 */ 765 public boolean isDirty() { 766 return dirty; 767 } 768 769 /** 770 * Clear the dirty status 771 */ 772 public void clearDirty() { 773 dirty = false; 774 valueUpdated(); 775 } 776 777 /** 778 * Revert to original value. 779 */ 780 public void revertToOriginal() { 781 revert(); 782 } 783 784 protected void valueUpdated() { 785 boolean dirty = isDirty(); 786 if( oldDirty != dirty ) { 787 oldDirty = dirty; 788 firePropertyChange( DIRTY_PROPERTY, !dirty, dirty ); 789 } 790 } 791 } 792 }