/********************************************************************************* * TotalCross Software Development Kit * * Copyright (C) 2000-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.dialog.*; import totalcross.ui.event.*; import totalcross.ui.gfx.*; import totalcross.util.*; /** Creates a control with two arrows, so you can scroll values and show * the current one. * It supports auto-scroll (by clicking and holding) and can also * dynamically compute the items based on ranges. * * The SpinList can be horizontal or vertical. You can use something like: * <pre> * SpinList sl = new SpinList(..., !Settings.fingerTouch); * </pre> * This way, in finger-touch devices, it will use the horizontal appearance, * which is easier to deal on such devices. */ public class SpinList extends Control { protected String []choices; protected int selected; protected TimerEvent timer; /** Timer interval in which the scroll will be done. */ public int timerInterval=300; /** Number of ticks of the timer interval that will be waiten until the scroll starts. */ public int timerInitialDelay = 3; /** The horizontal text alignment of the SpinList: LEFT, CENTER or RIGHT */ public int hAlign = LEFT; private boolean goingUp; private int tick; private boolean isVertical; /** Set to true if there are only numbers in the SpinList and you want to open a NumericBox to let the user * enter a value. The SpinList is divided into 3 areas: left (decrease), right (increase), middle (opens the NumericBox). * Works only when isVertical is false. * The area that pops up the NumericBox is drawn in a darker background. * @since TotalCross 1.5 * * Setting this to true shows a numeric box. */ public boolean useNumericBox; /** Set to true if there are only numbers in the SpinList and you want to open a NumericBox to let the user * enter a value. The SpinList is divided into 3 areas: left (decrease), right (increase), middle (opens the NumericBox). * Works only when isVertical is false. * The area that pops up the NumericBox is drawn in a darker background. * * Setting this to true shows a calculator box. * @since TotalCross 1.53 */ public boolean useCalculatorBox; /** Set to false to disallow the wrap around that happens when the user is at the first or last items. * @since TotalCross 2.0 */ public boolean wrapAround = true; /** By default, equals the choices' length. You can define its length and then create a single array shared * by a set of SpinLists with different lengths on each SpinList. */ public int choicesLen; /** Constructs a vertical SpinList with the given choices, selecting index 0 by default. * @see #setChoices */ public SpinList(String[] choices) throws InvalidNumberException { this(choices, true); } /** Constructs a vertical SpinList with the given choices, selecting index 0 by default. * @see #setChoices */ public SpinList(String[] choices, boolean isVertical) throws InvalidNumberException { this.isVertical = isVertical; setChoices(choices); } public int getPreferredWidth() { int w=fm.getMaxWidth(choices,0,choicesLen); if (w == 0) return Settings.screenWidth/2; int aw = getArrowHeight() * 2; return isVertical ? w + aw : w + aw+2; } public int getPreferredHeight() { return fmH + Edit.prefH; } /** Sets the choices to the given ones. Searches for [i0,if] and then expands the items. * For example, passing some string as "Day [1,31]" will expand that to an array of * <code>"Day 1","Day 2",...,"Day 31"</code>. */ public void setChoices(String []choices) throws InvalidNumberException { if (choices == null) choices = new String[]{""}; else { this.choicesLen = choices.length; Vector v = new Vector(choicesLen+10); for (int i =0; i < choicesLen; i++) if (choices[i].indexOf('[') != -1) expand(v,choices[i]); else v.addElement(choices[i]); if (choicesLen != v.size()) choices = (String[])v.toObjectArray(); } this.choices = choices; this.choicesLen = choices.length; selected = 0; Window.needsPaint = true; } /** Just replaces the choices array. */ public void replaceChoices(String []choices) throws InvalidNumberException { this.choices = choices; this.choicesLen = choices.length; if (selected >= choicesLen) selected = choicesLen; } /** Expands the items in the format "prefix [start,end] suffix", where prefix and suffix are optional. * For example, passing some string as "Day [1,31]" will expand that to an array of * <code>"Day 1","Day 2",...,"Day 31"</code>. */ public static void expand(Vector v, String str) throws InvalidNumberException { int ini = str.indexOf('['); int fim = str.indexOf(']'); String prefix = str.substring(0,ini); String sufix = str.substring(fim+1); int j; int start = Convert.toInt(str.substring(ini+1,j=str.indexOf(',',ini+1))); int end = Convert.toInt(str.substring(j+1,fim)); for (int k =start; k <= end; k++) v.addElement(prefix+k+sufix); } /** Returns the choices array, after the expansion (if any). */ public String[] getChoices() { return choices; } /** Returns the selected item. */ public String getSelectedItem() { return choices[selected]; } /** Returns the selected index. */ public int getSelectedIndex() { return selected; } /** Sets the selected item; -1 is NOT accepted. */ public void setSelectedIndex(int i) { if (0 <= i && i < choicesLen && selected != i) { selected = i; Window.needsPaint = true; if (Settings.sendPressEventOnChange) postPressedEvent(); } } /** Selects the given item. If the item is not found, the selected index remains unchanged. */ public void setSelectedItem(String item) { setSelectedIndex(indexOf(item)); } /** Removes the item at the given index. */ public String removeAt(int index) { String ret = choices[index]; int last = choicesLen-1; String []ch = new String[last]; Vm.arrayCopy(choices,0,ch,0,index); if (index < last) Vm.arrayCopy(choices,index+1,ch,index,last-index); else selected--; this.choices = ch; Window.needsPaint = true; return ret; } /** Removes the current item */ public String removeCurrent() { return removeAt(selected); } /** Returns the index of the given item. */ public int indexOf(String elem) { for (int i = 0; i < choicesLen; i++) if (choices[i].equals(elem)) return i; return -1; } /** Inserts the given element in order (based in the assumption that the original choices was ordered). */ public void insertInOrder(String elem) { // find the correct position to insert int index = 0; while (index < choicesLen && elem.compareTo(choices[index]) > 0) index++; if (index == choicesLen || !elem.equals(choices[index])) { String []ch = new String[choicesLen+1]; Vm.arrayCopy(choices,0,ch,0,index); ch[index] = elem; if (index < choicesLen) Vm.arrayCopy(choices,index,ch,index+1,choicesLen-index); choices = ch; selected = index; Window.needsPaint = true; } } private int getArrowHeight() { return isVertical ? 4*fmH/11 : fmH/2; } public void onPaint(Graphics g) { g.backColor = backColor; // guich@341_3 g.fillRect(0,0,width,height); int fore = isEnabled() ? foreColor : Color.getCursorColor(foreColor); g.foreColor = fore; int yoff = (height - fmH) / 2 + 1; int wArrow = getArrowHeight(); String s = choicesLen > 0 ? choices[selected] : ""; if (isVertical) { g.drawArrow(0,yoff,wArrow,Graphics.ARROW_UP,false,fore); g.drawArrow(0,yoff+height/2,wArrow,Graphics.ARROW_DOWN,false,fore); if (choicesLen > 0) g.drawText(choices[selected],hAlign==LEFT?wArrow*2:hAlign==RIGHT?width-fm.stringWidth(s):(width-fm.stringWidth(s))/2,yoff-1, textShadowColor != -1, textShadowColor); } else { if (useNumericBox || useCalculatorBox) { g.backColor = Color.darker(g.backColor,16); g.fillRect(width/3,0,width/3,height); } g.drawArrow(0,yoff,wArrow,Graphics.ARROW_LEFT,false,fore); g.drawArrow(width-wArrow,yoff,wArrow,Graphics.ARROW_RIGHT,false,fore); if (choicesLen > 0) g.drawText(choices[selected],hAlign==LEFT?wArrow:hAlign==RIGHT?width-fmH/2-1-fm.stringWidth(s):(width-fm.stringWidth(s))/2,yoff-1, textShadowColor != -1, textShadowColor); } } private void scroll(boolean up, boolean doPostEvent) { int max = choicesLen-1; if (!wrapAround && ((up && selected == 0) || (!up && selected == max))) return; if (up) { selected--; if (selected < 0) selected = max; } else { selected++; if (selected > max) selected = 0; } Window.needsPaint = true; if (doPostEvent) postPressedEvent(); } public void onEvent(Event event) { switch (event.type) { case KeyEvent.KEY_PRESS: { KeyEvent ke = (KeyEvent)event; int key = ke.key; if (key == ' ') selected = 0; // restart a search else { key = Convert.toLowerCase((char)key); // converts to uppercase for (int i =0; i < choicesLen; i++) if (choices[i].charAt(0) == (char)key) { selected = i; Window.needsPaint = true; break; } } break; } case KeyEvent.SPECIAL_KEY_PRESS: { KeyEvent ke = (KeyEvent)event; if (Settings.keyboardFocusTraversable && ke.isActionKey()) // guich@550_15 postPressedEvent(); else if (ke.isUpKey()) scroll(true,!Settings.keyboardFocusTraversable); else if (ke.isDownKey()) scroll(false,!Settings.keyboardFocusTraversable); break; } case PenEvent.PEN_DOWN: { PenEvent pe = (PenEvent)event; goingUp = isVertical ? pe.y > height/2 : pe.x < width/2; if (!Settings.fingerTouch) doScroll((PenEvent)event); if (timer == null) { tick = 0; timer = addTimer(timerInterval); } break; } case PenEvent.PEN_UP: { PenEvent pe = (PenEvent)event; stopTimer(); if (Settings.fingerTouch && !hadParentScrolled()) { if (!isVertical && (useNumericBox || useCalculatorBox) && width/3 <= pe.x && pe.x <= 2*width/3) { CalculatorBox nb = new CalculatorBox(useCalculatorBox); nb.cOrigDefault = this; if (useNumericBox) nb.maxLength = Math.max(choices[0].length(),choices[choicesLen-1].length()); nb.popup(); } else doScroll((PenEvent)event); } break; } case TimerEvent.TRIGGERED: if (hadParentScrolled() || !isTopMost()) stopTimer(); else if (timer.triggered && tick++ > timerInitialDelay) scroll(goingUp,!Settings.keyboardFocusTraversable); break; } } private void doScroll(PenEvent pe) { if (!isVertical || pe.x < getArrowHeight()*2) { goingUp = isVertical ? pe.y > height/2 : pe.x < width/2; scroll(goingUp,true); } } private void stopTimer() { if (timer != null) { removeTimer(timer); timer = null; } } /** Clears this control, selecting element clearValueInt. */ public void clear() { setSelectedIndex(clearValueInt); } public Control handleGeographicalFocusChangeKeys(KeyEvent ke) { if (!ke.isUpKey() && !ke.isDownKey()) return null; _onEvent(ke); return this; } }