/* * Copyright (c) 2008, 2010, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. Oracle designates this * particular file as subject to the "Classpath" exception as provided * by Oracle in the LICENSE file that accompanied this code. * * This code is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Please contact Oracle, 500 Oracle Parkway, Redwood Shores * CA 94065 USA or visit www.oracle.com if you need additional information or * have any questions. */ package com.codename1.ui; import com.codename1.ui.util.EventDispatcher; import com.codename1.ui.animations.Motion; import com.codename1.ui.geom.Dimension; import com.codename1.ui.geom.Rectangle; import com.codename1.ui.events.ActionEvent; import com.codename1.ui.events.ActionListener; import com.codename1.ui.events.DataChangedListener; import com.codename1.ui.events.SelectionListener; import com.codename1.ui.list.DefaultListCellRenderer; import com.codename1.ui.list.DefaultListModel; import com.codename1.ui.list.ListCellRenderer; import com.codename1.ui.list.ListModel; import com.codename1.ui.plaf.Border; import com.codename1.ui.plaf.LookAndFeel; import com.codename1.ui.plaf.Style; import com.codename1.ui.plaf.UIManager; import java.util.Collection; import java.util.Vector; /** * <p>A set of elements that is rendered using a {@link com.codename1.ui.list.ListCellRenderer} * and are extracted via the {@link com.codename1.ui.list.ListModel}, <b>notice</b> that * <a href="https://www.codenameone.com/blog/avoiding-lists.html">we strongly * discourage usage of lists</a>.<br> * A list can represent many UI concepts ranging from a carousel to a "todo" checklist, this * is made possible thanks to extensive use of Swing's style of MVC. Specifically a list * component is relatively simple, it invokes the model in order to extract the displayed/selected * information and shows it to the user by invoking the cell renderer.</p> * <p>The list class itself is completely decoupled from everything, thus it allows us to extract its * content from any source (e.g. network, storage etc.) and display the information in any form * (e.g. checkboxed elemenents, icons etc.).</p> * <h3>Important</h3> * <p> * {@code List} is a pretty complex class to use so we generally recommend developers use * {@link com.codename1.ui.Container}, * {@link com.codename1.components.InfiniteScrollAdapter} or {@link com.codename1.ui.InfiniteContainer} * coupled with widgets such as {@link com.codename1.components.MultiButton}. Arranging those in a * {@link com.codename1.ui.layouts.BoxLayout} on the {@link com.codename1.ui.layouts.BoxLayout#Y_AXIS} * can produce the functionality of the {@code List} with better performance and far simpler code! * </p> * <p> * Another simpler alternative although not as attractive is the {@link com.codename1.ui.list.MultiList} class * that removes a lot of the {@link com.codename1.ui.list.ListCellRenderer} related complexities inherent * in building a list. * </p> * * <h4>Sample Usage</h4> * <p> * The sample below uses the {@link com.codename1.ui.list.GenericListCellRenderer} class instead of the * {@link com.codename1.ui.list.DefaultListCellRenderer}. We generally recommend using the builtin classes * as the renderer is probably the greatest source of pitfalls in {@code Lists}. * </p> * * <script src="https://gist.github.com/codenameone/15a2370c500e07a8fcf8.js"></script> * <img src="https://www.codenameone.com/img/developer-guide/components-generic-list-cell-renderer.png" alt="Sample of using the generic list cell renderer" /> * * @see com.codename1.ui.Container * @see com.codename1.ui.InfiniteContainer * @see com.codename1.components.InfiniteScrollAdapter * @see com.codename1.components.MultiButton * @see com.codename1.ui.list * @see com.codename1.ui.list.MultiList * @author Chen Fishbein */ public class List<T> extends Component { /** * Indicates the list isn't fixed and that selection is movable */ public static final int FIXED_NONE = 0; /** * Indicates that the list is not fixed in place but cycles its elements */ public static final int FIXED_NONE_CYCLIC = 1; /** * Indicates the list selection will only reach the edge when there are no more * elements in the list. */ public static final int FIXED_NONE_ONE_ELEMENT_MARGIN_FROM_EDGE = 2; /** * Allows to test for fixed none */ private static final int FIXED_NONE_BOUNDRY = 9; /** * Indicates the list selection is fixed into place at the top of the list * or at the left of the list */ public static final int FIXED_LEAD = 10; /** * Indicates the list selection is fixed into place at the bottom of the list * or at the right of the list */ public static final int FIXED_TRAIL = 11; /** * Indicates the list selection is fixed into place at the center of the list */ public static final int FIXED_CENTER = 12; Style spinnerOverlay; /** * Indicates whether the list should not paint the focus component if the list * itself has no focus. * @return the defaultIgnoreFocusComponentWhenUnfocused */ public static boolean isDefaultIgnoreFocusComponentWhenUnfocused() { return defaultIgnoreFocusComponentWhenUnfocused; } /** * Indicates whether the list should not paint the focus component if the list * itself has no focus. * @param aDefaultIgnoreFocusComponentWhenUnfocused the defaultIgnoreFocusComponentWhenUnfocused to set */ public static void setDefaultIgnoreFocusComponentWhenUnfocused(boolean aDefaultIgnoreFocusComponentWhenUnfocused) { defaultIgnoreFocusComponentWhenUnfocused = aDefaultIgnoreFocusComponentWhenUnfocused; } /** * Default value for the fire on click behavior * * @return the defaultFireOnClick */ public static boolean isDefaultFireOnClick() { return defaultFireOnClick; } /** * Default value for the fire on click behavior * * @param aDefaultFireOnClick the defaultFireOnClick to set */ public static void setDefaultFireOnClick(boolean aDefaultFireOnClick) { defaultFireOnClick = aDefaultFireOnClick; } /** * @see setRenderingPrototype */ private T renderingPrototype; /** * Indicates whether selection is fixable to place in which case all the * elements in the list move and selection stays in place. Fixed selection * can be one of: FIXED_NONE, FIXED_TRAIL, FIXED_LEAD, FIXED_CENTER */ private int fixedSelection; private ListModel<T> model; private ListCellRenderer<T> renderer = new DefaultListCellRenderer<T>(); private int orientation = VERTICAL; /** * Indicates the list orientation is VERTICAL */ public static final int VERTICAL = 0; /** * Indicates the list orientation is HORIZONTAL */ public static final int HORIZONTAL = 1; static final int COMBO = 2; EventDispatcher dispatcher = new EventDispatcher(); Object eventSource = this; private Dimension elemSize; private Dimension selectedElemSize; private boolean inputOnFocus = true; private boolean numericKeyActions = true; private boolean paintFocusBehindList = true; /** * Indicates the number of elements the list should check to determine the element * sizes. This is ignored when a rendering prototype is present. */ private int listSizeCalculationSampleCount = 5; /** * Minimum number of elements shown in a list, this member is used to calculate * the list preferred size. If the number of elements in the model is smaller than * this then this value is used in the calculations. */ private int minElementHeight = 0; /** * Maximum number of elements shown in a list, this member is used to calculate * the list preferred size. If the number of elements in the model is larger than * this then this value is used in the calculations. */ private int maxElementHeight = Integer.MAX_VALUE; /** * Indicates the gap between each item in the list */ private int itemGap = 2; private Listeners listener; /** * Indicates the position within the current animation, 0 means no animation * is in progress */ private int animationPosition; private int fixedDraggedAnimationPosition; private int fixedDraggedPosition; private Motion fixedDraggedMotion; private int destination; private Motion listMotion; private static boolean defaultFireOnClick = true; private boolean fireOnClick = defaultFireOnClick; private boolean fireOnRelease; /** * Initial x/y positions for the fixed mode drag */ private int fixedDraggedSelection = 0; private boolean commandList; /** * Indicates whether the list should not paint the focus component if the list * itself has no focus. */ private static boolean defaultIgnoreFocusComponentWhenUnfocused = true; /** * Indicates whether the list should not paint the focus component if the list * itself has no focus. */ private boolean ignoreFocusComponentWhenUnfocused = defaultIgnoreFocusComponentWhenUnfocused; /** * Used internally by the combo box */ boolean disposeDialogOnSelection; /** * Indicates that the background of a cell renderer might mutate between one entry and the next, * it is recommended that this flag remains false for performance reasons. */ private boolean mutableRendererBackgrounds; /** * This flag indicates if the List should automatically scroll to the * selected element when it's been initialized. */ private boolean scrollToSelected = true; private Label hintLabel; private boolean longPointerPressAction; /** * Creates a new instance of List * * @param items set of items placed into the list model */ public List(Vector items) { this(new DefaultListModel(items)); } /** * Creates a new instance of List * * @param items set of items placed into the list model */ public List(T... items) { this(new DefaultListModel(items)); } /** * Creates a new instance of List with an empty default model */ public List() { this(new DefaultListModel()); } /** * Creates a new instance of List with the given model * * @param model the model instance */ public List(ListModel model) { setUIID("List"); setModel(model); } /** * {@inheritDoc} */ protected void initLaf(UIManager uim) { super.initLaf(uim); setSmoothScrolling(uim.getLookAndFeel().isDefaultSmoothScrolling()); fixedSelection = uim.getThemeConstant("fixedSelectionInt", fixedSelection); itemGap = uim.getThemeConstant("listItemGapInt", itemGap); longPointerPressAction = uim.isThemeConstant("listLongPressBool", true); } /** * {@inheritDoc} */ void initComponentImpl() { dataChanged(0, 0); // lazily bind listeners to prevent a memory leak in cases where models // are stored separately from view bindListeners(); super.initComponentImpl(); int index = model.getSelectedIndex(); if(index >= 0){ model.setSelectedIndex(index); } } /** * {@inheritDoc} */ protected void laidOut() { super.laidOut(); if (isScrollable() && isInitialized() && scrollToSelected) { int index = model.getSelectedIndex(); if (index >= 0) { selectElement(index); } } } /** * {@inheritDoc} */ void deinitializeImpl() { super.deinitializeImpl(); // cleanup to allow garbage collection even if the user keeps the model in // memory which is a valid use case if (listener != null) { model.removeDataChangedListener(listener); model.removeSelectionListener(listener); listener = null; } } /** * Callback to allow subclasses to react to a change in the model * * @param status the type data change; REMOVED, ADDED or CHANGED * @param index item index in a list model */ protected void modelChanged(int status, int index) { } /** * Callback to allow subclasses to react to a selection change in the model * * @param oldSelected the old selection value * @param newSelected the new selection value */ protected void listSelectionChanged(int oldSelected, int newSelected) { } /** * {@inheritDoc} */ public int getSideGap() { // isScrollableY() in the base method is very expensive since it triggers getScrollDimension before the layout is complete! if (isScrollVisible() && orientation != HORIZONTAL) { return getUIManager().getLookAndFeel().getVerticalScrollWidth(); } return 0; } /** * {@inheritDoc} */ public boolean isScrollableY() { return (getScrollDimension().getHeight() > getHeight() || isAlwaysTensile()) && getHeight() > 0 && (fixedSelection < FIXED_NONE_BOUNDRY) && orientation != HORIZONTAL; } /** * {@inheritDoc} */ public boolean isScrollableX() { return (getScrollDimension().getWidth() > getWidth()) && (fixedSelection < FIXED_NONE_BOUNDRY) && orientation == HORIZONTAL; } /** * Maximum number of elements shown in a list, this member is used to calculate * the list preferred size. If the number of elements in the model is larger than * this then this value is used in the calculations. * * @return the maximum number of elements */ public int getMaxElementHeight() { return maxElementHeight; } /** * Maximum number of elements shown in a list, this member is used to calculate * the list preferred size. If the number of elements in the model is larger than * this then this value is used in the calculations. * * @param maxElementHeight the maximum number of elements */ public void setMaxElementHeight(int maxElementHeight) { this.maxElementHeight = maxElementHeight; } /** * Minimum number of elements shown in a list, this member is used to calculate * the list preferred size. If the number of elements in the model is smaller than * this then this value is used in the calculations. * * @return the minimum number of elements */ public int getMinElementHeight() { return minElementHeight; } /** * Minimum number of elements shown in a list, this member is used to calculate * the list preferred size. If the number of elements in the model is smaller than * this then this value is used in the calculations. * * @param minElementHeight the minimum number of elements */ public void setMinElementHeight(int minElementHeight) { this.minElementHeight = minElementHeight; } /** * * Returns the number of elements in the list, shorthand for * getModel().getSize() * * @return the number of elements in the list */ public int size() { return model.getSize(); } /** * Returns the visual selection during a drag operation, otherwise equivalent to model.getSelectedIndex * @return visual selection */ public int getCurrentSelected(){ if(fixedSelection > FIXED_NONE_BOUNDRY && isDragActivated()){ return fixedDraggedSelection; } return model.getSelectedIndex(); } /** * Returns the current selected offset in the list * * @return the current selected offset in the list */ public int getSelectedIndex() { return model.getSelectedIndex(); } /** * Sets the current selected offset in the list, by default this implementation * will scroll the list to the selection if the selection is outside of the screen * * @param index the current selected offset in the list */ public void setSelectedIndex(int index) { setSelectedIndex(index, true); } /** * {@inheritDoc} */ protected Rectangle getVisibleBounds() { Rectangle pos = new Rectangle(); Dimension rendererSize = getElementSize(false, true); Style style = getStyle(); int width = getWidth() - style.getHorizontalPadding() - getSideGap(); calculateComponentPosition(getCurrentSelected(), width, pos, rendererSize, getElementSize(true, true), true); pos.setX(pos.getX() + getX()); pos.setY(pos.getY() + getY()); return pos; } /** * {@inheritDoc} */ protected int getDragRegionStatus(int x, int y) { if(!isScrollable()) { return DRAG_REGION_NOT_DRAGGABLE; } if(getOrientation() == HORIZONTAL) { return DRAG_REGION_POSSIBLE_DRAG_X; } return DRAG_REGION_POSSIBLE_DRAG_Y; } /** * Sets the current selected offset in the list * * @param index the current selected offset in the list * @param scrollToSelection indicates whether scrolling to selection should * occur if the selection is outside of view */ public void setSelectedIndex(int index, boolean scrollToSelection) { if (index < 0) { throw new IllegalArgumentException("Selection index is negative:" + index); } model.setSelectedIndex(index); if(!isInitialized()) { Form f = getComponentForm(); if(f == null) { return; } f.revalidate(); } if (scrollToSelection/* && isInitialized() */) { selectElement(index); } } /** * Returns the current selected item in the list or null for no selection * * @return the current selected item in the list */ public T getSelectedItem() { int idx = model.getSelectedIndex(); if (idx < model.getSize() && idx > -1) { return model.getItemAt(idx); } return null; } /** * Sets the current selected item in the list * * @param item the current selected item in the list */ public void setSelectedItem(T item) { int size = model.getSize(); for (int iter = 0; iter < size; iter++) { Object current = model.getItemAt(iter); if (current == item || (current != null && current.equals(item))) { model.setSelectedIndex(iter); break; } } } /** * Returns the model underlying the list * * @return the model underlying the list */ public ListModel<T> getModel() { return model; } /** * {@inheritDoc} */ public void setShouldCalcPreferredSize(boolean shouldCalcPreferredSize) { super.setShouldCalcPreferredSize(shouldCalcPreferredSize); elemSize = null; selectedElemSize = null; // we should try passing the should calcPreferredSize to the renderer so it can revalidate too if(shouldCalcPreferredSize) { ListCellRenderer r = getRenderer(); Object val; if (renderingPrototype != null) { val = renderingPrototype; } else { if (getModel().getSize() > 0) { val = getModel().getItemAt(0); } else { return; } } Component c = r.getListCellRendererComponent(this, val, 0, false); c.setShouldCalcPreferredSize(shouldCalcPreferredSize); c = r.getListCellRendererComponent(this, val, 0, true); c.setShouldCalcPreferredSize(shouldCalcPreferredSize); } } void dataChanged(int status, int index) { setShouldCalcPreferredSize(true); if (getSelectedIndex() >= model.getSize()) { setSelectedIndex(Math.max(model.getSize() - 1, 0)); } modelChanged(status, index); repaint(); } private void bindListeners() { if (listener == null) { listener = new Listeners(); model.addDataChangedListener(listener); model.addSelectionListener(listener); } } /** * Replaces/sets the model underlying the list * * @param model the new model underlying the list */ public void setModel(ListModel model) { if (this.model != null) { setShouldCalcPreferredSize(true); this.model.removeDataChangedListener(listener); this.model.removeSelectionListener(listener); this.model = model; listener = null; // when replacing a model on a scrolled list reset the scrolling if necessary if (getScrollDimension().getHeight() < getScrollY() + getHeight()) { setScrollY(0); } if (getScrollDimension().getWidth() < getScrollX() + getWidth()) { setScrollX(0); } } this.model = model; if (isInitialized()) { bindListeners(); } repaint(); } /** * Indicate whether pressing the number keys should trigger an action * * @return true if pressing the number keys should trigger an action */ public boolean isNumericKeyActions() { return numericKeyActions; } /** * Indicate whether pressing the number keys should trigger an action * * @param numericKeyActions true to trigger an action on number keys */ public void setNumericKeyActions(boolean numericKeyActions) { this.numericKeyActions = numericKeyActions; } /** * Indicates that the list should be treated as a list of commands, if the * user "clicks" a command from the list its action performed method is invoked. * * @return true if the list is treated as a command list */ public boolean isCommandList() { return commandList; } /** * Indicates that the list should be treated as a list of commands, if the * user "clicks" a command from the list its action performed method is invoked. * * @param commandList true for the list to be treated as a command list */ public void setCommandList(boolean commandList) { this.commandList = commandList; } /** * Indicates whether the list should not paint the focus component if the list * itself has no focus. * * @return the ignoreFocusComponentWhenUnfocused */ public boolean isIgnoreFocusComponentWhenUnfocused() { return ignoreFocusComponentWhenUnfocused; } /** * Indicates whether the list should not paint the focus component if the list * itself has no focus. * * @param ignoreFocusComponentWhenUnfocused true to ignore the focus component false otherwise */ public void setIgnoreFocusComponentWhenUnfocused(boolean ignoreFocusComponentWhenUnfocused) { this.ignoreFocusComponentWhenUnfocused = ignoreFocusComponentWhenUnfocused; } /** * Indicates that the background of a cell renderer might mutate between one entry and the next, * it is recommended that this flag remains false for performance reasons. * @return the value of the flag */ public boolean isMutableRendererBackgrounds() { return mutableRendererBackgrounds; } /** * Indicates that the background of a cell renderer might mutate between one entry and the next, * it is recommended that this flag remains false for performance reasons. * @param mutableRendererBackgrounds the new value for the flag */ public void setMutableRendererBackgrounds(boolean mutableRendererBackgrounds) { this.mutableRendererBackgrounds = mutableRendererBackgrounds; } /** * Indicates the number of elements the list should check to determine the element * sizes. This is ignored when a rendering prototype is present. * @return the listSizeCalculationSampleCount */ public int getListSizeCalculationSampleCount() { return listSizeCalculationSampleCount; } /** * Indicates the number of elements the list should check to determine the element * sizes. This is ignored when a rendering prototype is present. * @param listSizeCalculationSampleCount the listSizeCalculationSampleCount to set */ public void setListSizeCalculationSampleCount(int listSizeCalculationSampleCount) { this.listSizeCalculationSampleCount = listSizeCalculationSampleCount; } /** * Enable/disable list action on long pointer press event * @return the longPointerPressAction */ public boolean isLongPointerPressActionEnabled() { return longPointerPressAction; } /** * Enable/disable list action on long pointer press event * @param longPointerPressAction the longPointerPressAction to set */ public void setLongPointerPressActionEnabled(boolean longPointerPressAction) { this.longPointerPressAction = longPointerPressAction; } private class Listeners implements DataChangedListener, SelectionListener { public void dataChanged(int status, int index) { List.this.dataChanged(status, index); } public void selectionChanged(int oldSelected, int newSelected) { repaint(); List.this.listSelectionChanged(oldSelected, newSelected); } } /** * Sets the renderer which is used to draw list elements * * @param renderer cell renderer instance */ public void setRenderer(ListCellRenderer renderer) { setListCellRenderer(renderer); } /** * Sets the renderer which is used to draw list elements * * @param renderer cell renderer instance * @deprecated use setRenderer instead, this method was deprecated to confirm * better to JavaBean convention of having the getter/setter with the same name */ public void setListCellRenderer(ListCellRenderer renderer) { if (this.renderer != null) { //calculate the item list size and the list size. elemSize = null; selectedElemSize = null; setShouldCalcPreferredSize(true); } this.renderer = renderer; } /** * Returns the renderer which is used to draw list elements * * @return the renderer which is used to draw list elements */ public ListCellRenderer getRenderer() { return renderer; } /** * Returns the list orientation * * @return the list orientation HORIZONTAL or VERTICAL * @see #HORIZONTAL * @see #VERTICAL */ public int getOrientation() { return orientation; } /** * {@inheritDoc} */ public void refreshTheme(boolean merge) { ListCellRenderer r = getRenderer(); if (renderingPrototype != null) { r.getListCellRendererComponent(this, renderingPrototype, 0, false).refreshTheme(merge); } else { if (getModel().getSize() > 0) { r.getListCellRendererComponent(this, getModel().getItemAt(0), 0, false).refreshTheme(merge); } else { r.getListCellRendererComponent(this, "", 0, false).refreshTheme(merge); } } Component focus = r.getListFocusComponent(this); if (focus != null) { focus.refreshTheme(merge); } super.refreshTheme(merge); } /** * Sets the list orientation HORIZONTAL or VERTICAL * * @param orientation the list orientation HORIZONTAL or VERTICAL * @see #HORIZONTAL * @see #VERTICAL */ public void setOrientation(int orientation) { this.orientation = orientation; } /** * Makes sure the selected index is visible if it is not in the current view * rect the list will scroll so it fits within * * @param rect the rectangle area to scroll to */ public void scrollRectToVisible(Rectangle rect) { if (fixedSelection < FIXED_NONE_BOUNDRY) { //Dimension elemSize = getElementSize(); Rectangle toScroll; if (orientation != HORIZONTAL) { toScroll = new Rectangle(getScrollX(), rect.getY(), rect.getSize().getWidth(), rect.getSize().getHeight() + itemGap); } else { toScroll = new Rectangle(rect.getX(), getScrollY(), rect.getSize().getWidth() + itemGap, rect.getSize().getHeight()); } super.scrollRectToVisible(toScroll, this); } } /** * {@inheritDoc} */ public void setHandlesInput(boolean b) { Form f = getComponentForm(); if (f != null) { // prevent the list from losing focus if its the only element // or when the user presses fire and there is no other component super.setHandlesInput(b || f.isSingleFocusMode()); } else { super.setHandlesInput(b); } } void setHandlesInputParent(boolean b) { super.setHandlesInput(b); } /** * {@inheritDoc} */ protected void fireClicked() { boolean h = handlesInput(); setHandlesInput(!h); if (h) { fireActionEvent(); } repaint(); } /** * {@inheritDoc} */ protected boolean isSelectableInteraction() { return true; } /** * {@inheritDoc} */ public void keyReleased(int keyCode) { // other events are in keyReleased to prevent the next event from reaching the next form int gameAction = Display.getInstance().getGameAction(keyCode); if (gameAction == Display.GAME_FIRE) { boolean h = handlesInput(); setHandlesInput(!h); if (h) { fireActionEvent(); } repaint(); return; } if (numericKeyActions && gameAction != Display.GAME_LEFT && gameAction != Display.GAME_RIGHT && gameAction != Display.GAME_UP && gameAction != Display.GAME_DOWN) { if (keyCode >= '1' && keyCode <= '9') { int offset = keyCode - '1'; if (offset < getModel().getSize()) { setSelectedIndex(offset); fireActionEvent(); } } } } /** * {@inheritDoc} */ public void keyPressed(int keyCode) { // scrolling events are in keyPressed to provide immediate feedback if (!handlesInput()) { return; } int gameAction = Display.getInstance().getGameAction(keyCode); int keyFwd; int keyBck; if (getOrientation() != HORIZONTAL) { keyFwd = Display.GAME_DOWN; keyBck = Display.GAME_UP; if (gameAction == Display.GAME_LEFT || gameAction == Display.GAME_RIGHT) { setHandlesInput(false); } } else { if (isRTL()) { keyFwd = Display.GAME_LEFT; keyBck = Display.GAME_RIGHT; } else { keyFwd = Display.GAME_RIGHT; keyBck = Display.GAME_LEFT; } if (gameAction == Display.GAME_DOWN || gameAction == Display.GAME_UP) { setHandlesInput(false); } } int selectedIndex = model.getSelectedIndex(); if (gameAction == keyBck) { selectedIndex--; if (selectedIndex < 0) { if (fixedSelection != FIXED_NONE && fixedSelection != FIXED_NONE_ONE_ELEMENT_MARGIN_FROM_EDGE) { selectedIndex = size() - 1; } else { selectedIndex = 0; setHandlesInput(false); } } } else if (gameAction == keyFwd) { selectedIndex++; if (selectedIndex >= size()) { if (fixedSelection != FIXED_NONE && fixedSelection != FIXED_NONE_ONE_ELEMENT_MARGIN_FROM_EDGE) { selectedIndex = 0; } else { selectedIndex = size() - 1; setHandlesInput(false); } } } if (selectedIndex != model.getSelectedIndex()) { model.setSelectedIndex(selectedIndex); int direction = (gameAction == keyFwd ? 1 : -1); if ((isRTL()) && (getOrientation() == HORIZONTAL)) { direction = -direction; } updateAnimationPosition(direction); if (fixedSelection == FIXED_NONE || fixedSelection == FIXED_NONE_CYCLIC) { selectElement(selectedIndex); } if (fixedSelection == FIXED_NONE_ONE_ELEMENT_MARGIN_FROM_EDGE) { // are we going down? if (keyFwd == gameAction) { selectElement(Math.min(selectedIndex + 1, getModel().getSize() - 1)); } else { selectElement(Math.max(selectedIndex - 1, 0)); } } } repaint(); } void selectElement(int selectedIndex) { Dimension size = getElementSize(false, true); Rectangle rect; if (getOrientation() != HORIZONTAL) { rect = new Rectangle(getX(), (size.getHeight() + itemGap) * selectedIndex, getElementSize(true, true)); } else { int x = (size.getWidth() + itemGap) * selectedIndex; if(isRTL() && isScrollableX()){ x = getScrollDimension().getWidth() - x - (size.getWidth() + itemGap); } rect = new Rectangle(x, getY(), getElementSize(true, true)); } if(hasScrollableParent(getParent())) { if(hasFocus()) { scrollRectToVisible(rect); } } else { scrollRectToVisible(rect); } } private boolean hasScrollableParent(Container c) { if(c == null) { return false; } if(c.isScrollable()) { return true; } return hasScrollableParent(c.getParent()); } /** * Updates the animation constant to a new value based on a keypress * * @param direction direction of the animation 1 or -1 */ private void updateAnimationPosition(int direction) { if (animationPosition != 0) { animationPosition = 0; animate(); } if (isSmoothScrolling()) { if (orientation != HORIZONTAL) { animationPosition += (direction * getElementSize(false, true).getHeight()); } else { animationPosition += (direction * getElementSize(false, true).getWidth()); } destination = Math.abs(animationPosition); initListMotion(); } } private void initListMotion() { Form p = getComponentForm(); if(p != null) { p.registerAnimatedInternal(this); } listMotion = Motion.createSplineMotion(0, destination, getScrollAnimationSpeed()); listMotion.start(); } /** * Calculates the desired bounds for the component and returns them within the * given rectangle. */ private void calculateComponentPosition(int index, int defaultWidth, Rectangle rect, Dimension rendererSize, Dimension selectedSize, boolean beforeSelected) { Style style = getStyle(); int initialY = style.getPaddingTop(); int initialX = style.getPaddingLeftNoRTL(); boolean rtl = isRTL(); if (rtl) { initialX += getSideGap(); } int selection = getCurrentSelected(); Dimension d = rect.getSize(); int selectedDiff; // the algorithm illustrated here is very simple despite the "mess" of code... // The idea is that if we have a "fixed" element we just add up the amount of pixels // to get it into its place in the screen (nothing for top obviously). // In order to cause the list to be cyclic we just subtract the list size // which will cause the bottom elements to "return" from the top. if (orientation != HORIZONTAL) { int height = rendererSize.getHeight(); selectedDiff = selectedSize.getHeight() - height; rect.setX(initialX); d.setHeight(height); d.setWidth(defaultWidth); int y = 0; int listHeight = getHeight() - style.getVerticalPadding(); int totalHeight = (height + itemGap) * getModel().getSize() + selectedDiff; switch (fixedSelection) { case FIXED_CENTER: y = listHeight / 2 - (height + itemGap + selectedDiff) / 2 + (index - selection) * (height + itemGap); if (!beforeSelected) { y += selectedDiff; } y = recalcOffset(y, totalHeight, listHeight, height + itemGap); break; case FIXED_TRAIL: y = listHeight - (height + itemGap + selectedDiff); case FIXED_LEAD: y += (index - selection) * (height + itemGap); if (index - selection > 0) { y += selectedDiff; } y = recalcOffset(y, totalHeight, listHeight, height + itemGap); break; default: y = index * (height + itemGap); if (!beforeSelected) { y += selectedDiff; } break; } rect.setY(y + initialY); if (index == selection) { d.setHeight(d.getHeight() + selectedDiff); } } else { int width = rendererSize.getWidth(); selectedDiff = selectedSize.getWidth() - width; rect.setY(initialY); d.setHeight(getHeight() - style.getVerticalPadding()); d.setWidth(width); int x = 0; int listWidth = getWidth() - style.getHorizontalPadding(); int totalWidth = (width + itemGap) * getModel().getSize() + selectedDiff; switch (fixedSelection) { case FIXED_CENTER: x = listWidth / 2 - (width + itemGap + selectedDiff) / 2 + (index - selection) * (width + itemGap); if (!beforeSelected) { x += selectedDiff; } if(rtl) { x = listWidth - x - width; } x = recalcOffset(x, totalWidth, listWidth, width + itemGap); break; case FIXED_TRAIL: x = listWidth - (width + itemGap + selectedDiff); case FIXED_LEAD: x += (index - selection) * (width + itemGap); if (index - selection > 0) { x += selectedDiff; } if (rtl) { x = listWidth - x - width; } x = recalcOffset(x, totalWidth, listWidth, width + itemGap); break; default: x = index * (width + itemGap); if (!beforeSelected) { x += selectedDiff; } break; } int rectX=initialX + x; if ((rtl) && (fixedSelection<FIXED_NONE_BOUNDRY)) { rectX = initialX + totalWidth - (x - initialX) - (width + itemGap); if(index == getCurrentSelected()) { rectX -= selectedDiff; } if(totalWidth < listWidth) { rectX += (listWidth - totalWidth); } } rect.setX(rectX); if (index == selection) { d.setWidth(d.getWidth() + selectedDiff); } } } /** * Allows us to recalculate the bounds of a coordinate to make it "loop" back * into view * * @param offset either x or y coordinate * @param totalSize the total width or height of the list with all the elements (including scroll) * @param viewSize the size visible to the user * @param componentSize the size of the component * @return offset after manipulation if such manipulation was performed */ private int recalcOffset(int offset, int totalSize, int viewSize, int componentSize) { if (offset + (animationPosition % componentSize) + (fixedDraggedAnimationPosition % componentSize) >= viewSize) { offset -= totalSize; } else { if (offset + (animationPosition % componentSize) + (fixedDraggedAnimationPosition % componentSize) < 1 - componentSize) { offset += totalSize; } } return offset; } /** * {@inheritDoc} */ public void paint(Graphics g) { getUIManager().getLookAndFeel().drawList(g, this); Style style = getStyle(); int width = getWidth() - style.getHorizontalPadding() - getSideGap(); if (isScrollableX()) { width = Math.max(width, getScrollDimension().getWidth() - style.getHorizontalPadding() - getSideGap()); } int numOfcomponents = model.getSize(); if (numOfcomponents == 0) { paintHint(g); return; } int xTranslate = getX(); int yTranslate = getY(); g.translate(xTranslate, yTranslate); Rectangle pos = new Rectangle(); Dimension rendererSize = getElementSize(false, true); if(fixedSelection > FIXED_NONE_BOUNDRY){ if (animationPosition != 0 || isDragActivated() ) { if (orientation != HORIZONTAL) { yTranslate += (animationPosition + fixedDraggedAnimationPosition); g.translate(0, animationPosition + fixedDraggedAnimationPosition); } else { xTranslate += (animationPosition + fixedDraggedAnimationPosition); g.translate(animationPosition + fixedDraggedAnimationPosition, 0); } } } int clipX = g.getClipX(); int clipY = g.getClipY(); int clipWidth = g.getClipWidth(); int clipHeight = g.getClipHeight(); // this flag is for preformance improvements // if we figured out that the list items are not visible anymore // we should break from the List loop boolean shouldBreak = false; // improve performance for browsing the end of a very large list int startingPoint = 0; if (fixedSelection < FIXED_NONE_BOUNDRY) { int startX = clipX + getAbsoluteX(); if (isRTL()) { //In RTL the start of the list is not in the left side of the viewport, but rather the right side startX += getWidth(); } startingPoint = Math.max(0, pointerSelect(startX, clipY + getAbsoluteY()) - 1); } int startOffset = 0; int endOffset = numOfcomponents; if(mutableRendererBackgrounds) { for (int i = startingPoint; i < numOfcomponents; i++) { // skip on the selected if (i == getCurrentSelected() && animationPosition == 0 && fixedDraggedAnimationPosition == 0) { if(!shouldBreak) { startOffset = i; } endOffset = i; shouldBreak = true; continue; } calculateComponentPosition(i, width, pos, rendererSize, getElementSize(true, true), i <= getCurrentSelected()); // if the renderer is in the clipping region if (pos.intersects(clipX, clipY, clipWidth, clipHeight)) { if(!shouldBreak) { startOffset = i; } endOffset = i; Dimension size = pos.getSize(); Component selectionCmp = renderer.getListCellRendererComponent(this, getModel().getItemAt(i), i, i == getCurrentSelected()); renderComponentBackground(g, selectionCmp, pos.getX(), pos.getY(), size.getWidth(), size.getHeight()); shouldBreak = true; } else { //this is relevant only if the List is not fixed. if (shouldBreak && (fixedSelection < FIXED_NONE_BOUNDRY)) { break; } } } } else { T valueAt0 = getModel().getItemAt(0); Component selectionCmp; int selectedIndex = getSelectedIndex(); if(selectedIndex > -1 && selectedIndex < numOfcomponents) { // this is essential otherwise we constantly ticker based on the value of the first entry selectionCmp = renderer.getListCellRendererComponent(this, getModel().getItemAt(selectedIndex), 0, true); } else { selectionCmp = renderer.getListCellRendererComponent(this, valueAt0, 0, true); } Component unselectedCmp = renderer.getListCellRendererComponent(this, valueAt0, 0, false); for (int i = startingPoint; i < numOfcomponents; i++) { // skip on the selected if (i == getCurrentSelected() && animationPosition == 0) { if(!shouldBreak) { startOffset = i; } endOffset = i; shouldBreak = true; continue; } calculateComponentPosition(i, width, pos, rendererSize, getElementSize(true, true), i <= getCurrentSelected()); // if the renderer is in the clipping region if (pos.intersects(clipX, clipY, clipWidth, clipHeight)) { if(!shouldBreak) { startOffset = i; } endOffset = i; if(i == getCurrentSelected()) { Dimension size = pos.getSize(); renderComponentBackground(g, selectionCmp, pos.getX(), pos.getY(), size.getWidth(), size.getHeight()); } else { Dimension size = pos.getSize(); renderComponentBackground(g, unselectedCmp, pos.getX(), pos.getY(), size.getWidth(), size.getHeight()); } shouldBreak = true; } else { //this is relevant only if the List is not fixed. if (shouldBreak && (fixedSelection < FIXED_NONE_BOUNDRY)) { break; } } } } boolean shouldRendererSelectedEntry = (renderer.getListFocusComponent(this) == null && (fixedSelection < FIXED_NONE_BOUNDRY)) || animationPosition == 0 && model.getSize() > 0; Rectangle selectedPos = new Rectangle(); calculateComponentPosition(getCurrentSelected(), width, selectedPos, rendererSize, getElementSize(true, true), true); Dimension size = selectedPos.getSize(); int curSel = getCurrentSelected(); if(shouldRendererSelectedEntry && curSel > -1 && curSel < model.getSize()) { Component selected = renderer.getListCellRendererComponent(this, model.getItemAt(getCurrentSelected()), getCurrentSelected(), true); renderComponentBackground(g, selected, selectedPos.getX(), selectedPos.getY(), size.getWidth(), size.getHeight()); } if (paintFocusBehindList) { paintFocus(g, width, pos, rendererSize); } for (int i = startOffset; i <= endOffset; i++) { // skip on the selected if (i == getCurrentSelected() && animationPosition == 0) { continue; } calculateComponentPosition(i, width, pos, rendererSize, getElementSize(true, true), i <= getCurrentSelected()); if (pos.intersects(clipX, clipY, clipWidth, clipHeight)) { T value = model.getItemAt(i); Component cmp = renderer.getListCellRendererComponent(this, value, i, false); cmp.setCellRenderer(true); Dimension sizeC = pos.getSize(); renderComponent(g, cmp, pos.getX(), pos.getY(), sizeC.getWidth(), sizeC.getHeight()); } } //if the animation has finished draw the selected element if (shouldRendererSelectedEntry) { Component selected = renderer.getListCellRendererComponent(this, model.getItemAt(getCurrentSelected()), getCurrentSelected(), true); renderComponent(g, selected, selectedPos.getX(), selectedPos.getY(), size.getWidth(), size.getHeight()); } if (!paintFocusBehindList) { paintFocus(g, width, pos, rendererSize); } g.translate(-xTranslate, -yTranslate); if(spinnerOverlay != null) { if(spinnerOverlay.getBorder() != null) { spinnerOverlay.getBorder().paintBorderBackground(g, this); spinnerOverlay.getBorder().paint(g, this); } else { spinnerOverlay.getBgPainter().paint(g, getBounds()); } } } /** * Allows subclasses to override the selection rendering for the component, specifically the spinner * in some themes * @return true to render the selection, false otherwise. */ protected boolean shouldRenderSelection() { return Display.getInstance().shouldRenderSelection(this); } private void paintFocus(Graphics g, int width, Rectangle pos, Dimension rendererSize) { if(ignoreFocusComponentWhenUnfocused && !hasFocus()) { return; } if(!shouldRenderSelection()) { return; } calculateComponentPosition(getCurrentSelected(), width, pos, rendererSize, getElementSize(true, true), true); Dimension size = pos.getSize(); Component cmp = renderer.getListFocusComponent(this); if (cmp != null) { cmp.setCellRenderer(true); int x = pos.getX(); int y = pos.getY(); // prevent focus animation from working during a drag operation if (orientation != HORIZONTAL) { y -= (animationPosition + fixedDraggedAnimationPosition); } else { x -= (animationPosition + fixedDraggedAnimationPosition); } renderComponentBackground(g, cmp, x, y, size.getWidth(), size.getHeight()); renderComponent(g, cmp, x, y, size.getWidth(), size.getHeight()); } } /** * Renders the current component on the screen */ private void renderComponent(Graphics g, Component cmp, int x, int y, int width, int height) { Style s = cmp.getStyle(); int left = s.getMarginLeft(isRTL()); int top = s.getMarginTop(); cmp.setWidth(width - left - s.getMarginRight(isRTL())); cmp.setHeight(height - top - s.getMarginBottom()); cmp.setX(x + left); cmp.setY(y + top); int oX = g.getClipX(); int oY = g.getClipY(); int oWidth = g.getClipWidth(); int oHeight = g.getClipHeight(); //g.pushClip(); g.clipRect(cmp.getX(), cmp.getY(), cmp.getWidth(), cmp.getHeight()); cmp.paint(g); Border b = s.getBorder(); if(b != null && !b.isBackgroundPainter()) { cmp.paintBorder(g); } g.setClip(oX, oY, oWidth, oHeight); //g.popClip(); } private void renderComponentBackground(Graphics g, Component cmp, int x, int y, int width, int height) { Style s = cmp.getStyle(); int left = s.getMarginLeft(isRTL()); int top = s.getMarginTop(); cmp.setWidth(width - left - s.getMarginRight(isRTL())); cmp.setHeight(height - top - s.getMarginBottom()); cmp.setX(x + left); cmp.setY(y + top); int cX = g.getClipX(); int cY = g.getClipY(); int cW = g.getClipWidth(); int cH = g.getClipHeight(); //g.pushClip(); g.clipRect(cmp.getX(), cmp.getY(), cmp.getWidth(), cmp.getHeight()); cmp.paintComponentBackground(g); g.setClip(cX, cY, cW, cH); //g.popClip(); } /** * Invoked to indicate interest in future selection events * * @param l the selection listener to be added */ public void addSelectionListener(SelectionListener l) { model.addSelectionListener(l); } /** * Invoked to indicate no further interest in future selection events * * @param l the selection listener to be removed */ public void removeSelectionListener(SelectionListener l) { model.removeSelectionListener(l); } /** * Allows binding a listener to user selection actions * * @param l the action listener to be added */ public void addActionListener(ActionListener l) { dispatcher.addListener(l); } /** * This method allows extracting the action listeners from the current list * * @return vector containing the action listeners on the list * @deprecated use getListeners instead */ public Vector getActionListeners() { return dispatcher.getListenerVector(); } /** * This method allows extracting the action listeners from the current list * * @return Collection containing the action listeners on the list */ public Collection getListeners() { return dispatcher.getListenerCollection(); } /** * Allows binding a listener to user selection actions * * @param l the action listener to be removed */ public void removeActionListener(ActionListener l) { dispatcher.removeListener(l); } /** * {@inheritDoc} */ protected void fireActionEvent() { fireActionEvent(new ActionEvent(eventSource,ActionEvent.Type.Other)); } /** * Triggers the event to the listeners * @param evt the event to fire */ protected void fireActionEvent(ActionEvent a) { if(isEnabled() && !Display.getInstance().hasDragOccured()){ if(disposeDialogOnSelection) { ((Dialog)getComponentForm()).dispose(); } super.fireActionEvent(); dispatcher.fireActionEvent(a); if(isCommandList() && !a.isConsumed()) { T i = getSelectedItem(); if(i != null && i instanceof Command && ((Command)i).isEnabled()) { ((Command)i).actionPerformed(a); if(!a.isConsumed()) { Form f = getComponentForm(); if(f != null) { f.actionCommandImpl((Command)i); } } } } Display d = Display.getInstance(); if(d.isBuiltinSoundsEnabled()) { d.playBuiltinSound(Display.SOUND_TYPE_BUTTON_PRESS); } } } /** * A list can start handling input implicitly upon gaining focus, this can * make for a more intuitive UI when no other focus elements exist or when * their use case is infrequent. However, it might be odd in some cases * where the list "steals" focus. * * @param inputOnFocus true is a list can start handling input * implicitly upon gaining focus */ public void setInputOnFocus(boolean inputOnFocus) { this.inputOnFocus = inputOnFocus; } /** * This method determines if the animated focus is drawn on top of the List * or behind the List when moving. * * @param paintFocusBehindList */ public void setPaintFocusBehindList(boolean paintFocusBehindList) { this.paintFocusBehindList = paintFocusBehindList; } /** * {@inheritDoc} */ void focusGainedInternal() { super.focusGainedInternal(); if (inputOnFocus) { setHandlesInput(true); } } /** * {@inheritDoc} */ void focusLostInternal() { super.focusLostInternal(); } /** * Returns the gap between items * * @return the gap between items */ public int getItemGap() { return itemGap; } /** * Set the gap between items * * @param itemGap the gap between items */ public void setItemGap(int itemGap) { this.itemGap = itemGap; } /** * <p>The rendering prototype is optionally used in calculating the size of the * List and is recommended for performance reasons. You should invoke it with an object * representing a theoretical value in the list which will be used to calculate * the size required for each element in the list.</p> * <p>This allows list size calculations to work across look and feels and allows * developers to predetermine size for list elements.</p> * <p>e.g. For a list of Strings which you would like to always be 5 characters wide * you can use a prototype "XXXXX" which would use the preferred size of the XXXXX * String to determine the size of the list element. E.g. for a list of dates you can use * new Date(30, 12, 00) etc..</p> * <p>The example below was designed for {@link com.codename1.ui.list.MultiList} but * should work for any list. Its goal is to render 2 lines of text with 20 characters and a * 5mm square icon:</p> * <script src="https://gist.github.com/codenameone/dc9c7f13f6b312d1edc8.js"></script> * * @param renderingPrototype a value that can be passed to the renderer to indicate the preferred * size of a list component. */ public void setRenderingPrototype(T renderingPrototype) { this.renderingPrototype = renderingPrototype; } /** * See set rendering prototype * * @see #setRenderingPrototype(java.lang.Object) * @return the value of the rendering prototype */ public T getRenderingPrototype() { return renderingPrototype; } /** * Calculates the default size for elements on the list * * @return the default dimension for elements in a list */ Dimension getElementSize(boolean selected, boolean addMargin) { if (selected) { if (selectedElemSize == null) { // don't keep element size if there are no elements and no prototype... if (renderingPrototype == null) { if (model.getSize() == 0) { // put a sensible value as default when there are no elements or rendering prototype if(addMargin) { return new Label("XXXXXX").getPreferredSizeWithMargin(); } return new Label("XXXXXX").getPreferredSize(); } } selectedElemSize = calculateElementSize(true, addMargin); } return selectedElemSize; } else { if (elemSize == null) { // don't keep element size if there are no elements and no prototype... if (renderingPrototype == null) { if (model.getSize() == 0) { // put a sensible value as default when there are no elements or rendering prototype Label l = new Label("XXXXXX"); if(addMargin) { return l.getPreferredSizeWithMargin(); } else { return l.getPreferredSize(); } } } elemSize = calculateElementSize(false, addMargin); } return elemSize; } } /** * Calculates the size of an element based on a forumla or on rendering prototype */ private Dimension calculateElementSize(boolean selected, boolean addMargin) { if (renderingPrototype != null) { Component unselected = renderer.getListCellRendererComponent(this, renderingPrototype, 0, selected); if(addMargin) { return unselected.getPreferredSizeWithMargin(); } else { return unselected.getPreferredSize(); } } int width = 0; int height = 0; int elements = Math.min(listSizeCalculationSampleCount, model.getSize()); int marginY = 0; int marginX = 0; for (int iter = 0; iter < elements; iter++) { Component cmp = renderer.getListCellRendererComponent(this, model.getItemAt(iter), iter, selected); if(cmp instanceof Container) { cmp.setShouldCalcPreferredSize(true); } Dimension d = cmp.getPreferredSize(); width = Math.max(width, d.getWidth()); height = Math.max(height, d.getHeight()); if(iter == 0) { Style s = cmp.getStyle(); marginY = s.getVerticalMargins(); marginX = s.getHorizontalMargins(); } } return new Dimension(width + marginX, height + marginY); } /** * {@inheritDoc} */ public void longPointerPress(int x, int y) { if(!isEnabled()) { return; } if(longPointerPressAction) { int s = pointerSelect(x, y); if(s > -1) { model.setSelectedIndex(s); } pointerReleasedImpl(x, y, false, true); } } /** * {@inheritDoc} */ public void pointerPressed(int x, int y) { if(!isEnabled()) { return; } if(fixedSelection > FIXED_NONE_BOUNDRY) { // for a fixed list we need to store the initial drag position if(isSmoothScrolling()) { if(orientation != HORIZONTAL) { fixedDraggedPosition = y; } else { fixedDraggedPosition = x; } if(isDragActivated()){ int selected = getCurrentSelected(); model.setSelectedIndex(selected); fixedDraggedMotion = null; fixedDraggedAnimationPosition = 0; } fixedDraggedSelection = getModel().getSelectedIndex(); } } // prevent a hover event from activating the drag in case of a click screen, // this is essential for the Storm device setDragActivated(false); int current = model.getSelectedIndex(); int selection = pointerSelect(x, y); if (selection > -1 && fixedSelection < FIXED_NONE_BOUNDRY) { model.setSelectedIndex(selection); } fireOnRelease = current == selection; super.pointerPressed(x, y); } /** * {@inheritDoc} */ public void pointerHover(int[] x, int[] y) { } /** * {@inheritDoc} */ public void pointerDragged(int x, int y) { pointerDraggedImpl(x, y); } private void pointerDraggedImpl(int x, int y) { if(!isEnabled()) { return; } if (isSmoothScrolling()) { if(fixedSelection < FIXED_NONE_BOUNDRY) { super.pointerDragged(x, y); } else { if(!isDragActivated()){ setDragActivated(true); } Dimension size = getElementSize(false, true); boolean vertical = orientation == List.VERTICAL; int pos; int s; if(vertical){ pos = y; s = size.getHeight(); }else{ pos = x; s = size.getWidth(); } fixedDraggedAnimationPosition = fixedDraggedAnimationPosition - (fixedDraggedPosition - pos); fixedDraggedPosition = pos; if(fixedDraggedAnimationPosition <= -s){ fixedDraggedSelection++; if(fixedDraggedSelection >= model.getSize()){ fixedDraggedSelection = 0; } }else if(fixedDraggedAnimationPosition >= s){ fixedDraggedSelection--; if(fixedDraggedSelection < 0){ fixedDraggedSelection = model.getSize() - 1; } } fixedDraggedAnimationPosition = fixedDraggedAnimationPosition % s; } } else { int sel=pointerSelect(x, y); if (sel > -1) { model.setSelectedIndex(sel); } } } /** * {@inheritDoc} */ public Rectangle getSelectedRect() { Style style = getStyle(); Rectangle pos = new Rectangle(); int width = getWidth() - style.getHorizontalPadding() - getSideGap(); Dimension rendererSize = getElementSize(false, true); calculateComponentPosition(getSelectedIndex(), width, pos, rendererSize, getElementSize(true, true), true); pos.setX(pos.getX() + getParent().getAbsoluteX()); pos.setY(pos.getY() + getParent().getAbsoluteY()); return pos; } private int pointerSelect(int x, int y) { int selectedIndex = -1; int numOfcomponents = getModel().getSize(); Style style = getStyle(); Dimension rendererSize = getElementSize(false, true); Dimension selectedSize = getElementSize(true, true); Rectangle pos = new Rectangle(); int width = getWidth() - style.getHorizontalPadding() - getSideGap(); if (isScrollableX()) { width = Math.max(width, getScrollDimension().getWidth() - style.getHorizontalPadding() - getSideGap()); } y = y - getAbsoluteY(); x = x - getAbsoluteX(); if (fixedSelection < FIXED_NONE_BOUNDRY) { calculateComponentPosition(getSelectedIndex(), width, pos, rendererSize, getElementSize(true, true), true); if (orientation != HORIZONTAL) { if(y < pos.getY()){ selectedIndex = (y - style.getPaddingTop()) / (rendererSize.getHeight() + itemGap); }else{ int current = getSelectedIndex(); if(y < pos.getY() + selectedSize.getHeight()){ selectedIndex = current; }else{ selectedIndex = (current+1) + (y - (pos.getY() + selectedSize.getHeight()))/(rendererSize.getHeight() + itemGap); } } } else { if (isRTL()) { if (x > pos.getX() + selectedSize.getWidth()) { int delta = x - (pos.getX() + selectedSize.getWidth()); delta /= (rendererSize.getWidth() + itemGap); // should have been -1-delta, but works better like this. selectedIndex = getSelectedIndex() - 1 - delta; } else { if (x >= pos.getX()) { selectedIndex = getSelectedIndex(); } else { int delta = pos.getX() - x; delta /= (rendererSize.getWidth() + itemGap); selectedIndex = getSelectedIndex() + 1 + delta; } } } else { if (x < pos.getX()) { selectedIndex = (x - style.getPaddingLeftNoRTL()) / (rendererSize.getWidth() + itemGap); } else { int current = getSelectedIndex(); if (x < pos.getX() + selectedSize.getWidth()) { selectedIndex = current; }else{ selectedIndex = (current+1) + (x - (pos.getX() + selectedSize.getWidth()))/(rendererSize.getWidth() + itemGap); } } } } } else { for (int i = 0; i < numOfcomponents; i++) { calculateComponentPosition(i, width, pos, rendererSize, selectedSize, true); if (pos.contains(x, y)) { selectedIndex = i; break; } } } if (selectedIndex < 0 || selectedIndex >= size()) { return -1; } return selectedIndex; } /** * This method determines if the List fires the action event when the pointer * was clicked on one of the items, or only if the item was the selected item * By default the value is true, this setting is only relevant for none fixed * Lists * * @param fireOnClick */ public void setFireOnClick(boolean fireOnClick){ this.fireOnClick = fireOnClick; } /** * {@inheritDoc} */ public void pointerHoverReleased(int[] x, int[] y) { } private void pointerReleasedImpl(int x, int y, boolean isHover, boolean longPress) { if(!isEnabled()) { return; } if (isDragActivated()) { if(fixedSelection < FIXED_NONE_BOUNDRY) { super.pointerReleased(x, y); } else { boolean vertical = getOrientation() == VERTICAL; float speed = getDragSpeed(vertical); if (vertical) { fixedDraggedMotion = Motion.createFrictionMotion(-fixedDraggedAnimationPosition, Integer.MAX_VALUE, speed, 0.0007f); }else{ fixedDraggedMotion = Motion.createFrictionMotion(-fixedDraggedAnimationPosition, Integer.MAX_VALUE , speed, 0.0007f); } fixedDraggedPosition = fixedDraggedAnimationPosition; Form p = getComponentForm(); if (p != null) { p.registerAnimatedInternal(this); } fixedDraggedMotion.start(); } return; } if (!isHover && pointerSelect(x, y) > -1) { if(fixedSelection > FIXED_NONE_BOUNDRY){ int index = pointerSelect(x, y); updateAnimationPosition(index - getSelectedIndex()); setSelectedIndex(index); fireActionEvent(new ActionEvent(eventSource,ActionEvent.Type.Other)); return; } if ((fireOnClick && fixedSelection < FIXED_NONE_BOUNDRY) || fireOnRelease) { // fire the action event into the selected component Component selectionCmp = renderer.getListCellRendererComponent(this, getSelectedItem(), getSelectedIndex(), true); Style style = getStyle(); int width = getWidth() - style.getHorizontalPadding() - getSideGap(); Rectangle pos = new Rectangle(); Dimension rendererSize = getElementSize(false, true); calculateComponentPosition(getSelectedIndex(), width, pos, rendererSize, getElementSize(true, true), true); int absX = getAbsoluteX(); int posX = pos.getX(); int absY = getAbsoluteY(); int posY = pos.getY(); int newX = x - absX - posX; int newY = y - absY - posY; selectionCmp.setX(0); selectionCmp.setY(0); if(selectionCmp instanceof Container) { Component tmp = ((Container)selectionCmp).getComponentAt(newX, newY); if(tmp != null) { selectionCmp = tmp; } } if(longPress){ selectionCmp.longPointerPress(newX, newY); fireActionEvent(new ActionEvent(eventSource, newX, newY, true)); }else{ selectionCmp.pointerPressed(newX, newY); selectionCmp.pointerReleased(newX, newY); fireActionEvent(new ActionEvent(eventSource, newX, newY, false)); } } } } /** * {@inheritDoc} */ public void pointerReleased(int x, int y) { pointerReleasedImpl(x, y, false, false); } /** * {@inheritDoc} */ protected Dimension calcPreferredSize() { if(shouldShowHint()) { Label l = getHintLabelImpl(); if(l != null) { Dimension d1 = getUIManager().getLookAndFeel().getListPreferredSize(this); Dimension d2 = l.getPreferredSize(); return new Dimension(d1.getWidth() + d2.getWidth(), d1.getHeight() + d2.getHeight()); } } Dimension d = getUIManager().getLookAndFeel().getListPreferredSize(this); if(spinnerOverlay != null) { if(spinnerOverlay.getBorder() != null) { d.setWidth(Math.max(spinnerOverlay.getBorder().getMinimumWidth(), d.getWidth())); d.setHeight(Math.max(spinnerOverlay.getBorder().getMinimumHeight(), d.getHeight())); } } return d; } /** * Allows adding an element to a list if the underlying model supports this, notice that * it is an optional operation and if the model does not support it (default list model does) * then this operation may failed. * * @param item the item to be added to a list model */ public void addItem(T item) { model.addItem(item); } /** * Indicates whether selection is fixable to place in which case all the * elements in the list move and selection stays in place. * * @return one of: FIXED_NONE, FIXED_TRAIL, FIXED_LEAD, FIXED_CENTER, FIXED_NONE_CYCLIC */ public int getFixedSelection() { return fixedSelection; } /** * Indicates whether selection is fixable to place in which case all the * elements in the list move and selection stays in place. * * @param fixedSelection one of: FIXED_NONE, FIXED_TRAIL, FIXED_LEAD, * FIXED_CENTER, FIXED_NONE_CYCLIC */ public void setFixedSelection(int fixedSelection) { this.fixedSelection = fixedSelection; } void deregisterAnimatedInternal() { if (animationPosition == 0) { super.deregisterAnimatedInternal(); } } /** * {@inheritDoc} */ public boolean animate() { // parent is performing the animation we shouldn't do anything in this case // this is the scrolling animation which we don't want to interfear with boolean parentFinished = super.animate(); if ((animationPosition != 0) && listMotion != null && !isDragActivated()) { if (animationPosition < 0) { animationPosition = Math.min(listMotion.getValue() - destination, 0); } else { animationPosition = Math.max(destination - listMotion.getValue(), 0); } if (animationPosition == 0) { listMotion = null; deregisterAnimatedInternal(); } return true; } if (fixedDraggedMotion != null) { int val = -fixedDraggedMotion.getValue(); fixedDraggedAnimationPosition = fixedDraggedAnimationPosition - (fixedDraggedPosition - val); fixedDraggedPosition = val; Dimension size = getElementSize(false, true); int s; if (orientation == VERTICAL) { s = size.getHeight(); } else { s = size.getWidth(); } if (fixedDraggedMotion.isFinished()) { deregisterAnimatedInternal(); //if after dragging the list is in the middle check which item //is the closest and animate to it. if (fixedDraggedAnimationPosition <= -s / 2) { fixedDraggedSelection++; if (fixedDraggedSelection >= model.getSize()) { fixedDraggedSelection = 0; } } else if (fixedDraggedAnimationPosition >= s / 2) { fixedDraggedSelection--; if (fixedDraggedSelection < 0) { fixedDraggedSelection = model.getSize() - 1; } } if (fixedDraggedAnimationPosition != 0) { if (fixedDraggedAnimationPosition < 0) { if (fixedDraggedAnimationPosition < -s / 2) { destination = s+fixedDraggedAnimationPosition; animationPosition = destination; } else { destination = -fixedDraggedAnimationPosition; animationPosition = fixedDraggedAnimationPosition; } } else { if (fixedDraggedAnimationPosition > s / 2) { destination = (s-fixedDraggedAnimationPosition); animationPosition = -destination; } else { destination = fixedDraggedAnimationPosition; animationPosition = fixedDraggedAnimationPosition; } } initListMotion(); fixedDraggedAnimationPosition = 0; } // this happens when dragging an empty list causing an exception on a negative selection if (fixedDraggedSelection >= 0 && fixedDraggedSelection < getModel().getSize()) { setSelectedIndex(fixedDraggedSelection, false); } setDragActivated(false); fixedDraggedMotion = null; return false; } else { if (fixedDraggedAnimationPosition <= -s) { fixedDraggedSelection++; if (fixedDraggedSelection >= model.getSize()) { fixedDraggedSelection = 0; } fixedDraggedPosition = val; } else if (fixedDraggedAnimationPosition >= s) { fixedDraggedSelection--; if (fixedDraggedSelection < 0) { fixedDraggedSelection = model.getSize() - 1; } fixedDraggedPosition = val; } fixedDraggedAnimationPosition = fixedDraggedAnimationPosition % s; } return true; } return parentFinished; } /** * {@inheritDoc} */ protected boolean isTactileTouch(int x, int y) { // provide touch feedback only when pressing an entry in the list and not for the entire list if(isTactileTouch()) { int selection = pointerSelect(x, y); if (selection > -1) { return true; } } return false; } /** * This flag indicates to the List if the List should scroll to the selected * element when it's been initialized. * * @param scrollToSelected if true the List scrolls to the selected element * when It's been initialized. */ public void setScrollToSelected(boolean scrollToSelected) { this.scrollToSelected = scrollToSelected; } /** * {@inheritDoc} */ protected int getGridPosY() { int gridSize = getElementSize(false, true).getHeight() + itemGap; int scroll = getScrollY(); return calcGrid(scroll, gridSize); } private int calcGrid(int scroll, int gridSize) { int fraction = scroll % gridSize; if(Math.abs(fraction - gridSize) > 2) { if(fraction > gridSize / 2) { return scroll + gridSize - fraction; } else { return scroll - fraction; } } return scroll; } /** * {@inheritDoc} */ protected int getGridPosX() { int gridSize = getElementSize(false, true).getWidth() + itemGap; int scroll = getScrollX(); return calcGrid(scroll, gridSize); } /** * {@inheritDoc} */ protected String paramString() { String elemSizeStr = "element size = "; if (elemSize != null) { elemSizeStr += elemSize.toString(); } return super.paramString() + ", " + elemSizeStr + ", itemGap = " + itemGap + ", orientation = " + orientation + ", selected index = " + getSelectedIndex() + ", size = " + size(); } /** * Sets the TextArea hint text, the hint text is displayed on the TextArea * When there is no text in the TextArea * * @param hint the hint text to display */ public void setHint(String hint){ super.setHint(hint, getHintIcon()); } /** * Returns the hint text * * @return the hint text or null */ public String getHint() { return super.getHint(); } /** * Sets the TextArea hint icon, the hint is displayed on the TextArea * When there is no text in the TextArea * * @param icon the icon */ public void setHintIcon(Image icon){ setHint(getHint(), icon); } /** * Returns the hint icon * * @return the hint icon */ public Image getHintIcon() { return super.getHintIcon(); } /** * Sets the TextArea hint text and Icon, the hint text and icon are * displayed on the TextArea when there is no text in the TextArea * * @param hint the hint text to display * @param icon the hint icon to display */ public void setHint(String hint, Image icon){ super.setHint(hint, icon); } Label getHintLabelImpl() { return hintLabel; } void setHintLabelImpl(Label hintLabel) { this.hintLabel = hintLabel; } boolean shouldShowHint() { return getModel().getSize() == 0; } }