/********************************************************************************* * TotalCross Software Development Kit * * Copyright (C) 2003-2008 Greg Ouzounian * * Copyright (C) 2008-2012 SuperWaba Ltda. * * All Rights Reserved * * * * This library and virtual machine 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. * * * * This file is covered by the GNU LESSER GENERAL PUBLIC LICENSE VERSION 3.0 * * A copy of this license is located in file license.txt at the root of this * * SDK or can be downloaded here: * * http://www.gnu.org/licenses/lgpl-3.0.txt * * * *********************************************************************************/ package totalcross.ui; import totalcross.sys.*; import totalcross.ui.event.*; import totalcross.ui.gfx.*; import totalcross.util.*; /** MultiListBox is a listbox that allows more than one item to be selected. * The maximum number of selections can be defined using setMaxSelections. * <br><br> * To create a ComboBox with a MultiListBox, use: * <pre> * MultiListBox mlb; * new ComboBox(mlb = new MultiListBox()) * </pre> * Be sure to save a reference to the MultiListBox so you can call the specific * methods of this class. For instance, getSelectedIndex returns just the last selected * index; to retrieve all indexes, use getSelectedIndexes. * <br><br> * In penless devices, there will be a cursor which will be used to highlight an item; * to select or unselect it, you must press the left key. * <br><br> * MultiListBox requires the useFullWidthOnSelection on penless devices. * @since TotalCross 1.0. */ public class MultiListBox extends ListBox { protected static final int NO_OLD_SELECTION = -99; protected int maxSelections = 10000000; protected int oldSelection=NO_OLD_SELECTION; protected IntHashtable selectedIndexes = new IntHashtable(5); protected IntVector order; /** Internal use only. */ public boolean changed; private boolean drawingSel, hasFocus; private int cursorColor,mixedCursorColor; /** Fill this IntVector with the values that will be selected when the clear method is called. */ public IntVector clearValues = new IntVector(1); // guich@tc112_33 /** Set to false to don't show the number of selected elements as they are clicked. * @since TotalCross 1.3 */ public boolean showOrderInTip = true; /** Set to true if you want to unselect the first element once max is reached. * Note that this implies that setOrderIsImportant(true) and also setMaxSelections were both called, otherwise it has no effects. */ public boolean unselectFirstWhenMaxIsReached; /** Suffix used to display the number of items in a ComboBox. Defaults to " items". */ public static String itemsText = " items"; /** Constructs an empty MultiListBox. */ public MultiListBox() { useFullWidthOnSelection = true; // required } /** Constructs a MultiListBox with the given items. */ public MultiListBox(Object[] items) { super(items); useFullWidthOnSelection = true; // required } /** Call this method if you want to keep track of the order in which the items were selected. * Note that this makes the listbox slower. Calling this method clears all selected items. */ public void setOrderIsImportant(boolean set) { order = set ? new IntVector(50) : null; selectedIndexes.clear(); } /** Returns true if you requested that the order is important. */ public boolean isOrderImportant() { return order != null; } /** Returns the selected index. If more than one item is selected, returns the last one. */ public int getSelectedIndex() { try { return /*Settings.keyboardFocusTraversable ? selectedIndex : */order != null ? order.peek() : selectedIndexes.size() > 0 ? selectedIndexes.getKey(0) : -1; // guich@tc110_100: fixed problem in penless mode: a highlighted item was shown in the combobox instead of the selected item } catch (ElementNotFoundException e) {return -1;} } /** Returns the last selected item if you had set <i>order is important</i>, otherwise returns null. */ public Object getLastSelectedItem() { if (order == null) return null; int n = selectedIndexes.size(); if (n == 0) return null; try { return items.items[order.peek()]; } catch (ElementNotFoundException e) {return null;} } /** Defines the maximum number of items that can be selected. * If currently there are more selected than the allowed, all * selections are cleared. */ public void setMaxSelections(int max) { this.maxSelections = max; if (selectedIndexes.size() > max) { selectedIndexes.clear(); if (order != null) order.removeAllElements(); } } /** * Return a vector with the indexes which have been selected. The elements of the Vector are the * indexes in the Vector of the items. So if this Vector is holding * [3, 12, 5] it means that the items 3, 5 and 12 have been selected. * If order is important, then the order intvector is returned (caution: do not change the returned array!). * Note that the indexes are not in order; to order them, call <code>qsort</code>. */ public IntVector getSelectedIndexes() { return order != null ? order : selectedIndexes.getKeys(); } /** Returns if given index is selected. */ public boolean isSelected(int index) { return selectedIndexes.exists(index); } public void removeAll() { selectedIndexes.clear(); if (order != null) order.removeAllElements(); super.removeAll(); } protected void onColorsChanged(boolean colorsChanged) { super.onColorsChanged(colorsChanged); if (colorsChanged) { cursorColor = Color.brighter(back1,48); if (cursorColor == Color.WHITE) cursorColor = Color.BRIGHT; mixedCursorColor = Color.interpolate(back1,cursorColor); } } /** Draw all selected indexes */ protected void drawSelectedItem(Graphics g, int from, int to) { drawingSel = true; for (int i = from; i < to; i++) if (selectedIndexes.exists(i)) drawCursor(g,i,true); drawingSel = false; if (Settings.keyboardFocusTraversable) super.drawSelectedItem(g, from, to); } protected void drawItems(Graphics g, int dx, int dy, int greatestVisibleItemIndex) { for (int i = offset; i < greatestVisibleItemIndex; dy += getItemHeight(i++)) if (!selectedIndexes.exists(i)) drawItem(g,i,dx,dy); // guich@200b4: let the user extend ListBox and draw the items himself drawSelectedItem(g, offset, greatestVisibleItemIndex); } protected int getCursorColor(int index) { boolean exists = selectedIndexes.exists(index); if (Settings.keyboardFocusTraversable && !drawingSel) { if (!hasFocus) { if (!exists) return back0; } else if (index == selectedIndex && exists) return mixedCursorColor; } else if (!exists) return back1; return cursorColor; } /** In this MultiListBox, inverts the status of the given index, or clears all if i is -1. * @see #setSelectedIndex(int, boolean) */ public void setSelectedIndex(int index) { if (!Settings.keyboardFocusTraversable) handleClick(index); if (index == -1) { oldSelection = NO_OLD_SELECTION; // no old selection after unselecting everything selectedIndexes.clear(); if (order != null) order.removeAllElements(); } super.setSelectedIndex(index,false); } protected void leftReached() { handleClick(selectedIndex); } /** Sets or clear an index. Can also be used to set or clear all indexes, passing -1 as the index. * Both operations are limited by the defined max selections. */ public void setSelectedIndex(int index, boolean set) { if (0 <= index && index < itemCount) { if (set != isSelected(index)) setSelectedIndex(index); } else // clear or set all if (index < 0) { if (!set) setSelectedIndex(-1); else { selectedIndexes.clear(); if (order != null) order.removeAllElements(); if (unselectFirstWhenMaxIsReached && order != null && selectedIndexes.size() == maxSelections) setSelectedIndex(order.items[0], false); int n = Math.min(itemCount, maxSelections); for (int i = 0; i < n; i++) { selectedIndexes.put(i, selectedIndexes.size()); if (order != null) order.addElement(i); } } oldSelection = NO_OLD_SELECTION; } Window.needsPaint = true; } protected void handleSelection(int newSelection) { if (newSelection != oldSelection && newSelection < itemCount) // then they dragged outside the current selection { handleClick(newSelection); oldSelection = selectedIndex = newSelection; drawCursor(getGraphics(),newSelection,selectedIndexes.exists(newSelection)); } } /** Called by ComboBoxDropDown when its being popped. */ protected void cbddOnPopup() { changed = false; } /** Called by ComboBoxDropDown when its being unpopped. Sends a PRESSED event if a change was made in the selected indexes. */ protected void cbddOnUnpop() { if ((Settings.geographicalFocus || !Settings.keyboardFocusTraversable) && changed) // avoid duplicated event super.postPressedEvent(); } public void postPressedEvent() { if (Settings.keyboardFocusTraversable || !(parent instanceof ComboBoxDropDown)) // don't close on selection, otherwise the user will not be able to select more than one item super.postPressedEvent(); } protected void endSelection() { oldSelection = NO_OLD_SELECTION; // reset this for next pen down/drag } protected void handleClick(int index) { if (index >= 0) { if (!selectedIndexes.exists(index)) // not yet selected? { if (unselectFirstWhenMaxIsReached && order != null && selectedIndexes.size() == maxSelections) setSelectedIndex(order.items[0], false); if (selectedIndexes.size() < maxSelections) { selectedIndexes.put(index,0); if (order != null) { order.addElement(index); if (showOrderInTip) showTip(this, Convert.toString(order.size()),250,getIndexY(index)); } changed = true; } } else try { selectedIndexes.remove(index); if (order != null) order.removeElement(index); changed = true; } catch (ElementNotFoundException e) {} } else { selectedIndexes.clear(); // reset everything if (order != null) order.removeAllElements(); oldSelection = NO_OLD_SELECTION; // reset this for next pen down/drag } Window.needsPaint = true; } /** Returns the String with the selected item (if single) or a string with the number of selected items. * You can change the suffix itemsText to another one. * If order is important, returns the last selected item. */ public String getText() { int size = selectedIndexes.size(); if (size <= 1) return super.getText(); if (order != null) return getLastSelectedItem().toString(); return Convert.toString(size)+itemsText; } public void onEvent(Event e) { if (e.target == this) { if (e instanceof PenEvent && Settings.keyboardFocusTraversable) // if in kft and the user clicked in the control, handle the event as if we were not in kft { Settings.keyboardFocusTraversable = false; super.onEvent(e); Settings.keyboardFocusTraversable = true; return; } switch (e.type) { case KeyEvent.KEY_PRESS: if (((KeyEvent)e).key == ' ') { setSelectedIndex(-1,selectedIndexes.size() <= 1); e.consumed = true; } // no break here! case ControlEvent.FOCUS_IN: case KeyEvent.SPECIAL_KEY_PRESS: if (!hasFocus) { changed = false; hasFocus = true; } break; case ControlEvent.HIGHLIGHT_OUT: hasFocus = false; Window.needsPaint = true; break; } } super.onEvent(e); } public Control handleGeographicalFocusChangeKeys(KeyEvent ke) { if ((ke.isPrevKey() && !ke.isUpKey()) || (ke.isNextKey() && !ke.isDownKey())) { if (!hasFocus) changed = false; hasFocus = true; int oldXOffset = xOffset; _onEvent(ke); if (oldXOffset != xOffset || ke.isPrevKey()) return this; hasFocus = false; Window.needsPaint = true; cbddOnUnpop(); // return the event return null; } if ((ke.isUpKey() && selectedIndex <= 0) || (ke.isDownKey() && selectedIndex == itemCount -1)) return null; _onEvent(ke); return this; } public void clear() { if (clearValues.isEmpty()) super.clear(); else { setSelectedIndex(-1, false); for (int i = clearValues.size(); --i >= 0;) setSelectedIndex(clearValues.items[i],true); } } }