/*********************************************************************************
* 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.*;
/** Constructs a MenuBarDropDown with the given items.
* This class is used in conjunction with the MenuBar. However, you can also
* use it to create a stand alone "right click" menu.
* Here is an example of how to build the MenuBarDropDown for the DateBook:<br>
* <pre>
* MenuItem col1[] = // <b>note that the first String is always skipped</b>
* {
* new MenuItem("Record"),
* new MenuItem("NewEvent"),
* new MenuItem("Delete Note..."),
* new MenuItem("Purge..."),
* new MenuItem(), // create a dotted line
* new MenuItem("Beam Event"),
* };
* MenuBarDropDown pop = new MenuBarDropDown(10,10,col1);
* pop.popupNonBlocking();
* // a PRESSED event is generated when a item is selected. The Window
* // is closed when the user clicks outside or selects an item. Use the getSelectedIndex
* // method to discover the index of the MenuItem array that was selected (-1 if none).
* </pre>
* <b>Note</b>: the menu items must fit on screen. No clipping is applied.
* Also, the font and colors can be changed if desired.
*/
public class MenuBarDropDown extends Window
{
private MenuItem []items;
private int []ypos;
private int selected=-1;
private int popX,popY;
private PenEvent pe = new PenEvent();
private int lineHeight;
private int dColor,bColor,fColor,cursorColor=-1;
private int fourColors[] = new int[4];
/** Converts an old Menu format into the new one. */
public static MenuItem[] strings2items(String[] strings)
{
MenuItem[] mi = new MenuItem[strings.length];
MenuItem m;
for (int i = mi.length-1; i > 0; i--)
{
String caption = strings[i];
switch (caption.charAt(0))
{
case '*': m = new MenuItem(caption.substring(1)); m.isEnabled = false; break; // disabled
case '!': m = new MenuItem(caption.substring(1),true); break; // checked
case '?': m = new MenuItem(caption.substring(1),false); break; // unchecked
case '-': m = new MenuItem(); break; // separator
default:
m = new MenuItem(caption);
}
mi[i] = m;
}
mi[0] = new MenuItem(strings[0]);
return mi;
}
/** Constructs a MenuBarDropDown that will show at the given x,y position the given items. */
public MenuBarDropDown(int x, int y, MenuItem []items)
{
focusTraversable = true; // timo@tc100b3 - must be focusTraversable for geographical focus to work
this.items = items;
canDrag = false;
popX = x;
popY = y;
setFont(getFont().asBold()); // use a bold font.
setBackColor(Color.WHITE);
borderStyle = -1; // guich@220_48
started = true; // avoid calling the initUI method
pe.target = this;
pe.type = PenEvent.PEN_DOWN;
}
private MenuBar getMenuBar()
{
Window w = (Window)zStack.items[zStack.size()-2];
if (w instanceof MenuBar)
return (MenuBar)w;
return null;
}
/** Change the font and recompute some parameters */
protected void onFontChanged() // guich@200b4_153
{
// all this is recalculated because the font had changed
int w = 0;
lineHeight = fmH+1+titleGap;
int n = items.length;
ypos = new int[n];
// compute the maximum width
boolean haveCheckable = false;
for (int i =1; i < n; i++)
{
MenuItem mi = items[i];
if (mi.isCheckable)
haveCheckable = true;
w = Math.max(w,fm.stringWidth(mi.caption));
}
if (haveCheckable)
{
double factor = (double)fmH / 11f;
w += (int)(factor*8)+1; // guich@300_1
}
// compute the rects
int y=1;
int remH = (lineHeight-fmH)/2;
ypos[0] = y+remH;
for (int i=1; i < n; i++)
{
y += items[i].isSeparator ? 1 : lineHeight;
ypos[i] = y+remH;
}
// Check if we're not after the screen limits
w += 6;
if (popX+w > Settings.screenWidth)
popX = Settings.screenWidth-w;
if (popX < 0)
popX = 0;
setRect(popX,popY,w,y+2);
}
public void screenResized()
{
onFontChanged();
}
/** Selects the given index. */
public int setSelectedIndex(int index)
{
if (index <= -1)
index = selected = 1;
else
if (index == 1000) // Don't update contents on the current screen when unpop.
selected = -1;
else
if (selected > 0) // && (index > 0))
{
selected = index;
Window.needsPaint = true;
}
else
{
selected = index >= 1 ? index : 1;
Window.needsPaint = true;
}
return selected;
}
public int getYPos(int index)
{
return ypos[index];
}
/** Returns the selected index when this window was closed or -1 if non was selected */
public int getSelectedIndex()
{
return selected;
}
/** Setup some important variables */
protected void onPopup()
{
selected = -1;
onFontChanged(); // a screen rotation may have enlarged the screen
highlighted = this; // timo@tc100b3 - if highlighted != _focus, geographical focus doesn't work
}
protected void postPopup()
{
isHighlighting = Settings.keyboardFocusTraversable; // guich@573_26
}
protected void postUnpop()
{
if (selected != -1) // guich@580_27
postPressedEvent();
}
/** Close the popup list with a click outside its bounds */
protected boolean onClickedOutside(PenEvent event)
{
Window w;
selected = -1;
if (event.y < this.y && ((w = (Window)zStack.items[zStack.size()-2]) instanceof totalcross.ui.MenuBar)) // clicked on MenuBar? propagate the event to it
{
pe.absoluteX = event.absoluteX;
pe.x = event.x;
pe.absoluteY = event.absoluteY;
pe.y = event.y;
w._onEvent(pe);
if (topMost == this) requestFocus(); // we must get back the focus
}
else
if (event.x < this.x || event.y < this.y || event.x >= (this.x+this.width) || event.y >= (this.y+this.height)) // guich@300_39: added the last condition to fix a problem when this is used not with a menubar.
{
if (getTopMost() instanceof MenuBar) // guich@tc114_29
enableUpdateScreen = false; // avoids that the screen is updated in next event tick
unpop();
}
return true;
}
protected void loadBehind() // guich@tc120_38
{
try
{
if (!(zStack.peek() instanceof MenuBar)) // only update screen if our parent window is not a MenuBar
super.loadBehind();
}
catch (ElementNotFoundException e)
{
MessageBox.showException(e,true);
}
}
private int getItemAt(int y)
{
for (int i =1; i < ypos.length; i++)
{
MenuItem mi = items[i];
if (ypos[i-1] <= y && y <= ypos[i] && mi.isEnabled && !mi.isSeparator)
return i;
}
return -1;
}
public void onEvent(Event event)
{
switch (event.type)
{
case KeyEvent.SPECIAL_KEY_PRESS:
if ( ((KeyEvent)event).key == SpecialKeys.MENU)
{
selected = -1; // user canceled
unpop();
}
else
if (handleFocusChangeKeys((KeyEvent)event)) // guich@tc100b4_8
break;
case KeyEvent.KEY_PRESS:
Window w = Window.topMost;
int idx = ((KeyEvent)event).key - '0'; // guich@tc112_27: allow select using numbers
if (0 < idx && idx < items.length)
{
MenuItem mi = items[idx];
if (!mi.isSeparator && mi.isEnabled) // skip invalid lines
{
setSelectedIndex(idx);
unpop();
}
}
else
if (w instanceof MenuBar) ((MenuBar)w).close(); // close all menu windows
break;
case KeyEvent.ACTION_KEY_PRESS: // kmeehl@tc100
if (selected != -1)
unpop();
break;
case PenEvent.PEN_DOWN:
case PenEvent.PEN_DRAG:
case PenEvent.PEN_UP:
PenEvent pe = (PenEvent)event;
if (0 < pe.y && pe.y < height)
{
int newSelected = getItemAt(pe.y);
if (newSelected != selected)
{
selected = newSelected;
Window.needsPaint = true;
}
if (event.type == PenEvent.PEN_UP && selected != -1)
{
event.consumed = true;
unpop();
}
}
else
if (selected != -1) // outside valid area?
{
Window.needsPaint = true;
selected = -1;
}
break;
}
}
/** Sets the cursor color. By default, it is based in the background color */
public void setCursorColor(int c) // guich@220_49
{
cursorColor = c;
}
protected void onColorsChanged(boolean colorsChanged)
{
fColor = getForeColor();
bColor = getBackColor();
dColor = Color.interpolate(bColor,fColor);//getCursorColor(fColor);
if (colorsChanged)
{
Graphics.compute3dColors(true,backColor,foreColor,fourColors);
if (cursorColor == -1)
cursorColor = 0x0000F0;
}
}
public void onPaint(Graphics g)
{
// paint border
g.foreColor = fColor;
g.backColor = bColor;
// changed drawing of popup menu border to look more native
// PMD 25Oct2001
if (borderStyle == -1) // guich@220_48
g.draw3dRect(0,0,width,height,Graphics.R3D_SHADED,false,true,fourColors);
g.setFont(font);
int halfTG = titleGap/2;
// paing captions
for (int i =1; i < items.length; i++)
{
if (i == selected)
{
g.backColor = cursorColor != -1 ? cursorColor : Color.getCursorColor(bColor); // guich@220_49
g.fillRect(1,ypos[i-1]-titleGap/2,width-3,lineHeight);
g.backColor = bColor;
}
MenuItem mi = items[i];
if (mi.isSeparator) // dotted line?
g.drawDots(0,ypos[i-1]-halfTG,width-3,ypos[i-1]-halfTG);
else
{
// is the menu item disabled?
if (!mi.isEnabled)
g.foreColor = dColor;
// can the menu item be checkable?
if (mi.isChecked)
{
// draws the check
int x = width-9 - fmH/11; // guich@580_8: space a bit on highres devices
int y = ypos[i-1] + 5;
int my = 2;
int mx = 7;
int hh = 2 * fmH / 11; // guich@300_1
if (fmH > 11)
{
x -= hh;
y += (fmH==14 ? hh-1 : hh);
my += hh>>1;
mx += hh;
}
for (int j = 0; j < mx; j++)
{
g.drawLine(x, y, x, y + hh);
x++;
if (j < my)
y++;
else
y--;
}
}
g.drawText(mi.caption,3,ypos[i-1], textShadowColor != -1, textShadowColor);
if (!mi.isEnabled) g.foreColor = fColor;
}
}
}
protected boolean handleFocusChangeKeys(KeyEvent ke) // guich@512_1: transfer focus on tab keys - fdie@550_15 : transfer also on arrow keys
{
if (ke.isActionKey())
unpop();
else
if (ke.isUpKey() || ke.isDownKey())
{
// timo@tc100b3 - moved getNextSelectionIndex into its own method
int newSelected = getNextSelectionIndex(ke);
if (newSelected != selected && newSelected != -1) // guich@tc115_19: only if changed and not -1
setSelectedIndex(newSelected);
}
else
if (ke.key == SpecialKeys.LEFT || ke.key == SpecialKeys.RIGHT || ke.key == SpecialKeys.TAB)
{
MenuBar mb = getMenuBar();
if (mb != null)
mb.moveBy(ke.key==SpecialKeys.LEFT ? -1 : 1);
}
else return false;
return true;
}
public Control handleGeographicalFocusChangeKeys(KeyEvent ke) // kmeehl@tc100
{
handleFocusChangeKeys(ke);
return this;
}
/**
* Returns the index of the next menu item that is to be selected based on the direction of the KeyEvent.
* @param ke The key event
* @return The index of the next menu item based on the direction of the key event.
*/
protected int getNextSelectionIndex(KeyEvent ke)
{
int newSelected = selected==-1?0:selected;
boolean isDown = ke.isDownKey();
for (int i = items.length; i > 0; i--) // to avoid infinite loop if all items are non-clickable
{
if (isDown)
{
if (++newSelected == items.length)
newSelected = Settings.circularNavigation ? 1 : newSelected-1; // guich@tc115_19
}
else
{
if (--newSelected <= 0)
newSelected = Settings.circularNavigation ? items.length-1 : 1; // guich@tc115_19
}
MenuItem mi = items[newSelected];
if (!mi.isSeparator && mi.isEnabled) // skip invalid lines
return newSelected;
}
return -1;
}
}