/*********************************************************************************
* 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.dialog;
import totalcross.sys.*;
import totalcross.ui.*;
import totalcross.ui.event.*;
import totalcross.ui.font.FontMetrics;
import totalcross.ui.gfx.*;
import totalcross.ui.image.Image;
import totalcross.ui.image.ImageException;
/** This class implements a scrollable message box window with customized buttons, delayed
* unpop and scrolling text.
* <br>for example, to create an automatic unpop after 5 seconds, do:
* <pre>
* MessageBox mb = new MessageBox("TotalCross","TotalCross is the most exciting tool for developing totally cross-platform programs.",null);
* mb.setUnpopDelay(5000);
* mb.popup(mb);
* </pre>
*/
public class MessageBox extends Window
{
protected Label msg;
public PushButtonGroup btns;
private int selected = -1;
private boolean hasScroll;
protected int xa,ya,wa,ha; // arrow coords
private TimerEvent unpopTimer,buttonTimer;
private boolean oldHighlighting;
private static String[] ok = {"Ok"};
private int captionCount;
private String originalText;
private int labelAlign = CENTER;
private String[] buttonCaptions;
private int gap, insideGap;
private Image icon;
private ScrollContainer sc;
protected int lgap;
/**
* Set at the object creation. if true, all the buttons will have the same width, based on the width of the largest
* one.<br>
* Default value is false.
*
* @since TotalCross 1.27
*/
private boolean allSameWidth; //flsobral@tc126_50: set to make all buttons to always have the same width.
/** Defines the y position on screen where this window opens. Can be changed to TOP or BOTTOM. Defaults to CENTER.
* @see #CENTER
* @see #TOP
* @see #BOTTOM
*/
public int yPosition = CENTER; // guich@tc110_7
/** If you set the buttonCaptions array in the construction, you can also set this
* public field to an int array of the keys that maps to each of the buttons.
* For example, if you set the buttons to {"Ok","Cancel"}, you can map the enter key
* for the Ok button and the escape key for the Cancel button by assigning:
* <pre>
* buttonKeys = new int[]{SpecialKeys.ENTER,SpecialKeys.ESCAPE};
* </pre>
* Note that ENTER is also handled as ACTION, since the ENTER key is mapped to ACTION under some platforms.
* @since TotalCross 1.27
*/
public int[] buttonKeys; // guich@tc126_40
/**
* Constructs a message box with the text and one "Ok" button. The text may be separated by '\n' as the line
* delimiters; otherwise, it is automatically splitted if its too big to fit on screen.
*/
public MessageBox(String title, String msg)
{
this(title, msg, ok, false, 4, 6);
}
/**
* Constructs a message box with the text and the specified button captions. The text may be separated by '\n' as the
* line delimiters; otherwise, it is automatically splitted if its too big to fit on screen. if buttonCaptions is
* null, no buttons are displayed and you must dismiss the dialog by calling unpop or by setting the delay using
* setUnpopDelay method
*/
public MessageBox(String title, String text, String[] buttonCaptions)
{
this(title, text, buttonCaptions, false, 4, 6);
}
/**
* Constructs a message box with the text and the specified button captions. The text may be separated by '\n' as the
* line delimiters; otherwise, it is automatically splitted if its too big to fit on screen. If buttonCaptions is
* null, no buttons are displayed and you must dismiss the dialog by calling unpop or by setting the delay using
* setUnpopDelay method. The parameters allSameWidth is the same as in the constructor for PushButtonGroup.
*
* @since TotalCross 1.27
*/
public MessageBox(String title, String text, String[] buttonCaptions, boolean allSameWidth)
{
this(title, text, buttonCaptions, allSameWidth, 4, 6);
}
/**
* Constructs a message box with the text and the specified button captions. The text may be separated by '\n' as the
* line delimiters; otherwise, it is automatically splitted if its too big to fit on screen. If buttonCaptions is
* null, no buttons are displayed and you must dismiss the dialog by calling unpop or by setting the delay using
* setUnpopDelay method. The new parameters gap and insideGap are the same as in the constructor for PushButtonGroup.
*
* @since SuperWaba 4.11
*/
public MessageBox(String title, String text, String[] buttonCaptions, int gap, int insideGap)
{
this(title, text, buttonCaptions, false, gap, insideGap);
}
/**
* Constructs a message box with the text and the specified button captions. The text may be separated by '\n' as the
* line delimiters; otherwise, it is automatically splitted if its too big to fit on screen. If buttonCaptions is
* null, no buttons are displayed and you must dismiss the dialog by calling unpop or by setting the delay using
* setUnpopDelay method. The parameters allSameWidth, gap and insideGap are the same as in the constructor for PushButtonGroup.
*
* @since TotalCross 1.27
*/
public MessageBox(String title, String text, String[] buttonCaptions, boolean allSameWidth, int gap, int insideGap) // andrew@420_5
{
super(title,ROUND_BORDER);
this.buttonCaptions = buttonCaptions;
this.gap = gap;
this.insideGap = insideGap;
this.allSameWidth = allSameWidth;
if (!Settings.onJavaSE && Settings.vibrateMessageBox) // guich@tc122_51
Vm.vibrate(200);
uiAdjustmentsBasedOnFontHeightIsSupported = false;
fadeOtherWindows = Settings.fadeOtherWindows;
transitionEffect = Settings.enableWindowTransitionEffects ? TRANSITION_FADE : TRANSITION_NONE;
ha = 6 * Settings.screenHeight/160; // guich@450_24: increase arrow size if screen size change
wa = ha*2+1; // guich@570_52: now wa is computed from ha
if (text == null)
text = "";
this.originalText = text; // guich@tc100: now we use \n instead of |
if ((Settings.onJavaSE && Settings.screenWidth == 240) || Settings.isWindowsDevice()) // guich@tc110_53
setFont(font.asBold());
}
/** This method can be used to set the text AFTER the dialog was shown. However, the dialog will not be resized.
* @since TotalCross 1.3
*/
public void setText(String text)
{
int maxW = Settings.screenWidth-fmH - lgap;
originalText = text;
if (text.indexOf('\n') < 0 && fm.stringWidth(text) > maxW) // guich@tc100: automatically split the text if its too big to fit screen
text = Convert.insertLineBreak(maxW, fm, text.replace('\n',' '));
msg.setText(text);
msg.repaintNow();
}
protected void onPopup()
{
removeAll();
int maxW = Settings.screenWidth-fmH - lgap;
String text = originalText;
if (text.indexOf('\n') < 0 && fm.stringWidth(text) > maxW) // guich@tc100: automatically split the text if its too big to fit screen
text = Convert.insertLineBreak(maxW, fm, text.replace('\n',' '));
msg = new Label(text,labelAlign);
msg.setFont(font);
int wb,hb;
int androidGap = uiAndroid ? fmH/3 : 0;
if (androidGap > 0 && (androidGap&1) == 1) androidGap++;
boolean multiRow = false;
if (buttonCaptions == null)
wb = hb = 0;
else
{
captionCount = buttonCaptions.length;
btns = new PushButtonGroup(buttonCaptions,false,-1,gap,insideGap,1,allSameWidth || uiAndroid,PushButtonGroup.BUTTON);
btns.setFont(font);
wb = btns.getPreferredWidth();
if (wb > Settings.screenWidth-10) // guich@tc123_38: buttons too large? place them in a single column
{
multiRow = true;
btns = new PushButtonGroup(buttonCaptions,false,-1,gap,insideGap,captionCount,true,PushButtonGroup.BUTTON);
btns.setFont(font);
wb = btns.getPreferredWidth();
}
hb = btns.getPreferredHeight() + (multiRow ? insideGap*buttonCaptions.length : insideGap);
hb += androidGap;
}
int wm = Math.min(msg.getPreferredWidth()+(uiAndroid?fmH:1),maxW);
int hm = msg.getPreferredHeight();
FontMetrics fm2 = titleFont.fm; // guich@220_28
int iconH = icon == null ? 0 : icon.getHeight();
int iconW = icon == null ? 0 : icon.getWidth();
boolean removeTitleLine = uiAndroid && borderStyle == ROUND_BORDER && (title == null || title.length() == 0);
if (removeTitleLine) titleGap = 0;
else
if (uiAndroid) hm += fmH;
int captionH = (removeTitleLine ? 0 : Math.max(iconH,fm2.height)+titleGap)+8;
int ly = captionH - 6;
if (captionH+hb+hm > Settings.screenHeight) // needs scroll?
{
if (hb == 0) hb = ha;
hm = Math.max(fmH,Settings.screenHeight - captionH - hb - ha);
hasScroll = true;
}
else
if (removeTitleLine)
ly = androidBorderThickness+1;
int h = captionH + hb + hm;
if (uiAndroid) h += fmH/2;
int w = lgap + Math.max(Math.max(wb,wm),(iconW > 0 ? iconW+fmH : 0) + fm2.stringWidth(title!=null?title:""))+7; // guich@200b4_29 - guich@tc100: +7 instead of +6, to fix 565_11
w = Math.min(w,Settings.screenWidth); // guich@200b4_28: dont let the window be greater than the screen size
setRect(CENTER,yPosition,w,h);
if (!removeTitleLine && icon != null)
{
titleAlign = LEFT+fmH/2+iconW+fmH/2;
ImageControl ic = new ImageControl(icon);
ic.transparentBackground = true;
add(ic,LEFT+fmH/2,(captionH-iconH)/2 - titleFont.fm.descent);
}
if (!uiAndroid || !hasScroll)
add(msg);
else
{
add(sc = new ScrollContainer(false,true), LEFT+2+lgap,btns == null ? CENTER : ly+2,FILL-2,hm-2);
sc.add(msg,LEFT,TOP,FILL,PREFERRED);
hasScroll = false;
}
if (btns != null) add(btns);
if (sc == null)
msg.setRect(LEFT+2+lgap,btns == null ? CENTER : ly,FILL-2,hm); // guich@350_17: replaced wm by client_rect.width - guich@565_11: -2
if (btns != null)
{
if (uiAndroid && !multiRow)
btns.setRect(buttonCaptions.length > 1 ? LEFT+3 : CENTER,ly+hm+androidGap/2,buttonCaptions.length > 1 ? FILL-3 : Math.max(w/3,wb),FILL-2);
else
btns.setRect(CENTER,ly+2+hm+androidGap/2,wb,hb-androidGap);
}
Rect r = sc != null ? sc.getRect() : msg.getRect();
xa = r.x+r.width-(wa << 1);
ya = btns != null ? (btns.getY()+(btns.getHeight()-ha)/2) : (r.y2()+3); // guich@570_52: vertically center the arrow buttons if the ok button is present
if (backColor == UIColors.controlsBack) // guich@tc110_8: only change if the color was not yet set by the user
setBackColor(UIColors.messageboxBack);
if (foreColor == UIColors.controlsFore)
setForeColor(UIColors.messageboxFore);
msg.setBackForeColors(backColor, foreColor);
if (btns != null)
{
btns.setBackForeColors(UIColors.messageboxAction,Color.getBetterContrast(UIColors.messageboxAction, foreColor, backColor)); // guich@tc123_53
if (uiAndroid && !removeTitleLine) footerH = height - (sc != null ? sc.getY2()+2 : msg.getY2()) - 1;
if (buttonTimer != null)
btns.setVisible(false);
}
}
public void reposition()
{
onPopup();
}
/** Set an icon to be shown in the MessageBox's title, at left.
* It only works if there's a title. If you really need an empty title, pass as title a
* String with a couple of spaces, like " ".
*
* The icon's width and height will be set to title's font ascent.
* @since TotalCross 1.3
*/
public void setIcon(Image icon) throws ImageException
{
this.icon = icon.getSmoothScaledInstance(titleFont.fm.ascent,titleFont.fm.ascent);
}
/** Sets the alignment for the text. Must be CENTER (default), LEFT or RIGHT */
public void setTextAlignment(int align)
{
labelAlign = align; // guich@241_4
}
/** sets a delay for the unpop of this dialog */
public void setUnpopDelay(int unpopDelay)
{
if (unpopDelay <= 0)
throw new IllegalArgumentException("Argument 'unpopDelay' must have a positive value");
if (unpopTimer != null)
removeTimer(unpopTimer);
unpopTimer = addTimer(unpopDelay);
}
public void onPaint(Graphics g)
{
if (hasScroll)
{
g.drawArrow(xa,ya,ha,Graphics.ARROW_UP,false,msg.canScroll(false) ? foreColor : Color.getCursorColor(foreColor)); // guich@200b4_143: msg.canScroll
g.drawArrow(xa+wa,ya,ha,Graphics.ARROW_DOWN,false,msg.canScroll(true) ? foreColor : Color.getCursorColor(foreColor));
}
}
/** handle scroll buttons and normal buttons */
public void onEvent(Event e)
{
switch (e.type)
{
case TimerEvent.TRIGGERED:
if (buttonTimer != null && buttonTimer.triggered)
{
removeTimer(buttonTimer);
if (btns != null)
btns.setVisible(true);
}
else
if (e.target == this)
{
removeTimer(unpopTimer);
if (popped) // luciana@570_25 - Maybe the unpop was already called (the user can click OK button before the delay expires)
unpop();
}
break;
case PenEvent.PEN_DOWN:
if (hasScroll)
{
int px=((PenEvent)e).x;
int py=((PenEvent)e).y;
if (ya <= py && py <= ya+ha && xa <= px && px < xa+(wa<<1) && msg.scroll((px-xa)/wa != 0)) // at the arrow points?
Window.needsPaint = true;
else
if (msg.isInsideOrNear(px,py) && msg.scroll(py > msg.getHeight()/2))
Window.needsPaint = true;
}
break;
case KeyEvent.SPECIAL_KEY_PRESS: // guich@200b4_42
KeyEvent ke = (KeyEvent)e;
if (ke.isUpKey()) // guich@330_45
{
msg.scroll(false);
Window.needsPaint = true; // guich@300_16: update the arrow's state
}
else
if (ke.isDownKey()) // guich@330_45
{
msg.scroll(true);
Window.needsPaint = true; // guich@300_16: update the arrow's state
}
else
if (!Settings.keyboardFocusTraversable && captionCount == 1 && ke.isActionKey()) // there's a single button and the enter key was pressed?
{
selected = 0;
unpop();
}
else
if (buttonKeys != null && captionCount > 0)
{
int k = ke.key;
for (int i = buttonKeys.length; --i >= 0;)
if (buttonKeys[i] == k || (buttonKeys[i] == SpecialKeys.ENTER && k == SpecialKeys.ACTION)) // handle ENTER as ACTION too
{
selected = i;
btns.setSelectedIndex(i);
unpop();
break;
}
}
break;
case ControlEvent.PRESSED:
if (e.target == btns && (selected=btns.getSelectedIndex()) != -1)
{
btns.setSelectedIndex(-1);
unpop();
}
break;
}
}
/** Returns the pressed button index, starting from 0 */
public int getPressedButtonIndex()
{
return selected;
}
protected void postPopup()
{
if (Settings.keyboardFocusTraversable) // guich@570_39: use this instead of pen less
{
if (btns != null) // guich@572_
{
btns.requestFocus(); // without a pen, select the first button
btns.setSelectedIndex(0); // bcao@421_55 Added default control selection at dialog opening
}
oldHighlighting = isHighlighting;
isHighlighting = false; // allow a direct click to dismiss this dialog
}
}
protected void postUnpop()
{
if (Settings.keyboardFocusTraversable) // guich@573_1: put back the highlighting state
isHighlighting = oldHighlighting;
postPressedEvent(); // guich@580_27
}
/** Title shown in the showException dialog. */
public static String showExceptionTitle = "Exception Thrown"; // guich@tc113_8
/** Shows the exception, with its name, message and stack trace in a new MessageBox.
* @since TotalCross 1.0
*/
public static void showException(Throwable t, boolean dumpToConsole)
{
String exmsg = t.getMessage();
exmsg = exmsg == null ? "" : "Message: "+t.getMessage()+"\n";
String msg = "Exception: "+t.getClass()+"\n"+exmsg+"Stack trace:\n"+Vm.getStackTrace(t);
if (dumpToConsole)
Vm.debug(msg);
MessageBox mb = new MessageBox(showExceptionTitle,"");
mb.originalText = Convert.insertLineBreak(Settings.screenWidth-mb.fmH, mb.font.fm, msg);
mb.labelAlign = LEFT;
mb.popup();
}
protected void onFontChanged()
{
}
/** Calling this method will make the buttons initially hidden and will show them after
* the specified number of milisseconds.
*
* Here's a sample:
* <pre>
* MessageBox mb = new MessageBox("Novo Tweet!",tweet);
* mb.setTimeToShowButton(7000);
* mb.popup();
* </pre>
* @since TotalCross 1.53
*/
public void setDelayToShowButton(int ms)
{
buttonTimer = addTimer(ms);
}
}