/**
* Copyright (C) 2015 Valkyrie RCP
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.valkyriercp.widget.editor;
import com.google.common.base.Joiner;
import com.google.common.collect.Lists;
import com.jgoodies.forms.factories.Borders;
import com.jgoodies.forms.factories.FormFactory;
import com.jgoodies.forms.layout.*;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jdesktop.swingx.JXTable;
import org.springframework.beans.factory.annotation.Autowired;
import org.valkyriercp.application.support.DefaultButtonFocusListener;
import org.valkyriercp.binding.form.FormModel;
import org.valkyriercp.binding.form.NewFormObjectAware;
import org.valkyriercp.binding.validation.support.DefaultValidationResultsModel;
import org.valkyriercp.command.ActionCommandInterceptor;
import org.valkyriercp.command.CommandConfigurer;
import org.valkyriercp.command.support.AbstractCommand;
import org.valkyriercp.command.support.ActionCommand;
import org.valkyriercp.command.support.CommandGroup;
import org.valkyriercp.command.support.SplitPaneExpansionToggleCommand;
import org.valkyriercp.component.Focussable;
import org.valkyriercp.core.Messagable;
import org.valkyriercp.form.*;
import org.valkyriercp.util.MessageConstants;
import org.valkyriercp.util.ObjectUtils;
import org.valkyriercp.widget.AbstractTitledWidget;
import org.valkyriercp.widget.SelectionWidget;
import org.valkyriercp.widget.TitledWidget;
import org.valkyriercp.widget.Widget;
import org.valkyriercp.widget.editor.provider.DataProviderEventSource;
import org.valkyriercp.widget.editor.provider.DataProviderListener;
import org.valkyriercp.widget.table.TableWidget;
import javax.swing.*;
import javax.swing.border.Border;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.table.TableCellRenderer;
import javax.swing.table.TableColumn;
import javax.swing.text.JTextComponent;
import java.awt.*;
import java.awt.datatransfer.StringSelection;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.*;
/**
* AbstractDataEditorWidget implements a basic editor screen, based on 3 parts:
* <p/>
* <ol>
* <li>a table with sortable columns and a local quick search</li>
* <li>a filter to reduce the dataset</li>
* <li>a detail section for the details of 1 entity</li>
* </ol>
*/
public abstract class AbstractDataEditorWidget extends AbstractTitledWidget
implements
TitledWidget,
SelectionWidget
{
/**
* Log facility.
*/
private static final Log log = LogFactory.getLog(AbstractDataEditorWidget.class);
private static final String QUICKADD = "quickAdd";
protected static final String UPDATE_COMMAND_ID = "update";
protected static final String CREATE_COMMAND_ID = "create";
private static final String TOGGLE_DETAIL_COMMAND_ID = "opendetail";
private static final String REMOVE_CONTINUE_AFTER_ERROR = "remove.continue_after_error";
private static final String REMOVE_CONFIRMATION_ID = "remove.confirmation";
private static final String DBLCLICKSELECTS = "dblclick_for_edit";
public static final String UNSAVEDCHANGES_WARNING_ID = "unsavedchanges.warning";
public static final String UNSAVEDCHANGES_UNCOMMITTABLE_WARNING_ID = "unsavedchanges.uncommittable.warning";
public static final RowSpec FILL_ROW_SPEC = new RowSpec(RowSpec.FILL, Sizes.PREFERRED,
FormSpec.DEFAULT_GROW);
public static final ColumnSpec FILL_NOGROW_COLUMN_SPEC = new ColumnSpec(ColumnSpec.FILL, Sizes.DEFAULT,
FormSpec.NO_GROW);
public static final ColumnSpec FILL_COLUMN_SPEC = new ColumnSpec(ColumnSpec.FILL, Sizes.DEFAULT,
FormSpec.DEFAULT_GROW);
public static final boolean ON = true;
public static final boolean OFF = false;
private boolean selectMode = OFF;
private boolean multipleSelectionInSelectMode = OFF;
private String searchString;
private Object selectedRowObject;
private final CellConstraints cc = new CellConstraints();
private SplitPaneExpansionToggleCommand toggleDetailCommand;
private SplitPaneExpansionToggleCommand toggleFilterCommand;
private ActionCommand editRowCommand;
private ActionCommand addRowCommand;
private ActionCommand cloneRowCommand;
private ActionCommand removeRowsCommand;
private ActionCommand executeFilterCommand;
private ActionCommand refreshCommand;
private ActionCommand clearFilterCommand;
private ActionCommand emptyFilterCommand;
private ActionCommand selectionCommand;
private ActionCommand printCommand;
private ActionCommand updateCommand;
private ActionCommand createRowCommand;
private CardLayout saveUpdateSwitcher;
private JPanel saveUpdatePanel;
private JTextField textFilterField;
private Map dataProviderSources = null;
@Autowired
private CommandConfigurer commandConfigurer;
/**
* Observer listening to changes in the table selection.
*/
protected Observer tableSelectionObserver;
/**
* Default constructor will initialise the necessary listeners/observers.
*/
public AbstractDataEditorWidget()
{
tableSelectionObserver = createListSelectionObserver();
}
/**
* Creates the observer that listens to selections in the listView. Normally forwards the selection to the
* detailForm.
*/
protected Observer createListSelectionObserver()
{
return new ListSelectionObserver();
}
/**
* Set the select mode of this dataEditor.
*/
public void setSelectMode(boolean selectMode)
{
this.selectMode = selectMode;
}
public boolean isSelectMode()
{
return selectMode;
}
/**
* Set the local text filter field value
*
* @param queryString filterText.
*/
public void setSearchString(String queryString)
{
this.searchString = queryString;
if (this.textFilterField != null)
{
this.textFilterField.setText(this.searchString);
}
}
public String getSearchString()
{
return searchString;
}
public Object getSelectedRowObject()
{
return selectedRowObject;
}
public void setSelectedRowObject(Object selectedObject)
{
getTableWidget().selectRowObject(selectedObject, null);
}
public abstract Object setSelectedSearch(Object searchCriteria);
/**
* {@inheritDoc}
*/
@Override
public JComponent createWidgetContent()
{
return createDataEditorWidget();
}
protected final JComponent createDataEditorWidget()
{
JSplitPane splitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT);
splitPane.setBorder(BorderFactory.createEmptyBorder());
if (isSelectMode() == ON)
{
DefaultButtonFocusListener.setDefaultButton(getTableWidget().getComponent(), getSelectCommand());
getTableWidget().getTable().getInputMap(JTable.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), "none");
}
if ((isSelectMode() == ON) && (selectedRowObject == null))
{
splitPane.setDividerLocation(Integer.MAX_VALUE);
this.toggleDetailCommand = new SplitPaneExpansionToggleCommand(TOGGLE_DETAIL_COMMAND_ID,
splitPane, true);
}
else
{
splitPane.setDividerLocation(-1);
this.toggleDetailCommand = new SplitPaneExpansionToggleCommand(TOGGLE_DETAIL_COMMAND_ID,
splitPane, false);
}
splitPane.setLastDividerLocation(-1);
splitPane.setOneTouchExpandable(true);
splitPane.setResizeWeight(getTableResizeWeight());
splitPane.setLeftComponent(getTableFilterPanel());
splitPane.setRightComponent(getDetailPanel());
getDetailForm().setEnabled(getTableWidget().hasSelection());
createAddRowCommand();
createRemoveRowCommand();
createCloneRowCommand();
return splitPane;
}
protected double getTableResizeWeight()
{
return 0.75;
}
private JComponent getControlPanel()
{
AbstractCommand localToggleDetailCommand = getToggleDetailCommand();
AbstractCommand helpCommand = getHelpCommand();
AbstractCommand[] commands = getControlCommands();
ColumnSpec[] columnSpecs = new ColumnSpec[1 + (commands.length + 1) * 2];
columnSpecs[0] = FILL_NOGROW_COLUMN_SPEC;// open-detail btn
columnSpecs[1] = FormFactory.UNRELATED_GAP_COLSPEC; // gap
columnSpecs[2] = FILL_NOGROW_COLUMN_SPEC;// help btn
columnSpecs[3] = FILL_COLUMN_SPEC; // glue space
for (int i = 0; i < commands.length; i++)
{
// print | select-and-close | cancel
columnSpecs[4 + (i * 2)] = FILL_NOGROW_COLUMN_SPEC;
if (i != commands.length - 1)
{
// gap, but not after last
columnSpecs[5 + (i * 2)] = FormFactory.UNRELATED_GAP_COLSPEC;
}
}
RowSpec[] rowSpecs = new RowSpec[]{FILL_ROW_SPEC};
FormLayout formLayout = new FormLayout(columnSpecs, rowSpecs);
JPanel buttonPanel = new JPanel(formLayout);
buttonPanel.add(localToggleDetailCommand.createButton(), cc.xy(1, 1));
buttonPanel.add(helpCommand.createButton(), cc.xy(3, 1));
for (int i = 0; i < commands.length; i++)
{
buttonPanel.add(commands[i].createButton(), cc.xy(5 + i * 2, 1));
}
return buttonPanel;
}
/**
* {@inheritDoc}
*/
@Override
public java.util.List<AbstractCommand> getCommands()
{
return Arrays.asList(getToggleDetailCommand());
}
protected JComponent getDetailPanel()
{
ColumnSpec[] columnSpecs = new ColumnSpec[]{FILL_COLUMN_SPEC};
RowSpec[] rowSpecs = new RowSpec[]{FormFactory.LINE_GAP_ROWSPEC, // gap
FormFactory.DEFAULT_ROWSPEC, // buttons for detailpanel
FormFactory.LINE_GAP_ROWSPEC, // gap
FILL_ROW_SPEC
// detailpanel itself (form)
};
JPanel detailPanel = new JPanel(new FormLayout(columnSpecs, rowSpecs));
Form detailForm = getDetailForm();
newSingleLineResultsReporter(this);
detailPanel.add(getDetailControlPanel(), cc.xy(1, 2));
detailPanel.add(detailForm.getControl(), cc.xy(1, 4));
// force form readonly if adding & updating is not supported
if (!isAddRowSupported() && !isUpdateRowSupported())
{
detailForm.getFormModel().setReadOnly(true);
}
return detailPanel;
}
private JComponent getDetailControlPanel()
{
ColumnSpec[] columnSpecs = new ColumnSpec[]{FILL_NOGROW_COLUMN_SPEC, // edit buttons
FILL_COLUMN_SPEC, // glue
FILL_NOGROW_COLUMN_SPEC, // navigation buttons
FILL_COLUMN_SPEC, // glue
new ColumnSpec(ColumnSpec.RIGHT, Sizes.DEFAULT, FormSpec.DEFAULT_GROW) // list summary
};
RowSpec[] rowSpecs = new RowSpec[]{FILL_ROW_SPEC};
FormLayout formLayout = new FormLayout(columnSpecs, rowSpecs);
// coupled glue space around nav buttons!
formLayout.setColumnGroups(new int[][]{{2, 4}});
JPanel buttonPanel = new JPanel(formLayout);
JComponent editButtons = getEditButtons();
JComponent tableButtonBar = getTableWidget().getButtonBar();
if (editButtons != null)
{
buttonPanel.add(editButtons, cc.xy(1, 1));
}
if (tableButtonBar != null)
{
buttonPanel.add(tableButtonBar, cc.xy(3, 1));
}
buttonPanel.add(getTableWidget().getListSummaryLabel(), cc.xy(5, 1));
return buttonPanel;
}
protected JComponent getEditButtons()
{
if (!isAddRowSupported() && !isUpdateRowSupported())
{
return null;
}
ColumnSpec[] columnSpecs = new ColumnSpec[]{FILL_NOGROW_COLUMN_SPEC, // save
FormFactory.RELATED_GAP_COLSPEC, // gap
FILL_NOGROW_COLUMN_SPEC, // undo
FormFactory.RELATED_GAP_COLSPEC, // gap
FormFactory.DEFAULT_COLSPEC, // separator
FormFactory.RELATED_GAP_COLSPEC, // gap
FILL_NOGROW_COLUMN_SPEC, // quickadd
};
RowSpec[] rowSpecs = new RowSpec[]{FILL_ROW_SPEC};
FormLayout formLayout = new FormLayout(columnSpecs, rowSpecs);
JPanel buttonPanel = new JPanel(formLayout);
buttonPanel.add(getCommitComponent(), cc.xy(1, 1));
buttonPanel.add(getRevertCommand().createButton(), cc.xy(3, 1));
if (isAddRowSupported())
{
buttonPanel.add(new JSeparator(SwingConstants.VERTICAL), cc.xy(5, 1));
buttonPanel.add(createQuickAddCheckBox(), cc.xy(7, 1));
}
return buttonPanel;
}
protected JComponent getCommitComponent()
{
if (isAddRowSupported() && isUpdateRowSupported())
{
saveUpdateSwitcher = new CardLayout();
saveUpdatePanel = new JPanel(saveUpdateSwitcher);
saveUpdatePanel.add(getCreateCommand().createButton(), CREATE_COMMAND_ID);
saveUpdatePanel.add(getUpdateCommand().createButton(), UPDATE_COMMAND_ID);
return saveUpdatePanel;
}
if (isAddRowSupported())
{
DefaultButtonFocusListener.setDefaultButton(getDetailForm().getControl(), getCreateCommand());
return getCreateCommand().createButton();
}
DefaultButtonFocusListener.setDefaultButton(getDetailForm().getControl(), getUpdateCommand());
return getUpdateCommand().createButton();
}
/**
* Convenience method to retrieve the action command that should be used when changes are made in the detailForm.
* This can be update or add, according to whether a row is selected and changed or no row is selected.
*
* @return the command that should be used to save changes in the form.
*/
protected ActionCommand getCommitCommand()
{
if ((selectedRowObject == null) && (isAddRowSupported()))
return getCreateCommand();
else if (isUpdateRowSupported())
return getUpdateCommand();
return null;
}
/**
* Returns the save command, lazily creates one if needed.
*/
public ActionCommand getUpdateCommand()
{
if (updateCommand == null)
{
updateCommand = createUpdateCommand();
}
return updateCommand;
}
/**
* Creates the save command.
*
* @see #doUpdate()
*/
protected ActionCommand createUpdateCommand()
{
ActionCommand command = new ActionCommand(UPDATE_COMMAND_ID)
{
@Override
protected void doExecuteCommand()
{
doUpdate();
}
};
command.setSecurityControllerId(getId() + "." + UPDATE_COMMAND_ID);
getApplicationConfig().commandConfigurer().configure(command);
getDetailForm().addGuarded(command, FormGuard.LIKE_COMMITCOMMAND);
return command;
}
/**
* Save the changes made in the detailForm according to following steps:
* <p/>
* <ol>
* <li>commit form</li>
* <li>formObject sent to back-end</li>
* <li>changes are handled in back-end</li>
* <li>changed object is returned to client</li>
* <li>old object is replaced by changed object</li>
* </ol>
*/
protected void doUpdate()
{
getDetailForm().commit();
Object savedObject = null;
try
{
savedObject = saveEntity(getDetailForm().getFormObject());
setDetailFormObject(savedObject, tableSelectionObserver, false);
}
catch (RuntimeException e)
{
Object changedObject = getDetailForm().getFormObject();
// the following actually requests the object from the back-end
boolean success = setDetailFormObject(changedObject, tableSelectionObserver, true);
// set the changes back on the model if the object could be set on the form model
if (success)
ObjectUtils.mapObjectOnFormModel(getDetailForm().getFormModel(), changedObject);
throw e;
}
}
/**
* Returns the create command, lazily creates one if needed.
*/
public ActionCommand getCreateCommand()
{
if (createRowCommand == null)
{
createRowCommand = createCreateCommand();
}
return createRowCommand;
}
/**
* Creates the create command.
*
* @see #doCreate()
*/
protected ActionCommand createCreateCommand()
{
ActionCommand command = new ActionCommand(CREATE_COMMAND_ID)
{
@Override
protected void doExecuteCommand()
{
doCreate();
}
};
command.setSecurityControllerId(getId() + "." + CREATE_COMMAND_ID);
getApplicationConfig().commandConfigurer().configure(command);
getDetailForm().addGuarded(command, FormGuard.LIKE_COMMITCOMMAND);
return command;
}
/**
* Creates a new data object according to following steps:
* <p/>
* <ol>
* <li>form commit</li>
* <li>formObject sent to back-end</li>
* <li>back-end creates item</li>
* <li>back-end returns new item to client</li>
* <li>new item is selected in dataEditor if possible</li>
* </ol>
*/
protected void doCreate()
{
getDetailForm().commit();
Object newObject = null;
try
{
newObject = createNewEntity(getDetailForm().getFormObject());
// select row only if user hasn't made another selection in
// table and this commit is triggered by the save-changes
// dialog and if not in quick add mode
if (newObject != null && !getTableWidget().hasSelection())
{
if (getTableWidget().selectRowObject(newObject, null) == -1)
{
// select row wasn't succesfull, maybe search string
// was filled in?
setSearchString(null);
getTableWidget().selectRowObject(newObject, null);
}
}
}
catch (RuntimeException e)
{
Object changedFormObject = getDetailForm().getFormObject();
newRow(null);
ObjectUtils.mapObjectOnFormModel(getDetailForm().getFormModel(), changedFormObject);
throw e;
// make form dirty, show error in messagepane and throw exception to display error dialog
}
}
protected AbstractCommand getRevertCommand()
{
return getDetailForm().getRevertCommand();
}
abstract protected DefaultValidationResultsModel getValidationResults();
public ValidationResultsReporter newSingleLineResultsReporter(Messagable messagable)
{
return new SimpleValidationResultsReporter(getValidationResults(), messagable);
}
protected JComponent createQuickAddCheckBox()
{
quickAddCheckBox = new JCheckBox(getApplicationConfig().messageResolver().getMessage(getId(), QUICKADD, MessageConstants.TITLE));
quickAddCheckBox.setFocusable(false);
getCreateCommand().addCommandInterceptor(new ActionCommandInterceptor()
{
public boolean preExecution(ActionCommand command)
{
return true; // proceed
}
public void postExecution(ActionCommand command)
{
if (quickAddCheckBox.isSelected())
{
getAddRowCommand().execute();
}
}
});
quickAddCheckBox.setEnabled(getAddRowCommand().isEnabled());
getAddRowCommand().addPropertyChangeListener(AbstractCommand.ENABLED_PROPERTY_NAME,
new PropertyChangeListener()
{
public void propertyChange(PropertyChangeEvent evt)
{
Object newValue = evt.getNewValue();
quickAddCheckBox.setEnabled(((Boolean) newValue).booleanValue());
}
});
return quickAddCheckBox;
}
protected JComponent getTableFilterPanel()
{
ColumnSpec[] columnSpecs = new ColumnSpec[]{FILL_COLUMN_SPEC};
RowSpec[] rowSpecs = new RowSpec[]{
// buttons for list and filter
FormFactory.DEFAULT_ROWSPEC, FormFactory.LINE_GAP_ROWSPEC,
// splitpane with list and filter
FILL_ROW_SPEC};
JPanel top = new JPanel(new FormLayout(columnSpecs, rowSpecs));
final JTable table = getTableWidget().getTable();
if (isUpdateRowSupported())
{
String tooltip = getApplicationConfig().messageResolver().getMessage(getId(), DBLCLICKSELECTS, MessageConstants.CAPTION);
table.setToolTipText(tooltip);
}
CommandGroup tableGroup = getTablePopupMenuCommandGroup();
table.addMouseListener(new TableMouseListener(table, tableGroup.createPopupMenu()));
JComponent tableScroller = getTableWidget().getComponent();
JComponent tableAndOptionalFilter = tableScroller;
if (isFilterSupported()) // add filter too via splitpane
{
JSplitPane splitPane = new JSplitPane();
if (this.selectMode == ON)
{
splitPane.setDividerLocation(-1);
this.toggleFilterCommand = new SplitPaneExpansionToggleCommand("openfilter", splitPane, false);
}
else
{
splitPane.setDividerLocation(Integer.MAX_VALUE);
this.toggleFilterCommand = new SplitPaneExpansionToggleCommand("openfilter", splitPane, true);
}
splitPane.setLastDividerLocation(Integer.MAX_VALUE);
splitPane.setOneTouchExpandable(true);
splitPane.setResizeWeight(1.0);
this.toggleFilterCommand.setEnabled(isFilterSupported());
splitPane.setLeftComponent(tableScroller);
JComponent filterPanel = getFilterPanel();
splitPane.setRightComponent(filterPanel);
tableAndOptionalFilter = splitPane;
}
top.add(getTableFilterControlPanel(), cc.xy(1, 1));
top.add(tableAndOptionalFilter, cc.xy(1, 3));
return top;
}
/**
* Returns the commandGroup that should be used to create the popup menu for the table.
*/
protected CommandGroup getTablePopupMenuCommandGroup()
{
return getApplicationConfig().commandManager().createCommandGroup(Lists.newArrayList(getEditRowCommand(), "separator",
getAddRowCommand(), getCloneRowCommand(), getRemoveRowsCommand(), "separator",
getRefreshCommand(), "separator", getCopySelectedRowsToClipboardCommand()));
}
private JComponent getFilterPanel()
{
if (!isFilterSupported())
{
return null;
}
ColumnSpec[] columnSpecs = new ColumnSpec[]{FILL_COLUMN_SPEC};
RowSpec[] rowSpecs = new RowSpec[]{new RowSpec(RowSpec.FILL, Sizes.DEFAULT, FormSpec.DEFAULT_GROW), // form gewrapped in een scrollpane.
FormFactory.RELATED_GAP_ROWSPEC, FormFactory.DEFAULT_ROWSPEC, // buttons
};
JPanel filterPanel = new JPanel(new FormLayout(columnSpecs, rowSpecs));
JComponent filterFormControl = getFilterForm().getControl();
filterFormControl.setBorder(Borders.DIALOG_BORDER);
final JScrollPane filterScroller = new JScrollPane(filterFormControl,
JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
filterScroller.getHorizontalScrollBar().getModel().addChangeListener(new ChangeListener()
{
public void stateChanged(ChangeEvent e)
{
if (filterScroller.getVerticalScrollBar().isVisible())
{
filterScroller.setBorder(BorderFactory.createMatteBorder(0, 0, 1, 0, Color.gray));
}
else
{
filterScroller.setBorder(null);
}
}
});
// setting minimum size: always keep width of panel + scrollbar width
Dimension scrollDimension = new Dimension(filterScroller.getPreferredSize().width
+ filterScroller.getVerticalScrollBar().getPreferredSize().width, 50);
filterScroller.setMinimumSize(scrollDimension);
filterPanel.add(filterScroller, cc.xy(1, 1));
JComponent filterControlPanel = getFilterControlPanel();
filterControlPanel.setBorder(Borders.DIALOG_BORDER);
filterPanel.add(filterControlPanel, cc.xy(1, 3));
// register a default command to use when focus is on the filterPanel
DefaultButtonFocusListener.setDefaultButton(filterPanel, getExecuteFilterCommand());
return filterPanel;
}
private JComponent getFilterControlPanel()
{
CommandGroup controlCommands = getApplicationConfig().commandManager().createCommandGroup(Lists.newArrayList(
getExecuteFilterCommand(), getEmptyFilterCommand()));
return controlCommands.createButtonBar((Size) null, (Border) null);
}
private JCheckBox quickAddCheckBox;
private ActionCommand copySelectedRowsCommand;
private JComponent getTableFilterControlPanel()
{
CommandGroup tableFilterControlCommands = isFilterSupported()
? getTableFilterControlCommands()
: null;
ColumnSpec[] columnSpecs = getTableColumnSpecs(tableFilterControlCommands != null
&& tableFilterControlCommands.size() > 0);
RowSpec[] rowSpecs = new RowSpec[]{FILL_ROW_SPEC};
FormLayout formLayout = new FormLayout(columnSpecs, rowSpecs);
JPanel buttonPanel = new JPanel(formLayout);
int columnCounter = 1;
if (isAddRowSupported())
{
buttonPanel.add(getAddRowCommand().createButton(), cc.xy(columnCounter, 1));
columnCounter += 2;
}
if (isRemoveRowsSupported())
{
buttonPanel.add(getRemoveRowsCommand().createButton(), cc.xy(columnCounter, 1));
columnCounter += 2;
}
if (isAddRowSupported() || isRemoveRowsSupported())
{
buttonPanel.add(new JSeparator(SwingConstants.VERTICAL), cc.xy(columnCounter, 1));
columnCounter += 2;
}
buttonPanel.add(getRefreshCommand().createButton(), cc.xy(columnCounter, 1));
columnCounter += 2;
buttonPanel.add(new JSeparator(SwingConstants.VERTICAL), cc.xy(columnCounter, 1));
columnCounter += 2;
textFilterField = getTableWidget().getTextFilterField();
if (textFilterField != null)
{
textFilterField.setText(searchString);
textFilterField.setPreferredSize(new Dimension(50, 20));
buttonPanel.add(textFilterField, cc.xy(columnCounter, 1));
columnCounter += 2;
}
if (isFilterSupported() && tableFilterControlCommands.size() > 0)
{
buttonPanel.add(new JSeparator(SwingConstants.VERTICAL), cc.xy(columnCounter, 1));
columnCounter += 2;
buttonPanel.add(tableFilterControlCommands.createButtonBar(new ColumnSpec("fill:pref:nogrow"),
new RowSpec("fill:default:nogrow"), null), cc.xy(columnCounter, 1));
}
return buttonPanel;
}
protected CommandGroup getTableFilterControlCommands()
{
CommandGroup group = new CommandGroup();
group.add(getToggleFilterCommand());
group.add(getClearFilterCommand());
return group;
}
private ColumnSpec[] getTableColumnSpecs(boolean addFilterCommands)
{
java.util.List<ColumnSpec> columnSpecs = new ArrayList<ColumnSpec>();
if (isAddRowSupported())
{
columnSpecs.add(FILL_NOGROW_COLUMN_SPEC);// add btn
columnSpecs.add(FormFactory.RELATED_GAP_COLSPEC); // gap
}
if (isRemoveRowsSupported())
{
columnSpecs.add(FILL_NOGROW_COLUMN_SPEC); // remove btn
columnSpecs.add(FormFactory.RELATED_GAP_COLSPEC); // gap
}
if (isAddRowSupported() || isRemoveRowsSupported())
{
columnSpecs.add(FormFactory.DEFAULT_COLSPEC); // separator //
// --------------------
columnSpecs.add(FormFactory.RELATED_GAP_COLSPEC); // gap
}
columnSpecs.add(FILL_NOGROW_COLUMN_SPEC); // refresh btn
columnSpecs.add(FormFactory.RELATED_GAP_COLSPEC); // gap
columnSpecs.add(FormFactory.DEFAULT_COLSPEC); // separator
// --------------------
columnSpecs.add(FormFactory.RELATED_GAP_COLSPEC); // gap
columnSpecs.add(FILL_COLUMN_SPEC); // glue space |>> glazed-list-text
// filter
if (addFilterCommands)
{
columnSpecs.add(FormFactory.RELATED_GAP_COLSPEC); // gap
columnSpecs.add(FormFactory.DEFAULT_COLSPEC); // separator
// --------------------
columnSpecs.add(FormFactory.RELATED_GAP_COLSPEC); // gap
columnSpecs.add(FILL_NOGROW_COLUMN_SPEC); // filter commands panel
}
return columnSpecs.toArray(new ColumnSpec[]{});
}
private final class TableMouseListener extends MouseAdapter
{
private final JTable table;
private final JPopupMenu menu;
private TableMouseListener(JTable table, JPopupMenu menu)
{
this.table = table;
this.menu = menu;
}
private void handlePopupTrigger(MouseEvent e)
{
if (e.isPopupTrigger())
{
Point p = e.getPoint();
int row = this.table.rowAtPoint(p);
int column = this.table.columnAtPoint(p);
// The autoscroller can generate drag events outside the
// Table's range.
if (!this.table.isRowSelected(row) && (column != -1))
{
this.table.changeSelection(row, column, e.isControlDown(), e.isShiftDown());
}
this.menu.show(e.getComponent(), e.getX(), e.getY());
}
}
@Override
public void mousePressed(MouseEvent e)
{
handlePopupTrigger(e);
}
@Override
public void mouseReleased(MouseEvent e)
{
handlePopupTrigger(e);
}
@Override
public void mouseClicked(MouseEvent e)
{
if (e.getClickCount() == 2 && SwingUtilities.isLeftMouseButton(e))
{
if (selectionCommand != null)
{
selectionCommand.execute();
}
else if (isUpdateRowSupported())
{
getEditRowCommand().execute();
}
}
}
}
class RowObjectReplacer implements Runnable
{
private final Object oldObject;
private final Object newObject;
public RowObjectReplacer(final Object oldObject, final Object newObject)
{
this.oldObject = oldObject;
this.newObject = newObject;
}
public void run()
{
getTableWidget().replaceRowObject(oldObject, newObject, tableSelectionObserver);
}
}
protected void onRowSelection(Object rowObject)
{
if (rowObject instanceof Object[])
{
getEditRowCommand().setEnabled(false);
getCloneRowCommand().setEnabled(false);
}
else
{
getEditRowCommand().setEnabled(true && isUpdateRowSupported());
getCloneRowCommand().setEnabled(true && isCloneRowSupported());
}
}
protected void newRow(Object newClone)
{
getTableWidget().unSelectAll();
selectedRowObject = null;
AbstractForm detailForm = getDetailForm();
if (detailForm instanceof NewFormObjectAware)
{
((NewFormObjectAware) detailForm).setNewFormObject(newClone);
}
else
{
detailForm.setFormObject(newClone);
}
if (saveUpdateSwitcher != null)
{
DefaultButtonFocusListener.setDefaultButton(detailForm.getControl(), getCreateCommand());
saveUpdateSwitcher.show(saveUpdatePanel, CREATE_COMMAND_ID);
}
if (detailForm instanceof Focussable)
{
((Focussable) detailForm).grabFocus();
}
}
protected void removeRows()
{
Object[] selectedRows = getTableWidget().getSelectedRows();
if (selectedRows.length == 0)
{
return;
}
int answer = getApplicationConfig().dialogFactory().showWarningDialog(getComponent(), REMOVE_CONFIRMATION_ID,
new Object[]{Integer.valueOf(selectedRows.length)}, JOptionPane.YES_NO_OPTION);
int nextSelectionIndex = getTableWidget().getTable().getSelectionModel().getMinSelectionIndex();
for (int i = 0; i < selectedRows.length && (answer == JOptionPane.YES_OPTION); i++)
{
Object objectToRemove = selectedRows[i];
try
{
removeEntity(objectToRemove);
}
catch (RuntimeException e)
{
log.error("Error removing row in DataEditor of type " + this.getClass().getName(), e);
getApplicationConfig().registerableExceptionHandler().uncaughtException(Thread.currentThread(), e);
int remaining = selectedRows.length - i - 1;
if (remaining > 0)
{
String ttl = getApplicationConfig().messageResolver()
.getMessage(getId(), REMOVE_CONTINUE_AFTER_ERROR, MessageConstants.TITLE);
String errMsg = getApplicationConfig().messageResolver().getMessage(getId(), REMOVE_CONTINUE_AFTER_ERROR,
MessageConstants.TEXT, new Object[]{Integer.valueOf(remaining)});
answer = JOptionPane.showConfirmDialog(getComponent(), errMsg, ttl,
JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE);
}
}
}
int nrOfRows = getTableWidget().nrOfRows();
if (nrOfRows > 0 && (getTableWidget().getSelectedRows().length == 0))
{
if (nextSelectionIndex >= nrOfRows)
{
nextSelectionIndex = nrOfRows - 1;
}
getTableWidget().selectRowObject(nextSelectionIndex, null);
}
}
public AbstractCommand getToggleDetailCommand()
{
return toggleDetailCommand;
}
public SplitPaneExpansionToggleCommand getToggleFilterCommand()
{
return toggleFilterCommand;
}
protected ActionCommand getAddRowCommand()
{
if (this.addRowCommand == null)
{
this.addRowCommand = createAddRowCommand();
}
return this.addRowCommand;
}
protected ActionCommand createAddRowCommand()
{
ActionCommand addRowCommand = new ActionCommand("addrow")
{
@Override
protected void doExecuteCommand()
{
if (canClose())
{
newRow(null);
toggleDetailCommand.doShow();
}
}
};
addRowCommand.setSecurityControllerId(getId() + ".addrow");
commandConfigurer.configure(addRowCommand);
addRowCommand.setEnabled(isAddRowSupported());
return addRowCommand;
}
protected ActionCommand getEditRowCommand()
{
if (this.editRowCommand == null)
{
this.editRowCommand = createEditRowCommand();
}
return this.editRowCommand;
}
protected ActionCommand createEditRowCommand()
{
ActionCommand editRow = new ActionCommand("editrow")
{
@Override
protected void doExecuteCommand()
{
toggleDetailCommand.doShow();
if (getDetailForm() instanceof Focussable)
{
((Focussable) getDetailForm()).grabFocus();
}
}
};
editRow.setSecurityControllerId(getId() + ".editrow");
commandConfigurer.configure(editRow);
editRow.setEnabled(isUpdateRowSupported());
return editRow;
}
protected ActionCommand getCloneRowCommand()
{
if (this.cloneRowCommand == null)
{
this.cloneRowCommand = createCloneRowCommand();
}
return this.cloneRowCommand;
}
protected ActionCommand createCloneRowCommand()
{
ActionCommand cloneRow = new ActionCommand("clonerow")
{
@Override
protected void doExecuteCommand()
{
Object newClone = cloneEntity(selectedRowObject);
newRow(newClone);
toggleDetailCommand.doShow();
}
};
cloneRow.setSecurityControllerId(getId() + ".clonerow");
commandConfigurer.configure(cloneRow);
cloneRow.setEnabled(isCloneRowSupported());
return cloneRow;
}
protected ActionCommand getRemoveRowsCommand()
{
if (this.removeRowsCommand == null)
{
this.removeRowsCommand = createRemoveRowCommand();
}
return this.removeRowsCommand;
}
protected ActionCommand createRemoveRowCommand()
{
ActionCommand removeRowCommand = new ActionCommand("removerow")
{
@Override
protected void doExecuteCommand()
{
removeRows();
}
};
removeRowCommand.setSecurityControllerId(getId() + ".removerow");
commandConfigurer.configure(removeRowCommand);
removeRowCommand.setEnabled(isRemoveRowsSupported());
return removeRowCommand;
}
public ActionCommand getRefreshCommand()
{
if (this.refreshCommand == null)
{
this.refreshCommand = createRefreshCommand();
}
return this.refreshCommand;
}
public ActionCommand getCopySelectedRowsToClipboardCommand()
{
if (this.copySelectedRowsCommand == null)
{
this.copySelectedRowsCommand = createCopySelectedRowsToClipboardCommand();
}
return this.copySelectedRowsCommand;
}
private ActionCommand createCopySelectedRowsToClipboardCommand()
{
ActionCommand command = new ActionCommand("copyToClipboard")
{
@Override
protected void doExecuteCommand()
{
String clipboardContent = createTabDelimitedSelectedRowsContent();
StringSelection selection = new StringSelection(clipboardContent);
Toolkit.getDefaultToolkit().getSystemClipboard().setContents(selection, null);
}
private String createTabDelimitedSelectedRowsContent()
{
java.util.List<java.util.List<String>> formattedRowList = new ArrayList<java.util.List<String>>();
JXTable jxTable = (JXTable) getTableWidget().getTable();
java.util.List<String> headerList = new ArrayList<String>();
for (TableColumn tableColumn : jxTable.getColumns())
{
Object headerValue = tableColumn.getHeaderValue();
headerList.add(headerValue == null ? "" : headerValue.toString());
}
formattedRowList.add(headerList);
for (int rowIndex : jxTable.getSelectedRows())
{
java.util.List<String> columnList = new ArrayList<String>();
for (TableColumn tableColumn : jxTable.getColumns())
{
Object unformattedValue = jxTable.getModel().getValueAt(rowIndex,
tableColumn.getModelIndex());
int columnViewIndex = jxTable.convertColumnIndexToView(tableColumn.getModelIndex());
TableCellRenderer renderer = jxTable.getCellRenderer(rowIndex, columnViewIndex);
Component component = renderer.getTableCellRendererComponent(jxTable,
unformattedValue, false, false, rowIndex, columnViewIndex);
columnList.add(getFormattedValue(component));
}
formattedRowList.add(columnList);
}
StringBuilder builder = new StringBuilder(formattedRowList.size() * 200);
for (java.util.List<String> row : formattedRowList)
{
builder.append(Joiner.on("\t").join(row.iterator()) + "\n");
}
return builder.toString();
}
private String getFormattedValue(Component component)
{
if (component instanceof JLabel)
{
return ((JLabel) component).getText();
}
else if (component instanceof JTextComponent)
{
return ((JTextComponent) component).getText();
}
else if (component instanceof JToggleButton)
{
JToggleButton button = (JToggleButton) component;
return getApplicationConfig().messageResolver().getMessage("boolean.yesno." + button.isSelected());
}
else
{
return component.toString();
}
}
};
return command;
}
protected ActionCommand createRefreshCommand()
{
return makeExecuteFilterCommand("refresh", false);
}
public ActionCommand getClearFilterCommand()
{
if (this.clearFilterCommand == null)
{
this.clearFilterCommand = createClearFilterCommand();
}
return this.clearFilterCommand;
}
protected ActionCommand createClearFilterCommand()
{
ActionCommand newCommand = makeClearFilterCommand("clearfilter", true);
newCommand.setEnabled(isFilterSupported());
return newCommand;
}
private ActionCommand makeClearFilterCommand(String id, final boolean refreshAndHideAfterClear)
{
ActionCommand newCommand = new ActionCommand(id)
{
@Override
protected void doExecuteCommand()
{
getFilterForm().resetCriteria();
if (refreshAndHideAfterClear)
{
executeFilter();
AbstractDataEditorWidget.this.toggleFilterCommand.doHide();
}
}
};
commandConfigurer.configure(newCommand);
return newCommand;
}
public ActionCommand getExecuteFilterCommand()
{
if (this.executeFilterCommand == null)
{
this.executeFilterCommand = createExecuteFilterCommand();
}
return this.executeFilterCommand;
}
protected ActionCommand createExecuteFilterCommand()
{
ActionCommand newCommand = makeExecuteFilterCommand("executefilter", true);
newCommand.setEnabled(isFilterSupported());
return newCommand;
}
private ActionCommand makeExecuteFilterCommand(String id, final boolean commitFilter)
{
ActionCommand newCommand = new ActionCommand(id)
{
@Override
protected void doExecuteCommand()
{
if (textFilterField != null)
{
textFilterField.setText("");
}
if (commitFilter && isFilterSupported())
{
getFilterForm().commit();
}
executeFilter();
}
};
if (isFilterSupported())
{
new FormGuard(getFilterForm().getFormModel(), newCommand, FormGuard.ON_NOERRORS);
}
commandConfigurer.configure(newCommand);
return newCommand;
}
protected ActionCommand getEmptyFilterCommand()
{
if (this.emptyFilterCommand == null)
{
this.emptyFilterCommand = createEmptyFilterCommand();
}
return this.emptyFilterCommand;
}
protected ActionCommand createEmptyFilterCommand()
{
ActionCommand newCommand = makeClearFilterCommand("emptyfilter", false);
newCommand.setEnabled(isFilterSupported());
return newCommand;
}
protected AbstractCommand getHelpCommand()
{
return getApplicationConfig().commandManager().createDummyCommand("help", "Behulpzaam");
}
private AbstractCommand getCloseCommand()
{
return getApplicationConfig().commandManager().createDummyCommand("exit", "deuren toe.");
}
protected Object[] getFilterCriteria()
{
Object[] criteria;
criteria = new Object[1];
criteria[0] = getFilterForm().getFilterCriteria();
return criteria;
}
protected ActionCommand getSelectCommand()
{
return getApplicationConfig().commandManager().createDummyCommand("select", "Chosen!");
}
protected AbstractCommand[] getControlCommands()
{
if (isSelectMode())
{
return new AbstractCommand[]{getSelectCommand(), // select
getCloseCommand()
// close
};
}
return new AbstractCommand[]{
getCloseCommand()
// close
};
}
protected abstract boolean isFilterSupported();
protected abstract boolean isUpdateRowSupported();
protected abstract boolean isAddRowSupported();
protected abstract boolean isCloneRowSupported();
protected abstract boolean isRemoveRowsSupported();
protected abstract FilterForm getFilterForm();
public abstract AbstractForm getDetailForm();
public abstract Widget createDetailWidget();
public abstract TableWidget getTableWidget();
protected abstract void executeFilter();
public abstract void executeFilter(Map<String, Object> parameters);
protected final Object loadEntityDetails(Object baseObject)
{
return loadEntityDetails(baseObject, false);
}
/**
* Fetch the detailed object from the back-end. If the baseObject is already detailed, the baseObject can
* be returned directly if and only if no forceLoad is requested. This logic is also apparent in the
* {@link org.valkyriercp.widget.editor.provider.DataProvider} class.
*
* @param baseObject object containing enough information to fetch a detailed version.
* @param forceLoad if <code>true</code> always load the detailed object from the back-end, if
* <code>false</code> a shortcut can be implemented by returning the baseObject directly.
* @return the detailed object
*/
protected abstract Object loadEntityDetails(Object baseObject, boolean forceLoad);
protected abstract Object saveEntity(Object committedObject);
protected abstract Object createNewEntity(Object committedObject);
protected abstract Object cloneEntity(Object sampleObject);
protected abstract void removeEntity(Object objectToRemove);
@Override
public boolean canClose()
{
boolean userBreak = false;
int answer = JOptionPane.NO_OPTION;
FormModel detailFormModel = getDetailForm().getFormModel();
if(detailFormModel.isDirty() && !(getCreateCommand().isAuthorized() && saveUpdatePanel.getComponents()[0].isVisible()) && !(getUpdateCommand().isAuthorized() && saveUpdatePanel.getComponents()[1].isVisible())) {
detailFormModel.revert();
}
if (detailFormModel.isEnabled() && detailFormModel.isDirty())
{
if (detailFormModel.isCommittable())
{
answer = getApplicationConfig().dialogFactory().showWarningDialog(getComponent(), UNSAVEDCHANGES_WARNING_ID,
JOptionPane.YES_NO_CANCEL_OPTION);
}
else // form is uncomittable, change it or revert it
{
answer = getApplicationConfig().dialogFactory().showWarningDialog(getComponent(),
UNSAVEDCHANGES_UNCOMMITTABLE_WARNING_ID, JOptionPane.YES_NO_OPTION);
// the following might seem strange, but it aligns the answer with the other part of this if construction
// if we said 'yes keep editing': don't discard changes, continue editing to save it later on == CANCEL in previous if
// if we said 'no discard changes': discard changed and switch to other row == NO in previous if
answer = answer == JOptionPane.YES_OPTION ? JOptionPane.CANCEL_OPTION : JOptionPane.NO_OPTION;
}
switch (answer)
{
case JOptionPane.CANCEL_OPTION:
userBreak = true;
break;
case JOptionPane.YES_OPTION:
getCommitCommand().execute();
break;
case JOptionPane.NO_OPTION:
detailFormModel.revert();
break;
}
}
return !userBreak;
}
private boolean setDetailFormObject(Object rowObject, Observer reportingObserver, boolean forceLoad)
{
// quick check to avoid multiple runs
if (!forceLoad && (rowObject == selectedRowObject))
{
return true;
}
if (saveUpdateSwitcher != null)
{
DefaultButtonFocusListener.setDefaultButton(getDetailForm().getControl(), getUpdateCommand());
saveUpdateSwitcher.show(saveUpdatePanel, UPDATE_COMMAND_ID);
}
// check on current detailObject, if user isn't ready to switch, select
// previous object again
if (!canClose())
{
getTableWidget().selectRowObject(selectedRowObject, reportingObserver);
return false;
}
boolean success = true;
// nothing is stopping us from setting the newly selected object
if (rowObject != null)
{
Object detailedObject = loadEntityDetails(rowObject, forceLoad);
// if null, remove from list, set rowObject null and fall through (exception will be displayed)
if (detailedObject == null)
{
getTableWidget().removeRowObject(rowObject);
rowObject = null;
success = false;
}
else if (detailedObject != rowObject)
{
replaceRowObject(rowObject, detailedObject);
rowObject = detailedObject;
}
}
getDetailForm().setFormObject(rowObject);
selectedRowObject = rowObject;
onRowSelection(rowObject);
return success;
}
protected void replaceRowObject(Object oldRowObject, Object newRowObject)
{
EventQueue.invokeLater(new RowObjectReplacer(oldRowObject, newRowObject));
}
public final void setDataProviderEventSources(java.util.List dataProviderEventSources)
{
if (this.dataProviderSources == null)
{
this.dataProviderSources = new HashMap();
}
for (Iterator sourceIter = dataProviderEventSources.iterator(); sourceIter.hasNext();)
{
DataProviderEventSource source = (DataProviderEventSource) sourceIter.next();
this.dataProviderSources.put(source.getClass(), source);
}
}
public final void addDataProviderListener(Class dataProviderEventSource, DataProviderListener listener)
{
Object source = this.dataProviderSources.get(dataProviderEventSource);
if (source != null)
{
((DataProviderEventSource) source).addDataProviderListener(listener);
}
}
public final void removeDataProviderListener(Class dataProviderEventSource, DataProviderListener listener)
{
Object source = this.dataProviderSources.get(dataProviderEventSource);
if (source != null)
{
((DataProviderEventSource) source).removeDataProviderListener(listener);
}
}
/**
* @inheritDoc
*/
public Object getSelection()
{
return getTableWidget().getSelectedRows();
}
/**
* @inheritDoc
*/
public void setSelectionCommand(ActionCommand command)
{
setSelectMode(AbstractDataEditorWidget.ON);
selectionCommand = command;
enableSelectButton(getSelection());
}
public void setMultipleSelectionInSelectMode(boolean multipleSelection)
{
multipleSelectionInSelectMode = multipleSelection;
}
/**
* @inheritDoc
*/
public void removeSelectionCommand()
{
selectionCommand = null;
setSelectMode(AbstractDataEditorWidget.OFF);
}
private void enableSelectButton(Object selection)
{
if (selectionCommand != null)
{
if (selection == null)
{
selectionCommand.setEnabled(false);
}
else if (multipleSelectionInSelectMode == ON)
{
selectionCommand.setEnabled(true);
}
else
{
selectionCommand.setEnabled(!(selection instanceof Object[])
|| (((Object[]) selection).length == 1));
}
}
}
private class ListSelectionObserver implements Observer
{
public void update(Observable o, Object rowObject)
{
AbstractDataEditorWidget.this.setDetailFormObject(rowObject instanceof Object[]
? null
: rowObject, tableSelectionObserver, false);
enableSelectButton(rowObject);
}
}
}