/********************************************************************************* * TotalCross Software Development Kit * * Copyright (C) 2001 Daniel Tauchke * * Copyright (C) 2001-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.res.*; import totalcross.sys.*; import totalcross.ui.event.*; import totalcross.ui.gfx.*; import totalcross.ui.image.*; import totalcross.util.*; /** * ComboBox is an implementation of a ComboBox, with the drop down window implemented by the ComboBoxDropDown class. * <p> * Note: the color used in the setBackground method will be used in the button * only. The background color of the control will be a lighter version of the * given color. */ public class ComboBox extends Container { public static final int ARROWSTYLE_DOWNDOT = 0; public static final int ARROWSTYLE_PAGEFLIP = 1; public static int arrowStyle = ARROWSTYLE_PAGEFLIP; protected ComboBoxDropDown pop; Button btn; private boolean armed; private boolean opened; private int btnW; private int bColor, fColor; private int fourColors[] = new int[4]; private Image npback,nparmed; private int selOnPopup = -2; private boolean wasOpen; // guich@tc152: fixed ComboBox problem of clicking on an open combobox was making it open again /** If set to true, the popup window will have the height of the screen */ public boolean fullHeight; /** If set to true, the popup window will have the width of the screen */ public boolean fullWidth; // guich@550_20 /** The default value that is set to the clearValueInt of all ComboBox. * Usually this value is 0, but sometimes you may wish set it to -1, to unselect the ComboBox when clear is called. */ public static int defaultClearValueInt; /** The check color used to fill the radio button used in Android. Defaults to the fore color. * @since TotalCross 1.3 */ public int checkColor = -1; /** The title of the PopupMenu when in Android user interface style. * @since TotalCross 1.3 */ public String popupTitle; /** Parameter passed to PopupMenu. Defauts to true. * @see PopupMenu#enableSearch */ public boolean enableSearch=true; /** Set to false to don't use the PopupMenu when the user interface style is Android. * This affects all ComboBoxes. If you want to change a particular ComboBox to use the standard * popup list, but keep others with the PopupMenu, you can do something like: * <pre> * // at the begining of your program: * ComboBox.usePopupMenu = true; * // when you want to create the standalone ComboBox * ComboBox.usePopupMenu = false; // turn flag off * .. create the ComboBox * ComboBox.usePopupMenu = true; // turn flag on again * </pre> * An internal copy of the flag is set at the constructor. * @since TotalCross 1.5 */ public static boolean usePopupMenu = true; private boolean _usePopupMenu; /** Creates an empty ComboBox */ public ComboBox() { this((Object[]) null); } /** Creates a ComboBox with the given items */ public ComboBox(Object[] items) { this(new ListBox(items)); } /** * Creates a ComboBox with a PopList containing the given ListBox. You can * extend the ListBox to draw the items by yourself and use this constructor * so the PopList will use your class and not the default ListBox one. * This constructor forces the ListBox.simpleBorder to true. Note: the * listbox items must be already set. */ public ComboBox(ListBox userListBox) { this(new ComboBoxDropDown(userListBox)); // guich@340_36 } /** Constructs a ComboBox with the given PopList. */ public ComboBox(ComboBoxDropDown userPopList) // guich@340_36 { _usePopupMenu = usePopupMenu; clearValueInt = defaultClearValueInt; ignoreOnAddAgain = ignoreOnRemove = true; pop = userPopList; if (uiAndroid) { btn = new Button(getArrowImage()); btn.setBorder(Button.BORDER_NONE); btn.transparentBackground = true; } else { btn = new ArrowButton(Graphics.ARROW_DOWN, getArrowWidth(), Color.BLACK); btn.setBorder(Button.BORDER_NONE); if (uiVista) { btn.flatBackground = false; btn.setBackColor(Color.darker(backColor,32)); } } btn.focusTraversable = false; super.add(btn); started = true; // avoid calling the initUI method this.focusTraversable = true; // kmeehl@tc100 } private int getArrowWidth() { return fmH * 3 / 11; } /** Does nothing */ public void add(Control control) { } void add(Control control, boolean dummy) { super.add(control); } /** Does nothing */ public void remove(Control control) { } /** Adds an array of Objects to the Listbox */ public void add(Object[] items) { pop.lb.add(items); Window.needsPaint = true; } /** Adds an array of Objects to the Listbox */ public void add(Object[] items, int startAt, int size) { pop.lb.add(items, startAt, size); Window.needsPaint = true; } /** * Adds an Object to the Listbox. This method is very slow if used in loop; use the * <code>add(Object[])</code> to add a bunch of objects instead. */ public void add(Object item) { pop.lb.add(item); Window.needsPaint = true; } /** Adds the given text to this ListBox, breaking the text if it goes beyond the ListBox' limits, and also breaking if it contains \n. * Returns the number of lines. Note that each part of the text is considered a new item. This method is slower than the other <code>add</code> methods. * @since TotalCross 1.24 */ public int addWrapping(String text) // guich@tc124_21 { Window.needsPaint = true; return pop.lb.addWrapping(text); } /** Adds an Object to the Listbox at the given index */ public void insert(Object item, int index) { pop.lb.insert(item, index); Window.needsPaint = true; } /** Empties the ListBox */ public void removeAll() // guich@210_13 { pop.lb.removeAll(); Window.needsPaint = true; } /** Removes an Object from the Listbox */ public void remove(Object item) { pop.lb.remove(item); Window.needsPaint = true; } /** Removes an Object from the Listbox at the given index. */ public void remove(int itemIndex) { pop.lb.remove(itemIndex); Window.needsPaint = true; } /** Sets the Object at the given Index, starting from 0 */ public void setItemAt(int i, Object s) { pop.lb.setItemAt(i, s); Window.needsPaint = true; } /** Get the Object at the given Index */ public Object getItemAt(int i) { return pop.lb.getItemAt(i); } /** Returns the selected item of the ListBox */ public Object getSelectedItem() { return pop.lb.getSelectedItem(); } /** Returns the position of the selected item of the ListBox */ public int getSelectedIndex() { return pop.lb.selectedIndex; } /** Returns all items in this ComboBox */ public Object[] getItems() { return pop.lb.getItems(); } /** Returns the index of the item specified by the name */ public int indexOf(Object name) { return pop.lb.indexOf(name); } /** * Sets the cursor color for this ComboBox. The default is equal to the * background slightly darker. */ public void setCursorColor(int color) // guich@210_19 { pop.lb.setCursorColor(color); } /** * Selects an item. If the name is not found, the currently selected item is * not changed. * * @since SuperWaba 4.01 */ public void setSelectedItem(Object name) // guich@401_25 { int idx = pop.lb.selectedIndex; pop.lb.setSelectedItem(name); if (Settings.sendPressEventOnChange && pop.lb.selectedIndex != idx) postPressedEvent(); Window.needsPaint = true; } /** * Select the given index. Choose "-1" if you want to blank the ComboBox view * box. */ public void setSelectedIndex(int i) { setSelectedIndex(i,Settings.sendPressEventOnChange); } /** * Select the given index, and optionally sends a PRESSED event. Choose "-1" if you want to blank the ComboBox view * box. */ public void setSelectedIndex(int i, boolean sendPressEvent) { int idx = pop.lb.selectedIndex; pop.lb.setSelectedIndex(i); if (sendPressEvent && pop.lb.selectedIndex != idx) postPressedEvent(); Window.needsPaint = true; } /** Returns the number of items */ public int size() { return pop.lb.itemCount; } public int getPreferredWidth() { return pop.getPreferredWidth() + 1 + insets.left+insets.right + (Settings.fingerTouch ? btn.getPreferredWidth()+4 : 0); } private Image getArrowImage() { Image img; Image img0 = arrowStyle == ARROWSTYLE_PAGEFLIP ? Resources.comboArrow2 : Resources.comboArrow; int s = fmH; try { img = arrowStyle == ARROWSTYLE_PAGEFLIP ? img0.smoothScaledFixedAspectRatio(s,false) : img0.getSmoothScaledInstance(s,s); img.applyColor2(backColor); } catch (ImageException e) { img = img0; } return img; } public int getPreferredHeight() { return (pop.lb.itemCount > 0 && !isSupportedListBox() ? pop.lb.getItemHeight(0) : fmH) + Edit.prefH + insets.top+insets.bottom; } /** Passes the font to the pop list */ protected void onFontChanged() // guich@200b4_153 { if (pop != null) pop.setFont(font); // guich@tc100b3: resize the arrow based on the font. boolean uiAndroid = Control.uiAndroid; if (!uiAndroid) { ArrowButton ab = (ArrowButton)btn; int newWH = getArrowWidth(); if (ab.prefWH != newWH) { ab.prefWH = newWH; onBoundsChanged(false); } } } /** Sets the ihtForeColors and ihtBackColors for the ListBox used with this ComboBox. * Note that null is a valid value, used when you always want to use the default color. * @since TotalCross 1.0 beta 4 */ public void setBackForeItemColors(IntHashtable ihtFore, IntHashtable ihtBack) { pop.lb.ihtForeColors = ihtFore; pop.lb.ihtBackColors = ihtBack; } protected void onBoundsChanged(boolean screenChanged) { btnW = btn.getPreferredWidth(); switch (Settings.uiStyle) { case Settings.Android: btn.setImage(getArrowImage()); if (arrowStyle == ARROWSTYLE_PAGEFLIP) btn.setRect(width - btnW - 1, height-fmH-2, btnW, fmH,null,screenChanged); else btn.setRect(width - btnW - 3, 2, btnW, height,null,screenChanged); break; default: // guich@573_6: both Flat and Vista use this btn.setRect(width - btnW - 3, 1, btnW + 2, height - 2, null, screenChanged); break; } if (screenChanged && pop.isVisible()) // guich@tc100b4_29: reposition the pop too if its visible updatePopRect(); npback = nparmed = null; } public void onEvent(Event event) { PenEvent pe; boolean inside; if (opened && event.type != ControlEvent.WINDOW_CLOSED) return; if (isEnabled()) switch (event.type) { case PenEvent.PEN_DRAG: pe = (PenEvent) event; inside = isInsideOrNear(pe.x, pe.y); if (event.target == this && inside != armed && pop.lb.itemCount > 0) { armed = inside; if (uiAndroid) Window.needsPaint = true; // guich@580_25: just call repaint instead of drawing the cursor else btn.press(armed); } break; case PenEvent.PEN_DOWN: if (event.target == this && !armed && pop.lb.itemCount > 0) { wasOpen = false; if (uiAndroid) Window.needsPaint = true; // guich@580_25: just call repaint instead of drawing the cursor else btn.press(true); armed = true; } break; case PenEvent.PEN_UP: pe = (PenEvent) event; if (event.target == this && !wasOpen && (armed || isActionEvent(event))) { if (uiAndroid) Window.needsPaint = true; // guich@580_25: just call repaint instead of drawing the cursor else btn.press(false); armed = false; inside = isInsideOrNear(pe.x, pe.y); // is a method of class Control that uses absolute coords if (inside && (!Settings.fingerTouch || !hadParentScrolled())) { opened = true; selOnPopup =pop.lb.selectedIndex; popup(); } } break; case ControlEvent.WINDOW_CLOSED: if (event.target == pop) // an item was selected? { wasOpen = true; opened = false; boolean isMulti = pop.lb instanceof MultiListBox; if (pop.lb.selectedIndex >= 0 && ((!isMulti && (!Settings.sendPressEventOnChange || pop.lb.selectedIndex != selOnPopup)) || (isMulti && ((MultiListBox)pop.lb).changed))) postPressedEvent(); selOnPopup = -2; } break; case ControlEvent.PRESSED: if (event.target == btn && pop.lb.itemCount > 0) { btn.appId = this.appId; // guich@502_3: make the button have the same appid than us. event.consumed = true; // kevinsmotherman@450_22: avoid passing this to the other controls. popup(); } break; case ControlEvent.FOCUS_IN: case ControlEvent.FOCUS_OUT: if (event.target == btn) // guich@240_11: change the target focus so parents can catch the focus_in and focus_out event event.target = this; break; case KeyEvent.ACTION_KEY_PRESS: btn.simulatePress(); popup(); break; } } protected void updatePopRect() { Rect r = getAbsoluteRect(); pop.fullHeight = fullHeight; // guich@330_52 pop.fullWidth = fullWidth; pop.setRect(r.x, r.y, width, height); } private boolean isSupportedListBox() { String cl = pop.lb.getClass().getName(); return cl.equals("totalcross.ui.ListBox") || cl.equals("litebase.ui.DBListBox"); } /** Pops up the ComboBoxDropDown */ public void popup() { requestFocus(); // guich@240_6: avoid opening the combobox when its popped up and the user presses the arrow again - guich@tc115_36: moved from the event handler to here boolean isMultiListBox = pop.lb instanceof MultiListBox; if (uiAndroid && _usePopupMenu && !isMultiListBox && isSupportedListBox()) // we don't support yet user-defined ListBox types yet { if (pop.lb.itemCount == 0) opened = false; else try { PopupMenu pm = new PopupMenu(popupTitle != null ? popupTitle : "("+pop.lb.size()+")",pop.lb.getItemsArray(), isMultiListBox); pm.makeUnmovable(); pm.enableSearch = enableSearch; pm.itemCount = pop.lb.size(); pm.dataCol = pop.lb.dataCol; pm.checkColor = checkColor; pm.setBackForeColors(pop.lb.backColor,pop.lb.foreColor); pm.setCursorColor(pop.lb.back1); pm.setSelectedIndex(pop.lb.selectedIndex); pm.setFont(this.font); if (pm.itemCount > 100) Flick.defaultLongestFlick = pm.itemCount > 1000 ? 9000 : 6000; pm.popup(); Event.clearQueue(PenEvent.PEN_UP); // prevent problem when user selects an item that is at the top of this ComboBox Flick.defaultLongestFlick = 2500; opened = false; int sel = pm.getSelectedIndex(); if (sel != -1) { pop.lb.selectedIndex = sel; if (sel != -1) postPressedEvent(); Window.needsPaint = true; } } catch (Exception e) { e.printStackTrace(); } } else { updatePopRect(); if (pop.lb.hideScrollBarIfNotNeeded()) // guich@tc115_77 updatePopRect(); // guich@320_17 if (!isMultiListBox) { int sel = pop.lb.selectedIndex; pop.lb.selectedIndex = -2; pop.lb.setSelectedIndex(sel); } pop.popupNonBlocking(); pop.lb.requestFocus(); isHighlighting = false; // kmeehl@tc100: allow immediate keyboard navigation of the dropdown } } /** Unpops the ComboBoxDropDown. * @since TotalCross 1.2 */ public void unpop() // guich@tc120_64 { if (pop.isVisible()) pop.unpop(); } protected void onColorsChanged(boolean colorsChanged) { npback = nparmed = null; bColor = UIColors.sameColors ? backColor : Color.brighter(getBackColor()); // guich@572_15 fColor = getForeColor(); if (colorsChanged) { if (uiAndroid) btn.setImage(getArrowImage()); else { btn.setBackForeColors(uiVista ? Color.darker(bColor,32) : uiFlat ? bColor : backColor, foreColor); ((ArrowButton)btn).arrowColor = fColor; } pop.lb.setBackForeColors(backColor, foreColor); } if (!uiAndroid) Graphics.compute3dColors(isEnabled(), backColor, foreColor, fourColors); } /** paint the combo's border and the current selected item */ public void onPaint(Graphics g) { boolean enabled = isEnabled(); // guich@200b4_126: repaint the background. if (!transparentBackground) // guich@tc115_18 if (!uiAndroid && uiVista && enabled) // guich@573_6 g.fillVistaRect(0, 0, width, height, bColor, false, false); else { g.backColor = uiAndroid ? parent.backColor : bColor; g.fillRect(0, 0, width, height); } if (uiAndroid) try { if (npback == null) npback = NinePatch.getInstance().getNormalInstance(NinePatch.COMBOBOX, width, height, enabled ? bColor : Color.interpolate(bColor,parent.backColor), false); if ((armed || btn.armed) && nparmed == null) nparmed = npback.getTouchedUpInstance((byte)25,(byte)0); Image img = armed || btn.armed ? nparmed : npback; g.drawImage(img, 0,0); // Graphics gg = img.getGraphics(); // g.fillShadedRect(width-btnW-5,1,1,height-3,true,false,gg.getPixel(width/2,1),gg.getPixel(width/2,height-3),30); // draw the line - TODO: fix if this is inside a ScrollContainer (see Button.onPaint) g.setClip(2,2,width-btnW-(arrowStyle == ARROWSTYLE_PAGEFLIP ? 0 : 8),height-4); } catch (ImageException e) {e.printStackTrace();} else { g.draw3dRect(0, 0, width, height, Graphics.R3D_CHECK, false, false, fourColors); g.setClip(2, 2, width - btnW - 3, height - 4); } if (pop.lb.itemCount > 0 && pop.lb.selectedIndex >= 0) // guich@402_31: avoid drawing invalid index drawSelectedItem(g); } protected void drawSelectedItem(Graphics g) { g.foreColor = fColor; boolean trickW = pop.lb.width == 0; // guich@tc125_35 if (trickW) pop.lb.width = width - btnW; int ih = pop.lb.itemCount > 0 ? pop.lb.getItemHeight(0) : fmH; boolean isString = pop.lb.itemCount > 0 && pop.lb.items.items[0] instanceof String; pop.lb.drawSelectedItem(g, pop.lb.selectedIndex, 3, height == fmH+Edit.prefH ? Edit.prefH/2 : isString && uiAndroid ? (height-(fmH+Edit.prefH))/2 : (height - ih)/2); // guich@200b4: let the listbox draw the item if (trickW) pop.lb.width = 0; } /** Sorts the items of this combobox, and then unselects the current item. */ public void qsort() // guich@220_35 { pop.lb.qsort(); setSelectedIndex(-1); } /** Sorts the elements of this ListBox. The current selection is cleared. * @param caseless Pass true to make a caseless sort, if the items are Strings. */ public void qsort(boolean caseless) // guich@tc113_5 { pop.lb.qsort(caseless); setSelectedIndex(-1); } /** * Adds support for horizontal scroll on this listbox. Two buttons will * appear below the vertical scrollbar. The add, replace and remove * operations will be a bit slower because the string's width will have to be * computed in order to correctly set the maximum horizontal scroll. * * @since SuperWaba 5.6 */ public void enableHorizontalScroll() // guich@560_9 { pop.lb.enableHorizontalScroll(); } /** * Selects the last item added to this combobox, doing a scroll if needed. * Calls repaintNow. * * @since SuperWaba 5.6 */ public void selectLast() { pop.lb.selectLast(); repaintNow(); } /** Clears this control, selecting index clearValueInt (0 by default). */ public void clear() // guich@572_19 { setSelectedIndex(clearValueInt); } public void getFocusableControls(Vector v) // kmeehl@tc100 { if (visible && isEnabled()) v.addElement(this); } public Control handleGeographicalFocusChangeKeys(KeyEvent ke) // kmeehl@tc100 { if (armed) { _onEvent(ke); return this; } return null; } /** Returns the ListBox used when this combobox is opened. */ public ListBox getListBox() { return pop.lb; } /** Selects the item that starts with the given text * @param text The text string to search for * @param caseInsensitive If true, the text and all searched strings are first converted to lowercase. * @return If an item was found and selected. * @since TotalCross 1.13 */ public boolean setSelectedItemStartingWith(String text, boolean caseInsensitive) // guich@tc113_2 { int idx = pop.lb.selectedIndex; boolean b = pop.lb.setSelectedItemStartingWith(text, caseInsensitive); if (Settings.sendPressEventOnChange && pop.lb.selectedIndex != idx) postPressedEvent(); return b; } /** Replaces the original ComboBoxDropDown by the given one. * @since TotalCross 1.5 */ public void setPop(ComboBoxDropDown lb) { pop = lb; setSelectedIndex(-1); } }