/********************************************************************************* * TotalCross Software Development Kit * * Copyright (C) 1998, 1999 Wabasoft <www.wabasoft.com> * * 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.event.*; import totalcross.ui.font.*; import totalcross.ui.gfx.*; import totalcross.ui.image.*; import totalcross.util.*; /** * TabbedContainer is a bar of text or image tabs. * It is assumed that all images will have the same height, but they may have different widths. * <br> * A scroll is automatically added when the total width of the titles is bigger than the control's width. * <br> * The containers are created automatically and switched when the user press the corresponding tab. * <p> * Here is an example showing a tab bar being used: * * <pre> * public class MyProgram extends MainWindow * { * TabbedContainer tab; * * public void initUI() * { * String names[] = {"Edition","Report"}; * tab = new TabbedContainer(names); * add(tab); * tab.setGaps(2,2,2,2); // set it before setting the rect * tab.setRect(LEFT,TOP,FILL,FILL); * tab.setContainer(0,new Edition()); // replace container 1 by a class that extends Container. * tab.getContainer(1).add(new Label("Not implemented"),CENTER,CENTER); * } * * public void onEvent(Event event) * { * if (event.type == ControlEvent.PRESSED && event.target == tp) * { * int activeIndex = tp.getActiveTab(); * ... handle tab being pressed * } * } * } * </pre> * Here's another sample that will show two TabbedContainers, one with images and another one with scrolling tabs. * Note that you must create img1.png and img2.png. * <pre> * TabbedContainer tp1 = new TabbedContainer(new Image[]{new Image("img1.png"), new Image("img2.png")}, null); * add(tp1); * tp1.setRect(LEFT,TOP,Settings.screenWidth/2,Settings.screenHeight/2); * tp1.activeTabBackColor = Color.getRGB(222,222,222); * * TabbedContainer tp2 = new TabbedContainer(new String[]{"verinha","marcelo","denise","guilherme","renato","michelle","rafael","barbara","lucas","ronaldo","nenem",}); * add(tp2); * tp2.setRect(LEFT,AFTER+2,FILL,FILL); * </pre> * * When the user interface is Android, the tabs do not look good if the background is the same of the parent's. * In this case, we force the background to be slighly darker. There are a few fields that you can use to change * the color, like activeTabBackColor, useOnTabTheContainerColor and pressedColor. * * Important: starting in TotalCross 1.3, with Settings.fingerTouch=true, you CANNOT call setRect in your container. * Otherwise, the flick and drag will not work and your container will be positioned incorrectly. */ public class TabbedContainer extends ClippedContainer implements Scrollable { private int activeIndex=-1; private String []strCaptions; private Image []imgCaptions,imgDis, imgCaptions0; private Image activeIcon, activeIcon0; private boolean isTextCaption=true; private Container containers[]; private int count; private int tabH; private int captionColor = Color.BLACK; private boolean atTop=true; private Rect [] rects,rSel,rNotSel; private int fColor,cColor; private Rect clientRect; private ArrowButton btnLeft, btnRight; private static final byte FOCUSMODE_OUTSIDE = 0; private static final byte FOCUSMODE_CHANGING_TABS = 1; private static final byte FOCUSMODE_INSIDE_CONTAINERS = 2; private byte focusMode; private boolean brightBack; /** Set the arrows color right after the constructor and after calling setCaptionsColor, which also change this property. */ public int arrowsColor = Color.BLACK; private Font bold; private int btnX; private int style = Window.RECT_BORDER; private boolean []disabled; // guich@tc110_58 // flick support private boolean isScrolling; private boolean flickTimerStarted=true; private int tempSelected=-1; private int []wplains,wbolds; private boolean scScrolled; private String[] strCaptions0; /** Set to true to automatically shrink the captions to prevent using arrows. Works only for String-based captions. */ public boolean autoShrinkCaptions; /** Enables or not the arrows if scroll is needed. */ public boolean showArrows = true; /** This color is the one used to paint the background of the active tab. * This is specially useful for image tabs. * @see #setBackColor * @see #useOnTabTheContainerColor * @see #tabsBackColor * @since SuperWaba 5.64 */ public int activeTabBackColor=-1; // guich@564_14 /** Sets the colors used on each tab. You must create and set the array with the colors. Pass -1 to keep * the original color. This array has precedence over the other ways that changes colors, except activeTabBackColor. * @see #setBackColor * @see #useOnTabTheContainerColor * @see #activeTabBackColor * @since TotalCross 1.52 */ public int[] tabsBackColor; /** Sets the tabs with the same colors of the container. * @see #setBackColor * @see #tabsBackColor * @see #activeTabBackColor * @since SuperWaba 5.72 */ public boolean useOnTabTheContainerColor; // guich@572_12 /** Stores the last active tab index, or -1 if none was previously selected. * @since SuperWaba 4.21 */ public int lastActiveTab = -1; // guich@421_30: changed name to conform with getActiveIndex /** In finger touch devices, the user still can flick into a disabled tab. To disable this behaviour, * set this flag to false; so when a disabled tab is reached, the user will not be able to flick into it, and will * have to click on an enabled tab to continue flicking. * @since TotalCross 1.3 */ public boolean flickIntoDisabledTabs = true; /** To be used on the setType method: specifies that the tabs will be placed on the top. */ public static final byte TABS_TOP = 0; /** To be used on the setType method: specifies that the tabs will be placed on the bottom. */ public static final byte TABS_BOTTOM = 1; /** Set the color when the user clicks on the tab. * @since TotalCross 1.3.4 */ public int pressedColor = -1; /** Set to true to make all tabs have the same width. * @since TotalCross 1.3.4 */ public boolean allSameWidth; /** Define an extra height for the tabs. Use something line fmH/2. * Required when setIcons is called. * @see #setIcons(Image[]) * @since TotalCross 1.3.4 */ public int extraTabHeight; /** The color used for the text of unselected tabs. Defaults to the foreground color. */ public int unselectedTextColor = -1; private Container prevScr,curScr,nextScr; /** The Flick object listens and performs flick animations on PenUp events when appropriate. */ protected Flick flick; /** Set to false to disable flicking between tabs. You can still switch between the tabs by clicking on them. * Sample: * <pre> TabbedContainer.allowFlick = false; TabbedContainer tc = new TabbedContainer(caps); TabbedContainer.allowFlick = true; * </pre> */ public static boolean allowFlick = Settings.fingerTouch; private TabbedContainer(int count) { ignoreOnAddAgain = ignoreOnRemove = true; this.count = count; this.focusTraversable = true; // kmeehl@tc100 started = true; focusHandler = true; containers = new Container[count]; if (allowFlick) { flick = new Flick(this); flick.forcedFlickDirection = Flick.HORIZONTAL_DIRECTION_ONLY; flick.maximumAccelerationMultiplier = 1; } // create the rects since we want to reuse them rects = new Rect[count]; for (int i = count-1; i >= 0; i--) { rects[i] = new Rect(); Container c = containers[i] = new Container(); if (flick != null) flick.addEventSource(c); c.ignoreOnAddAgain = c.ignoreOnRemove = true; } disabled = new boolean[count]; } protected void computeClipRect() { bagClipY0 = 0; // include top otherwise the arrows will not be drawn bagClipYf = this.height; // y0 + parent.height; bagClipX0 = clientRect.x; // -this.x; bagClipXf = bagClipX0 + clientRect.width; // x0 + parent.width; } public void initUI() { onBoundsChanged(false); } /** Returns the number of tabs. * @since TotalCross 1.15 */ public int getTabCount() { return count; } /** Sets the given tab index as enabled or not. When a tab is disabled, it is displayed faded, * and if the user clicks on it, nothing happens. However, you still can activate it by calling * setActiveTab. If there are no tabs enabled, the current tab will be made active and the controls will * also be enabled. So, if you plan to disable all tabs, better disable the TabbedContainer control instead. * @param on If true, the tab is enabled, if false it is disabled. * @param tabIndex The tab's index (0 to count-1) * @since TotalCross 1.01 * @see #setActiveTab */ public void setEnabled(int tabIndex, boolean on) // guich@tc110_58 { disabled[tabIndex] = !on; if (!on && (!isTextCaption || imgCaptions != null) && (imgDis[tabIndex] == null)) try { imgDis[tabIndex] = imgCaptions[tabIndex].getFadedInstance(); } catch (ImageException e) { imgDis[tabIndex] = imgCaptions[tabIndex]; } if (!on && activeIndex == tabIndex) // move to next tab setActiveTab(nextEnabled(activeIndex,true)); if (Settings.fingerTouch) { containers[tabIndex].setEnabled(on); if (!on) // tell Control.postEvent that the flick still needs to be called containers[tabIndex].eventsEnabled = true; } Window.needsPaint = true; } /** Returns if the given tab index is enabled. * @since TotalCross 1.01 */ public boolean isEnabled(int tabIndex) { return !disabled[tabIndex]; } /** Constructs a tab bar control with Strings as captions. */ public TabbedContainer(String []strCaptions) { this(strCaptions.length); this.strCaptions = this.strCaptions0 = strCaptions; onFontChanged(); } /** Constructor to keep compilation compatibility with TC 1; transparentColor is ignored. */ public TabbedContainer(Image []imgCaptions, int transparentColor) // guich@564_13 { this(imgCaptions); } /** Constructs a tab bar control with images as captions, using the given color as transparent color. * If you don't want to use transparent colors, just pass -1 to the color. */ public TabbedContainer(Image []imgCaptions) // guich@564_13 { this(imgCaptions.length); this.imgCaptions = imgCaptions; setupImageProps(); isTextCaption = false; onFontChanged(); } /** Sets the active icon. */ public void setActiveIcon(Image newActiveIcon) { if (isTextCaption) activeIcon0 = newActiveIcon; else activeIcon = newActiveIcon; } /** Set the given icons to appear at the top (or bottom, if TABS_BOTTOM) of a text TabbedContainer. * The icon images must be squared. You must also set the extraTabHeight, because the icons * will be resized to (extraTabHeight-fmH) in both directions. * @since TotalCross 1.3.4 */ public void setIcons(Image[] icons) { if (icons.length != count) throw new RuntimeException("Image array passed in setIcons must have the same length of the captions."); imgCaptions0 = icons; imgCaptions = new Image[icons.length]; setupImageProps(); } /** Set the given icons to appear at the top (or bottom, if TABS_BOTTOM) of a text TabbedContainer. * The icon images must be squared. You must also set the extraTabHeight, because the icons * will be resized to (extraTabHeight-fmH) in both directions. Also, sets the active icon. */ public void setIcons(Image[] icons, Image activeIcon) { setIcons(icons); this.activeIcon0 = activeIcon; } private void setupImageProps() { imgDis = new Image[count]; } /** Sets the position of the tabs. use constants TABS_TOP or TABS_BOTTOM. * Since the tabs are not changed dinamicaly, this method must be called * after the constructor. */ public void setType(byte type) { atTop = type == TABS_TOP; onFontChanged(); } /** Returns the Container for tab i */ public Container getContainer(int i) { return containers[i]; } /** Sets the type of border. Currently, only the Window.NO_BORDER and Window.RECT_BORDER are supported. NO_BORDER only draws the line under the tabs. */ public void setBorderStyle(byte style) { this.style = style; } /** Replaces the default created Container with the given one. This way you can * avoid adding a container to a container and, as such, waste memory. * Note that you must do this before the first setRect for this TabbedContainer; otherwise, * you must explicitly call setRect again to update the added container bounds */ public void setContainer(int i, Container container) { if (containers != null && i >= 0 && i < containers.length) { Container old = containers[i]; containers[i] = container; if (i == activeIndex) // guich@300_34: fixed problem when the current tab was changed { remove(old); if (flick != null) flick.removeEventSource(old); add(container); tabOrder.removeAllElements(); // don't let the cursor keys get into our container container.requestFocus(); } if (!container.started) // guich@340_58: set the container's rect { if (flick != null) { add(container); container.setRect(old.getRect()); flick.addEventSource(container); } else { Container cp = container.parent; container.parent = this; container.setRect(clientRect); container.parent = cp; } container.setBackColor(container.getBackColor()); } } if (Settings.keyboardFocusTraversable) // otherwise, in an app where there's only a TabbedContainer, the last added container would remain highlighted requestFocus(); } /** * Sets the currently active tab. A PRESSED event will be posted to * the given tab if it is not the currently active tab; then, the containers will be switched. */ public void setActiveTab(int tab) { if (tab != activeIndex && tab >= 0) { boolean firstTabChange = activeIndex == -1; if (!firstTabChange && flick == null) remove(containers[activeIndex]); lastActiveTab = activeIndex; // guich@402_4 activeIndex = tab; if (flick != null) for (int xx = -activeIndex * width + clientRect.x, i = 0; i < containers.length; i++, xx += width) containers[i].x = xx; else add(containers[activeIndex]); tabOrder.removeAllElements(); // don't let the cursor keys get into our container computeTabsRect(); scrollTab(activeIndex); Window.needsPaint = true; if (!firstTabChange) // guich@200b4_87 postPressedEvent(); } } /** Returns the index of the selected tab */ public int getActiveTab() { return activeIndex; } /** Returns the container of the active tab. * @since TotalCross 1.2 */ public Container getActiveContainer() // guich@tc120_16 { return containers[activeIndex]; } /** Returns the caption height for this TabbedContainer. Note that it is not possible to compute the correct height of * each container, since they will be added AFTER this TabbedContainer has their bounds set. So, you should actually use some * other way to specify the bounds, like FILL or FIT; using PREFERRED in the height of setRect will make your application abort. */ public int getPreferredHeight() { return tabH /* guich@564_12: + 20 */ + insets.top+insets.bottom; } /** Returns the minimum width (based on the sizes of the captions) for this TabbedContainer */ public int getPreferredWidth() { int sum = 0; if (count > 0) { // the max size is the size of the biggest bolded title plus the size of the plain titles int maxw = 0, maxi = 0; for (int i = count; --i >= 0;) { int w = rSel[i].width; if (w > maxw) { maxi = i; maxw = w; } sum += rNotSel[i].width-1; } sum += maxw - rNotSel[maxi].width; // add the diff between the bold and the plain fonts of the biggest title } return sum+getExtraSize(); // guich@573_11: changed from 3 to 2 } private int getExtraSize() { return 2 + insets.left+insets.right; } /** Returns the index of the next/prev enabled tab, or the current tab if there's none. */ private int nextEnabled(int from, boolean forward) { for (int i =0; i < count; i++) { boolean limitsReached = (forward && from == containers.length-1) || (!forward && from == 0); if (limitsReached) from = forward? 0 : count-1; else from = forward?from+1:from-1; if (!disabled[from]) break; } return from < 0 ? 0 : from; } /** Used internally. resizes all the containers and add the arrows if scroll is needed. */ protected void onBoundsChanged(boolean screenChanged) { int i; if (autoShrinkCaptions) { Vm.arrayCopy(strCaptions0, 0, strCaptions = new String[strCaptions0.length], 0, strCaptions.length); onFontChanged(); int idx = 0; double med = 0; for (i = 0; i < strCaptions.length; i++) med += strCaptions[i].length(); int tries = (int)med; med /= strCaptions.length; while (mustScroll() && tries-- > 0) { String s = strCaptions[idx]; int l = s.length(); if (l >= med) { if (s.charAt(l-1) == '.') l--; s = s.substring(0,l-1).concat("."); strCaptions[idx] = s; onFontChanged(); med = 0; for (i = 0; i < strCaptions.length; i++) med += strCaptions[i].length(); med /= strCaptions.length; } if (++idx == strCaptions.length) idx = 0; } } onFontChanged(); computeTabsRect(); int borderGap = style==Window.NO_BORDER || uiAndroid ? 0 : 1; // guich@400_89 int xx = insets.left+borderGap; int yy = (atTop?tabH:borderGap)+insets.top; int ww = width-insets.left-insets.right-(borderGap<<1); int hh = height-insets.top-insets.bottom-(borderGap<<1)-(atTop?yy:tabH); clientRect = new Rect(xx,yy,ww,hh); for (i = 0; i < count; i++) { Container c = containers[i]; if (flick != null && c.parent == null) add(c); c.setRect(xx,yy,ww,hh,null,screenChanged); c.reposition(); if (flick != null) xx += width; } if (flick != null) flick.setScrollDistance(width); if (activeIndex == -1) setActiveTab(nextEnabled(-1,true)); // fvincent@340_40 addArrows(); } private boolean mustScroll() { if (!allSameWidth) return count > 1 && getPreferredWidth() > this.width; // guich@564_10: support scroll - guich@573_2: only add arrows if there's more than one tab // guich@tc306: if all same width, use a different formula int each = width / count - 12; // 12 = space between tabs for (int i = 0; i < count; i++) if (wbolds[i] > each) return true; return false; } private void addArrows() { boolean scroll = mustScroll(); if (scroll && showArrows) { int c = parent != null ? parent.backColor : UIColors.controlsBack; // guich@573_4 if (btnLeft == null) { int hh = Settings.fingerTouch ? fmH*3/4 : Math.max(fmH/2,tabH/4); // guich@tc110_90: use tab height if its larger than font's height btnRight = new ArrowButton(Graphics.ARROW_RIGHT, hh, arrowsColor); btnRight.setBackColor(c); btnRight.setBorder(Button.BORDER_NONE); btnLeft = new ArrowButton(Graphics.ARROW_LEFT, hh, arrowsColor); btnLeft.setBackColor(c); btnLeft.setBorder(Button.BORDER_NONE); int yy = (tabH+btnRight.getPreferredHeight()) >> 1; super.add(btnRight,RIGHT,atTop ? (tabH-yy) : (this.height-yy),PREFERRED+(Settings.fingerTouch ? fmH : 0),PREFERRED); super.add(btnLeft,BEFORE-2,SAME,SAME,SAME); btnLeft.setEnabled(false); btnLeft.setFocusLess(true); // guich@570_39 btnRight.setFocusLess(true); // guich@570_39 btnRight.autoRepeat = btnLeft.autoRepeat = true; // guich@tc122_46 btnRight.AUTO_DELAY = btnLeft.AUTO_DELAY = 500; } btnX = btnLeft.x-2; } else btnX = this.width; if (btnLeft != null) { btnRight.setVisible(scroll); btnLeft.setVisible(scroll); } } public void setEnabled(boolean b) { super.setEnabled(b); if (btnLeft != null) { boolean canGoLeft = activeIndex > 0; boolean canGoRight = activeIndex < count-1; btnLeft.setEnabled(isEnabled() && canGoLeft); btnRight.setEnabled(isEnabled() && canGoRight); } } /** compute the rects that represents each tab on the screen. */ public void computeTabsRect() { int x0 = 1; int y0 = atTop?0:(height-tabH); int n = count; if (!allSameWidth && transparentBackground) // using balls? center on screen { int ww = 0; for (int i = 0; i < n; i++) ww += (i == activeIndex ? rSel[i] : rNotSel[i]).width; x0 = (width-ww)/2; } for (int i =0; i < n; i++) { Rect r = rects[i]; Rect r0 = i == activeIndex ? rSel[i] : rNotSel[i]; r.x = x0; r.y = r0.y + y0; r.width = r0.width; r.height = r0.height; x0 += r.width-1; rects[i] = r; } } /** Scroll the TabbedContainer to the given tab */ private void scrollTab(int toIdx) // guich@564_10 { if (btnLeft != null && mustScroll()) { boolean canGoLeft = toIdx > 0; boolean canGoRight = toIdx < count-1; btnLeft.setEnabled(canGoLeft); btnRight.setEnabled(canGoRight); if (canGoLeft || canGoRight) { int xOfs; if (toIdx == 0) xOfs = 0; else { xOfs = Settings.fingerTouch ? fmH*2 : 7*fmH/11; // keep part of the previous tab on screen for (int i =0; i < toIdx; i++) xOfs -= rNotSel[i].width-1; } offsetRects(xOfs); // make sure that the last tab is near the left button if (rects[count-1].x2() < btnX || toIdx == count-1) { int dif = btnX - rects[count-1].x2(); offsetRects(-xOfs); xOfs += dif; offsetRects(xOfs); } Window.needsPaint = true; } } } /** Offsets all rectangles by the given value */ private void offsetRects(int xOfs) { // offset the rectangles for (int i = count-1; i >= 0; i--) rects[i].x += xOfs; } /** Compute the rectangles of the tabs based on the selected * (bolded) and unselected (plain) titles. */ public void onFontChanged() // guich@564_11 { boolean isText = isTextCaption; if (wplains == null) { wplains = new int[count]; wbolds = new int[count]; rSel = new Rect[count]; rNotSel = new Rect[count]; } tabH = isText ? uiAndroid ? (fmH + 8 + extraTabHeight) : (fmH + 4) : (imgCaptions[0].getHeight() + 4); int y0 = atTop && !uiAndroid ?2:0; bold = uiAndroid ? font : font.asBold(); FontMetrics fmb = bold.fm; int medW = (this.width-getExtraSize()) / count; for (int i = count; --i >= 0;) { wplains[i] = isText ? fm .stringWidth(strCaptions[i]) : imgCaptions[i].getWidth(); wbolds[i] = isText && !uiAndroid ? fmb.stringWidth(strCaptions[i]) : wplains[i]; // in uiandroid there's no bold font } int wp = allSameWidth ? Math.max(medW,Convert.max(wplains)) : 0; int wb = allSameWidth ? Math.max(medW,Convert.max(wbolds)) : 0; for (int i = count; --i >= 0;) { if (uiAndroid) { rSel[i] = new Rect(0,0,allSameWidth ? wp : wplains[i]+12,tabH); rNotSel[i] = imgCaptions == null ? new Rect(0,atTop ? extraTabHeight/2 : 0,allSameWidth ? wp : wplains[i]+12,tabH-extraTabHeight/2) : new Rect(0,0,allSameWidth ? wp : wplains[i]+12,tabH); } else { rSel[i] = new Rect(0,0,allSameWidth ? wb : wbolds[i]+5,tabH); rNotSel[i] = new Rect(0,y0,allSameWidth ? wp : wplains[i]+4,tabH-2); } } if (isText && imgCaptions != null) // have icons? resize them if (extraTabHeight == 0) Vm.warning("setIcon was called but extraTabHeight was not set."); else try { int size = extraTabHeight-fmH/2; for (int i = 0; i < count; i++) imgCaptions[i] = imgCaptions0[i].getSmoothScaledInstance(size,size); if (activeIcon0 != null) activeIcon = activeIcon0.getSmoothScaledInstance(size,size); else activeIcon = null; } catch (ImageException ie) {if (Settings.onJavaSE) ie.printStackTrace();} } protected void onColorsChanged(boolean colorsChanged) { if (uiAndroid && parent != null && backColor == parent.backColor) // same background color in uiandroid does not look good. { activeTabBackColor = Color.brighter(backColor,32); backColor = Color.darker(backColor,32); } if (colorsChanged) brightBack = Color.getAlpha(foreColor) > 128; fColor = (isEnabled() || !brightBack) ? getForeColor() : Color.darker(foreColor); cColor = (isEnabled() || !brightBack) ? getCaptionColor() : Color.darker(captionColor); if (colorsChanged && btnLeft != null) { btnRight.arrowColor = btnLeft.arrowColor = arrowsColor; btnRight.backColor = btnLeft.backColor = parent != null ? parent.backColor : UIColors.controlsBack; // guich@573_4 } } /** Called by the system to draw the tab bar. */ public void onPaint(Graphics g) { if (activeIndex == -1) return; Rect r; int n = count; int y = atTop?(tabH-1):0; int h = atTop?(height-y):(height-tabH+1); int yl = atTop?y:(y+h-1); // erase area with parent's color int containerColor = containers[activeIndex].backColor; // guich@580_26: use current container's backcolor instead of TabbedContainer's backcolor g.backColor = parent.backColor; if (!transparentBackground) { if (parent.backColor == containerColor) // same color? fill the whole rect g.fillRect(0,0,width,height); else { // otherwise, erase tab area... if (atTop) g.fillRect(0,0,width,y); else g.fillRect(0,yl,width,height-yl); // ...and erase containers area g.backColor = containerColor; g.fillRect(0,y,width,h); } } if (!uiAndroid) { g.foreColor = fColor; if (style != Window.NO_BORDER) g.drawRect(0,y,width,h); // guich@200b4: now the border is optional else g.drawLine(0,yl,width,yl); } int back = backColor; g.backColor = backColor; if (btnLeft != null && mustScroll()) // if we have scroll, don't let the title be drawn over the arrow buttons g.setClip(1,0,btnX,height); // draw the tabs boolean drawSelectedTabAlone = !transparentBackground && (activeTabBackColor >= 0 || uiAndroid); if (!transparentBackground && (uiAndroid || useOnTabTheContainerColor || tabsBackColor != null || parent.backColor != backColor || uiVista)) // guich@400_40: now we need to first fill, if needed, and at last draw the border, since the text will overlap the last pixel (bottom-right or top-right) - guich@tc100b4_10: uivista also needs this for (int i = 0; i < n; i++) { if (drawSelectedTabAlone && i == activeIndex) continue; r = rects[i]; g.backColor = back = getTabColor(i); // guich@580_7: use the container's color if containersColor was not set - guich@tc110_59: use default back color if container was not yet shown. if (uiAndroid) try { NinePatch.tryDrawImage(g, NinePatch.getInstance().getNormalInstance(NinePatch.TAB, r.width,r.height, i == tempSelected && pressedColor != -1 ? pressedColor : back, !atTop), r.x,r.y); } catch (ImageException ie) {if (Settings.onJavaSE) ie.printStackTrace();} else if (uiFlat) // the flat style has rect borders instead of hatched ones. g.fillRect(r.x,r.y,r.width,r.height); else if (uiVista && isEnabled()) g.fillVistaRect(r.x+1,r.y+1,r.width-2,r.height-2, back, atTop,false); else g.fillHatchedRect(r.x,r.y,r.width,r.height,atTop,!atTop); // (*) } if (drawSelectedTabAlone) // draw again for the selected tab if we want to use a different color { int b = containers[activeIndex].backColor; if (tabsBackColor == null && useOnTabTheContainerColor && activeTabBackColor != -1) g.backColor = b == backColor ? activeTabBackColor : b; else { boolean dontUseTabs = tabsBackColor == null || tabsBackColor[activeIndex] == -1; g.backColor = activeTabBackColor != -1 && dontUseTabs ? activeTabBackColor : activeTabBackColor != -1 && !dontUseTabs ? Color.interpolate(activeTabBackColor,tabsBackColor[activeIndex]) : getTabColor(activeIndex); } r = rects[activeIndex]; if (uiAndroid) try { Image img = NinePatch.getInstance().getNormalInstance(NinePatch.TAB, r.width,r.height, g.backColor, !atTop); NinePatch.tryDrawImage(g, img, r.x,r.y); } catch (ImageException ie) {if (Settings.onJavaSE) ie.printStackTrace();} else if (uiFlat) // the flat style has rect borders instead of hatched ones. g.fillRect(r.x,r.y,r.width,r.height); else g.fillHatchedRect(r.x,r.y,r.width,r.height,atTop,!atTop); // (*) g.backColor = backColor; } // draw text boolean isText = isTextCaption; for (int i =0; i < n; i++) { r = rects[i]; int xx = r.x + (r.width-(i==activeIndex ? wbolds[i] : wplains[i]))/2; int yy = r.y + (r.height-fmH)/2; if (isText) { g.foreColor = disabled[i] ? Color.getCursorColor(cColor) : i != activeIndex && unselectedTextColor != -1 ? unselectedTextColor : cColor; // guich@200b4_156 if (uiAndroid) g.drawText(strCaptions[i],xx, atTop ? (extraTabHeight > 0 ? r.y + r.height-fmH-7 : yy-2) : (extraTabHeight > 0 ? r.y + 7 : yy), textShadowColor != -1, textShadowColor); else if (i != activeIndex) g.drawText(strCaptions[i],xx, yy, textShadowColor != -1, textShadowColor); else { g.setFont(bold); // guich@564_11 g.drawText(strCaptions[i],xx, yy, textShadowColor != -1, textShadowColor); g.setFont(font); } if (disabled[i]) g.foreColor = Color.getCursorColor(cColor); if (imgCaptions != null && imgCaptions[i] != null) g.drawImage(i == activeIndex && activeIcon != null ? activeIcon : disabled[i] ? imgDis[i] : imgCaptions[i], r.x+(r.width-imgCaptions[i].getWidth())/2, atTop ? r.y+(extraTabHeight-imgCaptions[i].getHeight())/2 : r.y+(extraTabHeight+imgCaptions[i].getHeight())/2); } else { g.drawImage(i == activeIndex && activeIcon != null ? activeIcon : disabled[i] ? imgDis[i] : imgCaptions[i], r.x+(r.width-imgCaptions[i].getWidth())/2, r.y+1); } if (uiFlat) g.drawRect(r.x,r.y,r.width,r.height); else if (!uiAndroid) g.drawHatchedRect(r.x,r.y,r.width,r.height,atTop,!atTop); // guich@400_40: moved from (*) to here } // guich@200b4: remove the underlaying line of the active tab. r = rects[activeIndex]; if (!uiAndroid) { g.foreColor = getTabColor(activeIndex); // guich@580_7: use the container's back color g.drawLine(r.x,yl,r.x2(),yl); g.drawLine(r.x+1,yl,r.x2()-1,yl); } if (Settings.keyboardFocusTraversable && focusMode == FOCUSMODE_CHANGING_TABS) // draw the focus around the current tab - guich@580_52: draw the cursor only when changing tabs { g.drawDottedRect(r.x+1,r.y+1,r.width-2,r.height-2); if (Settings.screenWidth == 320) g.drawDottedRect(r.x+2,r.y+2,r.width-4,r.height-4); } } /** Returns the color of the given tab. * @since TotalCross 1.52 */ public int getTabColor(int tab) { return tabsBackColor != null && tabsBackColor[tab] != -1 ? tabsBackColor[tab] : useOnTabTheContainerColor && containers[tab].backColor != -1 ? containers[tab].backColor : backColor; } /** Sets the text color of the captions in the tabs. */ public void setCaptionColor(int capColor) { this.captionColor = this.arrowsColor = capColor; onColorsChanged(true); // guich@200b4_169 } /** Gets the text color of the captions. return a grayed value if this control is not enabled. */ public int getCaptionColor() { return isEnabled()?captionColor:Color.brighter(captionColor); } /** Returns the area excluding the tabs and borders for this TabbedContainer. * Note: do not change the returning rect object ! */ public Rect getClientRect() // guich@340_27 { return clientRect; } /** Returns the area excluding the tabs and borders for this TabbedContainer. * In this version, you provide the created Rect to be filled with the coords.*/ protected void getClientRect(Rect r) // guich@450_36 { r.set(clientRect); } /** Called by the system to pass events to the tab bar control. */ public void onEvent(Event event) { if (event.type == PenEvent.PEN_DOWN) scScrolled = false; if (event.target != this) { if (event.type == ControlEvent.PRESSED && (event.target == btnLeft || event.target == btnRight)) setActiveTab(nextEnabled(activeIndex,event.target == btnRight)); if (!(flick != null && (event.type == PenEvent.PEN_DRAG || event.type == PenEvent.PEN_UP))) return; } switch (event.type) { case PenEvent.PEN_UP: if (tempSelected != -1) setActiveTab(tempSelected); tempSelected = -1; if (uiAndroid) Window.needsPaint = true; if (!flickTimerStarted) flickEnded(false); isScrolling = false; break; case PenEvent.PEN_DRAG: if (flick != null) { Window w = getParentWindow(); if (w != null && w._focus == w.focusOnPenUp) break; DragEvent de = (DragEvent)event; if (isScrolling) { scrollContent(-de.xDelta, 0, true); event.consumed = true; } else { int direction = DragEvent.getInverseDirection(de.direction); event.consumed = direction == DragEvent.LEFT || direction == DragEvent.RIGHT; if (canScrollContent(direction, de.target) && scrollContent(-de.xDelta, 0, true)) { if (Settings.optimizeScroll) takeScreenShots(); flickTimerStarted = false; isScrolling = scScrolled = true; } } } break; case ControlEvent.FOCUS_IN: // guich@580_53: when focus is set, activate tab changing mode. if (Settings.keyboardFocusTraversable) focusMode = FOCUSMODE_CHANGING_TABS; break; case PenEvent.PEN_DOWN: PenEvent pe = (PenEvent)event; tempSelected = -1; if (uiAndroid) Window.needsPaint = true; if (pe.x < btnX && (flick != null || (rects[0].y <= pe.y && pe.y <= rects[0].y2()))) // guich@tc100b4_7 - guich@tc120_48: when fingerTouch, the y position may be below the tabbed container { int sel = -1; if (flick != null) // guich@tc120_48 { int minDist = Settings.touchTolerance; for (int i = count-1; i >= 0; i--) { Rect r = rects[i]; int d = (int)(Convert.getDistancePoint2Rect(pe.x,pe.y, r.x,r.y,r.x+r.width,r.y+r.height)+0.5); if (d <= minDist) { minDist = d; sel = i; } } } else { for (int i = count-1; i >= 0; i--) if (rects[i].contains(pe.x,pe.y)) { sel = i; break; } } if (sel != activeIndex && sel >= 0 && !disabled[sel]) { tempSelected = sel; if (!uiAndroid) setActiveTab(sel); } } break; case KeyEvent.ACTION_KEY_PRESS: focusMode = FOCUSMODE_CHANGING_TABS; // guich@573_23 - super.drawHighlight(); // remove the highlight around the TabbedContainer Window.needsPaint = true; // guich@573_23 break; case KeyEvent.SPECIAL_KEY_PRESS: if (Settings.keyboardFocusTraversable) { KeyEvent ke = (KeyEvent)event; int key = ke.key; if (focusMode == FOCUSMODE_CHANGING_TABS) { if (key == SpecialKeys.LEFT || key == SpecialKeys.RIGHT) setActiveTab(nextEnabled(activeIndex, key == SpecialKeys.RIGHT)); else if (ke.isUpKey() || ke.isDownKey()) { focusMode = FOCUSMODE_INSIDE_CONTAINERS; Window.needsPaint = true; // guich@573_23 - drawHighlight(); containers[activeIndex].changeHighlighted(containers[activeIndex],ke.isDownKey()); isHighlighting = true; } } if (ke.isActionKey()) { focusMode = FOCUSMODE_OUTSIDE; //getParent().requestFocus(); - guich@580_54 isHighlighting = true; Window.needsPaint = true; // guich@573_23 - drawHighlight(); } } break; } } private void takeScreenShots() { try { if (activeIndex > 0) (prevScr = containers[activeIndex-1]).takeScreenShot(); (curScr = containers[activeIndex]).takeScreenShot(); if (activeIndex < count-1) (nextScr = containers[activeIndex+1]).takeScreenShot(); } catch (Throwable t) { if (Settings.onJavaSE) t.printStackTrace(); releaseScreenShots(); } } private void releaseScreenShots() { if (prevScr != null) {prevScr.releaseScreenShot(); prevScr = null;} if (curScr != null) {curScr.releaseScreenShot(); curScr = null;} if (nextScr != null) {nextScr.releaseScreenShot(); nextScr = null;} } /** Tranfer the focus between the containers on this TabbedContainer */ public void changeHighlighted(Container p, boolean forward) { Window w = getParentWindow(); if (w != null) switch (focusMode) { case FOCUSMODE_OUTSIDE: // focus just got here if (w.getHighlighted() != this) w.setHighlighted(this); else super.changeHighlighted(p,forward); break; case FOCUSMODE_INSIDE_CONTAINERS: // was changing a control and the limits has been reached focusMode = FOCUSMODE_CHANGING_TABS; w.setHighlighted(this); // remove the focus from the last control Window.needsPaint = true; // guich@573_23 - drawHighlight(); requestFocus(); isHighlighting = false; break; default: super.changeHighlighted(p,forward); } } /** Only return to highlighting when we want */ public void setHighlighting() { isHighlighting = false; } public void reposition() { super.reposition(); computeTabsRect(); addArrows(); // this is needed because the btnX was not yet repositioned when onBounds called addArrows. if (mustScroll()) scrollTab(activeIndex); if (Settings.fingerTouch) { int tab = activeIndex; activeIndex = -1; setActiveTab(tab); } } public void getFocusableControls(Vector v) { if (visible && isEnabled()) v.addElement(this); super.getFocusableControls(v); } public Control handleGeographicalFocusChangeKeys(KeyEvent ke) { if (MainWindow.mainWindowInstance._focus == this) { if ((atTop && ke.isUpKey()) || (!atTop && ke.isDownKey())) return null; if ((atTop && ke.isDownKey()) || (!atTop && ke.isUpKey())) { Control c = containers[activeIndex].children; while (c != null && !c.focusTraversable) c = c.next; return c; } if ((ke.isNextKey() && activeIndex == containers.length-1) || (ke.isPrevKey() && activeIndex == 0)) return null; ke.target = this; _onEvent(ke); return this; } int direction = 0; if (ke.isUpKey()) direction = SpecialKeys.UP; // this order must else if (ke.isDownKey()) direction = SpecialKeys.DOWN; // be preserved else if (ke.isNextKey()) direction = SpecialKeys.RIGHT; else if (ke.isPrevKey()) direction = SpecialKeys.LEFT; else return null; Control c = findNextFocusControl(MainWindow.mainWindowInstance._focus, direction); if (c == null) { boolean prev = direction == SpecialKeys.UP || direction == SpecialKeys.LEFT; c = (prev == atTop) ? this : MainWindow.mainWindowInstance.findNextFocusControl(this, direction); } return c; } /** Returns true of the type is set to TABS_TOP. */ public boolean isAtTop() { return atTop; } /** Resizes the height of each added container and sets the height of this TabbedContainer to the maximum height of the containers. */ public void resizeHeight() // guich@tc120_12 { int h = 0; for (int i=0; i < containers.length; i++) { containers[i].resizeHeight(); h = Math.max(h, containers[i].getHeight()); } setRect(KEEP,KEEP,KEEP,getPreferredHeight() + h + 3); } public boolean flickStarted() { flickTimerStarted = true; return isScrolling; } public void flickEnded(boolean atPenDown) { int tab = getPositionedTab(false); setActiveTab(tab); releaseScreenShots(); } private int getPositionedTab(boolean exact) { int betterV = width; int betterI = -1; for (int i = 0; i < containers.length; i++) { int dif = containers[i].x - clientRect.x; if (dif < 0) dif = -dif; if (dif < betterV) { betterV = dif; betterI = i; } } return !exact || betterV == 0 ? betterI : -1; } public boolean canScrollContent(int direction, Object target) // called when { return getPositionedTab(true) == -1 || (direction == DragEvent.LEFT && activeIndex > 0 && (flickIntoDisabledTabs || !disabled[activeIndex-1])) || (direction == DragEvent.RIGHT && activeIndex < containers.length-1 && (flickIntoDisabledTabs || !disabled[activeIndex+1])); } public boolean scrollContent(int xDelta, int yDelta, boolean fromFlick) { if (containers.length == 1) return false; // prevent it from going beyond limits int maxX = -(containers[0].width * (containers.length-1) + containers.length)-1; int minX = 1; int curX = containers[0].x; int newX = curX - xDelta; if (newX > minX) newX = minX; else if (newX < maxX) newX = maxX; xDelta = curX - newX; if (xDelta == 0) return false; for (int i = containers.length; --i >= 0;) containers[i].x -= xDelta; Window.needsPaint = true; return true; } public int getScrollPosition(int direction) { return containers[0].getX() - clientRect.x; } public Flick getFlick() { return flick; } public boolean wasScrolled() { return scScrolled; } /** Changes the tab captions. The new array must have the same length or an exception is thrown. */ public void setCaptions(String[] caps) { if (strCaptions == null || caps.length != strCaptions.length) throw new IllegalArgumentException("The TabbedContainer's captions does not match the given ones."); strCaptions = strCaptions0 = caps; onFontChanged(); computeTabsRect(); } }