001    package org.springframework.richclient.widget.editor;
002    
003    import com.jgoodies.forms.factories.Borders;
004    import com.jgoodies.forms.factories.FormFactory;
005    import com.jgoodies.forms.layout.*;
006    import org.apache.commons.lang.StringUtils;
007    import org.apache.commons.logging.Log;
008    import org.apache.commons.logging.LogFactory;
009    import org.jdesktop.swingx.JXTable;
010    import org.springframework.binding.form.FormModel;
011    import org.springframework.binding.form.NewFormObjectAware;
012    import org.springframework.binding.validation.support.DefaultValidationResultsModel;
013    import org.springframework.richclient.application.ApplicationServicesLocator;
014    import org.springframework.richclient.application.DefaultButtonFocusListener;
015    import org.springframework.richclient.command.*;
016    import org.springframework.richclient.command.config.CommandConfigurer;
017    import org.springframework.richclient.components.Focussable;
018    import org.springframework.richclient.dialog.Messagable;
019    import org.springframework.richclient.form.*;
020    import org.springframework.richclient.util.RcpSupport;
021    import org.springframework.richclient.widget.AbstractTitledWidget;
022    import org.springframework.richclient.widget.SelectionWidget;
023    import org.springframework.richclient.widget.TitledWidget;
024    import org.springframework.richclient.widget.Widget;
025    import org.springframework.richclient.widget.editor.provider.DataProviderEventSource;
026    import org.springframework.richclient.widget.editor.provider.DataProviderListener;
027    import org.springframework.richclient.widget.table.TableWidget;
028    
029    import javax.swing.*;
030    import javax.swing.border.Border;
031    import javax.swing.event.ChangeEvent;
032    import javax.swing.event.ChangeListener;
033    import javax.swing.table.TableCellRenderer;
034    import javax.swing.table.TableColumn;
035    import javax.swing.text.JTextComponent;
036    import java.awt.*;
037    import java.awt.datatransfer.StringSelection;
038    import java.awt.event.KeyEvent;
039    import java.awt.event.MouseAdapter;
040    import java.awt.event.MouseEvent;
041    import java.beans.PropertyChangeEvent;
042    import java.beans.PropertyChangeListener;
043    import java.util.*;
044    import java.util.List;
045    
046    /**
047     * AbstractDataEditorWidget implements a basic editor screen, based on 3 parts:
048     * <p/>
049     * <ol>
050     * <li>a table with sortable columns and a local quick search</li>
051     * <li>a filter to reduce the dataset</li>
052     * <li>a detail section for the details of 1 entity</li>
053     * </ol>
054     */
055    public abstract class AbstractDataEditorWidget extends AbstractTitledWidget
056            implements
057            TitledWidget,
058            SelectionWidget
059    {
060    
061        /**
062         * Log facility.
063         */
064        private static final Log log = LogFactory.getLog(AbstractDataEditorWidget.class);
065    
066        private static final String QUICKADD = "quickAdd";
067    
068        protected static final String UPDATE_COMMAND_ID = "update";
069    
070        protected static final String CREATE_COMMAND_ID = "create";
071    
072        private static final String TOGGLE_DETAIL_COMMAND_ID = "opendetail";
073    
074        private static final String REMOVE_CONTINUE_AFTER_ERROR = "remove.continue_after_error";
075    
076        private static final String REMOVE_CONFIRMATION_ID = "remove.confirmation";
077    
078        private static final String DBLCLICKSELECTS = "dblclick_for_edit";
079    
080        public static final String UNSAVEDCHANGES_WARNING_ID = "unsavedchanges.warning";
081    
082        public static final String UNSAVEDCHANGES_UNCOMMITTABLE_WARNING_ID = "unsavedchanges.uncommittable.warning";
083    
084        public static final RowSpec FILL_ROW_SPEC = new RowSpec(RowSpec.FILL, Sizes.DEFAULT,
085                FormSpec.DEFAULT_GROW);
086    
087        public static final ColumnSpec FILL_NOGROW_COLUMN_SPEC = new ColumnSpec(ColumnSpec.FILL, Sizes.DEFAULT,
088                FormSpec.NO_GROW);
089    
090        public static final ColumnSpec FILL_COLUMN_SPEC = new ColumnSpec(ColumnSpec.FILL, Sizes.DEFAULT,
091                FormSpec.DEFAULT_GROW);
092    
093        public static final boolean ON = true;
094    
095        public static final boolean OFF = false;
096    
097        private boolean selectMode = OFF;
098    
099        private boolean multipleSelectionInSelectMode = OFF;
100    
101        private String searchString;
102    
103        private Object selectedRowObject;
104    
105        private final CellConstraints cc = new CellConstraints();
106    
107        private SplitPaneExpansionToggleCommand toggleDetailCommand;
108    
109        private SplitPaneExpansionToggleCommand toggleFilterCommand;
110    
111        private ActionCommand editRowCommand;
112    
113        private ActionCommand addRowCommand;
114    
115        private ActionCommand cloneRowCommand;
116    
117        private ActionCommand removeRowsCommand;
118    
119        private ActionCommand executeFilterCommand;
120    
121        private ActionCommand refreshCommand;
122    
123        private ActionCommand clearFilterCommand;
124    
125        private ActionCommand emptyFilterCommand;
126    
127        private ActionCommand selectionCommand;
128    
129        private ActionCommand printCommand;
130    
131        private ActionCommand updateCommand;
132    
133        private ActionCommand createRowCommand;
134    
135        private CardLayout saveUpdateSwitcher;
136    
137        private JPanel saveUpdatePanel;
138    
139        private JTextField textFilterField;
140    
141        private Map dataProviderSources = null;
142    
143        private final CommandConfigurer commandConfigurer = (CommandConfigurer) ApplicationServicesLocator
144                .services().getService(CommandConfigurer.class);
145    
146        /**
147         * Observer listening to changes in the table selection.
148         */
149        protected Observer tableSelectionObserver;
150    
151        /**
152         * Default constructor will initialise the necessary listeners/observers.
153         */
154        public AbstractDataEditorWidget()
155        {
156            tableSelectionObserver = createListSelectionObserver();
157        }
158    
159        /**
160         * Creates the observer that listens to selections in the listView. Normally forwards the selection to the
161         * detailForm.
162         */
163        protected Observer createListSelectionObserver()
164        {
165            return new ListSelectionObserver();
166        }
167    
168        /**
169         * Set the select mode of this dataEditor.
170         */
171        public void setSelectMode(boolean selectMode)
172        {
173            this.selectMode = selectMode;
174        }
175    
176        public boolean isSelectMode()
177        {
178            return selectMode;
179        }
180    
181        /**
182         * Set the local text filter field value
183         *
184         * @param queryString filterText.
185         */
186        public void setSearchString(String queryString)
187        {
188            this.searchString = queryString;
189            if (this.textFilterField != null)
190            {
191                this.textFilterField.setText(this.searchString);
192            }
193        }
194    
195        public String getSearchString()
196        {
197            return searchString;
198        }
199    
200        public Object getSelectedRowObject()
201        {
202            return selectedRowObject;
203        }
204    
205        public void setSelectedRowObject(Object selectedObject)
206        {
207            getTableWidget().selectRowObject(selectedObject, null);
208        }
209    
210        public abstract Object setSelectedSearch(Object searchCriteria);
211    
212        /**
213         * {@inheritDoc}
214         */
215        @Override
216        public JComponent createWidgetContent()
217        {
218            return createDataEditorWidget();
219        }
220    
221        protected final JComponent createDataEditorWidget()
222        {
223            JSplitPane splitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT);
224            splitPane.setBorder(BorderFactory.createEmptyBorder());
225            if (isSelectMode() == ON)
226            {
227                DefaultButtonFocusListener.setDefaultButton(getTableWidget().getComponent(), getSelectCommand());
228                getTableWidget().getTable().getInputMap(JTable.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), "none");
229            }
230    
231            if ((isSelectMode() == ON) && (selectedRowObject == null))
232            {
233                splitPane.setDividerLocation(Integer.MAX_VALUE);
234                this.toggleDetailCommand = new SplitPaneExpansionToggleCommand(TOGGLE_DETAIL_COMMAND_ID,
235                        splitPane, true);
236            }
237            else
238            {
239                splitPane.setDividerLocation(-1);
240                this.toggleDetailCommand = new SplitPaneExpansionToggleCommand(TOGGLE_DETAIL_COMMAND_ID,
241                        splitPane, false);
242            }
243    
244            splitPane.setLastDividerLocation(-1);
245            splitPane.setOneTouchExpandable(true);
246            splitPane.setResizeWeight(getTableResizeWeight());
247    
248            splitPane.setLeftComponent(getTableFilterPanel());
249            splitPane.setRightComponent(getDetailPanel());
250    
251            getDetailForm().setEnabled(getTableWidget().hasSelection());
252    
253            createAddRowCommand();
254            createRemoveRowCommand();
255            createCloneRowCommand();
256    
257            return splitPane;
258        }
259    
260        protected double getTableResizeWeight()
261        {
262            return 0.75;
263        }
264    
265        private JComponent getControlPanel()
266        {
267            AbstractCommand localToggleDetailCommand = getToggleDetailCommand();
268            AbstractCommand helpCommand = getHelpCommand();
269            AbstractCommand[] commands = getControlCommands();
270    
271            ColumnSpec[] columnSpecs = new ColumnSpec[1 + (commands.length + 1) * 2];
272            columnSpecs[0] = FILL_NOGROW_COLUMN_SPEC;// open-detail btn
273            columnSpecs[1] = FormFactory.UNRELATED_GAP_COLSPEC; // gap
274            columnSpecs[2] = FILL_NOGROW_COLUMN_SPEC;// help btn
275            columnSpecs[3] = FILL_COLUMN_SPEC; // glue space
276    
277            for (int i = 0; i < commands.length; i++)
278            {
279                // print | select-and-close | cancel
280                columnSpecs[4 + (i * 2)] = FILL_NOGROW_COLUMN_SPEC;
281                if (i != commands.length - 1)
282                {
283                    // gap, but not after last
284                    columnSpecs[5 + (i * 2)] = FormFactory.UNRELATED_GAP_COLSPEC;
285                }
286            }
287            RowSpec[] rowSpecs = new RowSpec[]{FILL_ROW_SPEC};
288            FormLayout formLayout = new FormLayout(columnSpecs, rowSpecs);
289            JPanel buttonPanel = new JPanel(formLayout);
290    
291            buttonPanel.add(localToggleDetailCommand.createButton(), cc.xy(1, 1));
292    
293            buttonPanel.add(helpCommand.createButton(), cc.xy(3, 1));
294    
295            for (int i = 0; i < commands.length; i++)
296            {
297                buttonPanel.add(commands[i].createButton(), cc.xy(5 + i * 2, 1));
298            }
299    
300            return buttonPanel;
301        }
302    
303        /**
304         * {@inheritDoc}
305         */
306        @Override
307        public List<AbstractCommand> getCommands()
308        {
309            return Arrays.asList(getToggleDetailCommand());
310        }
311    
312        protected JComponent getDetailPanel()
313        {
314            ColumnSpec[] columnSpecs = new ColumnSpec[]{FILL_COLUMN_SPEC};
315            RowSpec[] rowSpecs = new RowSpec[]{FormFactory.LINE_GAP_ROWSPEC, // gap
316                    FormFactory.DEFAULT_ROWSPEC, // buttons for detailpanel
317                    FormFactory.LINE_GAP_ROWSPEC, // gap
318                    FILL_ROW_SPEC
319                    // detailpanel itself (form)
320            };
321            JPanel detailPanel = new JPanel(new FormLayout(columnSpecs, rowSpecs));
322    
323            Form detailForm = getDetailForm();
324            newSingleLineResultsReporter(this);
325            detailPanel.add(getDetailControlPanel(), cc.xy(1, 2));
326            detailPanel.add(detailForm.getControl(), cc.xy(1, 4));
327    
328            // force form readonly if adding & updating is not supported
329            if (!isAddRowSupported() && !isUpdateRowSupported())
330            {
331                detailForm.getFormModel().setReadOnly(true);
332            }
333    
334            return detailPanel;
335        }
336    
337        private JComponent getDetailControlPanel()
338        {
339            ColumnSpec[] columnSpecs = new ColumnSpec[]{FILL_NOGROW_COLUMN_SPEC, // edit buttons
340                    FILL_COLUMN_SPEC, // glue
341                    FILL_NOGROW_COLUMN_SPEC, // navigation buttons
342                    FILL_COLUMN_SPEC, // glue
343                    new ColumnSpec(ColumnSpec.RIGHT, Sizes.DEFAULT, FormSpec.DEFAULT_GROW) // list summary
344            };
345    
346            RowSpec[] rowSpecs = new RowSpec[]{FILL_ROW_SPEC};
347            FormLayout formLayout = new FormLayout(columnSpecs, rowSpecs);
348            // coupled glue space around nav buttons!
349            formLayout.setColumnGroups(new int[][]{{2, 4}});
350            JPanel buttonPanel = new JPanel(formLayout);
351    
352            JComponent editButtons = getEditButtons();
353            JComponent tableButtonBar = getTableWidget().getButtonBar();
354            if (editButtons != null)
355            {
356                buttonPanel.add(editButtons, cc.xy(1, 1));
357            }
358            if (tableButtonBar != null)
359            {
360                buttonPanel.add(tableButtonBar, cc.xy(3, 1));
361            }
362    
363            buttonPanel.add(getTableWidget().getListSummaryLabel(), cc.xy(5, 1));
364    
365            return buttonPanel;
366        }
367    
368        protected JComponent getEditButtons()
369        {
370            if (!isAddRowSupported() && !isUpdateRowSupported())
371            {
372                return null;
373            }
374    
375            ColumnSpec[] columnSpecs = new ColumnSpec[]{FILL_NOGROW_COLUMN_SPEC, // save
376                    FormFactory.RELATED_GAP_COLSPEC, // gap
377                    FILL_NOGROW_COLUMN_SPEC, // undo
378                    FormFactory.RELATED_GAP_COLSPEC, // gap
379                    FormFactory.DEFAULT_COLSPEC, // separator
380                    FormFactory.RELATED_GAP_COLSPEC, // gap
381                    FILL_NOGROW_COLUMN_SPEC, // quickadd
382            };
383    
384            RowSpec[] rowSpecs = new RowSpec[]{FILL_ROW_SPEC};
385            FormLayout formLayout = new FormLayout(columnSpecs, rowSpecs);
386            JPanel buttonPanel = new JPanel(formLayout);
387            buttonPanel.add(getCommitComponent(), cc.xy(1, 1));
388            buttonPanel.add(getRevertCommand().createButton(), cc.xy(3, 1));
389            if (isAddRowSupported())
390            {
391                buttonPanel.add(new JSeparator(SwingConstants.VERTICAL), cc.xy(5, 1));
392                buttonPanel.add(createQuickAddCheckBox(), cc.xy(7, 1));
393            }
394            return buttonPanel;
395        }
396    
397        protected JComponent getCommitComponent()
398        {
399            if (isAddRowSupported() && isUpdateRowSupported())
400            {
401                saveUpdateSwitcher = new CardLayout();
402                saveUpdatePanel = new JPanel(saveUpdateSwitcher);
403                saveUpdatePanel.add(getCreateCommand().createButton(), CREATE_COMMAND_ID);
404                saveUpdatePanel.add(getUpdateCommand().createButton(), UPDATE_COMMAND_ID);
405                return saveUpdatePanel;
406            }
407            if (isAddRowSupported())
408            {
409                DefaultButtonFocusListener.setDefaultButton(getDetailForm().getControl(), getCreateCommand());
410                return getCreateCommand().createButton();
411            }
412    
413            DefaultButtonFocusListener.setDefaultButton(getDetailForm().getControl(), getUpdateCommand());
414            return getUpdateCommand().createButton();
415        }
416    
417        /**
418         * Convenience method to retrieve the action command that should be used when changes are made in the detailForm.
419         * This can be update or add, according to whether a row is selected and changed or no row is selected.
420         *
421         * @return the command that should be used to save changes in the form.
422         */
423        protected ActionCommand getCommitCommand()
424        {
425            if ((selectedRowObject == null) && (isAddRowSupported()))
426                return getCreateCommand();
427    
428            else if (isUpdateRowSupported())
429                return getUpdateCommand();
430    
431            return null;
432        }
433    
434        /**
435         * Returns the save command, lazily creates one if needed.
436         */
437        public ActionCommand getUpdateCommand()
438        {
439            if (updateCommand == null)
440            {
441                updateCommand = createUpdateCommand();
442            }
443            return updateCommand;
444        }
445    
446        /**
447         * Creates the save command.
448         *
449         * @see #doUpdate()
450         */
451        protected ActionCommand createUpdateCommand()
452        {
453            ActionCommand command = new ActionCommand(UPDATE_COMMAND_ID)
454            {
455    
456                @Override
457                protected void doExecuteCommand()
458                {
459                    doUpdate();
460                }
461            };
462            command.setSecurityControllerId(getId() + "." + UPDATE_COMMAND_ID);
463            getCommandConfigurer().configure(command);
464            getDetailForm().addGuarded(command, FormGuard.LIKE_COMMITCOMMAND);
465            return command;
466        }
467    
468        /**
469         * Save the changes made in the detailForm according to following steps:
470         * <p/>
471         * <ol>
472         * <li>commit form</li>
473         * <li>formObject sent to back-end</li>
474         * <li>changes are handled in back-end</li>
475         * <li>changed object is returned to client</li>
476         * <li>old object is replaced by changed object</li>
477         * </ol>
478         */
479        protected void doUpdate()
480        {
481            getDetailForm().commit();
482            Object savedObject = null;
483            try
484            {
485                savedObject = saveEntity(getDetailForm().getFormObject());
486                setDetailFormObject(savedObject, tableSelectionObserver, false);
487            }
488            catch (RuntimeException e)
489            {
490                Object changedObject = getDetailForm().getFormObject();
491                // the following actually requests the object from the back-end
492                boolean success = setDetailFormObject(changedObject, tableSelectionObserver, true);
493                // set the changes back on the model if the object could be set on the form model
494                if (success)
495                    RcpSupport.mapObjectOnFormModel(getDetailForm().getFormModel(), changedObject);
496                throw e;
497            }
498        }
499    
500        /**
501         * Returns the create command, lazily creates one if needed.
502         */
503        public ActionCommand getCreateCommand()
504        {
505            if (createRowCommand == null)
506            {
507                createRowCommand = createCreateCommand();
508            }
509            return createRowCommand;
510        }
511    
512        /**
513         * Creates the create command.
514         *
515         * @see #doCreate()
516         */
517        protected ActionCommand createCreateCommand()
518        {
519            ActionCommand command = new ActionCommand(CREATE_COMMAND_ID)
520            {
521    
522                @Override
523                protected void doExecuteCommand()
524                {
525                    doCreate();
526                }
527            };
528            command.setSecurityControllerId(getId() + "." + CREATE_COMMAND_ID);
529            getCommandConfigurer().configure(command);
530            getDetailForm().addGuarded(command, FormGuard.LIKE_COMMITCOMMAND);
531            return command;
532        }
533    
534        /**
535         * Creates a new data object according to following steps:
536         * <p/>
537         * <ol>
538         * <li>form commit</li>
539         * <li>formObject sent to back-end</li>
540         * <li>back-end creates item</li>
541         * <li>back-end returns new item to client</li>
542         * <li>new item is selected in dataEditor if possible</li>
543         * </ol>
544         */
545        protected void doCreate()
546        {
547            getDetailForm().commit();
548            Object newObject = null;
549            try
550            {
551                newObject = createNewEntity(getDetailForm().getFormObject());
552                // select row only if user hasn't made another selection in
553                // table and this commit is triggered by the save-changes
554                // dialog and if not in quick add mode
555                if (newObject != null && !getTableWidget().hasSelection())
556                {
557                    if (getTableWidget().selectRowObject(newObject, null) == -1)
558                    {
559                        // select row wasn't succesfull, maybe search string
560                        // was filled in?
561                        setSearchString(null);
562                        getTableWidget().selectRowObject(newObject, null);
563                    }
564                }
565            }
566            catch (RuntimeException e)
567            {
568                Object changedFormObject = getDetailForm().getFormObject();
569                newRow(null);
570                RcpSupport.mapObjectOnFormModel(getDetailForm().getFormModel(), changedFormObject);
571                throw e;
572                // make form dirty, show error in messagepane and throw exception to display error dialog
573            }
574        }
575    
576        protected AbstractCommand getRevertCommand()
577        {
578            return getDetailForm().getRevertCommand();
579        }
580    
581        abstract protected DefaultValidationResultsModel getValidationResults();
582    
583        /**
584         * {@inheritDoc}
585         */
586        @Override
587        public ValidationResultsReporter newSingleLineResultsReporter(Messagable messagable)
588        {
589            return new SimpleValidationResultsReporter(getValidationResults(), messagable);
590        }
591    
592        protected JComponent createQuickAddCheckBox()
593        {
594            quickAddCheckBox = new JCheckBox(RcpSupport.getMessage(getId(), QUICKADD, RcpSupport.TITLE));
595            quickAddCheckBox.setFocusable(false);
596    
597            getCreateCommand().addCommandInterceptor(new ActionCommandInterceptor()
598            {
599    
600                public boolean preExecution(ActionCommand command)
601                {
602                    return true; // proceed
603                }
604    
605                public void postExecution(ActionCommand command)
606                {
607                    if (quickAddCheckBox.isSelected())
608                    {
609                        getAddRowCommand().execute();
610                    }
611                }
612            });
613    
614            quickAddCheckBox.setEnabled(getAddRowCommand().isEnabled());
615            getAddRowCommand().addPropertyChangeListener(AbstractCommand.ENABLED_PROPERTY_NAME,
616                    new PropertyChangeListener()
617                    {
618    
619                        public void propertyChange(PropertyChangeEvent evt)
620                        {
621                            Object newValue = evt.getNewValue();
622                            quickAddCheckBox.setEnabled(((Boolean) newValue).booleanValue());
623                        }
624                    });
625    
626            return quickAddCheckBox;
627        }
628    
629        protected JComponent getTableFilterPanel()
630        {
631            ColumnSpec[] columnSpecs = new ColumnSpec[]{FILL_COLUMN_SPEC};
632            RowSpec[] rowSpecs = new RowSpec[]{
633                    // buttons for list and filter
634                    FormFactory.DEFAULT_ROWSPEC, FormFactory.LINE_GAP_ROWSPEC,
635                    // splitpane with list and filter
636                    FILL_ROW_SPEC};
637            JPanel top = new JPanel(new FormLayout(columnSpecs, rowSpecs));
638    
639            final JTable table = getTableWidget().getTable();
640            if (isUpdateRowSupported())
641            {
642                String tooltip = RcpSupport.getMessage(getId(), DBLCLICKSELECTS, RcpSupport.CAPTION);
643                table.setToolTipText(tooltip);
644            }
645    
646            CommandGroup tableGroup = getTablePopupMenuCommandGroup();
647    
648            table.addMouseListener(new TableMouseListener(table, tableGroup.createPopupMenu()));
649    
650            JComponent tableScroller = getTableWidget().getComponent();
651            JComponent tableAndOptionalFilter = tableScroller;
652            if (isFilterSupported()) // add filter too via splitpane
653            {
654                JSplitPane splitPane = new JSplitPane();
655                if (this.selectMode == ON)
656                {
657                    splitPane.setDividerLocation(-1);
658                    this.toggleFilterCommand = new SplitPaneExpansionToggleCommand("openfilter", splitPane, false);
659                }
660                else
661                {
662                    splitPane.setDividerLocation(Integer.MAX_VALUE);
663                    this.toggleFilterCommand = new SplitPaneExpansionToggleCommand("openfilter", splitPane, true);
664                }
665    
666                splitPane.setLastDividerLocation(Integer.MAX_VALUE);
667                splitPane.setOneTouchExpandable(true);
668                splitPane.setResizeWeight(1.0);
669    
670                this.toggleFilterCommand.setEnabled(isFilterSupported());
671    
672                splitPane.setLeftComponent(tableScroller);
673                JComponent filterPanel = getFilterPanel();
674                splitPane.setRightComponent(filterPanel);
675                tableAndOptionalFilter = splitPane;
676            }
677    
678            top.add(getTableFilterControlPanel(), cc.xy(1, 1));
679            top.add(tableAndOptionalFilter, cc.xy(1, 3));
680    
681            return top;
682        }
683    
684        /**
685         * Returns the commandGroup that should be used to create the popup menu for the table.
686         */
687        protected CommandGroup getTablePopupMenuCommandGroup()
688        {
689            return CommandGroup.createCommandGroup(new Object[]{getEditRowCommand(), "separator",
690                    getAddRowCommand(), getCloneRowCommand(), getRemoveRowsCommand(), "separator",
691                    getRefreshCommand(), "separator", getCopySelectedRowsToClipboardCommand()});
692        }
693    
694        private JComponent getFilterPanel()
695        {
696            if (!isFilterSupported())
697            {
698                return null;
699            }
700    
701            ColumnSpec[] columnSpecs = new ColumnSpec[]{FILL_COLUMN_SPEC};
702            RowSpec[] rowSpecs = new RowSpec[]{new RowSpec(RowSpec.FILL, Sizes.DEFAULT, FormSpec.DEFAULT_GROW), // form gewrapped in een scrollpane.
703                    FormFactory.RELATED_GAP_ROWSPEC, FormFactory.DEFAULT_ROWSPEC, // buttons
704            };
705            JPanel filterPanel = new JPanel(new FormLayout(columnSpecs, rowSpecs));
706    
707            JComponent filterFormControl = getFilterForm().getControl();
708            filterFormControl.setBorder(Borders.DIALOG_BORDER);
709    
710            final JScrollPane filterScroller = new JScrollPane(filterFormControl,
711                    JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
712    
713            filterScroller.getHorizontalScrollBar().getModel().addChangeListener(new ChangeListener()
714            {
715    
716                public void stateChanged(ChangeEvent e)
717                {
718                    if (filterScroller.getVerticalScrollBar().isVisible())
719                    {
720                        filterScroller.setBorder(BorderFactory.createMatteBorder(0, 0, 1, 0, Color.gray));
721                    }
722                    else
723                    {
724                        filterScroller.setBorder(null);
725                    }
726                }
727            });
728    
729            // setting minimum size: always keep width of panel + scrollbar width
730            Dimension scrollDimension = new Dimension(filterScroller.getPreferredSize().width
731                    + filterScroller.getVerticalScrollBar().getPreferredSize().width, 50);
732            filterScroller.setMinimumSize(scrollDimension);
733    
734            filterPanel.add(filterScroller, cc.xy(1, 1));
735            JComponent filterControlPanel = getFilterControlPanel();
736            filterControlPanel.setBorder(Borders.DIALOG_BORDER);
737            filterPanel.add(filterControlPanel, cc.xy(1, 3));
738    
739            // register a default command to use when focus is on the filterPanel
740            DefaultButtonFocusListener.setDefaultButton(filterPanel, getExecuteFilterCommand());
741    
742            return filterPanel;
743        }
744    
745        private JComponent getFilterControlPanel()
746        {
747            CommandGroup controlCommands = CommandGroup.createCommandGroup(new Object[]{
748                    getExecuteFilterCommand(), getEmptyFilterCommand()});
749    
750            return controlCommands.createButtonBar((Size) null, (Border) null);
751        }
752    
753        private JCheckBox quickAddCheckBox;
754    
755        private ActionCommand copySelectedRowsCommand;
756    
757        private JComponent getTableFilterControlPanel()
758        {
759            CommandGroup tableFilterControlCommands = isFilterSupported()
760                    ? getTableFilterControlCommands()
761                    : null;
762            ColumnSpec[] columnSpecs = getTableColumnSpecs(tableFilterControlCommands != null
763                    && tableFilterControlCommands.size() > 0);
764            RowSpec[] rowSpecs = new RowSpec[]{FILL_ROW_SPEC};
765            FormLayout formLayout = new FormLayout(columnSpecs, rowSpecs);
766            JPanel buttonPanel = new JPanel(formLayout);
767            int columnCounter = 1;
768            if (isAddRowSupported())
769            {
770                buttonPanel.add(getAddRowCommand().createButton(), cc.xy(columnCounter, 1));
771                columnCounter += 2;
772            }
773            if (isRemoveRowsSupported())
774            {
775                buttonPanel.add(getRemoveRowsCommand().createButton(), cc.xy(columnCounter, 1));
776                columnCounter += 2;
777            }
778            if (isAddRowSupported() || isRemoveRowsSupported())
779            {
780                buttonPanel.add(new JSeparator(SwingConstants.VERTICAL), cc.xy(columnCounter, 1));
781                columnCounter += 2;
782            }
783            buttonPanel.add(getRefreshCommand().createButton(), cc.xy(columnCounter, 1));
784            columnCounter += 2;
785            buttonPanel.add(new JSeparator(SwingConstants.VERTICAL), cc.xy(columnCounter, 1));
786            columnCounter += 2;
787            textFilterField = getTableWidget().getTextFilterField();
788            if (textFilterField != null)
789            {
790                textFilterField.setText(searchString);
791                textFilterField.setPreferredSize(new Dimension(50, 20));
792                buttonPanel.add(textFilterField, cc.xy(columnCounter, 1));
793                columnCounter += 2;
794            }
795            if (isFilterSupported() && tableFilterControlCommands.size() > 0)
796            {
797                buttonPanel.add(new JSeparator(SwingConstants.VERTICAL), cc.xy(columnCounter, 1));
798                columnCounter += 2;
799                buttonPanel.add(tableFilterControlCommands.createButtonBar(new ColumnSpec("fill:pref:nogrow"),
800                        new RowSpec("fill:default:nogrow"), null), cc.xy(columnCounter, 1));
801            }
802    
803            return buttonPanel;
804        }
805    
806        protected CommandGroup getTableFilterControlCommands()
807        {
808            CommandGroup group = new CommandGroup();
809            group.add(getToggleFilterCommand());
810            group.add(getClearFilterCommand());
811            return group;
812        }
813    
814        private ColumnSpec[] getTableColumnSpecs(boolean addFilterCommands)
815        {
816            java.util.List<ColumnSpec> columnSpecs = new ArrayList<ColumnSpec>();
817            if (isAddRowSupported())
818            {
819                columnSpecs.add(FILL_NOGROW_COLUMN_SPEC);// add btn
820                columnSpecs.add(FormFactory.RELATED_GAP_COLSPEC); // gap
821            }
822            if (isRemoveRowsSupported())
823            {
824                columnSpecs.add(FILL_NOGROW_COLUMN_SPEC); // remove btn
825                columnSpecs.add(FormFactory.RELATED_GAP_COLSPEC); // gap
826            }
827            if (isAddRowSupported() || isRemoveRowsSupported())
828            {
829                columnSpecs.add(FormFactory.DEFAULT_COLSPEC); // separator //
830                // --------------------
831                columnSpecs.add(FormFactory.RELATED_GAP_COLSPEC); // gap
832            }
833            columnSpecs.add(FILL_NOGROW_COLUMN_SPEC); // refresh btn
834            columnSpecs.add(FormFactory.RELATED_GAP_COLSPEC); // gap
835            columnSpecs.add(FormFactory.DEFAULT_COLSPEC); // separator
836            // --------------------
837            columnSpecs.add(FormFactory.RELATED_GAP_COLSPEC); // gap
838            columnSpecs.add(FILL_COLUMN_SPEC); // glue space |>> glazed-list-text
839            // filter
840            if (addFilterCommands)
841            {
842                columnSpecs.add(FormFactory.RELATED_GAP_COLSPEC); // gap
843                columnSpecs.add(FormFactory.DEFAULT_COLSPEC); // separator
844                // --------------------
845                columnSpecs.add(FormFactory.RELATED_GAP_COLSPEC); // gap
846                columnSpecs.add(FILL_NOGROW_COLUMN_SPEC); // filter commands panel
847            }
848            return columnSpecs.toArray(new ColumnSpec[]{});
849        }
850    
851        private final class TableMouseListener extends MouseAdapter
852        {
853    
854            private final JTable table;
855            private final JPopupMenu menu;
856    
857            private TableMouseListener(JTable table, JPopupMenu menu)
858            {
859                this.table = table;
860                this.menu = menu;
861            }
862    
863            private void handlePopupTrigger(MouseEvent e)
864            {
865                if (e.isPopupTrigger())
866                {
867                    Point p = e.getPoint();
868                    int row = this.table.rowAtPoint(p);
869                    int column = this.table.columnAtPoint(p);
870                    // The autoscroller can generate drag events outside the
871                    // Table's range.
872                    if (!this.table.isRowSelected(row) && (column != -1))
873                    {
874                        this.table.changeSelection(row, column, e.isControlDown(), e.isShiftDown());
875                    }
876    
877                    this.menu.show(e.getComponent(), e.getX(), e.getY());
878                }
879            }
880    
881            @Override
882            public void mousePressed(MouseEvent e)
883            {
884                handlePopupTrigger(e);
885            }
886    
887            @Override
888            public void mouseReleased(MouseEvent e)
889            {
890                handlePopupTrigger(e);
891            }
892    
893            @Override
894            public void mouseClicked(MouseEvent e)
895            {
896                if (e.getClickCount() == 2 && SwingUtilities.isLeftMouseButton(e))
897                {
898                    if (selectionCommand != null)
899                    {
900                        selectionCommand.execute();
901                    }
902                    else if (isUpdateRowSupported())
903                    {
904                        getEditRowCommand().execute();
905                    }
906                }
907            }
908        }
909    
910        class RowObjectReplacer implements Runnable
911        {
912    
913            private final Object oldObject;
914    
915            private final Object newObject;
916    
917            public RowObjectReplacer(final Object oldObject, final Object newObject)
918            {
919                this.oldObject = oldObject;
920                this.newObject = newObject;
921            }
922    
923            public void run()
924            {
925                getTableWidget().replaceRowObject(oldObject, newObject, tableSelectionObserver);
926            }
927        }
928    
929        protected void onRowSelection(Object rowObject)
930        {
931            if (rowObject instanceof Object[])
932            {
933                getEditRowCommand().setEnabled(false);
934                getCloneRowCommand().setEnabled(false);
935            }
936            else
937            {
938                getEditRowCommand().setEnabled(true && isUpdateRowSupported());
939                getCloneRowCommand().setEnabled(true && isCloneRowSupported());
940            }
941        }
942    
943        protected void newRow(Object newClone)
944        {
945            getTableWidget().unSelectAll();
946            selectedRowObject = null;
947            AbstractForm detailForm = getDetailForm();
948    
949            if (detailForm instanceof NewFormObjectAware)
950            {
951                ((NewFormObjectAware) detailForm).setNewFormObject(newClone);
952            }
953            else
954            {
955                detailForm.setFormObject(newClone);
956            }
957    
958            if (saveUpdateSwitcher != null)
959            {
960                DefaultButtonFocusListener.setDefaultButton(detailForm.getControl(), getCreateCommand());
961                saveUpdateSwitcher.show(saveUpdatePanel, CREATE_COMMAND_ID);
962            }
963            if (detailForm instanceof Focussable)
964            {
965                ((Focussable) detailForm).grabFocus();
966            }
967        }
968    
969        protected void removeRows()
970        {
971            Object[] selectedRows = getTableWidget().getSelectedRows();
972    
973            if (selectedRows.length == 0)
974            {
975                return;
976            }
977    
978            int answer = RcpSupport.showWarningDialog(getComponent(), REMOVE_CONFIRMATION_ID,
979                    new Object[]{Integer.valueOf(selectedRows.length)}, JOptionPane.YES_NO_OPTION);
980            int nextSelectionIndex = getTableWidget().getTable().getSelectionModel().getMinSelectionIndex();
981    
982            for (int i = 0; i < selectedRows.length && (answer == JOptionPane.YES_OPTION); i++)
983            {
984                Object objectToRemove = selectedRows[i];
985    
986                try
987                {
988                    removeEntity(objectToRemove);
989                }
990                catch (RuntimeException e)
991                {
992                    log.error("Error removing row in DataEditor of type " + this.getClass().getName(), e);
993    
994                    RcpSupport.handleException(e);
995    
996                    int remaining = selectedRows.length - i - 1;
997                    if (remaining > 0)
998                    {
999                        String ttl = RcpSupport
1000                                .getMessage(getId(), REMOVE_CONTINUE_AFTER_ERROR, RcpSupport.TITLE);
1001                        String errMsg = RcpSupport.getMessage(getId(), REMOVE_CONTINUE_AFTER_ERROR,
1002                                RcpSupport.TEXT, new Object[]{Integer.valueOf(remaining)});
1003                        answer = JOptionPane.showConfirmDialog(getComponent(), errMsg, ttl,
1004                                JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE);
1005                    }
1006                }
1007            }
1008            int nrOfRows = getTableWidget().nrOfRows();
1009            if (nrOfRows > 0 && (getTableWidget().getSelectedRows().length == 0))
1010            {
1011                if (nextSelectionIndex >= nrOfRows)
1012                {
1013                    nextSelectionIndex = nrOfRows - 1;
1014                }
1015                getTableWidget().selectRowObject(nextSelectionIndex, null);
1016            }
1017        }
1018    
1019        public AbstractCommand getToggleDetailCommand()
1020        {
1021            return toggleDetailCommand;
1022        }
1023    
1024        public SplitPaneExpansionToggleCommand getToggleFilterCommand()
1025        {
1026            return toggleFilterCommand;
1027        }
1028    
1029        protected ActionCommand getAddRowCommand()
1030        {
1031            if (this.addRowCommand == null)
1032            {
1033                this.addRowCommand = createAddRowCommand();
1034            }
1035            return this.addRowCommand;
1036        }
1037        protected ActionCommand createAddRowCommand()
1038        {
1039            ActionCommand addRowCommand = new ActionCommand("addrow")
1040            {
1041    
1042                @Override
1043                protected void doExecuteCommand()
1044                {
1045                    if (canClose())
1046                    {
1047                        newRow(null);
1048                        toggleDetailCommand.doShow();
1049                    }
1050                }
1051            };
1052            addRowCommand.setSecurityControllerId(getId() + ".addrow");
1053            commandConfigurer.configure(addRowCommand);
1054            addRowCommand.setEnabled(isAddRowSupported());
1055            return addRowCommand;
1056        }
1057    
1058        protected ActionCommand getEditRowCommand()
1059        {
1060            if (this.editRowCommand == null)
1061            {
1062                this.editRowCommand = createEditRowCommand();
1063            }
1064            return this.editRowCommand;
1065        }
1066    
1067        protected ActionCommand createEditRowCommand()
1068        {
1069            ActionCommand editRow = new ActionCommand("editrow")
1070            {
1071    
1072                @Override
1073                protected void doExecuteCommand()
1074                {
1075                    toggleDetailCommand.doShow();
1076                    if (getDetailForm() instanceof Focussable)
1077                    {
1078                        ((Focussable) getDetailForm()).grabFocus();
1079                    }
1080                }
1081            };
1082            editRow.setSecurityControllerId(getId() + ".editrow");
1083            commandConfigurer.configure(editRow);
1084            editRow.setEnabled(isUpdateRowSupported());
1085            return editRow;
1086        }
1087    
1088        protected ActionCommand getCloneRowCommand()
1089        {
1090            if (this.cloneRowCommand == null)
1091            {
1092                this.cloneRowCommand = createCloneRowCommand();
1093            }
1094            return this.cloneRowCommand;
1095        }
1096    
1097        protected ActionCommand createCloneRowCommand()
1098        {
1099            ActionCommand cloneRow = new ActionCommand("clonerow")
1100            {
1101    
1102                @Override
1103                protected void doExecuteCommand()
1104                {
1105                    Object newClone = cloneEntity(selectedRowObject);
1106                    newRow(newClone);
1107                    toggleDetailCommand.doShow();
1108                }
1109    
1110            };
1111            cloneRow.setSecurityControllerId(getId() + ".clonerow");
1112            commandConfigurer.configure(cloneRow);
1113            cloneRow.setEnabled(isCloneRowSupported());
1114            return cloneRow;
1115        }
1116    
1117        protected ActionCommand getRemoveRowsCommand()
1118        {
1119            if (this.removeRowsCommand == null)
1120            {
1121                this.removeRowsCommand = createRemoveRowCommand();
1122            }
1123            return this.removeRowsCommand;
1124        }
1125    
1126        protected ActionCommand createRemoveRowCommand()
1127        {
1128            ActionCommand removeRowCommand = new ActionCommand("removerow")
1129            {
1130    
1131                @Override
1132                protected void doExecuteCommand()
1133                {
1134                    removeRows();
1135                }
1136            };
1137            removeRowCommand.setSecurityControllerId(getId() + ".removerow");
1138            commandConfigurer.configure(removeRowCommand);
1139            removeRowCommand.setEnabled(isRemoveRowsSupported());
1140            return removeRowCommand;
1141        }
1142    
1143        public ActionCommand getRefreshCommand()
1144        {
1145            if (this.refreshCommand == null)
1146            {
1147                this.refreshCommand = createRefreshCommand();
1148            }
1149            return this.refreshCommand;
1150        }
1151    
1152        public ActionCommand getCopySelectedRowsToClipboardCommand()
1153        {
1154            if (this.copySelectedRowsCommand == null)
1155            {
1156                this.copySelectedRowsCommand = createCopySelectedRowsToClipboardCommand();
1157            }
1158            return this.copySelectedRowsCommand;
1159        }
1160    
1161        private ActionCommand createCopySelectedRowsToClipboardCommand()
1162        {
1163            ActionCommand command = new ActionCommand("copyToClipboard")
1164            {
1165    
1166                @Override
1167                protected void doExecuteCommand()
1168                {
1169                    String clipboardContent = createTabDelimitedSelectedRowsContent();
1170                    StringSelection selection = new StringSelection(clipboardContent);
1171                    Toolkit.getDefaultToolkit().getSystemClipboard().setContents(selection, null);
1172                }
1173    
1174                private String createTabDelimitedSelectedRowsContent()
1175                {
1176                    java.util.List<java.util.List<String>> formattedRowList = new ArrayList<java.util.List<String>>();
1177                    JXTable jxTable = (JXTable) getTableWidget().getTable();
1178                    java.util.List<String> headerList = new ArrayList<String>();
1179                    for (TableColumn tableColumn : jxTable.getColumns())
1180                    {
1181                        Object headerValue = tableColumn.getHeaderValue();
1182                        headerList.add(headerValue == null ? "" : headerValue.toString());
1183                    }
1184                    formattedRowList.add(headerList);
1185                    for (int rowIndex : jxTable.getSelectedRows())
1186                    {
1187                        java.util.List<String> columnList = new ArrayList<String>();
1188                        for (TableColumn tableColumn : jxTable.getColumns())
1189                        {
1190                            Object unformattedValue = jxTable.getModel().getValueAt(rowIndex,
1191                                    tableColumn.getModelIndex());
1192                            int columnViewIndex = jxTable.convertColumnIndexToView(tableColumn.getModelIndex());
1193                            TableCellRenderer renderer = jxTable.getCellRenderer(rowIndex, columnViewIndex);
1194                            Component component = renderer.getTableCellRendererComponent(jxTable,
1195                                    unformattedValue, false, false, rowIndex, columnViewIndex);
1196    
1197                            columnList.add(getFormattedValue(component));
1198                        }
1199                        formattedRowList.add(columnList);
1200                    }
1201    
1202                    StringBuilder builder = new StringBuilder(formattedRowList.size() * 200);
1203                    for (java.util.List<String> row : formattedRowList)
1204                    {
1205                        builder.append(StringUtils.join(row.iterator(), "\t") + "\n");
1206                    }
1207                    return builder.toString();
1208                }
1209    
1210                private String getFormattedValue(Component component)
1211                {
1212                    if (component instanceof JLabel)
1213                    {
1214                        return ((JLabel) component).getText();
1215                    }
1216                    else if (component instanceof JTextComponent)
1217                    {
1218                        return ((JTextComponent) component).getText();
1219                    }
1220                    else if (component instanceof JToggleButton)
1221                    {
1222                        JToggleButton button = (JToggleButton) component;
1223                        return RcpSupport.getMessage("boolean.yesno." + button.isSelected());
1224                    }
1225                    else
1226                    {
1227                        return component.toString();
1228                    }
1229                }
1230    
1231            };
1232    
1233            return command;
1234        }
1235    
1236        protected ActionCommand createRefreshCommand()
1237        {
1238            return makeExecuteFilterCommand("refresh", false);
1239        }
1240    
1241        public ActionCommand getClearFilterCommand()
1242        {
1243            if (this.clearFilterCommand == null)
1244            {
1245                this.clearFilterCommand = createClearFilterCommand();
1246            }
1247            return this.clearFilterCommand;
1248        }
1249    
1250        protected ActionCommand createClearFilterCommand()
1251        {
1252            ActionCommand newCommand = makeClearFilterCommand("clearfilter", true);
1253            newCommand.setEnabled(isFilterSupported());
1254            return newCommand;
1255        }
1256    
1257        private ActionCommand makeClearFilterCommand(String id, final boolean refreshAndHideAfterClear)
1258        {
1259            ActionCommand newCommand = new ActionCommand(id)
1260            {
1261    
1262                @Override
1263                protected void doExecuteCommand()
1264                {
1265                    getFilterForm().resetCriteria();
1266                    if (refreshAndHideAfterClear)
1267                    {
1268                        executeFilter();
1269                        AbstractDataEditorWidget.this.toggleFilterCommand.doHide();
1270                    }
1271                }
1272            };
1273            commandConfigurer.configure(newCommand);
1274            return newCommand;
1275        }
1276    
1277        public ActionCommand getExecuteFilterCommand()
1278        {
1279            if (this.executeFilterCommand == null)
1280            {
1281                this.executeFilterCommand = createExecuteFilterCommand();
1282            }
1283            return this.executeFilterCommand;
1284        }
1285    
1286        protected ActionCommand createExecuteFilterCommand()
1287        {
1288            ActionCommand newCommand = makeExecuteFilterCommand("executefilter", true);
1289            newCommand.setEnabled(isFilterSupported());
1290            return newCommand;
1291        }
1292    
1293        private ActionCommand makeExecuteFilterCommand(String id, final boolean commitFilter)
1294        {
1295            ActionCommand newCommand = new ActionCommand(id)
1296            {
1297    
1298                @Override
1299                protected void doExecuteCommand()
1300                {
1301                    if (textFilterField != null)
1302                    {
1303                        textFilterField.setText("");
1304                    }
1305                    if (commitFilter && isFilterSupported())
1306                    {
1307                        getFilterForm().commit();
1308                    }
1309                    executeFilter();
1310                }
1311            };
1312            if (isFilterSupported())
1313            {
1314                new FormGuard(getFilterForm().getFormModel(), newCommand, FormGuard.ON_NOERRORS);
1315            }
1316            commandConfigurer.configure(newCommand);
1317            return newCommand;
1318        }
1319    
1320        protected ActionCommand getEmptyFilterCommand()
1321        {
1322            if (this.emptyFilterCommand == null)
1323            {
1324                this.emptyFilterCommand = createEmptyFilterCommand();
1325            }
1326            return this.emptyFilterCommand;
1327        }
1328    
1329        protected ActionCommand createEmptyFilterCommand()
1330        {
1331            ActionCommand newCommand = makeClearFilterCommand("emptyfilter", false);
1332            newCommand.setEnabled(isFilterSupported());
1333            return newCommand;
1334        }
1335    
1336        protected AbstractCommand getHelpCommand()
1337        {
1338            return RcpSupport.createDummyCommand("help", "Behulpzaam");
1339        }
1340    
1341        private AbstractCommand getCloseCommand()
1342        {
1343            return RcpSupport.createDummyCommand("exit", "deuren toe.");
1344        }
1345    
1346        protected Object[] getFilterCriteria()
1347        {
1348            Object[] criteria;
1349            criteria = new Object[1];
1350            criteria[0] = getFilterForm().getFilterCriteria();
1351            return criteria;
1352        }
1353    
1354        protected ActionCommand getSelectCommand()
1355        {
1356            return RcpSupport.createDummyCommand("select", "Chosen!");
1357        }
1358    
1359        protected AbstractCommand[] getControlCommands()
1360        {
1361            if (isSelectMode())
1362            {
1363                return new AbstractCommand[]{getSelectCommand(), // select
1364                        getCloseCommand()
1365                        // close
1366                };
1367            }
1368    
1369            return new AbstractCommand[]{
1370                    getCloseCommand()
1371                    // close
1372            };
1373        }
1374    
1375        protected abstract boolean isFilterSupported();
1376    
1377        protected abstract boolean isUpdateRowSupported();
1378    
1379        protected abstract boolean isAddRowSupported();
1380    
1381        protected abstract boolean isCloneRowSupported();
1382    
1383        protected abstract boolean isRemoveRowsSupported();
1384    
1385        protected abstract FilterForm getFilterForm();
1386    
1387        public abstract AbstractForm getDetailForm();
1388    
1389        public abstract Widget createDetailWidget();
1390    
1391        public abstract TableWidget getTableWidget();
1392    
1393        protected abstract void executeFilter();
1394    
1395        public abstract void executeFilter(Map<String, Object> parameters);
1396    
1397        protected final Object loadEntityDetails(Object baseObject)
1398        {
1399            return loadEntityDetails(baseObject, false);
1400        }
1401    
1402        /**
1403         * Fetch the detailed object from the back-end. If the baseObject is already detailed, the baseObject can
1404         * be returned directly if and only if no forceLoad is requested. This logic is also apparent in the
1405         * {@link org.springframework.richclient.widget.editor.provider.DataProvider} class.
1406         *
1407         * @param baseObject object containing enough information to fetch a detailed version.
1408         * @param forceLoad  if <code>true</code> always load the detailed object from the back-end, if
1409         *                   <code>false</code> a shortcut can be implemented by returning the baseObject directly.
1410         * @return the detailed object
1411         */
1412        protected abstract Object loadEntityDetails(Object baseObject, boolean forceLoad);
1413    
1414        protected abstract Object saveEntity(Object committedObject);
1415    
1416        protected abstract Object createNewEntity(Object committedObject);
1417    
1418        protected abstract Object cloneEntity(Object sampleObject);
1419    
1420        protected abstract void removeEntity(Object objectToRemove);
1421    
1422        @Override
1423        public boolean canClose()
1424        {
1425            boolean userBreak = false;
1426            int answer = JOptionPane.NO_OPTION;
1427    
1428            FormModel detailFormModel = getDetailForm().getFormModel();
1429    
1430            if (detailFormModel.isEnabled() && detailFormModel.isDirty())
1431            {
1432                if (detailFormModel.isCommittable())
1433                {
1434                    answer = RcpSupport.showWarningDialog(getComponent(), UNSAVEDCHANGES_WARNING_ID,
1435                            JOptionPane.YES_NO_CANCEL_OPTION);
1436                }
1437                else // form is uncomittable, change it or revert it
1438                {
1439                    answer = RcpSupport.showWarningDialog(getComponent(),
1440                            UNSAVEDCHANGES_UNCOMMITTABLE_WARNING_ID, JOptionPane.YES_NO_OPTION);
1441                    // the following might seem strange, but it aligns the answer with the other part of this if construction
1442                    // if we said 'yes keep editing': don't discard changes, continue editing to save it later on == CANCEL in previous if
1443                    // if we said 'no discard changes': discard changed and switch to other row == NO in previous if
1444                    answer = answer == JOptionPane.YES_OPTION ? JOptionPane.CANCEL_OPTION : JOptionPane.NO_OPTION;
1445                }
1446    
1447                switch (answer)
1448                {
1449                    case JOptionPane.CANCEL_OPTION:
1450                        userBreak = true;
1451                        break;
1452                    case JOptionPane.YES_OPTION:
1453                        getCommitCommand().execute();
1454                        break;
1455                    case JOptionPane.NO_OPTION:
1456                        detailFormModel.revert();
1457                        break;
1458                }
1459            }
1460    
1461            return !userBreak;
1462        }
1463    
1464        private boolean setDetailFormObject(Object rowObject, Observer reportingObserver, boolean forceLoad)
1465        {
1466            // quick check to avoid multiple runs
1467            if (!forceLoad && (rowObject == selectedRowObject))
1468            {
1469                return true;
1470            }
1471    
1472            if (saveUpdateSwitcher != null)
1473            {
1474                DefaultButtonFocusListener.setDefaultButton(getDetailForm().getControl(), getUpdateCommand());
1475                saveUpdateSwitcher.show(saveUpdatePanel, UPDATE_COMMAND_ID);
1476            }
1477    
1478            // check on current detailObject, if user isn't ready to switch, select
1479            // previous object again
1480            if (!canClose())
1481            {
1482                getTableWidget().selectRowObject(selectedRowObject, reportingObserver);
1483                return false;
1484            }
1485    
1486            boolean success = true;
1487    
1488            // nothing is stopping us from setting the newly selected object
1489            if (rowObject != null)
1490            {
1491                Object detailedObject = loadEntityDetails(rowObject, forceLoad);
1492                // if null, remove from list, set rowObject null and fall through (exception will be displayed)
1493                if (detailedObject == null)
1494                {
1495                    getTableWidget().removeRowObject(rowObject);
1496                    rowObject = null;
1497                    success = false;
1498                }
1499                else if (detailedObject != rowObject)
1500                {
1501                    replaceRowObject(rowObject, detailedObject);
1502                    rowObject = detailedObject;
1503                }
1504            }
1505            getDetailForm().setFormObject(rowObject);
1506            selectedRowObject = rowObject;
1507            onRowSelection(rowObject);
1508            return success;
1509        }
1510    
1511        protected void replaceRowObject(Object oldRowObject, Object newRowObject)
1512        {
1513            EventQueue.invokeLater(new RowObjectReplacer(oldRowObject, newRowObject));
1514        }
1515    
1516        public final void setDataProviderEventSources(java.util.List dataProviderEventSources)
1517        {
1518            if (this.dataProviderSources == null)
1519            {
1520                this.dataProviderSources = new HashMap();
1521            }
1522            for (Iterator sourceIter = dataProviderEventSources.iterator(); sourceIter.hasNext();)
1523            {
1524                DataProviderEventSource source = (DataProviderEventSource) sourceIter.next();
1525                this.dataProviderSources.put(source.getClass(), source);
1526            }
1527        }
1528    
1529        public final void addDataProviderListener(Class dataProviderEventSource, DataProviderListener listener)
1530        {
1531            Object source = this.dataProviderSources.get(dataProviderEventSource);
1532            if (source != null)
1533            {
1534                ((DataProviderEventSource) source).addDataProviderListener(listener);
1535            }
1536        }
1537    
1538        public final void removeDataProviderListener(Class dataProviderEventSource, DataProviderListener listener)
1539        {
1540            Object source = this.dataProviderSources.get(dataProviderEventSource);
1541            if (source != null)
1542            {
1543                ((DataProviderEventSource) source).removeDataProviderListener(listener);
1544            }
1545        }
1546    
1547        /**
1548         * @inheritDoc
1549         */
1550        public Object getSelection()
1551        {
1552            return getTableWidget().getSelectedRows();
1553        }
1554    
1555        /**
1556         * @inheritDoc
1557         */
1558        public void setSelectionCommand(ActionCommand command)
1559        {
1560            setSelectMode(AbstractDataEditorWidget.ON);
1561            selectionCommand = command;
1562            enableSelectButton(getSelection());
1563        }
1564    
1565        public void setMultipleSelectionInSelectMode(boolean multipleSelection)
1566        {
1567            multipleSelectionInSelectMode = multipleSelection;
1568        }
1569    
1570        /**
1571         * @inheritDoc
1572         */
1573        public void removeSelectionCommand()
1574        {
1575            selectionCommand = null;
1576            setSelectMode(AbstractDataEditorWidget.OFF);
1577        }
1578    
1579        private void enableSelectButton(Object selection)
1580        {
1581            if (selectionCommand != null)
1582            {
1583                if (selection == null)
1584                {
1585                    selectionCommand.setEnabled(false);
1586                }
1587                else if (multipleSelectionInSelectMode == ON)
1588                {
1589                    selectionCommand.setEnabled(true);
1590                }
1591                else
1592                {
1593                    selectionCommand.setEnabled(!(selection instanceof Object[])
1594                            || (((Object[]) selection).length == 1));
1595                }
1596            }
1597        }
1598    
1599        private class ListSelectionObserver implements Observer
1600        {
1601    
1602            public void update(Observable o, Object rowObject)
1603            {
1604                AbstractDataEditorWidget.this.setDetailFormObject(rowObject instanceof Object[]
1605                        ? null
1606                        : rowObject, tableSelectionObserver, false);
1607                enableSelectButton(rowObject);
1608            }
1609        }
1610    }