/********************************************************************************* * TotalCross Software Development Kit * * 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.sys.Settings; import totalcross.ui.event.ControlEvent; import totalcross.ui.event.PressListener; import totalcross.ui.gfx.Color; import totalcross.ui.image.Image; import totalcross.ui.image.ImageException; /** This class adds a multi-button menu that can be scrolled horizontally (single-row) or vertically (multiple-rows), * using a scrollbar or flicking. * * The buttons can have almost all properties present in the Button class, like: * <ul> * <li> textPosition * <li> borderType * <li> cornerRadius3DG * <li> borderWidth3DG * <li> borderColor3DG * <li> topColor3DG * <li> bottomColor3DG * </ul> * There are also other properties that can be set, like: * <ul> * <li> textGap * <li> buttonVertGap * <li> buttonHorizGap * <li> imageSize * <li> borderGap * </ul> * The sizes above are not in pixels, but in percentage of the font's height. So, a value of 25 means * 25% of the font's height, or 1/4; 150 means 150% of the font's height, or 1.5x; and so on. * This enabled the gaps be constant in physical inches no matter the screen DPI or resolution. * * See the AndroidUI program for a sample of how to use this class. * * @see #pagePositionDisposition * @since TotalCross 1.3 */ public class ButtonMenu extends ScrollContainer implements PressListener { private Button[] btns; private Image[] images; private String[] names; private int disposition; private int prefBtnW,prefBtnH; private int selected; private Spacer spacer; /** The control that keeps track of the current position. */ protected PagePosition pagepos; /** The gap between the image and the text, in percentage of the font's height. * Defaults to 25 (%). */ public int textGap=25; /** The gap between two vertical buttons, in percentage of the font's height. * Also used as gap between the button and the ButtonMenu's border. * Defaults to 100 (%). */ public int buttonVertGap=100; /** The gap between two horizontal buttons, in percentage of the font's height. * Also used as gap between the button and the ButtonMenu's border. * Defaults to 100 (%). */ public int buttonHorizGap=100; /** The size of the image, in percentage of the font's height. * Defaults to 200 (%). Set to -1 to keep the original size. */ public int imageSize=200; /** The gap between the text or image and the button borders, in percentage of the font's height. Defaults to 10 (%). */ public int borderGap = 10; /** Used in the pagePositionDisposition. Place it at bottom. */ public final static int PAGEPOSITION_AT_BOTTOM = 0; /** Used in the pagePositionDisposition. Place it at top. */ public final static int PAGEPOSITION_AT_TOP = 1; /** Used in the pagePositionDisposition. Don't use a PagePosition, use the ScrollPosition instead. */ public final static int NO_PAGEPOSITION = 2; /** If disposition is a MULTIPLE_HORIZONTAL, set how the PagePosition will replace the ScrollPosition control. * @see #PAGEPOSITION_AT_BOTTOM * @see #PAGEPOSITION_AT_TOP * @see #NO_PAGEPOSITION */ public int pagePositionDisposition; // default at bottom // fields that will be passed to all buttons created. /** Where to place the text (supports only LEFT, TOP, RIGHT, BOTTOM, CENTER - no adjustments!). * Also supports RIGHT_OF (relativeToText is computed automatically). Defaults to CENTER. */ public int textPosition = CENTER; /** @see Button#setBorder(byte) */ public byte borderType = Button.BORDER_3D; /** @see Button#cornerRadius3DG */ public int cornerRadius3DG = 10; /** @see Button#borderWidth3DG */ public int borderWidth3DG = 2; /** @see Button#borderColor3DG */ public int borderColor3DG = 0x00108A; /** @see Button#topColor3DG */ public int topColor3DG = 0xDCDCFF; /** @see Button#bottomColor3DG */ public int bottomColor3DG = Color.BLUE; /** @see Button#setPressedColor(int) */ public int pressedColor = -1; /** Used in the disposition member of the constructor. The menu will have a single column and multiple rows and will scroll vertically. */ public static final int SINGLE_COLUMN = 1; /** Used in the disposition member of the constructor. The menu will have a single row and multiple columns and will scroll horizontally. */ public static final int SINGLE_ROW = 2; /** Used in the disposition member of the constructor. The menu will have multiple columns and rows and will scroll horizontally. */ public static final int MULTIPLE_HORIZONTAL = 3; /** Used in the disposition member of the constructor. The menu will have multiple columns and rows and will scroll vertically. */ public static final int MULTIPLE_VERTICAL = 4; /** Constructs an ButtonMenu with the giving images and no names. * @see #SINGLE_COLUMN * @see #SINGLE_ROW * @see #MULTIPLE_HORIZONTAL * @see #MULTIPLE_VERTICAL */ public ButtonMenu(Image[] images, int disposition) { this(images,null,disposition); } /** Constructs an ButtonMenu with the giving names and no images. * @see #SINGLE_COLUMN * @see #SINGLE_ROW * @see #MULTIPLE_HORIZONTAL * @see #MULTIPLE_VERTICAL */ public ButtonMenu(String[] names, int disposition) { this(null,names,disposition); } /** Constructs an ButtonMenu with the giving images ant names. * @see #SINGLE_COLUMN * @see #SINGLE_ROW * @see #MULTIPLE_HORIZONTAL * @see #MULTIPLE_VERTICAL */ public ButtonMenu(Image[] images, String[] names, int disposition) { super(disposition == SINGLE_ROW || disposition == MULTIPLE_HORIZONTAL, disposition == SINGLE_COLUMN || disposition == MULTIPLE_VERTICAL); this.images = images; this.names = names; this.disposition = disposition; } /** Changes all the buttons to the given parameters. If you don't want to set the images or the names, pass null * in the proper place. Calls onFontChanged and initUI to reset the buttons. * @since TotalCross 1.66 */ public void replaceWith(Image[] images, String[] names) { this.images = images; this.names = names; prefBtnW = 0; if (btns != null && btns[0].isChildOf(this)) // if button was already added to this container, remove it (may occur during rotation) for (int i = btns.length; --i >= 0;) { Button b = btns[i]; b.removePressListener(this); b.getParent().remove(b); } btns = null; reposition(); } /** Returns the button at the given index. You may customize it. * @since TotalCross 2.0 */ public Button getButton(int idx) { return btns[idx]; } /** Creates and resizes all Button and images. For better performance, call setFont for this control BEFORE * calling add or setRect (this is a general rule for all other controls as well). */ public void onFontChanged() { // compute the maximum width and height for the buttons. int n = images != null ? images.length : names.length; int imageS = fmH*imageSize/100; if (btns == null) btns = new Button[n]; else for (int i = btns.length; --i >= 0;) { btns[i].removePressListener(this); btns[i] = null; // release memory } // if border type is RIGHT_OF, compute the biggest string String relativeToText = null; if (textPosition == RIGHT_OF && names != null) { int maxW = fm.stringWidth(relativeToText = names[0]); for (int i = names.length; --i >= 1;) { int m = fm.stringWidth(names[i]); if (m > maxW) { maxW = m; relativeToText = names[i]; } } } prefBtnH = prefBtnW = 0; int tg = fmH*textGap/100; for (int i = n; --i >= 0;) { Image img = images == null ? null : images[i]; String name = names == null ? null : names[i]; if (img != null && imageSize != -1 && img.getHeight() != imageS) // should we resize the image? try { img = Settings.enableWindowTransitionEffects ? img.getSmoothScaledInstance(img.getWidth() * imageS / img.getHeight(),imageS) : img.getHwScaledInstance(img.getWidth() * imageS / img.getHeight(),imageS); } catch (ImageException ie) { // just keep old image if there's no memory } Button btn = btns[i] = createButton(name, img, textPosition, tg); btn.relativeToText = relativeToText; btn.appId = i; btn.addPressListener(this); btn.borderColor3DG = borderColor3DG; btn.setBorder(borderType); // setBorder uses borderColor3DG and resets the other values to default. btn.borderColor3DG = borderColor3DG; btn.cornerRadius3DG = cornerRadius3DG; btn.borderWidth3DG = borderWidth3DG; btn.topColor3DG = topColor3DG; btn.bottomColor3DG = bottomColor3DG; if (pressedColor != -1) btn.setPressedColor(pressedColor); btn.setFont(this.font); int pw = btns[i].getPreferredWidth(); int ph = btns[i].getPreferredHeight(); if (pw > prefBtnW) prefBtnW = pw; if (ph > prefBtnH) prefBtnH = ph; } prefBtnW += borderGap*fmH/100*2; prefBtnH += borderGap*fmH/100*2; } protected Button createButton(String name, Image img, int textPosition, int tg) { return new Button(name, img, textPosition, tg); } public void initUI() { if (super.sbH != null && super.sbH instanceof ScrollPosition) ((ScrollPosition)super.sbH).barColor = pressedColor != -1 ? pressedColor : foreColor; if (super.sbV != null && super.sbV instanceof ScrollPosition) ((ScrollPosition)super.sbV).barColor = pressedColor != -1 ? pressedColor : foreColor; if (btns != null && btns[0].isChildOf(this)) // if button was already added to this container, remove it (may occur during rotation) for (int i = btns.length; --i >= 0;) btns[i].getParent().remove(btns[i]); if (prefBtnW == 0) onFontChanged(); if (spacer != null) { remove(spacer); spacer = null; } if (pagepos != null) { pagepos.parent.remove(pagepos); pagepos = null; } int n = images != null ? images.length : names.length; int hgap = fmH*buttonHorizGap/100; int vgap = fmH*buttonVertGap/100; int imageW0 = prefBtnW; int imageH0 = prefBtnH; if (height-vgap < imageH0+vgap) vgap += imageH0-height; if (width-hgap < imageW0+hgap) hgap += imageW0-width; int imageW = imageW0 + hgap; int imageH = imageH0 + vgap; int pageW = width-hgap; int pageH = height-vgap; int cols = disposition == SINGLE_COLUMN ? 1 : pageW / imageW; imageW = pageW / cols; cols = disposition == SINGLE_ROW ? n : pageW / imageW; imageW0 = imageW - hgap; int pages = 0; int colsPerPage = (width-hgap)/imageW; int rowsPerPage = (height-vgap)/imageH; if (rowsPerPage == 0) rowsPerPage = 1; if (disposition == MULTIPLE_VERTICAL && height/imageH > rowsPerPage) // guich: prevent problem when the gap is too high and just a few buttons are shown { vgap = height % imageH; rowsPerPage++; pageH = height-vgap; imageH = imageH0 + vgap; } if (disposition == MULTIPLE_HORIZONTAL) { int rows = pageH / imageH; if (rows == 0) rows = 1; cols = n / rows; if ((n % rows) != 0) cols++; pages = cols / colsPerPage; if ((cols % colsPerPage) != 0) pages++; } int x = LEFT+hgap,x0=x; int y = TOP+vgap; int maxX2=0; int difX = width - (hgap + imageW * colsPerPage); int difY = height -(vgap + imageH * rowsPerPage); int itemsPerPage = rowsPerPage * colsPerPage; int divX = difX / colsPerPage; int remX = difX % colsPerPage; int divY = difY / rowsPerPage; int remY = difY % rowsPerPage; for (int i = 0,c=0; i < n; i++) { // make sure that the page has the exact width and height if (disposition == MULTIPLE_HORIZONTAL) x += (i % colsPerPage) == 0 ? divX + remX : divX; else if (disposition == MULTIPLE_VERTICAL) y += (i % colsPerPage) == 0 ? (i % itemsPerPage) == 0 ? divY + remY : divY : 0; add(btns[i], x, y, imageW0, imageH0); int x2 = btns[i].x + btns[i].width; if (x2 > maxX2) maxX2 = x2; if (++c == cols) { x = x0; y = AFTER+vgap; c = 0; } else { x = AFTER+hgap; y = SAME; } } boolean isVertical = disposition == SINGLE_COLUMN || disposition == MULTIPLE_VERTICAL; // checks if there's enough space to fit all buttons in our height, and if there is, prevent it from scrolling int top = btns[0].y-1; int bot = 0; for (int i = Math.max(0,n - 2 * colsPerPage * rowsPerPage); i < n; i++) // guich@tc153: correctly find the max y2 { int yy = btns[i].getY2(); if (yy > bot) bot = yy; } if (isVertical) { // check if need to center horizontally int spaceAtLeft = btns[0].x; Button rbut = btns[Math.min(cols,btns.length)-1]; int spaceAtRight = width - (rbut.x + rbut.width); if (spaceAtLeft != spaceAtRight) { int inc = (spaceAtRight-spaceAtLeft)/2; for (int i =0; i < n; i++) btns[i].x += inc; } } if ((bot-top) < height) { // check how many space we have at top and bottom, and change the buttons y so they are centered vertically bot = height - bot; // how much it leaves at bottom? top = (bot-top) / 2; if (top != 0) for (int i = 0; i < n; i++) btns[i].y += top; if (isVertical) // guich@tc153: include MULTIPLE_VERTICAL return; // don't put a new spacer } boolean hasPagePosition = pagePositionDisposition != NO_PAGEPOSITION && disposition == MULTIPLE_HORIZONTAL && sbH != null && sbH instanceof ScrollPosition; if (isVertical) { int v = top; // guich@tc153: put at bottom the same of the top if (v > 0) add(spacer = new Spacer(1,v),LEFT,AFTER); } else { int v = hasPagePosition ? hgap + (maxX2%(width-hgap)) : hgap; if (v > 0) add(spacer = new Spacer(v,1),maxX2,TOP); // in a multipage, make sure that the last page has a full width } if (Settings.fingerTouch) { if (disposition == SINGLE_ROW || disposition == MULTIPLE_HORIZONTAL) { flick.setScrollDistance(width - hgap); flick.forcedFlickDirection = Flick.HORIZONTAL_DIRECTION_ONLY; if (hasPagePosition) { pagepos = new PagePosition(Math.min(pages,7)); pagepos.setCount(pages); addToSC(pagepos); pagepos.setRect(CENTER,pagePositionDisposition == PAGEPOSITION_AT_BOTTOM ? BOTTOM : TOP,PREFERRED,PREFERRED); flick.setPagePosition(pagepos); ((ScrollPosition)sbH).barColor = sbH.getBackColor(); // "hide" the bar } } else { flick.setScrollDistance(pageH); flick.setDistanceToAbortScroll(0); // we deliberably disable the scroll abort on vertical scrolls flick.forcedFlickDirection = Flick.VERTICAL_DIRECTION_ONLY; } } } public void reposition() { super.reposition(false); initUI(); } /** Returns the preferred width as if all images were in a single row. */ public int getPreferredWidth() { return getPreferredWidth(names != null ? names.length : images.length); } /** Returns the preferred width for the given number of columns. */ public int getPreferredWidth(int cols) { if (prefBtnW == 0) onFontChanged(); int bh = fmH*buttonHorizGap/100; int sb = Settings.fingerTouch || sbV == null || !sbV.isVisible() ? 0 : sbV.getPreferredWidth(); return prefBtnW * cols + bh * (cols-1) + bh*2 + sb; } /** Returns the preferred height as if all images were in a single row. */ public int getPreferredHeight() { return getPreferredHeight(1); } /** Returns the preferred height for the given number of rows. * For example: * <pre> * add(ib2,LEFT+10,CENTER,FILL-10,ib2.getPreferredHeight(4)); * </pre> */ public int getPreferredHeight(int rows) { if (prefBtnH == 0) onFontChanged(); int bv = fmH*buttonVertGap/100; int sb = Settings.fingerTouch || sbH == null || !sbH.isVisible() ? 0 : sbH.getPreferredHeight(); return prefBtnH * rows + bv*(rows-1) + bv*2 + sb; } public int getSelectedIndex() { return selected; } public void controlPressed(ControlEvent e) { e.consumed = true; if (!wasScrolled()) { selected = ((Control)e.target).appId; postPressedEvent(); } } }