/**
* 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.form.binding.swing.editor;
import com.google.common.base.Function;
import net.miginfocom.swing.MigLayout;
import org.jdesktop.swingx.JXPanel;
import org.springframework.util.Assert;
import org.valkyriercp.binding.form.FormModel;
import org.valkyriercp.command.support.ActionCommand;
import org.valkyriercp.dialog.ApplicationDialog;
import org.valkyriercp.form.HasValidationComponent;
import org.valkyriercp.form.binding.support.CustomBinding;
import org.valkyriercp.text.SelectAllFocusListener;
import org.valkyriercp.util.HasInnerComponent;
import org.valkyriercp.widget.TitledWidgetApplicationDialog;
import org.valkyriercp.widget.editor.AbstractDataEditorWidget;
import org.valkyriercp.widget.editor.DataEditorWidgetViewCommand;
import org.valkyriercp.widget.editor.DefaultDataEditorWidget;
import javax.annotation.PostConstruct;
import javax.swing.*;
import javax.swing.text.JTextComponent;
import java.awt.*;
import java.awt.event.*;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.beans.PropertyVetoException;
import java.beans.VetoableChangeListener;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
public class LookupBinding<T> extends CustomBinding {
public static final String ON_ABOUT_TO_CHANGE = "on-about-to-change";
/**
* Parameter used to pass to the dataEditorCommand in order to skip initialization of the dataEditor.
*/
public static final String NO_INITIALIZE_DATA_EDITOR = "no-initialize-dataeditor";
/** The following are masks that trigger the dataEditor pop-up. */
/**
* Pop-up dataEditor when unique match is found.
*/
public static final int AUTOPOPUPDIALOG_UNIQUE_MATCH = 1;
/**
* Pop-up dataEditor when no match is found.
*/
public static final int AUTOPOPUPDIALOG_NO_MATCH = 1 << 1;
/**
* Pop-up dataEditor when multiple matches are found.
*/
public static final int AUTOPOPUPDIALOG_MULTIPLE_MATCH = 1 << 2;
/**
* Always pop-up.
*/
public static final int AUTOPOPUPDIALOG_ALWAYS = AUTOPOPUPDIALOG_UNIQUE_MATCH + AUTOPOPUPDIALOG_NO_MATCH
+ AUTOPOPUPDIALOG_MULTIPLE_MATCH;
/**
* Pop-up if no unique match is found. This is considered to be the sensible default.
*/
public static final int AUTOPOPUPDIALOG_NO_UNIQUE_MATCH = AUTOPOPUPDIALOG_NO_MATCH
+ AUTOPOPUPDIALOG_MULTIPLE_MATCH;
/**
* Internal mask used to determine the pop-up behavior.
*/
private int autoPopupDialog = AUTOPOPUPDIALOG_NO_UNIQUE_MATCH;
/**
* Standard on option to use with parameters.
*/
public static final Boolean ON = Boolean.TRUE;
/**
* Standard off option to use with parameters.
*/
public static final Boolean OFF = Boolean.FALSE;
/**
* Default id to configure the dialog.
*/
public static final String DEFAULT_SELECTDIALOG_ID = "foreignKeySelectDialog";
/**
* Default id to configure the command.
*/
public static final String DEFAULT_SELECTDIALOG_COMMAND_ID = "foreignKeyPropertyEditorCommand";
/**
* DataEditor that will be used to find matches.
*/
private final DefaultDataEditorWidget dataEditor;
/**
* Should changes be reverted when focus is lost and no value was selected?
*/
private boolean revertValueOnFocusLost = true;
/**
* Id to configure dialog.
*/
private String selectDialogId = DEFAULT_SELECTDIALOG_ID;
/**
* Id to configure the dialog command.
*/
private String selectDialogCommandId = DEFAULT_SELECTDIALOG_COMMAND_ID;
/**
* Map of parameters to pass to the command, configuring its behavior.
*/
private final Map<Object, Object> parameters;
/**
* The button to access the dataEditor dialog.
*/
private AbstractButton dataEditorButton;
/**
* The command to access the dataEditor dialog.
*/
private ActionCommand dataEditorCommand;
/**
* The textComponent for this referable.
*/
private JComponent keyField;
private PropertyChangeMonitor propertyChangeMonitor = new PropertyChangeMonitor();
/**
* Id to configure the view command.
*/
private String dataEditorViewCommandId;
/**
* Enable the command that is used to switch to the dataEditor view of this referable.
*/
private boolean enableViewCommand = false;
/**
* The command that allows to switch to the dataEditor view of this referable.
*/
private ReferableDataEditorViewCommand referableDataEditorViewCommand;
private boolean loadDetailedObject = false;
private Object filter;
private Function<T, String> objectLabelFunction;
private Function<String, ? extends Object> createFilterFromFieldFunction;
private Dimension dialogSize;
private LookupBindingComponent editor;
public LookupBinding(DefaultDataEditorWidget dataEditor, FormModel formModel, String formPropertyPath, Class<?> requiredClass) {
super(formModel, formPropertyPath, requiredClass);
this.dataEditor = dataEditor;
// a parameter hashMap with a key to not initialize the dataEditor anymore
// this will prevent initialize from being called twice: when losing focus (searching for
// matches) and when auto-pop-up is on.
this.parameters = new HashMap<Object, Object>();
this.parameters.put(NO_INITIALIZE_DATA_EDITOR, ON);
referableDataEditorViewCommand = new ReferableDataEditorViewCommand();
formModel.getValueModel(formPropertyPath).addValueChangeListener(new PropertyChangeListener() {
public void propertyChange(PropertyChangeEvent evt) {
referableDataEditorViewCommand.setEnabled(evt.getNewValue() != null);
if (evt.getNewValue() != null)
referableDataEditorViewCommand.setSelectedObject(evt.getNewValue());
}
});
getApplicationConfig().commandConfigurer().configure(referableDataEditorViewCommand);
}
public Function<T, String> getObjectLabelFunction() {
return objectLabelFunction;
}
public void setObjectLabelFunction(Function<T, String> objectLabelFunction) {
this.objectLabelFunction = objectLabelFunction;
}
public Function<String, ? extends Object> getCreateFilterFromFieldFunction() {
return createFilterFromFieldFunction;
}
public void setCreateFilterFromFieldFunction(Function<String, ? extends Object> createFilterFromFieldFunction) {
this.createFilterFromFieldFunction = createFilterFromFieldFunction;
}
/**
* Returns the parameter map that is passed to the dataEditor command. This allows for eg turning
* initialization of the dataEditor on/off.
*/
protected Map<Object, Object> getParameters() {
return parameters;
}
/**
* Returns the id for the TitledApplicationDialog that shows up when pressing the button.
*/
protected String getSelectDialogId() {
return this.selectDialogId;
}
/**
* Set the id for the TitledApplicationDialog that shows up when pressing the button.
*/
public void setSelectDialogId(String selectDialogId) {
this.selectDialogId = selectDialogId;
}
/**
* Returns the id for the command that shows the dialog.
*/
protected String getSelectDialogCommandId() {
return this.selectDialogCommandId;
}
/**
* Set the id for the command that shows the dialog.
*/
public void setSelectDialogCommandId(String selectDialogCommandId) {
this.selectDialogCommandId = selectDialogCommandId;
}
/**
* Returns the mask defining the behavior of the pop-up.
*/
protected int getAutoPopupDialog() {
return this.autoPopupDialog;
}
/**
* Set the mask defining the pop-up behavior.
*
* @see #AUTOPOPUPDIALOG_ALWAYS
* @see #AUTOPOPUPDIALOG_UNIQUE_MATCH
* @see #AUTOPOPUPDIALOG_MULTIPLE_MATCH
* @see #AUTOPOPUPDIALOG_NO_MATCH
* @see #AUTOPOPUPDIALOG_NO_UNIQUE_MATCH
*/
public void setAutoPopupdialog(int autoPopupDialog) {
this.autoPopupDialog = autoPopupDialog;
}
/**
* Return <code>true</code> if the value should be reverted when focus is lost and no value is selected.
*/
protected boolean revertValueOnFocusLost() {
return this.revertValueOnFocusLost;
}
/**
* Set to <code>true</code> if the value should be reverted when focus is lost and no value is selected.
*/
public void setRevertValueOnFocusLost(boolean revertValueOnFocusLost) {
this.revertValueOnFocusLost = revertValueOnFocusLost;
}
/**
* {@inheritDoc}
* <p/>
* Sets the textComponent to reflect the label of the object
*/
@Override
protected void valueModelChanged(Object newValue) {
if (newValue == null)
setKeyComponentText(null);
else
setKeyComponentText(getObjectLabel(newValue));
readOnlyChanged();
}
public String getObjectLabel(Object o) {
return getObjectLabelFunction().apply((T) o);
}
@Override
protected JComponent doBindControl() {
MigLayout layout = new MigLayout("fill, insets 0");
editor = new LookupBindingComponent(layout, this);
editor.setKeyComponent(getKeyComponent());
editor.setDataEditorButton(getDataEditorButton());
editor.add(editor.getKeyComponent(), "push,grow");
editor.add(editor.getDataEditorButton(), "w 40px!");
if (isEnableViewCommand()) {
AbstractButton viewButton = referableDataEditorViewCommand.createButton();
viewButton.setFocusable(false);
editor.setViewButton(viewButton);
editor.add(editor.getViewButton(), "w 40px!");
}
editor.setFocusable(true);
valueModelChanged(getValue());
return editor;
}
/**
* Returns a JTextComponent to display the key, creates it if necessary.
*
* @deprecated use {@link #getKeyComponent()} instead or to access the text directly, use
* {@link #getKeyComponentText()} and {@link #setKeyComponentText(String)}.
*/
@Deprecated
protected JTextComponent getOrCreateKeyTextComponent() {
return (JTextComponent) getKeyComponent();
}
protected JComponent getKeyComponent() {
if (keyField == null)
keyField = createKeyComponent();
return keyField;
}
protected JComponent createKeyComponent() {
JTextField textField = new JTextField();
// Focustraversal keys moeten afgezet worden, anders wordt de keylistener niet getriggered.
textField.setFocusTraversalKeys(KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS,
new HashSet<AWTKeyStroke>());
textField.addKeyListener(createKeyListener());
textField.addFocusListener(new SelectAllFocusListener(textField));
textField.addFocusListener(createFocusListener());
return textField;
}
/**
* Return the text that is shown on the keyComponent and that should be used to lookup the referable. Most
* commonly this string contains the label of the referable.
*/
protected String getKeyComponentText() {
if (getKeyComponent() instanceof JTextComponent)
return ((JTextComponent) getKeyComponent()).getText();
return "";
}
/**
* Set the text of the referable label on the key component (normally the textField).
*/
protected void setKeyComponentText(String text) {
if (getKeyComponent() instanceof JTextComponent)
((JTextComponent) getKeyComponent()).setText(text);
}
/**
* Create a keyListener that reacts on tabs. Pop-up the dialog as defined in the
* {@link #getAutoPopupDialog()} mask.
*/
protected TabKeyListener createKeyListener() {
return new TabKeyListener() {
@Override
public void onTabKey(Component component) {
String textFieldValue = getKeyComponentText();
boolean empty = "".equals(textFieldValue.trim());
Object ref = LookupBinding.this.getValue();
// if something was filled in and it doesn't match the internal value
if (!empty && ((ref == null) || !textFieldValue.equals(getObjectLabel(ref)))) {
// call the dataEditor to fire the search
Object result = initializeDataEditor();
//no match
if (result == null || ((result instanceof java.util.List) && (((java.util.List<?>) result).size() == 0))) {
if (!revertValueOnFocusLost())
getValueModel().setValue(createFilterFromString(textFieldValue));
if ((getAutoPopupDialog() & AUTOPOPUPDIALOG_NO_MATCH) == AUTOPOPUPDIALOG_NO_MATCH)
getDataEditorCommand().execute(parameters);
}
// multiple matches
else if ((result instanceof java.util.List) && (((java.util.List<?>) result).size() > 1)) {
if (!revertValueOnFocusLost())
getValueModel().setValue(createFilterFromString(textFieldValue));
if ((getAutoPopupDialog() & AUTOPOPUPDIALOG_MULTIPLE_MATCH) == AUTOPOPUPDIALOG_MULTIPLE_MATCH)
getDataEditorCommand().execute(parameters);
}
// exact match
else {
// in dit geval krijg je een object uit de lijst terug, dit is niet gedetaileerd,
// daarom moet het eventueel gedetaileerd geladen worden.
setValue(result, true);
if ((getAutoPopupDialog() & AUTOPOPUPDIALOG_UNIQUE_MATCH) == AUTOPOPUPDIALOG_UNIQUE_MATCH)
getDataEditorCommand().execute(parameters);
}
}
// nothing filled in, underlying value isn't empty and we should not revert, set null
else if (!revertValueOnFocusLost() && empty && ref != null) {
getValueModel().setValue(null);
}
getDataEditorButton().transferFocus();
}
};
}
/**
* Create a focus listener to attach to the textComponent and dataEditorButton that will decide what
* happens with the changed value. Here a revert can be done if no value is selected or a new value can be
* created as needed.
*
* @return a focus listener.
*/
protected FocusListener createFocusListener() {
return new FocusAdapter() {
@Override
public void focusLost(FocusEvent e) {
String textFieldValue = getKeyComponentText();
boolean empty = "".equals(textFieldValue.trim());
Object ref = LookupBinding.this.getValue();
if (evaluateFocusLost(e)) {
// Revert if value isn't empty
if (revertValueOnFocusLost()) {
if (empty)
getValueModel().setValue(null);
else
valueModelChanged(LookupBinding.super.getValue());
}
// Create new referable if value isn't empty
else {
if (empty && (ref != null))
getValueModel().setValue(null);
else if (!empty && ((ref == null) || !textFieldValue.equals(getObjectLabel(ref))))
getValueModel().setValue(createFilterFromString(textFieldValue));
}
}
}
};
}
protected boolean evaluateFocusLost(FocusEvent e) {
Component oppositeComponent = e.getOppositeComponent();
return (oppositeComponent != getDataEditorButton()) && (oppositeComponent != getKeyComponent());
}
@Override
protected void readOnlyChanged() {
if (getKeyComponent() instanceof JTextComponent)
((JTextComponent) getKeyComponent()).setEditable(isEnabled() && !isReadOnly());
getDataEditorCommand().setEnabled(isEnabled() && !isReadOnly());
}
@Override
protected void enabledChanged() {
getKeyComponent().setEnabled(isEnabled());
readOnlyChanged();
}
/**
* Initialize the dataEditor by passing the search referable as search parameter.
*
* @return a single object if the search has an unique match, a list if multiple matches occurred or
* <code>null</code> if nothing was found.
*/
protected Object initializeDataEditor() {
final String textFieldValue = getKeyComponentText();
Object ref = super.getValue();
if ((ref != null) && textFieldValue.equals(getObjectLabel(ref)))
return getDataEditor().setSelectedSearch(ref);
return getDataEditor().setSelectedSearch(createFilterFromString(textFieldValue));
}
/**
* Create an empty referable that is used to pass onto the dataEditor search method and that is used to
* set onto the valueModel if this binding is set to not revert upon yielding a <code>null</code> search
* result.
*
* @param textFieldValue the value of the textComponent.
* @return a Referable that represents the state of this binding when no real object is available.
*/
protected Object createFilterFromString(final String textFieldValue) {
return createFilterFromFieldFunction.apply(textFieldValue);
}
/**
* Get/create the button to open the dataEditor in selection mode
*/
protected AbstractButton getDataEditorButton() {
if (dataEditorButton == null) {
dataEditorButton = getDataEditorCommand().createButton();
dataEditorButton.setFocusable(false);
//dataEditorButton.addFocusListener(createFocusListener());
}
return dataEditorButton;
}
/**
* Return the dataEditorCommand.
*/
protected final ActionCommand getDataEditorCommand() {
if (dataEditorCommand == null)
dataEditorCommand = createDataEditorCommand();
return dataEditorCommand;
}
/**
* Create the dataEditorCommand.
*/
protected ActionCommand createDataEditorCommand() {
ActionCommand selectDialogCommand = new ActionCommand(getSelectDialogCommandId()) {
private ApplicationDialog dataEditorDialog;
@Override
protected void doExecuteCommand() {
if (LookupBinding.this.propertyChangeMonitor.proceedOnChange()) {
if (dataEditorDialog == null) {
dataEditorDialog = new TitledWidgetApplicationDialog(getDataEditor(),
TitledWidgetApplicationDialog.SELECT_CANCEL_MODE) {
protected boolean onFinish() {
if (getDataEditor().canClose())
return LookupBinding.this.onFinish();
return false;
}
@Override
protected boolean onSelectNone() {
getDataEditor().getTableWidget().unSelectAll();
return super.onSelectNone();
}
@Override
protected void onCancel() {
if (getDataEditor().canClose())
super.onCancel();
}
};
dataEditorDialog.setParentComponent(getDataEditorButton());
getDataEditor().setSelectMode(AbstractDataEditorWidget.ON);
getApplicationConfig().applicationObjectConfigurer().configure(dataEditorDialog, getSelectDialogId());
}
if (getParameter(NO_INITIALIZE_DATA_EDITOR) != ON)
initializeDataEditor();
if (getDialogSize() != null) {
dataEditorDialog.getDialog().setMinimumSize(getDialogSize());
}
dataEditorDialog.showDialog();
}
}
};
getApplicationConfig().commandConfigurer().configure(selectDialogCommand);
return selectDialogCommand;
}
/**
* Return the dataEditor used to select a referable.
*/
protected DefaultDataEditorWidget getDataEditor() {
return this.dataEditor;
}
/**
* When a value is selected, set it on the valueModel.
*
* @return <code>true</code> if successful.
*/
protected boolean onFinish() {
setValue(getDataEditor().getSelectedRowObject(), false);
return true;
}
private void setValue(Object value, boolean doLoadDetailedObject) {
if (value != null && !loadDetailedObject) {
value = getDataEditor().getDataProvider().getSimpleObject(value);
} else if (value != null && doLoadDetailedObject) {
value = getDataEditor().getDataProvider().getDetailObject(value, false);
}
getValueModel().setValue(value);
}
private static class PropertyChangeMonitor extends JComponent {
private static final long serialVersionUID = -5117792596024956433L;
public boolean proceedOnChange() {
boolean proceedNotVetoed = true;
try {
fireVetoableChange(ON_ABOUT_TO_CHANGE, false, true);
} catch (PropertyVetoException e) {
proceedNotVetoed = false;
}
return proceedNotVetoed;
}
}
private class ReferableDataEditorViewCommand extends ActionCommand {
private Object selectedObject;
public ReferableDataEditorViewCommand() {
super("referableDataEditorViewCommand");
}
public void setSelectedObject(Object selectedObject) {
this.selectedObject = selectedObject;
}
@Override
protected void doExecuteCommand() {
Assert.notNull(dataEditorViewCommandId);
DataEditorWidgetViewCommand command = (DataEditorWidgetViewCommand) getApplicationConfig().commandManager().getCommand(dataEditorViewCommandId);
executeViewDataEditorCommand(command, filter, selectedObject);
}
public void executeViewDataEditorCommand(DataEditorWidgetViewCommand command, Object filter,
Object defaultSelectedObject) {
org.springframework.util.Assert.notNull(command, "Command mag niet null zijn!");
Map<String, Object> dataEditorParameters = new HashMap<String, Object>(2);
dataEditorParameters.put(DefaultDataEditorWidget.PARAMETER_FILTER, filter);
dataEditorParameters.put(DefaultDataEditorWidget.PARAMETER_DEFAULT_SELECTED_OBJECT,
defaultSelectedObject);
Map<String, Object> commandParameters = new HashMap<String, Object>(1);
commandParameters.put(DefaultDataEditorWidget.PARAMETER_MAP, dataEditorParameters);
command.execute(commandParameters);
}
}
/**
* Set the id used to configure the viewCommand.
*/
public void setDataEditorViewCommandId(String dataEditorViewCommandId) {
this.dataEditorViewCommandId = dataEditorViewCommandId;
}
/**
* Returns the id used to configure the viewCommand.
*/
public String getDataEditorViewCommandId() {
return dataEditorViewCommandId;
}
/**
* Enable the viewCommand that switches the view to the dataEditor of this referable.
*/
public void setEnableViewCommand(boolean enableViewCommand) {
this.enableViewCommand = enableViewCommand;
}
/**
* Returns <code>true</code> if the viewCommand should be shown. Default value is <code>false</code>.
*/
public boolean isEnableViewCommand() {
return enableViewCommand;
}
public boolean isLoadDetailedObject() {
return loadDetailedObject;
}
public void setLoadDetailedObject(boolean loadDetailedObject) {
this.loadDetailedObject = loadDetailedObject;
}
public void setFilter(Object filter) {
this.filter = filter;
}
public Object getFilter() {
return filter;
}
/**
* Helper class to build a {@link java.awt.event.KeyListener} that reacts on tabs. Implement the
* {@link #onShiftTabKey(Component)} and/or {@link #onTabKey(Component)} methods as needed.
*
* @author Jan Hoskens
*/
protected static class TabKeyListener extends KeyAdapter {
/**
* Action to do when tab is pressed. Default behavior is to transfer focus.
*
* @param component the component which should handle the tab.
*/
public void onTabKey(Component component) {
component.transferFocus();
}
/**
* Action to do when shift-tab is pressed. Default behavior is to transfer focus.
*
* @param component the component which should handle the shift-tab.
*/
public void onShiftTabKey(Component component) {
component.transferFocusBackward();
}
@Override
public void keyTyped(KeyEvent e) {
if (e.getKeyChar() == '\t' && !e.isShiftDown()) {
onTabKey(e.getComponent());
} else if (e.getKeyChar() == '\t' && e.isShiftDown()) {
onShiftTabKey(e.getComponent());
}
}
}
public Dimension getDialogSize() {
return dialogSize;
}
public void setDialogSize(Dimension dialogSize) {
this.dialogSize = dialogSize;
}
public static class LookupBindingComponent extends JXPanel implements HasValidationComponent, HasInnerComponent {
private JComponent keyComponent;
private AbstractButton dataEditorButton;
private AbstractButton viewButton;
private LookupBinding parent;
public LookupBindingComponent(LayoutManager layout, LookupBinding parent) {
super(layout);
this.parent = parent;
}
public AbstractButton getDataEditorButton() {
return dataEditorButton;
}
public void setDataEditorButton(AbstractButton dataEditorButton) {
this.dataEditorButton = dataEditorButton;
}
public JComponent getKeyComponent() {
return keyComponent;
}
public void setKeyComponent(JComponent keyComponent) {
this.keyComponent = keyComponent;
}
public AbstractButton getViewButton() {
return viewButton;
}
public void setViewButton(AbstractButton viewButton) {
this.viewButton = viewButton;
}
@Override
public void setEnabled(boolean enabled) {
super.setEnabled(enabled);
getKeyComponent().setEnabled(enabled);
getDataEditorButton().setEnabled(enabled);
}
@Override
public JComponent getValidationComponent() {
return getKeyComponent();
}
@Override
public synchronized void addVetoableChangeListener(VetoableChangeListener listener) {
parent.propertyChangeMonitor.addVetoableChangeListener(listener);
}
@Override
public boolean requestFocusInWindow() {
return getKeyComponent().requestFocusInWindow();
}
@Override
public JComponent getInnerComponent() {
return getKeyComponent();
}
}
}