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 }