| /** |
| * |
| * 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); |
| } |
| } |
| } |