org.nuclos.client.ui.collect.result.ResultController.java Source code

Java tutorial

Introduction

Here is the source code for org.nuclos.client.ui.collect.result.ResultController.java

Source

//Copyright (C) 2011  Novabit Informationssysteme GmbH
//
//This file is part of Nuclos.
//
//Nuclos is free software: you can redistribute it and/or modify
//it under the terms of the GNU Affero General Public License as published by
//the Free Software Foundation, either version 3 of the License, or
//(at your option) any later version.
//
//Nuclos is distributed in the hope that it will be useful,
//but WITHOUT ANY WARRANTY; without even the implied warranty of
//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//GNU Affero General Public License for more details.
//
//You should have received a copy of the GNU Affero General Public License
//along with Nuclos.  If not, see <http://www.gnu.org/licenses/>.
package org.nuclos.client.ui.collect.result;

import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;

import javax.swing.Action;
import javax.swing.JOptionPane;
import javax.swing.JTable;
import javax.swing.KeyStroke;
import javax.swing.ListSelectionModel;
import javax.swing.RowSorter.SortKey;
import javax.swing.SortOrder;
import javax.swing.SwingUtilities;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import javax.swing.event.TableColumnModelEvent;
import javax.swing.event.TableColumnModelListener;
import javax.swing.table.TableColumn;

import org.apache.commons.lang.NotImplementedException;
import org.apache.commons.lang.NullArgumentException;
import org.apache.log4j.Logger;
import org.nuclos.client.common.EnabledListener;
import org.nuclos.client.common.NuclosCollectableEntityProvider;
import org.nuclos.client.common.Utils;
import org.nuclos.client.common.WorkspaceUtils;
import org.nuclos.client.common.security.SecurityCache;
import org.nuclos.client.genericobject.GenericObjectCollectController;
import org.nuclos.client.main.Main;
import org.nuclos.client.main.mainframe.MainFrame;
import org.nuclos.client.main.mainframe.MainFrameTabbedPane;
import org.nuclos.client.ui.CommonAbstractAction;
import org.nuclos.client.ui.Errors;
import org.nuclos.client.ui.Icons;
import org.nuclos.client.ui.UIUtils;
import org.nuclos.client.ui.collect.CollectController;
import org.nuclos.client.ui.collect.CollectState;
import org.nuclos.client.ui.collect.CollectStateModel;
import org.nuclos.client.ui.collect.CollectableTableHelper;
import org.nuclos.client.ui.collect.DeleteSelectedCollectablesController;
import org.nuclos.client.ui.collect.SelectColumnsController;
import org.nuclos.client.ui.collect.ToolTipsTableHeader;
import org.nuclos.client.ui.collect.component.model.ChoiceEntityFieldList;
import org.nuclos.client.ui.collect.model.CollectableTableModel;
import org.nuclos.client.ui.collect.model.SortableCollectableTableModel;
import org.nuclos.client.ui.table.SortableTableModel;
import org.nuclos.client.ui.table.TableUtils;
import org.nuclos.common.Actions;
import org.nuclos.common.NuclosEntity;
import org.nuclos.common.WorkspaceDescription.EntityPreferences;
import org.nuclos.common.collect.collectable.Collectable;
import org.nuclos.common.collect.collectable.CollectableEntity;
import org.nuclos.common.collect.collectable.CollectableEntityField;
import org.nuclos.common.collect.collectable.CollectableSorting;
import org.nuclos.common.collect.collectable.CollectableUtils;
import org.nuclos.common.collection.CollectionUtils;
import org.nuclos.common.collection.PredicateUtils;
import org.nuclos.common.dal.vo.EntityFieldMetaDataVO;
import org.nuclos.common.dal.vo.PivotInfo;
import org.nuclos.common.dal.vo.SystemFields;
import org.nuclos.common.entityobject.CollectableEOEntityField;
import org.nuclos.common2.CommonRunnable;
import org.nuclos.common2.SpringLocaleDelegate;
import org.nuclos.common2.exception.CommonBusinessException;
import org.nuclos.common2.exception.CommonFatalException;
import org.nuclos.common2.exception.CommonPermissionException;

/**
 * Controller for the Result panel.
 * <p>
 * This implementation is to use with {@link CollectController}s for which
 * no subtype is defined.
 * </p><p>
 * The ResultController is used for displaying a search result on
 * ResultPanel. The result shown are (list) representations of
 * (DB) persisted entities.
 * </p><p>
 * Since Nuclos 3.1.01 ResultController is a Controller hierarchy of its own.
 * The main intention of this is to remove as many methods away from
 * CollectController as possible. In principle, the ResultController hierarchy
 * should mimic the hierarchy of CollectControllers. In practice, subtypes of
 * ResultController are only implemented in case they change the behaviour
 * defined here.
 * </p>
 * @see ResultPanel
 * @since Nuclos 3.1.01 this is a top-level class.
 * @author Thomas Pasch
 */
public class ResultController<Clct extends Collectable> {

    private static final Logger LOG = Logger.getLogger(ResultController.class);

    private static final String RESULT_ACTIONS_VISIBLE = "resultactionsvisible";

    private static final int ESC = KeyEvent.VK_ESCAPE;

    private static final int UP = KeyEvent.VK_UP;

    private static final int DOWN = KeyEvent.VK_DOWN;

    /**
     * The entity for which the results are displayed.
     *
     * @since Nuclos 3.1.01
     * @author Thomas Pasch
     */
    private final CollectableEntity clcte;

    /**
     * The SearchResultStrategy for the result displayed.
     *
     * @since Nuclos 3.1.01
     * @author Thomas Pasch
     */
    private final ISearchResultStrategy<Clct> srs;

    /**
     * TODO: Try to avoid cyclic dependency: The ResultController shouldn't depend on the CollectController.
     *       While this would be desirable, it is - in real - completely unrealistic at present. Even
     *       more, a Result is always the result (pun intended!) of some other (controller) operation!
     *       (Thomas Pasch)
     */
    private CollectController<Clct> clctctl;

    /**
     * the lists of available and selected fields, resp.
     * 
     * Not final because must be reset in close() to avoid memory leak. (tp)
     */
    private ChoiceEntityFieldList fields = new ChoiceEntityFieldList(null);

    /**
     * action: Edit selected Collectable (in Result panel)
     */
    private Action actEditSelectedCollectables;

    /**
     * action: Delete selected Collectable (in Result panel)
     */
    private final Action actDeleteSelectedCollectables;

    private MouseListener mouselistenerTableDblClick;

    /**
     * action: Define as new Search result
     *
     * @deprecated Does nothing.
     */
    private final Action actDefineAsNewSearchResult = new CommonAbstractAction(
            getSpringLocaleDelegate().getMessage("ResultController.3", "Als neues Suchergebnis"),
            Icons.getInstance().getIconEmpty16(), getSpringLocaleDelegate().getMessage("ResultController.4",
                    "Ausgew\u00e4hlte Datens\u00e4tze als neues Suchergebnis anzeigen")) {

        @Override
        public void actionPerformed(ActionEvent ev) {
            cmdDefineSelectedCollectablesAsNewSearchResult();
        }
    };

    protected boolean isIgnorePreferencesUpdate = true;

    private EnabledListener actionsVisibleListener = new EnabledListener() {
        @Override
        public void enabledChanged(boolean visible) {
            getCollectController().getPreferences().putBoolean(RESULT_ACTIONS_VISIBLE, visible);
        }
    };

    /**
     * Don't make this public!
     *
     * @deprecated You should normally do sth. like this:<code><pre>
     * ResultController<~> rc = new ResultController<~>();
     * *CollectController<~> cc = new *CollectController<~>(.., rc);
     * </code></pre>
     */
    ResultController(CollectController<Clct> clctctl, CollectableEntity clcte, ISearchResultStrategy<Clct> srs) {
        this(clcte, srs);
        setCollectController(clctctl);
    }

    /**
     * @deprecated You should really provide a CollectableEntity here.
     */
    public ResultController(String entityName, ISearchResultStrategy<Clct> srs) {
        this(NuclosCollectableEntityProvider.getInstance().getCollectableEntity(entityName), srs);
    }

    public ResultController(CollectableEntity clcte, ISearchResultStrategy<Clct> srs) {
        this.srs = srs;
        srs.setResultController(this);
        actDeleteSelectedCollectables = new CommonAbstractAction(
                SpringLocaleDelegate.getInstance().getMessage("ResultController.10", "L\u00f6schen..."),
                (clctctl instanceof GenericObjectCollectController) ? // quick and dirty... I know
                        Icons.getInstance().getIconDelete16() : Icons.getInstance().getIconRealDelete16(),
                SpringLocaleDelegate.getInstance().getMessage("ResultController.5",
                        "Ausgew\u00e4hlte Datens\u00e4tze l\u00f6schen")) {

            @Override
            public void actionPerformed(ActionEvent ev) {
                cmdDeleteSelectedCollectables();
            }
        };
        this.clcte = clcte;
    }

    protected SpringLocaleDelegate getSpringLocaleDelegate() {
        return SpringLocaleDelegate.getInstance();
    }

    protected MainFrame getMainFrame() {
        return Main.getInstance().getMainFrame();
    }

    protected WorkspaceUtils getWorkspaceUtils() {
        return WorkspaceUtils.getInstance();
    }

    /**
     * TODO: Could this be protected?
     */
    public final ISearchResultStrategy<Clct> getSearchResultStrategy() {
        return srs;
    }

    public final Action getEditSelectedCollectablesAction() {
        return actEditSelectedCollectables;
    }

    public final Action getDeleteSelectedCollectablesAction() {
        return actDeleteSelectedCollectables;
    }

    public final MouseListener getTableDblClickML() {
        return mouselistenerTableDblClick;
    }

    public final void setCollectController(CollectController<Clct> controller) {
        this.clctctl = controller;
    }

    protected final CollectController<Clct> getCollectController() {
        return clctctl;
    }

    /**
     * TODO: Make protected again.
     */
    public final CollectableEntity getEntity() {
        return clcte;
    }

    private ResultPanel<Clct> getResultPanel() {
        if (clctctl == null) {
            return null;
        }
        return this.clctctl.getResultPanel();
    }

    /**
     * TODO: Make this package visible again.
     */
    public void setupResultPanel() {
        if (NuclosEntity.isNuclosEntity(getEntity().getName())) {
            this.getResultPanel().setActionsEnabled(false);
        }
        this.getResultPanel().addActionsVisibleListener(actionsVisibleListener);

        setupActions();

        // add selection listener for Result table:
        final JTable tblResult = this.getResultPanel().getResultTable();

        tblResult.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
        tblResult.getSelectionModel().addListSelectionListener(newListSelectionListener(tblResult));

        this.getResultPanel().btnToggleSelectionMode.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                getResultPanel().setToggleSelection(!getResultPanel().isToggleSelection());
            }
        });
        this.getResultPanel().btnSelectAllRows.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                if (tblResult.getRowCount() > 0) {
                    tblResult.getSelectionModel().setSelectionInterval(0, tblResult.getRowCount() - 1);
                }
            }
        });
        this.getResultPanel().btnDeSelectAllRows.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                tblResult.getSelectionModel().clearSelection();
            }
        });
        this.getResultPanel().addResultKeyListener(new ResultKeyListener() {
            @Override
            public boolean processKeyBinding(KeyStroke ks, KeyEvent e, int condition, boolean pressed) {
                if (e.getKeyCode() == ESC) {
                    if (!pressed) {
                        tblResult.getSelectionModel().clearSelection();
                        return true;
                    }
                } else if (e.getKeyCode() == UP || e.getKeyCode() == DOWN) {
                    if (pressed) {
                        /* Warum wird dies bentigt?
                         * BasicTableUI.actionPerformed(...) sendet ein ungewolltes changeSelection mit toggle=false bei PFEIL-NACH-OBEN/-UNTEN (siehe unten).
                         * Der Standard in der Ergebnisansicht soll aber ein umgedrehtes Verhalten fr Mausklicks sein. (siehe Implementierung 
                         *       ResultPanel: super.changeSelection(rowIndex, columnIndex, alternateSelectionToggle? !toggle: toggle, extend);)  
                         * 
                         * else if (!inSelection) {
                              *       moveWithinTableRange(table, dx, dy);
                              *       table.changeSelection(leadRow, leadColumn, false, extend);
                             * }
                         */
                        getResultPanel().setAlternateSelectionToggle(false);
                        SwingUtilities.invokeLater(new Runnable() {
                            public void run() {
                                getResultPanel().setAlternateSelectionToggle(true);
                            }
                        });
                    }
                }
                return false;
            }
        });

        // add mouse listener for double click in table:
        this.mouselistenerTableDblClick = new MouseAdapter() {

            private long lastClick = 0l;

            @Override
            public void mouseClicked(MouseEvent ev) {
                if (SwingUtilities.isLeftMouseButton(ev)) {
                    if (lastClick + MainFrameTabbedPane.DOUBLE_CLICK_SPEED > System.currentTimeMillis()) {
                        int iRow = tblResult.rowAtPoint(ev.getPoint());
                        if (iRow >= 0 && iRow < tblResult.getRowCount()) {
                            tblResult.getSelectionModel().setSelectionInterval(iRow, iRow);
                            SwingUtilities.invokeLater(new Runnable() {
                                @Override
                                public void run() {
                                    if (getSelectedCollectableFromTableModel() != null) {
                                        clctctl.cmdViewSelectedCollectables();
                                    }
                                }
                            });
                        }
                    }

                    lastClick = System.currentTimeMillis();
                }
            }
        };
        getResultPanel().addDoubleClickMouseListener(this.mouselistenerTableDblClick);

        if (!SecurityCache.getInstance()
                .isActionAllowed(Actions.ACTION_WORKSPACE_CUSTOMIZE_ENTITY_AND_SUBFORM_COLUMNS)
                && getMainFrame().getWorkspace().isAssigned()) {
            getResultPanel().getResultTable().getTableHeader().setReorderingAllowed(false);
        }

        // change column ordering in table model when table columns are reordered by dragging a column with the mouse:
        getResultPanel().addColumnModelListener(newColumnModelListener());
        PreferencesUpdateListener pul = newResultTablePreferencesUpdateListener();
        getResultPanel().addColumnModelListener(pul);

        getResultPanel().addPopupMenuListener();
        getResultPanel().getSearchFilterBar().addEnabledListener(new ResetMainFilterEnabledListener());

        getResultPanel().setActionsVisible(
                getCollectController().getPreferences().getBoolean(RESULT_ACTIONS_VISIBLE, true));
    }

    private void setupActions() {
        final ResultPanel<Clct> pnlResult = this.getResultPanel();

        // action: Refresh (search again)
        final Action actRefresh = new CommonAbstractAction(pnlResult.btnRefresh) {

            @Override
            public void actionPerformed(ActionEvent ev) {
                srs.cmdRefreshResult();
            }
        };
        pnlResult.btnRefresh.setAction(actRefresh);

        // action: New
        pnlResult.btnNew.setAction(this.clctctl.getNewAction());

        // action: Bookmark
        pnlResult.btnBookmark.setAction(this.clctctl.getBookmarkAction());

        // action: Clone
        pnlResult.btnClone.setAction(this.clctctl.getCloneAction());

        // action: Delete
        pnlResult.btnDelete.setAction(this.actDeleteSelectedCollectables);
        this.actDeleteSelectedCollectables.setEnabled(false);

        pnlResult.btnResetMainFilter.setAction(this.clctctl.getResetMainFilterAction());
        this.clctctl.getResetMainFilterAction().setEnabled(false);
        pnlResult.btnResetMainFilter.setVisible(false);

        // action: View
        this.actEditSelectedCollectables = new CommonAbstractAction(pnlResult.btnEdit) {

            @Override
            public void actionPerformed(ActionEvent ev) {
                clctctl.cmdViewSelectedCollectables();
            }
        };
        pnlResult.btnEdit.setAction(this.actEditSelectedCollectables);
        actEditSelectedCollectables.setEnabled(false);

        // action: Select Columns
        pnlResult.btnSelectColumns.setAction(new CommonAbstractAction(pnlResult.btnSelectColumns) {

            @Override
            public void actionPerformed(ActionEvent ev) {
                cmdSelectColumns(clctctl.getFields());
            }
        });

        if (!clctctl.isTransferable()) {
            pnlResult.btnExport.setVisible(false);
            pnlResult.btnImport.setVisible(false);
        } else {
            // action: Export
            pnlResult.btnExport.setAction(new CommonAbstractAction(pnlResult.btnExport) {

                @Override
                public void actionPerformed(ActionEvent ev) {
                    pnlResult.cmdExport(clctctl);
                }
            });

            // action: Import
            pnlResult.btnImport.setAction(new CommonAbstractAction(pnlResult.btnImport) {

                @Override
                public void actionPerformed(ActionEvent ev) {
                    pnlResult.cmdImport(clctctl);
                }
            });
        }

        pnlResult.miPopupEdit.setAction(actEditSelectedCollectables);
        pnlResult.miPopupClone.setAction(clctctl.getCloneAction());
        pnlResult.miPopupDelete.setAction(this.actDeleteSelectedCollectables);
        pnlResult.miPopupOpenInNewTab.setAction(clctctl.getOpenInNewTabAction());
        pnlResult.miPopupBookmark.setAction(clctctl.getBookmarkAction());
        pnlResult.miPopupDefineAsNewSearchResult.setAction(actDefineAsNewSearchResult);
        pnlResult.miPopupCopyCells.setAction(clctctl.getCopyCellsAction());
        pnlResult.miPopupCopyRows.setAction(clctctl.getCopyRowsAction());

    }

    /**
     * Initializes the <code>fields</code> field as follows:
     * <ol>
     *   <li>(re-)set the list of avaiable fields to {@link #getFieldsAvailableForResult}</li>
     *   <li>(re-)set the list of selected fields to {@link #getSelectedFieldsFromPreferences}</li>
     *   <li>(re)-set the comparator to {@link #getCollectableEntityFieldComparator()}</li>
     * </ul>
     * @param clcte
     * @param preferences
     *
     * TODO: Make protected again.
     */
    public void initializeFields() {
        final Comparator<CollectableEntityField> comp = getCollectableEntityFieldComparator();
        fields.set(getFieldsAvailableForResult(comp), new ArrayList<CollectableEntityField>(), comp);

        // select the previously selected fields according to user preferences:
        fields.moveToSelectedFields(getSelectedFieldsFromPreferences());
    }

    /**
     * initializes the <code>fields</code> field, while setting selected columns.
     * @param clcte
     * @param clctctl
     * @param lstSelectedNew
     * @param lstFixedNew
     * @param lstColumnWiths
     */
    public final void initializeFields(final ChoiceEntityFieldList fields,
            final List<CollectableEntityField> lstSelectedNew) {
        fields.setSelectedFields(lstSelectedNew);
    }

    /**
     * tries to read the selected fields from the preferences, making sure they contain at least one field.
     * @param clcte
     * @return List<CollectableEntityField> the selected fields from the user preferences.
     * @postcondition !result.isEmpty()
     */
    private List<CollectableEntityField> getSelectedFieldsFromPreferences() {
        final List<CollectableEntityField> result = (List<CollectableEntityField>) readSelectedFieldsFromPreferences();
        clctctl.makeSureSelectedFieldsAreNonEmpty(clcte, result);

        // Here we have at least one field as selected column:
        assert !result.isEmpty();
        return result;
    }

    private ListSelectionListener newListSelectionListener(final JTable tblResult) {
        return new ListSelectionListener() {
            @Override
            public void valueChanged(ListSelectionEvent ev) {
                try {
                    final ListSelectionModel lsm = (ListSelectionModel) ev.getSource();

                    final CollectStateModel<?> clctstatemodel = clctctl.getCollectStateModel();
                    if (clctstatemodel.getOuterState() == CollectState.OUTERSTATE_RESULT) {
                        final int iResultMode = CollectStateModel.getResultModeFromSelectionModel(lsm);
                        if (iResultMode != clctctl.getCollectStateModel().getResultMode()) {
                            clctctl.setCollectState(CollectState.OUTERSTATE_RESULT, iResultMode);
                        }
                    }

                    if (!ev.getValueIsAdjusting()) {
                        // Autoscroll selection. It's okay to do that here rather than in the CollectStateListener.
                        if (!lsm.isSelectionEmpty() && tblResult.getAutoscrolls()) {
                            // ensure that the last selected row is visible:
                            final int iRowIndex = lsm.getLeadSelectionIndex();
                            final int iColIndex = tblResult.getSelectedColumn();
                            final Rectangle rectCell = tblResult.getCellRect(iRowIndex,
                                    iColIndex != -1 ? iColIndex : 0, true);
                            if (rectCell != null) {
                                tblResult.scrollRectToVisible(rectCell);
                            }
                        }
                    }
                } catch (CommonBusinessException ex) {
                    Errors.getInstance().showExceptionDialog(clctctl.getTab(), ex);
                }
            } // valueChanged
        };
    }

    /**
     * change column ordering in table model when table columns
     * are reordered by dragging a column with the mouse
     */
    private TableColumnModelListener newColumnModelListener() {
        return new TableColumnModelListener() {
            @Override
            public void columnMoved(TableColumnModelEvent ev) {
                final int iFromColumn = ev.getFromIndex();
                final int iToColumn = ev.getToIndex();
                if (iFromColumn != iToColumn) {
                    //log.debug("column moved from " + iFromColumn + " to " + iToColumn);
                    // Sync the "selected fields" with the table column model:
                    getResultPanel().columnMovedInHeader(clctctl.getFields());

                    // Note that the columns of the result table model are not adjusted here.
                    // That means, the table column model and the table model are not 1:1 -
                    // use JTable.convertColumnIndexToModel to convert between the two.
                }
            }

            @Override
            public void columnAdded(TableColumnModelEvent ev) {
            }

            @Override
            public void columnRemoved(TableColumnModelEvent ev) {
            }

            @Override
            public void columnMarginChanged(ChangeEvent ev) {
            }

            @Override
            public void columnSelectionChanged(ListSelectionEvent ev) {
            }
        };
    }

    protected final PreferencesUpdateListener newResultTablePreferencesUpdateListener() {
        return new PreferencesUpdateListener();
    }

    protected class PreferencesUpdateListener implements ChangeListener, TableColumnModelListener {
        @Override
        public void stateChanged(ChangeEvent ev) {
            //         System.out.println("stateChanged " + ev);
            if (!isIgnorePreferencesUpdate) {
                clctctl.writeColumnOrderToPreferences();
            }
        }

        @Override
        public void columnSelectionChanged(ListSelectionEvent ev) {
            //         System.out.println("columnSelectionChanged " + ev);
            if (!isIgnorePreferencesUpdate) {
                //            writeSelectedFieldsAndWidthsToPreferences();
            }
        }

        @Override
        public void columnMoved(TableColumnModelEvent ev) {
            //         System.out.println("columnMoved " + ev);
            if (!isIgnorePreferencesUpdate) {
                writeSelectedFieldsAndWidthsToPreferences();
            }
        }

        @Override
        public void columnMarginChanged(ChangeEvent ev) {
            //         System.out.println("columnMarginChanged " + ev);
            if (!isIgnorePreferencesUpdate) {
                writeSelectedFieldsAndWidthsToPreferences();
            }
        }

        @Override
        public void columnAdded(TableColumnModelEvent ev) {
        }

        @Override
        public void columnRemoved(TableColumnModelEvent ev) {
        }
    }

    /**
     * releases the resources (esp. listeners) for this controller.
     */
    public void close() {
        fields = null;
        final ResultPanel<Clct> pnlResult = getResultPanel();
        if (pnlResult != null) {
            pnlResult.btnRefresh.setAction(null);
            pnlResult.btnNew.setAction(null);
            pnlResult.btnBookmark.setAction(null);
            pnlResult.btnClone.setAction(null);
            pnlResult.btnDelete.setAction(null);
            pnlResult.btnEdit.setAction(null);
            pnlResult.btnSelectColumns.setAction(null);
            pnlResult.btnExport.setAction(null);
            pnlResult.btnImport.setAction(null);
            pnlResult.miPopupEdit.setAction(null);
            pnlResult.miPopupClone.setAction(null);
            pnlResult.miPopupDelete.setAction(null);
            pnlResult.miPopupOpenInNewTab.setAction(null);
            pnlResult.miPopupBookmark.setAction(null);
            pnlResult.miPopupDefineAsNewSearchResult.setAction(null);
            pnlResult.removeActionsVisibleListener(actionsVisibleListener);
            UIUtils.removeAllMouseListeners(pnlResult.getResultTable().getTableHeader());
        }
    }

    /**
     * @param tblResult
     * @param bResultTruncated
     * @param iTotalNumberOfRecords
     *
     * TODO: Make package visible again.
     */
    public void setStatusBar(JTable tblResult, boolean bResultTruncated, int iTotalNumberOfRecords) {
        String sStatus;
        if (iTotalNumberOfRecords == 0) {
            sStatus = getSpringLocaleDelegate().getMessage("ResultController.9",
                    "Keinen zur Suchanfrage passenden Datensatz gefunden.");
        } else if (iTotalNumberOfRecords == 1) {
            sStatus = getSpringLocaleDelegate().getMessage("ResultController.2", "1 Datensatz gefunden.");
        } else {
            sStatus = getSpringLocaleDelegate().getMessage("ResultController.1", "{0} Datens\u00e4tze gefunden.",
                    Integer.toString(iTotalNumberOfRecords));
            if (bResultTruncated) {
                sStatus += getSpringLocaleDelegate().getMessage("ResultController.6",
                        " Das Ergebnis wurde nach {0} Zeilen abgeschnitten.", tblResult.getRowCount());
            }
        }
        this.getResultPanel().tfStatusBar.setText(sStatus);
    }

    /**
     * Command: Delete selected <code>Collectable</code>s in the Result panel.
     */
    private void cmdDeleteSelectedCollectables() {
        assert clctctl.getCollectStateModel().getOuterState() == CollectState.OUTERSTATE_RESULT;
        assert CollectState.isResultModeSelected(clctctl.getCollectStateModel().getResultMode());

        if (clctctl.multipleCollectablesSelected()) {
            final int iCount = clctctl.getResultTable().getSelectedRowCount();
            final String sMessagePattern = getSpringLocaleDelegate().getMessage("ResultController.13",
                    "Sollen die ausgew\u00e4hlten {0} Datens\u00e4tze wirklich gel\u00f6scht werden?");
            final String sMessage = MessageFormat.format(sMessagePattern, iCount);
            final int btn = JOptionPane.showConfirmDialog(clctctl.getTab(), sMessage,
                    getSpringLocaleDelegate().getMessage("ResultController.7", "Datens\u00e4tze l\u00f6schen"),
                    JOptionPane.YES_NO_OPTION);
            if (btn == JOptionPane.YES_OPTION) {
                new DeleteSelectedCollectablesController<Clct>(clctctl)
                        .run(clctctl.getMultiActionProgressPanel(iCount));
            }
        } else {
            final String sMessagePattern = getSpringLocaleDelegate().getMessage("ResultController.12",
                    "Soll der ausgew\u00e4hlte Datensatz ({0}) wirklich gel\u00f6scht werden?");
            final String sMessage = MessageFormat.format(sMessagePattern,
                    getSelectedCollectableFromTableModel().getIdentifierLabel());
            final int btn = JOptionPane.showConfirmDialog(clctctl.getTab(), sMessage,
                    getSpringLocaleDelegate().getMessage("ResultController.8", "Datensatz l\u00f6schen"),
                    JOptionPane.YES_NO_OPTION);

            if (btn == JOptionPane.YES_OPTION) {
                UIUtils.runCommand(clctctl.getTab(), new Runnable() {
                    @Override
                    public void run() {
                        try {
                            clctctl.checkedDeleteSelectedCollectable();
                        } catch (CommonPermissionException ex) {
                            final String sErrorMsg = getSpringLocaleDelegate().getMessage("ResultController.11",
                                    "Sie verf\u00fcgen nicht \u00fcber die ausreichenden Rechte, um diesen Datensatz zu l\u00f6schen.");
                            Errors.getInstance().showExceptionDialog(clctctl.getTab(), sErrorMsg, ex);
                        } catch (CommonBusinessException ex) {
                            Errors.getInstance().showExceptionDialog(clctctl.getTab(),
                                    "Der Datensatz konnte nicht gel\u00f6scht werden.", ex);
                        }
                    }
                });
            }
        }
    }

    /**
     * @return the fields displayed in the result panel
     */
    public ChoiceEntityFieldList getFields() {
        return fields;
    }

    /**
     * reads the previously selected fields from the user preferences, ignoring unknown fields that might occur when
     * the database schema has changed from one software release to another. This method tries to avoid throwing exceptions.
     * @param clcte
     * @return List<CollectableEntityField> the previously selected fields from the user preferences.
     * @see #writeSelectedFieldsToPreferences(List)
     * TODO: make private?
     */
    protected List<? extends CollectableEntityField> readSelectedFieldsFromPreferences() {
        List<String> lstSelectedFieldNames = getWorkspaceUtils().getSelectedColumns(clctctl.getEntityPreferences());

        final List<CollectableEntityField> result = Utils.createCollectableEntityFieldListFromFieldNames(this,
                clcte, lstSelectedFieldNames);
        return result;
    }

    /**
     * @param clcte
     * @return List<CollectableEntityField> the fields that are available for the result. This default implementation
     * returns all fields of the given entity that are to be display in the table.
     * Successors may want to do weird things like appending fields from subentities here...
     * TODO Make this private.
     */
    public SortedSet<CollectableEntityField> getFieldsAvailableForResult(Comparator<CollectableEntityField> comp) {
        final SortedSet<CollectableEntityField> result = new TreeSet<CollectableEntityField>(comp);
        for (String sFieldName : clcte.getFieldNames()) {
            if (this.isFieldToBeDisplayedInTable(sFieldName)) {
                result.add(getCollectableEntityFieldForResult(clcte, sFieldName));
            }
        }
        return result;
    }

    /**
     * can be used to hide columns in the table.
     * @param sFieldName
     * @return Is the field with the given name to be displayed in a table?
     * TODO move to ResultPanel.isFieldToBeShown
     */
    protected boolean isFieldToBeDisplayedInTable(String sFieldName) {
        return true;
    }

    /**
     * @param clcte
     * @param sFieldName
     * @return a <code>CollectableEntityField</code> of the given entity with the given field name, to be used in the Result metadata.
     * Some successors may want to do weird things here...
     * TODO: Make this private.
     */
    public CollectableEntityField getCollectableEntityFieldForResult(CollectableEntity sClcte, String sFieldName) {
        return clcte.getEntityField(sFieldName);
    }

    /**
     * @return the <code>Comparator</code> used for <code>CollectableEntityField</code>s (columns in the Result).
     * The default is to compare the column labels.
     * @postcondition result != null
     * TODO move to ResultController or ResultPanel
     */
    public Comparator<CollectableEntityField> getCollectableEntityFieldComparator() {
        return new CollectableEntityField.LabelComparator();
    }

    /**
     * sets all column widths to user preferences; set optimal width if no preferences yet saved
     * Copied from the SubFormController
     * @param tbl
     */
    public void setColumnWidths(final JTable tbl) {
        isIgnorePreferencesUpdate = true;
        this.getResultPanel().setColumnWidths(tbl, clctctl.getEntityPreferences());
        isIgnorePreferencesUpdate = false;
    }

    /**
     * @return the list of sorting columns.
     * TODO make this private
     */
    public List<CollectableSorting> getCollectableSortingSequence() {
        final String baseEntity = getEntity().getName();
        final Set<CollectableSorting> set = new HashSet<CollectableSorting>();
        final List<CollectableSorting> result = new ArrayList<CollectableSorting>();
        for (SortKey sortKey : clctctl.getResultTableModel().getSortKeys()) {
            final CollectableEntityField sortField = clctctl.getResultTableModel()
                    .getCollectableEntityField(sortKey.getColumn());
            final CollectableSorting sort;
            if (sortField instanceof CollectableEOEntityField) {
                final CollectableEOEntityField sf = (CollectableEOEntityField) sortField;
                final EntityFieldMetaDataVO mdField = sf.getMeta();
                final PivotInfo pinfo = mdField.getPivotInfo();
                if (pinfo != null) {
                    assert pinfo.getSubform().equals(sortField.getEntityName());
                    if (sortKey.getSortOrder() != SortOrder.UNSORTED) {
                        sort = new CollectableSorting(mdField, baseEntity.equals(pinfo.getSubform()),
                                sortKey.getSortOrder() == SortOrder.ASCENDING);
                    } else {
                        continue;
                    }
                } else {
                    throw new NotImplementedException();
                }
            } else if (sortField.getEntityName().equals(baseEntity)) {
                if (sortKey.getSortOrder() != SortOrder.UNSORTED) {
                    sort = new CollectableSorting(SystemFields.BASE_ALIAS, baseEntity,
                            baseEntity.equals(sortField.getEntityName()), sortField.getName(),
                            sortKey.getSortOrder() == SortOrder.ASCENDING);
                } else {
                    continue;
                }
            } else {
                // not a pivot element and not a field of the base entity => we cannot sort that
                continue;
            }
            // only use each sort column once
            if (set.add(sort)) {
                result.add(sort);
            }
        }
        // ??? is this needed (see above)
        CollectionUtils.removeDublicates(result);
        return result;
    }

    /**
     * @return the selected <code>Collectable</code>, if any, from the table model.
     * Note that there is no selected <code>Collectable</code> in New mode.
     * Note that the result might be incomplete, that means, some fields might be missing.
     * TODO make this private
     */
    public Clct getSelectedCollectableFromTableModel() {
        final int iSelectedRow = clctctl.getResultTable().getSelectedRow();
        return (iSelectedRow == -1) ? null : clctctl.getResultTableModel().getCollectable(iSelectedRow);
    }

    /**
     * @return the selected <code>Collectable</code>'s, if any, from the table model.
     * Note that there is no selected <code>Collectable</code> in New mode.
     * Note that the result might be incomplete, that means, some fields might be missing.
     */
    public Collection<Clct> getSelectedCollectablesFromTableModel() {
        int[] iSelectedRows = clctctl.getResultTable().getSelectedRows();
        Collection<Clct> result = new ArrayList<Clct>(iSelectedRows.length);
        if (iSelectedRows.length > 0) {
            for (int i = 0; i < iSelectedRows.length; i++) {
                result.add(clctctl.getResultTableModel().getCollectable(iSelectedRows[i]));
            }
        }
        return result;
    }

    /**
     * replaces the selected <code>Collectable</code> in the table model with <code>clct</code>.
     * @param clct
     * @precondition clct != null
     * TODO make this private
     */
    public final void replaceSelectedCollectableInTableModel(Clct clct) {
        if (clct == null) {
            throw new NullArgumentException("clct");
        }
        this.replaceCollectableInTableModel(clctctl.getResultTable().getSelectedRow(), clct);
    }

    /**
     * replaces the <code>Collectable</code> in the table model that has the same id as <code>clct</code>
     * with <code>clct</code>.
     * @param clct
     * @precondition clct != null
     * TODO make this private
     */
    public final void replaceCollectableInTableModel(Clct clct) {
        if (clct == null) {
            throw new NullArgumentException("clct");
        }
        final int iRow = clctctl.getResultTableModel().findRowById(clct.getId());
        if (iRow == -1) {
            throw new CommonFatalException(
                    "Der Datensatz mit der Id " + clct.getId() + " ist nicht im Suchergebnis vorhanden.");
        }
        this.replaceCollectableInTableModel(iRow, clct);
    }

    /**
     * replaces the <code>Collectable</code> in the given row of the table model with <code>clct</code>.
     * @param iRow
     * @param clct
     * @precondition clct != null
     * TODO move to ResultController
     */
    private void replaceCollectableInTableModel(int iRow, Clct clct) {
        if (clct == null) {
            throw new NullArgumentException("clct");
        }
        clctctl.getResultTableModel().setCollectable(iRow, clct);
    }

    /**
     * replaces the <code>Collectable</code>s the table model that have the same ids
     * as the given <code>Collectable</code>s, with those <code>Collectable</code>s.
     * @param collclct
     * @precondition collclct != null
     *
     * TODO Make this protected again.
     */
    public final void replaceCollectablesInTableModel(Collection<Clct> collclct) {
        if (collclct == null) {
            throw new NullArgumentException("collclct");
        }
        for (Clct clct : collclct) {
            this.replaceCollectableInTableModel(clct);
        }
    }

    /**
     * command: select columns
     * Lets the user select the columns to show in the result list.
     */
    public void cmdSelectColumns(final ChoiceEntityFieldList fields) {
        final SelectColumnsController ctl = new SelectColumnsController(clctctl.getTab());
        // final List<CollectableEntityField> lstAvailable = (List<CollectableEntityField>) fields.getAvailableFields();
        // final List<CollectableEntityField> lstSelected = (List<CollectableEntityField>) fields.getSelectedFields();
        final ResultPanel<Clct> panel = getResultPanel();
        final JTable tbl = panel.getResultTable();

        final Map<String, Integer> mpWidths = panel.getVisibleColumnWidth(fields.getSelectedFields());
        ctl.setModel(fields);
        final boolean bOK = ctl.run(getSpringLocaleDelegate().getMessage("SelectColumnsController.1",
                "Anzuzeigende Spalten ausw\u00e4hlen"));

        if (bOK) {
            UIUtils.runCommand(clctctl.getTab(), new CommonRunnable() {
                @Override
                public void run() throws CommonBusinessException {
                    final int iSelectedRow = tbl.getSelectedRow();
                    fields.set(ctl.getAvailableObjects(), ctl.getSelectedObjects(),
                            clctctl.getResultController().getCollectableEntityFieldComparator());
                    final List<? extends CollectableEntityField> lstSelectedNew = fields.getSelectedFields();
                    ((CollectableTableModel<?>) tbl.getModel()).setColumns(lstSelectedNew);
                    panel.setupTableCellRenderers(tbl);
                    Collection<CollectableEntityField> collNewlySelected = new ArrayList<CollectableEntityField>(
                            lstSelectedNew);
                    collNewlySelected.removeAll(fields.getSelectedFields());
                    if (!collNewlySelected.isEmpty()) {
                        if (!clctctl.getSearchStrategy().getCollectablesInResultAreAlwaysComplete()) {
                            // refresh the result:
                            clctctl.getResultController().getSearchResultStrategy().refreshResult();
                        }
                    }

                    // reselect the previously selected row (which gets lost be refreshing the model)
                    if (iSelectedRow != -1) {
                        tbl.setRowSelectionInterval(iSelectedRow, iSelectedRow);
                    }

                    isIgnorePreferencesUpdate = true;
                    panel.restoreColumnWidths(ctl.getSelectedObjects(), mpWidths);
                    isIgnorePreferencesUpdate = false;

                    // Set the newly added columns to optimal width
                    for (CollectableEntityField clctef : collNewlySelected) {
                        TableUtils.setOptimalColumnWidth(tbl,
                                tbl.getColumnModel().getColumnIndex(clctef.getLabel()));
                    }
                }
            });
        }
    }

    /**
     * adds the column with the given field name before column columnBefore to the table.
     * @param fields available and selected fields of the result table
     * @param columnBefore the column of the column model (as opposed to the column of the table model)
     * @param sFieldNameToAdd name of the column to add
     */
    protected void cmdAddColumn(final ChoiceEntityFieldList fields, TableColumn columnBefore,
            final String sFieldNameToAdd) throws CommonBusinessException {
        // find the field with the given name in available fields:
        final CollectableEntityField clctef = CollectionUtils.findFirst(fields.getAvailableFields(),
                PredicateUtils.transformedInputEquals(new CollectableEntityField.GetName(), sFieldNameToAdd));

        assert clctef != null;

        final CollectableTableModel<?> tblmodel = (CollectableTableModel<?>) getResultPanel().getResultTable()
                .getModel();
        final CollectableEntityField clctefBefore = tblmodel
                .getCollectableEntityField(columnBefore.getModelIndex());
        int insertPos = fields.getSelectedFields().indexOf(clctefBefore);
        insertPos = (insertPos >= 0) ? insertPos : 0;
        fields.moveToSelectedFields(insertPos, clctef);

        // Note that it is not enough to add the column to the result table model.
        // We must rebuild the table tblmodel's columns in order to sync it with the table column model:
        tblmodel.setColumns(fields.getSelectedFields());
    }

    /**
     * removes the given column from the table
     * @param entityField the column of the column model (as opposed to the column of the table model)
     */
    protected void cmdRemoveColumn(final ChoiceEntityFieldList fields, CollectableEntityField entityField) {
        fields.moveToAvailableFields(entityField);
        getWorkspaceUtils().addHiddenColumn(getCollectController().getEntityPreferences(), entityField.getName());

        // Note that it is not enough to remove the column from the result table model.
        // We must rebuild the table model's columns in order to sync it with the table column model:
        // this.getResultTableModel().removeColumn(iColumn);
        ((SortableCollectableTableModel<?>) getResultPanel().getResultTable().getModel())
                .setColumns(fields.getSelectedFields());
    }

    /**
     * TODO: Make this protected again.
     */
    public void setModel(CollectableTableModel<Clct> tblmodel) {
        final ResultPanel<Clct> panel = getResultPanel();
        final JTable resultTable = panel.getResultTable();
        resultTable.setModel(tblmodel);
        ((ToolTipsTableHeader) resultTable.getTableHeader()).setExternalModel(tblmodel);
        if (tblmodel instanceof SortableTableModel) {
            ((SortableTableModel) tblmodel).addSortingListener(newResultTablePreferencesUpdateListener());
        }
    }

    public void setIgnorePreferencesUpdate(boolean ignore) {
        this.isIgnorePreferencesUpdate = ignore;
    }

    protected void toggleColumnVisibility(TableColumn columnBefore, final String sFieldName) {
        final ResultPanel<Clct> panel = getResultPanel();
        try {
            final ChoiceEntityFieldList fields = clctctl.getFields();
            final Map<String, Integer> mpWidths = panel.getVisibleColumnWidth(fields.getSelectedFields());
            final CollectableEntityField clctef = clcte.getEntityField(sFieldName);
            final int iIndex = fields.getSelectedFields().indexOf(clctef);
            if (iIndex == -1) {
                cmdAddColumn(fields, columnBefore, sFieldName);
                if (!clctctl.getSearchStrategy().getCollectablesInResultAreAlwaysComplete()) {
                    clctctl.getResultController().getSearchResultStrategy().refreshResult();
                }
            } else {
                cmdRemoveColumn(fields, clctef);
            }
            isIgnorePreferencesUpdate = true;
            panel.restoreColumnWidths(fields.getSelectedFields(), mpWidths);
            isIgnorePreferencesUpdate = false;
        } catch (CommonBusinessException e) {
            // TODO Auto-generated catch block
            LOG.warn("toggleColumnVisibility failed: " + e, e);
        }
    }

    /**
     * writes the selected columns (fields) and their widths to the user preferences.
     * TODO make private again or refactor!
     */
    public void writeSelectedFieldsAndWidthsToPreferences() {
        writeSelectedFieldsAndWidthsToPreferences(null);
    }

    /**
     * writes the selected columns (fields) and their widths to the user preferences.
     * TODO make private again or refactor!
     * @param mpWidths 
     */
    public void writeSelectedFieldsAndWidthsToPreferences(Map<String, Integer> mpWidths) {
        writeSelectedFieldsAndWidthsToPreferences(clctctl.getEntityPreferences(),
                clctctl.getFields().getSelectedFields(), mpWidths);
    }

    protected void writeSelectedFieldsAndWidthsToPreferences(EntityPreferences entityPreferences,
            List<? extends CollectableEntityField> lstclctefSelected, Map<String, Integer> mpWidths) {
        final List<String> lstFields = getFieldsForPreferences(lstclctefSelected);
        final List<Integer> lstFieldWidths = new ArrayList<Integer>();
        if (mpWidths == null) {
            mpWidths = getResultPanel().getVisibleColumnWidth(lstclctefSelected);
        }
        for (CollectableEntityField clctef : lstclctefSelected) {
            final String field = clctef.getName();
            if (mpWidths.containsKey(field)) {
                lstFieldWidths.add(mpWidths.get(field));
            } else {
                lstFieldWidths.add(TableUtils.getMinimumColumnWidth(clctef.getJavaClass()));
            }
        }

        getWorkspaceUtils().setColumnPreferences(entityPreferences, lstFields, lstFieldWidths);
    }

    protected List<String> getFieldsForPreferences(List<? extends CollectableEntityField> lstclctefSelected) {
        return CollectableUtils.getFieldNamesFromCollectableEntityFields(lstclctefSelected);
    }

    protected List<Integer> getFieldWidthsForPreferences() {
        return CollectableTableHelper.getColumnWidths(getResultPanel().getResultTable());
    }

    /**
     * Command: Define selected <code>Collectable</code>s as a new search result.
     *
     * @deprecated Does nothing (but will be overridden by extending classes).
     */
    private void cmdDefineSelectedCollectablesAsNewSearchResult() {
    }

    private class ResetMainFilterEnabledListener implements EnabledListener {
        @Override
        public void enabledChanged(boolean enabled) {
            getResultPanel().btnResetMainFilter.setVisible(enabled);
        }
    }

} // class ResultController