/*********************************************************************************
* 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.event.*;
import totalcross.ui.gfx.*;
import totalcross.ui.image.*;
import totalcross.util.*;
/**
* This class provides a title area and a button area (at right). The title and the button are optional, although it doesn't make sense to have a
* <code>Bar</code> without title and buttons.
*
* You can add or remove buttons, and change the title text; the title text can have an icon at left.
*
* Here's an example of how to use it, taken from the old UIGadgets sample:
*
* <pre>
* final Bar h1,h2;
* Font f = Font.getFont(true,Font.NORMAL_SIZE+2);
* h1 = new Bar("fakeboot");
* h1.canSelectTitle = true;
* h1.setFont(f);
* h1.setBackForeColors(0x0A246A,Color.WHITE);
* h1.addButton(new Image("ic_dialog_alert.png"));
* h1.addButton(new Image("ic_dialog_info.png"));
* add(h1, LEFT,0,FILL,PREFERRED); // use 0 instead of TOP to overwrite the default menu area
* </pre>
*
* A <code>ControlEvent.PRESSED</code> is sent to the caller, and the button index can be retrieved using the <code>getSelectedIndex()</code> method.
*
* By default, the background is shaded. You can change it to plain using <code>h1.backgroundStyle = BACKGROUND_SOLID;</code>.
*/
public class Bar extends Container
{
private BarButton title;
private Vector icons = new Vector(2);
private boolean initialized;
private int selected=-1;
private int c1,c2,c3,c4,tcolor,pcolor;
private Spinner spinner;
/**
* Set to <code>true</code> to allow the title to be selected and send events.
*/
public boolean canSelectTitle;
/**
* The title horizontal alignment (<code>LEFT</code>, <code>CENTER</code>, or <code>RIGHT</code>). Defaults to <code>LEFT</code>.
*/
public int titleAlign = LEFT;
/** The preferred height on portrait or landscape, in pixels. */
public int portraitPrefH,landscapePrefH;
/** A Bar's button. You can create an extension of this class using:
* <pre>
Bar b = new Bar();
b.new BarButton("Hi",null)
{
... other methods
};
* </pre>
*/
public class BarButton extends Control
{
String title;
Image icon0,icon;
int gap,px,py;
boolean pressed;
Image leftIcon,leftIcon0;
int autoRepeatRate;
private TimerEvent repeatTimer;
private int startRepeat;
public BarButton(String title, Image icon) // title or icon
{
this.title = title;
this.icon0 = icon;
}
public void onFontChanged()
{
if (title != null)
{
gap = fm.charWidth(' ');
if (leftIcon0 != null)
try
{
leftIcon = null;
leftIcon = leftIcon0.getSmoothScaledInstance(leftIcon0.getWidth()*fmH/leftIcon0.getHeight(),fmH);
} catch (Exception e) {icon = icon0;}
}
else
try
{
icon = null;
if (icon0 != null)
icon = icon0.getSmoothScaledInstance(icon0.getWidth()*fmH/icon0.getHeight(),fmH);
} catch (Exception e) {icon = icon0;}
}
public void onBoundsChanged(boolean b)
{
onFontChanged();
if (title != null)
{
px = titleAlign == LEFT ? gap+1 : titleAlign == CENTER ? (width-fm.stringWidth(title))/2 : (width-fm.stringWidth(title)-gap);
py = (height-fmH)/2;
}
else
if (icon != null)
{
px = (width -icon.getWidth()) /2;
py = (height-icon.getHeight())/2;
}
}
public void onPaint(Graphics g)
{
int fc = Bar.this.foreColor;
int bc = Bar.this.backColor;
int w = width;
int h = height;
if (pressed)
g.fillShadedRect(0,0,w,h,true,false,fc,pcolor,30);
// draw borders
g.foreColor = c1; g.drawLine(0,0,w,0);
g.foreColor = c3; g.drawLine(w-1,0,w-1,h);
g.foreColor = c4; g.drawLine(0,h-1,w,h-1);
g.foreColor = c2;
if (backgroundStyle == BACKGROUND_SHADED)
g.fillShadedRect(0,1,1,h-2,true,false,fc,c2,30); // drawLine causes an unexpected effect on shaded backgrounds
else
g.drawLine(0,0,0,h);
// draw contents
if (title != null)
{
g.setClip(gap,0,w-gap-gap,h);
int tx = px;
if (leftIcon != null)
{
g.drawImage(leftIcon,px,(height-leftIcon.getHeight())/2);
tx += leftIcon.getWidth()+gap;
}
if (backgroundStyle != Container.BACKGROUND_SOLID)
{
g.foreColor = tcolor;
g.drawText(title, tx+1,py-1);
g.foreColor = bc;
g.drawText(title, tx-1,py+1);
}
g.foreColor = fc;
g.drawText(title, tx,py);
}
else
if (icon != null)
g.drawImage(icon, px,py);
}
public void onEvent(Event e)
{
if ((!canSelectTitle && title != null) || Flick.currentFlick != null)
return;
switch (e.type)
{
case TimerEvent.TRIGGERED:
if (repeatTimer != null && repeatTimer.triggered)
{
if (startRepeat-- <= 0)
{
selected = appId;
if (selected > 1000) selected -= 1000;
parent.postPressedEvent();
}
}
break;
case PenEvent.PEN_DOWN:
pressed = true;
Window.needsPaint = true;
if (autoRepeatRate != 0)
{
startRepeat = 2;
repeatTimer = addTimer(autoRepeatRate);
}
break;
case PenEvent.PEN_UP:
if (pressed)
{
selected = appId;
if (selected > 1000) selected -= 1000;
boolean fired = repeatTimer != null && startRepeat <= 0;
pressed = false;
if (repeatTimer != null)
removeTimer(repeatTimer);
if (!fired)
parent.postPressedEvent();
}
else
{
selected = -1;
if (repeatTimer != null)
removeTimer(repeatTimer);
}
Window.needsPaint = true;
break;
case PenEvent.PEN_DRAG:
{
PenEvent pe = (PenEvent)e;
boolean armed = isInsideOrNear(pe.x,pe.y);
if (armed != pressed)
{
pressed = armed;
Window.needsPaint = true;
}
break;
}
case KeyEvent.ACTION_KEY_PRESS:
selected = appId;
if (selected > 1000) selected -= 1000;
parent.postPressedEvent();
break;
}
}
}
/**
* Constructs a <code>Bar</code> object without a title. Note that if you call the <code>setTitle()</code> method, a <code>RuntimeException</code>
* will be thrown.
*
* If you want to change the title later, use the other constructor and pass an empty string (<code>""</code>).
*/
public Bar()
{
this(null);
}
/**
* Constructs a <code>Bar</code> object with the given title.
*
* @param title The bar title.
*/
public Bar(String title)
{
this.title = title != null ? new BarButton(title,null) : null;
this.backgroundStyle = BACKGROUND_SHADED;
//this.ignoreInsets = true;
setFont(font.asBold());
}
/**
* An image icon that can be placed at the left of the title. It only shows if there's a title set. Pass <code>null</code> to remove the icon if
* it was previously set.
*
* @param icon The image icon.
*/
public void setIcon(Image icon)
{
if (title != null)
{
title.leftIcon0 = icon;
title.leftIcon = null;
if (initialized) initUI();
}
}
/** Returns the icon set (and possibly resized) with setIcon, or null if none was set */
public Image getIcon()
{
return title != null ? title.leftIcon : null;
}
/**
* Changes the title to the given one.
*
* @param newTitle The bar new title.
*/
public void setTitle(String newTitle)
{
if (this.title == null)
throw new RuntimeException("You can only set a title if you set one in the Bar's constructor.");
title.title = newTitle;
title.onBoundsChanged(false);
Window.needsPaint = true;
}
/**
* Retrieves the current title.
*
* @return The bar title.
*/
public String getTitle()
{
return this.title == null ? "" : title.title;
}
/**
* Adds an image button at right.
*
* @param icon The image to the add to a button in the bar.
* @return The button index
*/
public int addButton(Image icon)
{
return addButton(icon,true);
}
/**
* Adds an image button at the given position.
*
* @param icon The image to the add to a button in the bar.
* @param atRight if true, button is added at right; if false, button is added at left.
* @return The button index
*/
public int addButton(Image icon, boolean atRight)
{
return addControl(new BarButton(null,icon), atRight);
}
/**
* Sets the given button with an auto-repeat interval in the given milliseconds.
*
* @param idx The index of the button in the bar.
* @param ms The auto-repeat interval in milliseconds.
*/
public void setButtonRepeatRate(int idx, int ms)
{
((BarButton)icons.items[idx]).autoRepeatRate = ms;
}
/**
* Adds a control to the bar at right. Not all types of controls are supported.
*
* @param c The control to be added.
* @return The button index
*/
public int addControl(Control c)
{
return addControl(c, true);
}
/**
* Adds a control to the bar. Not all types of controls are supported.
*
* @param atRight if true, button is added at right; if false, button is added at left.
* @param c The control to be added.
* @return The button index
*/
public int addControl(Control c, boolean atRight)
{
icons.addElement(c);
for (int i = icons.size(); --i >= 0;)
{
Control cc = (Control)icons.items[i];
cc.appId = cc.appId == 0 ? (atRight ? 1000 : 0) : (cc.appId > 1000 ? 1000 : 0); // update appId used for selection
cc.appId += i+1;
}
if (initialized) initUI();
return icons.size();
}
/**
* Removes a button at the given index, starting at 1.
*
* @param index The index of the button to be removed.
*/
public void removeButton(int index)
{
icons.removeElementAt(index-1);
for (int i = icons.size(); --i >= 0;)
{
Control c = (Control)icons.items[i];
c.appId = (c.appId > 1000 ? 1000 : 0) + i+1;
}
if (initialized) initUI();
}
/**
* Returns the selected button, or -1 if none was selected.
*
* The title always has index 0 (even if there's no title), and the button' index start at index 1.
*
* @return The index of the selected button.
*/
public int getSelectedIndex()
{
return selected;
}
/**
* Called to initialize the user interface of this container.
*/
public void initUI()
{
removeAll();
int n = icons.size();
if (n == 1 && !(icons.items[0] instanceof BarButton))
add((Control)icons.items[0],LEFT,TOP,FILL,FILL);
else
if (title == null) // if there's no title, make the icons take the whole size of the container
{
for (int i = n; --i > 0;)
add((Control)icons.items[i], i==n-1 ? RIGHT : BEFORE, TOP, width/n, FILL);
if (n > 0)
add((Control)icons.items[0], LEFT, TOP, n == 1 ? FILL : FIT, FILL);
}
else
{
Control lastAtRight = null, lastAtLeft = null;
for (int i = n; --i >= 0;)
{
Control c = (Control)icons.items[i];
boolean atRight = c.appId >= 1000;
int posX;
Control rel = null;
if (atRight)
{
posX = lastAtRight == null ? RIGHT : BEFORE;
rel = lastAtRight;
lastAtRight = c;
}
else
{
posX = lastAtLeft == null ? LEFT : AFTER;
rel = lastAtLeft;
lastAtLeft = c;
}
add(c, posX, TOP, height, FILL, rel);
}
if (n == 0)
add(title, AFTER, TOP, FILL, FILL);
else
{
Spacer spl = new Spacer(0,0), spr = new Spacer(0,0);
add(spl,lastAtLeft != null ? AFTER : LEFT,SAME,lastAtLeft);
add(spr,lastAtRight != null ? BEFORE : RIGHT,SAME,lastAtRight);
add(title, AFTER, TOP, FIT, FILL,spl);
}
if (spinner != null)
{
add(spinner,RIGHT_OF-(n==0 ? fmH/2 : height),CENTER_OF,fmH,fmH,this.title);
spinner.setVisible(false);
}
}
initialized = true;
}
/**
* Called after a <code>setEnabled()</code>, <code>setForeColor()</code>, or <code>setBackColor()</code>; or when a control has been added to a
* container. If <code>colorsChanged</code> is <code>true</code>, it was called from <code>setForeColor()</code>/<code>setBackColor()</code>/
* <code>Container.add()</code>; otherwise, it was called from <code>setEnabled()</code>.
*
* @param colorsChanged Indicates if the control colors have changed, which happens after a <code>setForeColor()</code>,
* <code>setBackColor()</code>, or <code>Container.add()</code>.
*/
public void onColorsChanged(boolean colorsChanged)
{
c1 = Color.brighter(backColor,30);
c2 = Color.brighter(backColor,60);
c3 = Color.darker(backColor,30);
c4 = Color.darker(backColor,60);
tcolor = Color.darker(backColor,32);
pcolor = Color.interpolate(backColor,foreColor);
}
/**
* Returns the preferred width of this control.
*
* @return The preferred width of this control.
*/
public int getPreferredWidth()
{
return parent == null ? FILL : parent.width;
}
/**
* Returns the preferred height of this control.
*
* @return The preferred height of this control.
*/
public int getPreferredHeight()
{
return Settings.isLandscape() ? (landscapePrefH != 0 ? landscapePrefH : fmH*2) : (portraitPrefH != 0 ? portraitPrefH : fmH*2);
}
/**
* Shows and starts the spinner (if one has been assigned to the <code>spinner</code> field).
*
* @see #spinner
*/
public void startSpinner()
{
spinner.setVisible(true);
spinner.start();
}
/** Updates the spinner; sets it visible if not yet. */
public void updateSinner()
{
if (!spinner.visible)
spinner.setVisible(true);
spinner.update();
}
/**
* Stops and hides the spinner (if createSpinner or setSpinner was called before)
*
* @see #spinner
*/
public void stopSpinner()
{
spinner.stop();
spinner.setVisible(false);
}
/**
* Repositions this control, calling again <code>setRect()</code> with the original parameters.
*/
public void reposition()
{
super.reposition();
initUI();
}
/**
* Assigns the BACK key on Android (mapped to <code>SpecialKeys.ESCAPE</code>) to the given button. This can only be called after the bar has been
* added to a container.
*
* For example, if button 1 is assigned with <code>totalcross.res.Resources.back</code>, call <code>assignBackKeyToButton(1);</code>.
*
* @param idx The index of the bar button, starting at 1.
*/
public void assignBackKeyToButton(int idx)
{
final int i = idx;
Window w = getParentWindow();
w.addKeyListener(new KeyListener()
{
public void keyPressed(KeyEvent e) {}
public void actionkeyPressed(KeyEvent e) {}
public void specialkeyPressed(KeyEvent e)
{
if (e.key == SpecialKeys.ESCAPE)
{
e.consumed = true;
selected = ((BarButton)icons.items[i]).appId;
if (selected > 1000) selected -= 1000;
postPressedEvent();
}
}
});
w.callListenersOnAllTargets = true;
}
/**
* Creates a spinner with the following color. The spinner will be placed at the right of the title (it only works if there's a title).
*
* @param color The spinner color.
*/
public void createSpinner(int color)
{
spinner = new Spinner();
spinner.setForeColor(color);
}
/** Sets the spinner to the given one.
*/
public void setSpinner(Spinner s)
{
spinner = s;
}
}