blob: 352dd2bc772c025f9c53b98b023836f39aebb275 [file] [log] [blame]
/**
*
* Copyright (c) 2011, 2016 - Loetz GmbH&Co.KG (69115 Heidelberg, Germany)
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Florian Pirchner <florian.pirchner@gmail.com> - Initial implementation
*/
package org.eclipse.osbp.vaadin.addons.suggesttext;
import java.io.Serializable;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Function;
import org.eclipse.osbp.vaadin.addons.suggesttext.client.OQueryDelegate.SuggestionResult;
import org.eclipse.osbp.vaadin.addons.suggesttext.client.SuggestTextFieldClientRpc;
import org.eclipse.osbp.vaadin.addons.suggesttext.client.SuggestTextFieldServerRpc;
import org.eclipse.osbp.vaadin.addons.suggesttext.client.SuggestTextFieldState;
import com.vaadin.data.Container;
import com.vaadin.data.Container.Filterable;
import com.vaadin.data.Item;
import com.vaadin.data.util.filter.Compare;
import com.vaadin.data.util.filter.SimpleStringFilter;
import com.vaadin.event.ShortcutAction;
import com.vaadin.external.org.slf4j.Logger;
import com.vaadin.external.org.slf4j.LoggerFactory;
import com.vaadin.ui.AbstractSingleComponentContainer;
import com.vaadin.ui.Component;
import com.vaadin.ui.TextField;
import com.vaadin.util.ReflectTools;
/**
* A field, which behaves like a textfield and offers suggestion functionality.
* <p>
* To get access to the underlying textfield, use {@link #getTextField()}. All
* functions available of {@link TextField} are also supported by this field.
* <p>
* To configure the suggestion functions, use the methods
* {@link #setSuggestionEnabled(boolean)}, {@link #setLimit(int)} and
* {@link #setPopupDelay(int)}.
* <p>
* To provide this field with queryable items, set a container using
* {@link #setContainerDataSource(Container) }(Filterable). It will use the
* given {@link #getFilterPropertyId() filterPropertyId} to query the container
* for suggestions.<br>
* The {@link #getCaptionPropertyId() captionPropertyId} is used to show results
* in the UI.<br>
* The {@link #getUniqueIdPropertyId()} is used to map the selected item
* clientside to the proper item in the container.
* <p>
* This field fires {@link SelectionChangedEvent} if a suggestion was selected
* at the clientside.
* <p>
* Additionally this field offers the functionality, that a suggestion is
* selected automatically, if only 1 record was returned by the query.
* <p>
* By default the popup can be opened pressing "ALT+KEY_DOWN". This can be
* adjusted by setters below.
*/
@SuppressWarnings("serial")
public class SuggestTextField extends AbstractSingleComponentContainer {
/** The Constant LOGGER. */
private static final Logger LOGGER = LoggerFactory.getLogger(SuggestTextField.class);
/** The lock. */
private Object lock = new Object();
/** The caption property id. */
private Object captionPropertyId;
/** The unique id property id. */
private Object uniqueIdPropertyId;
/** The filter property id. */
private Object filterPropertyId;
private String currentHandleId;
/** The rpc. */
private SuggestTextFieldServerRpc rpc = new SuggestTextFieldServerRpc() {
@Override
public void requestSuggestions(final String handleId, final String filter, final int limit) {
currentHandleId = handleId;
if (needsUUIDMapping) {
uuidmapping.clear();
}
query(handleId, filter, limit);
}
@Override
public void selectionChanged(String handleId, String id) {
if (!Objects.equals(handleId, currentHandleId)) {
return;
}
Object itemId = getItemById(id);
if (itemId != null) {
fireSelectionChanged(itemId);
}
}
@Override
public void setSuggestionEnabled(boolean value) {
SuggestTextField.this.setSuggestionEnabled(value);
}
@Override
public void openHistorizedDialog() {
SuggestTextField.this.openHistorizedDialog();
}
};
/** The container. */
private Container container;
/** The text field. */
private TextFieldCustom textField;
/** The needs uuid mapping. */
private boolean needsUUIDMapping;
/** The uuidmapping. */
private Map<String, Object> uuidmapping = new HashMap<>();
private Object currentItemId;
private Function<Object, String> captionFunction;
private ShowHistoryDialogCallback histDialogCallback;
/**
* Instantiates a new suggest text field.
*/
public SuggestTextField() {
registerRpc(rpc);
textField = createTextField();
textField.setSizeFull();
setContent(textField);
}
protected TextFieldCustom createTextField() {
return new TextFieldCustom();
}
/*
* (non-Javadoc)
*
* @see com.vaadin.ui.AbstractComponent#getState()
*/
@Override
protected SuggestTextFieldState getState() {
return (SuggestTextFieldState) super.getState();
}
/*
* (non-Javadoc)
*
* @see com.vaadin.ui.AbstractComponent#getState(boolean)
*/
@Override
protected SuggestTextFieldState getState(boolean markAsDirty) {
return (SuggestTextFieldState) super.getState(markAsDirty);
}
/**
* Gets the text field.
*
* @return the text field
*/
public TextField getTextField() {
return textField;
}
/**
* Sets the value into the text field.
*
* @param value
*/
public void setValue(String value) {
currentHandleId = null;
textField.setValue(value);
}
/**
* Sets the function to map the object (DTO) into a string representation.
*
* @param captionFunction
*/
public void setCaptionPropertyRetriever(Function<Object, String> captionFunction) {
this.captionFunction = captionFunction;
}
/**
* Sets the current itemId which is displayed.
*
* @param itemId
*/
public void setCurrentItemId(Object itemId) {
// boolean changed = currentItemId != itemId;
currentItemId = itemId;
// if (changed) {
// // null due to constructor call on init
// if (textField == null) {
// setValue("");
// return;
// }
//F
// setValue(captionFunction.apply(itemId));
// }
}
/**
* Returns the current selected itemId.
*
* @return
*/
public Object getCurrentItemId() {
return currentItemId;
}
/**
* Returns the value from the text field.
*
* @return
*/
public String getValue() {
return textField.getValue();
}
/**
* Sets the value into the text field. Also calls {@link #closePopup()} and
* {@link #openPopup()}
*
* @param value
*/
public void setKeys(String value) {
currentHandleId = null;
closePopup();
textField.setValue(value);
// TODO - update later bei RPC
setCurrentItemId(null);
if (isSuggestionEnabled()) {
openPopup();
}
}
/**
* Returns the value from the text field.
*
* @return
*/
public String getKeys() {
return textField.getValue();
}
/**
* Opens the popup if not already opened and shows the suggested values.
*/
public void openPopup() {
getRpcProxy(SuggestTextFieldClientRpc.class).openPopup();
}
/**
* Closes the popup if open. Else it has no effect.
*/
public void closePopup() {
getRpcProxy(SuggestTextFieldClientRpc.class).closePopup();
}
/**
* Navigates the selected suggestion to the next one available.
*/
public void navigateToNext() {
getRpcProxy(SuggestTextFieldClientRpc.class).navigateToNext();
}
/**
* Navigates the selected suggestion to the previous one available.
*/
public void navigateToPrevious() {
getRpcProxy(SuggestTextFieldClientRpc.class).navigateToPrevious();
}
/**
* Selects the current selected suggestion and accepts it.
*/
public void selectCurrent() {
getRpcProxy(SuggestTextFieldClientRpc.class).selectCurrent();
}
/**
* Returns the delay in ms.<br>
* The delay defines how many ms no keypress is done, before showing the
* popup.
*
* @return the popup delay
*/
public int getPopupDelay() {
return getState(false).popupDelay;
}
/**
* Sets the delay in ms. <br>
* The delay defines how many ms no keypress is done, before showing the
* popup.
*
* @param delay
* the new popup delay
*/
public void setPopupDelay(int delay) {
if (getState(false).popupDelay != delay) {
getState(true).popupDelay = delay;
}
}
/**
* Opens the historized dialog.
*/
protected void openHistorizedDialog() {
if(histDialogCallback != null) {
histDialogCallback.openHistoryDialog();
}
}
/**
* Tooltip for "disable suggestions" action.
*
* @param disableSuggestionTooltip
*/
public void setDisableSuggestionTooltip(String disableSuggestionTooltip) {
getState(true).disableSuggestionTooltip = disableSuggestionTooltip;
}
/**
* True, if the bean is historized. Allows the client to render an
* OpenHistorizedDialog button.
*
* @param historized
*/
public void setHistorized(boolean historized) {
getState(true).historized = historized;
}
/**
* Sets the historizedTooltip.
*
* @param historizedTooltip
*/
public void setHistorizedTooltip(String historizedTooltip) {
getState(true).historizedTooltip = historizedTooltip;
}
public void setShowHistoryDialogCallback(ShowHistoryDialogCallback histDialogCallback) {
this.histDialogCallback = histDialogCallback;
}
/**
* True, if suggestions should be enabled. False otherwise.
*
* @param suggestionEnabled
* the new suggestion enabled
*/
public void setSuggestionEnabled(boolean suggestionEnabled) {
if (getState(false).suggestionEnabled != suggestionEnabled) {
getState(true).suggestionEnabled = suggestionEnabled;
}
if (!suggestionEnabled && needsUUIDMapping) {
uuidmapping.clear();
}
}
/**
* Sets the number of items to be shown at clientside suggestions.
*
* @param limit
* the new limit
*/
public void setLimit(int limit) {
if (getState(false).limit != limit) {
getState(true).limit = limit;
}
}
/**
* See {@link #setSuggestionEnabled(boolean)}.
*
* @return true, if is suggestion enabled
*/
public boolean isSuggestionEnabled() {
return getState(false).suggestionEnabled;
}
/**
* See {@link #setLimit(int)}.
*
* @return the limit
*/
public int getLimit() {
return getState(false).limit;
}
/**
* See {@link #setAutoHide(boolean)}.
*
* @return the limit
*/
public boolean isAutoHide() {
return getState(false).autoHide;
}
/**
* If true, then the popup hides if the user clicks outside the popup.
*
* @param limit
* the new limit
*/
public void setAutoHide(boolean autoHide) {
if (getState(false).autoHide != autoHide) {
getState(true).autoHide = autoHide;
}
}
/**
* Sets the key which need to be pressed to open the popup.<br>
* See {@link ShortcutAction.KeyCode}
*
* @param key
* the new key
*/
public void setOpenPopupKey(int key) {
if (getState(false).openPopup_key != key) {
getState(true).openPopup_key = key;
}
}
/**
* Sets the key modifier which need to be pressed to open the popup.<br>
* See {@link ShortcutAction.ModifierKey}
*
* @param modifier
* the new modifier
*/
public void setOpenPopupKeyModifier(int modifier) {
if (modifier != ShortcutAction.ModifierKey.ALT && modifier != ShortcutAction.ModifierKey.CTRL) {
throw new IllegalArgumentException("Only ALT and CTRL are allowed as modifiers.");
}
if (getState(false).openPopup_modifier != modifier) {
getState(true).openPopup_modifier = modifier;
}
}
/**
* Returns the key which need to be pressed to open the popup.<br>
* See {@link ShortcutAction.KeyCode}
*
* @param key
* the new key
*/
public int getOpenPopupKey() {
return getState(false).openPopup_key;
}
/**
* Returns the key modifier which need to be pressed to open the popup.<br>
* See {@link ShortcutAction.ModifierKey}
*
* @param modifier
* the new modifier
*/
public int getOpenPopupKeyModifier(int modifier) {
return getState(false).openPopup_modifier;
}
/**
* Respond.
*
* @param handle
* the handle
* @param result
* the result
*/
protected void respond(String handle, List<SuggestionResult> result) {
getRpcProxy(SuggestTextFieldClientRpc.class).respond(handle, result);
}
/**
* See {@link #setFilterPropertyId(Object)}.
*
* @return the filter property id
*/
public Object getFilterPropertyId() {
return filterPropertyId;
}
/**
* Sets the filterPropertyId. This id is used to query the container for
* matching results.
*
* @param filterPropertyId
* the new filter property id
*/
public void setFilterPropertyId(Object filterPropertyId) {
this.filterPropertyId = filterPropertyId;
if (!container.getContainerPropertyIds().contains(filterPropertyId)) {
throw new IllegalArgumentException(filterPropertyId + " is not a valid container property.");
}
}
/**
* See {@link #setCaptionPropertyId(Object)}.
*
* @return the caption property id
*/
public Object getCaptionPropertyId() {
return captionPropertyId;
}
/**
* This propertyId is used, to show results in the user interface.
*
* @param captionPropertyId
* the new caption property id
*/
public void setCaptionPropertyId(Object captionPropertyId) {
this.captionPropertyId = captionPropertyId;
if (!container.getContainerPropertyIds().contains(captionPropertyId)) {
throw new IllegalArgumentException(captionPropertyId + " is not a valid container property.");
}
}
/**
* See {@link #setUniqueIdPropertyId(Object)}.
*
* @return the unique id property id
*/
public Object getUniqueIdPropertyId() {
return uniqueIdPropertyId;
}
/**
* This property is used to map the selected record at clientside to the
* matching item from the container. Each record in the container needs to
* have a unique id.
*
* @param uniqueIdPropertyId
* the new unique id property id
*/
public void setUniqueIdPropertyId(Object uniqueIdPropertyId) {
this.uniqueIdPropertyId = uniqueIdPropertyId;
if (!container.getContainerPropertyIds().contains(uniqueIdPropertyId)) {
throw new IllegalArgumentException(uniqueIdPropertyId + " is not a valid container property.");
}
Class<?> type = container.getType(uniqueIdPropertyId);
needsUUIDMapping = type == String.class ? false : true;
}
/**
* Sets the container datasource used to query suggestions.<br>
* <b>Attention:</b> The container must implement
* {@link Container.Filterable} and {@link Container.Indexed}.
*
* @param container
* the new container data source
*/
public void setContainerDataSource(Container container) {
if (!(container instanceof Container.Filterable) || !(container instanceof Container.Indexed)) {
throw new IllegalArgumentException(
"The container must implement Container.Filterable and Container.Indexed");
}
this.container = container;
}
/**
* Returns the container datasource.
*
* @return the container data source
*/
public Container getContainerDataSource() {
return container;
}
/**
* Query.
*
* @param handleId
* the handle id
* @param filter
* the filter
* @param limit
* the limit
*/
protected void query(String handleId, String filter, int limit) {
if (filterPropertyId == null) {
throw new IllegalArgumentException("filterPropertyId must be set for query.");
}
if (captionPropertyId == null) {
throw new IllegalArgumentException("captionPropertyId must be set for query.");
}
if (uniqueIdPropertyId == null) {
throw new IllegalArgumentException("uniqueIdPropertyId must be set for query.");
}
LOGGER.debug("Query - filter:" + filter + " limit:" + limit + " handleId:" + handleId);
List<SuggestionResult> result = new ArrayList<>();
synchronized (lock) {
Container.Filterable filterable = (Filterable) container;
filterable.removeAllContainerFilters();
filterable.addContainerFilter(new SimpleStringFilter(filterPropertyId, filter, true, false));
int count = 0;
for (Object itemId : ((Container.Indexed) container).getItemIds(0, Math.max(limit, 3))) {
Item item = container.getItem(itemId);
String caption = (String) item.getItemProperty(captionPropertyId).getValue();
Object id = item.getItemProperty(uniqueIdPropertyId).getValue();
if (needsUUIDMapping) {
uuidmapping.put(id.toString(), id);
}
result.add(new SuggestionResult(id.toString(), caption, handleId));
count++;
if (count >= limit) {
break;
}
}
}
respond(handleId, result);
}
/**
* Gets the item by id.
*
* @param id
* the id
* @return the item by id
*/
protected Object getItemById(String id) {
if (uniqueIdPropertyId == null) {
throw new IllegalArgumentException("uniqueIdPropertyId must be set for query.");
}
Object uuid = id;
if (needsUUIDMapping) {
uuid = uuidmapping.get(id);
uuidmapping.clear();
}
if (uuid == null) {
LOGGER.error("Can not access uuid==null. Given id was " + id);
return null;
}
synchronized (lock) {
Container.Filterable filterable = (Container.Filterable) container;
filterable.removeAllContainerFilters();
filterable.addContainerFilter(new Compare.Equal(uniqueIdPropertyId, uuid));
Container.Indexed indexed = (Container.Indexed) container;
return indexed.firstItemId();
}
}
/**
* Fire selection changed.
*
* @param itemId
* the item id
*/
protected void fireSelectionChanged(Object itemId) {
LOGGER.debug("Selection changed event fired: " + itemId);
fireEvent(new SelectionChangedEvent(this, itemId));
}
/**
* Add listener which is being notified if the selection changes.
*
* @param listener
* the listener
*/
public void addSelectionChangedListener(SelectionChangedListener listener) {
addListener(SelectionChangedEvent.class, listener, SelectionChangedListener.SELECTION_CHANGED_METHOD);
}
/**
* Removes listener which is being notified if the selection changes.
*
* @param listener
* the listener
*/
public void removeSelectionChangedListener(SelectionChangedListener listener) {
removeListener(SelectionChangedEvent.class, listener);
}
/**
* Is fired, if the selection of a suggestion clientside finished.
*/
public static class SelectionChangedEvent extends Event {
/** The item id. */
private final Object itemId;
/**
* Instantiates a new selection changed event.
*
* @param source
* the source
* @param itemId
* the item id
*/
public SelectionChangedEvent(Component source, Object itemId) {
super(source);
this.itemId = itemId;
}
/**
* Gets the item id.
*
* @return the item id
*/
public Object getItemId() {
return itemId;
}
}
/**
* The listener interface for receiving selectionChanged events. The class
* that is interested in processing a selectionChanged event implements this
* interface, and the object created with that class is registered with a
* component using the component's <code>addSelectionChangedListener</code>
* method. When the selectionChanged event occurs, that object's appropriate
* method is invoked.
*
* @see SelectionChangedEvent
*/
public interface SelectionChangedListener extends Serializable {
/** The Constant SELECTION_CHANGED_METHOD. */
public static final Method SELECTION_CHANGED_METHOD = ReflectTools.findMethod(SelectionChangedListener.class,
"selectionChanged", SelectionChangedEvent.class);
/**
* Selection changed.
*
* @param event
* the event
*/
void selectionChanged(SelectionChangedEvent event);
}
public interface ShowHistoryDialogCallback {
void openHistoryDialog();
}
public class TextFieldCustom extends TextField {
public TextFieldCustom() {
super();
}
@Override
protected void setInternalValue(String newValue) {
super.setInternalValue(newValue);
}
@Override
public void setValue(String newValue) throws ReadOnlyException {
super.setValue(newValue);
}
}
}