/********************************************************************************* * 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.res.*; import totalcross.sys.*; import totalcross.ui.dialog.*; import totalcross.ui.event.*; import totalcross.ui.font.*; import totalcross.ui.gfx.*; import totalcross.ui.image.*; import totalcross.unit.*; import totalcross.util.*; import totalcross.util.concurrent.*; /** * MainWindow is the main window of a UI-based application. * <p> * All TotalCross programs with an user-interface must have <b>one and only one</b> main window. * <p> * Here is an example showing a basic application: * * <pre> * public class MyProgram extends MainWindow * { * Edit edName; * public void initUI() * { * ... initialization code ... * add(new Label("Name:"), LEFT,TOP+2); * add(edName = new Edit(""), AFTER,SAME-2); * } * } * </pre> */ public class MainWindow extends Window implements totalcross.MainClass { protected TimerEvent firstTimer; private TimerEvent startTimer; static MainWindow mainWindowInstance; private static int lastMinInterval; private boolean restoreRegistry,initUICalled; private static int timeAvailable; static Font defaultFont; private static Thread mainThread; private Lock runnersLock = new Lock(); private Vector runners = new Vector(1); /** Constructs a main window with no title and no border. */ public MainWindow() { this(null,NO_BORDER); } /** Constructs a main window with the given title and border style. * @see #NO_BORDER * @see #RECT_BORDER * @see #ROUND_BORDER * @see #TAB_BORDER * @see #TAB_ONLY_BORDER * @since SuperWaba 2.0b4 */ public MainWindow(String title, byte style) // guich@112 { super(title,style); setX = 0; setY = 0; setW = Settings.screenWidth; setH = Settings.screenHeight; setFont = this.font; Settings.scrollDistanceOnMouseWheelMove = fmH; boolean isAndroid = Settings.platform.equals(Settings.ANDROID); boolean isIphone = Settings.isIOS(); if (isAndroid || isIphone) Settings.unmovableSIP = true; if (Settings.fingerTouch) // guich@tc120_48 Settings.touchTolerance = fmH/2; // update some settings setBackColor(UIColors.controlsBack = 0xA0D8EC); // guich@200b4_39 - guich@tc100: set the controlsBack to this color uitip = new ToolTip(null,""); if (mainWindowInstance == null) { mainWindowInstance = this; mainWindowCreate(); zStack.push(this); // guich topMost = this; } canDrag = false; // we can't drag the mainwindow. byte[] bytes = Vm.getFile("tcapp.prop"); if (bytes != null) Settings.appProps = new Hashtable(new String(bytes)); } /** Returns true if this is the main thread. * @since TotalCross 2.0 */ public static boolean isMainThread() { return mainThread == Thread.currentThread(); } void mainWindowCreate() { totalcross.Launcher.instance.registerMainWindow(this); } void mainWindowCreate4D() {} // not needed at device /** Sets the default font used in all controls created. To change the default font, assign it to this member in the MainWindow constructor, * making it the FIRST LINE in the constructor; you'll not be able to use super(title,border): change by setBorderStyle and setTitle, after * the defaultFont assignment. Example: * <pre> * public MyApp() * { * MainWindow.setDefaultFont(Font.getFont(false, Font.NORMAL_SIZE+2)); * setBorderStyle(TAB_ONLY_BORDER); * setTitle("My application"); * } * </pre> * @since TotalCross 1.0 beta3 */ public static void setDefaultFont(Font newFont) { defaultFont = newFont; mainWindowInstance.setFont(newFont); uitip.setFont(newFont); // guich@tc100b5_58 mainWindowInstance.setTitleFont(newFont.asBold()); // guich@tc125_4 mainWindowInstance.titleGap = 0; if (Settings.fingerTouch) // guich@tc120_48 Settings.touchTolerance = newFont.fm.height/2; } /** Returns the default font. * @since TotalCross 1.0 beta3 */ public static Font getDefaultFont() { return defaultFont; } /** Changes the user interface style to the given one. * This method must be called in the MainWindow's constructor, and only once. E.g.: * <pre> * public class Foo extends MainWindow * { * public Foo() * { * super("Hi bar",TAB_ONLY_BORDER); * setUIStyle(totalcross.sys.Settings.FLAT); * </pre> * Changing to Android style will also set Settings.fingerTouch to true. * If you don't like such behaviour in non finger devices, set this property to false after calling setUIStyle. * * @see totalcross.sys.Settings#Flat * @see totalcross.sys.Settings#Vista * @see totalcross.sys.Settings#Android * @since SuperWaba 5.05 */ public void setUIStyle(byte style) { Settings.uiStyle = style; if (style == Settings.Android) Settings.fingerTouch = true; Control.uiStyleChanged(); Resources.uiStyleChanged(); if (uiAndroid) { androidBorderThickness = Settings.screenWidth <= 320 ? 1 : Settings.screenWidth <= 640 ? 2 : 3; borderGaps[ROUND_BORDER] = androidBorderThickness == 3 ? 3 : 2; } } /** * Notifies the application that it should stop executing and exit. It will * exit after executing any pending events. If the underlying system supports * it, the exitCode passed is returned to the program that started the app. * Note: On AppletViewer/Browser the exitCode is useless. * Note 2: On Android, you can exit softly by using SOFT_EXIT as the exit code. * <p>If you want your code to be called when the VM exits, extend the onExit method. * @see #onExit */ public static final void exit(int exitCode) { totalcross.Launcher.instance.exit(exitCode); } native public static final void exit4D(int exitCode); /** * Notifies the application that it should be minimized, that is, transfered * to the background. Whenever the application is minimized, the following call back function * will be called: {@link #onMinimize()}. Note: On Android, calling {@link #minimize()} will * pause the application execution and it can only be restored manually by the user. This method is * also supported on Windows 32. * @see #onMinimize * @see #onRestore * @since TotalCross 1.10 */ public static void minimize() // bruno@tc110_89 { totalcross.Launcher.instance.minimize(); } native public static void minimize4D(); /** * Notifies the application that it should be restored, that is, transfered * to the foreground. * Whenever the application is restored, the following call back function will be called: * {@link #onRestore()}. Note: This method is supported on Android but the user must restore * the application manually. This method is also supported on Windows 32. * @since TotalCross 1.10 */ public static void restore() // bruno@tc110_89 { totalcross.Launcher.instance.restore(); } native public static void restore4D(); /** * Returns the instance of the current main window. You can use it to get access to methods of the <code>MainWindow</code> class from outside the class. * It is also possible to cast the returned class to the class that is extending <code>MainWindow</code> (this is a normal Java behavior). * So, if UiGadgets is running, it is correct to do: * <pre> * UIGadgets instance = (UIGadgets)MainWindow.getMainWindow(); * </pre> */ public static MainWindow getMainWindow() { return mainWindowInstance; } /** * Adds a timer to a control. This method is protected, the public * method to add a timer to a control is the addTimer() method in * the Control class. The Timer event will be issued to the target every millis milliseconds. */ protected TimerEvent addTimer(Control target, int millis) { TimerEvent t = new TimerEvent(); addTimer(t,target,millis); return t; } /** * Adds the timer t to the target control. This method is protected, the public * method to add a timer to a control is the addTimer() method in * the Control class. The Timer event will be issued to the target every millis milliseconds. */ protected void addTimer(TimerEvent t, Control target, int millis) { addTimer(t, target, millis, true); } /** * Adds the timer t to the target control. This method is protected, the public * method to add a timer to a control is the addTimer() method in * the Control class. The Timer event will be issued to the target every millis milliseconds. */ protected void addTimer(TimerEvent te, Control target, int millis, boolean append) { te.target = target; te.millis = millis; te.lastTick = Vm.getTimeStamp(); if (firstTimer == null) // first timer to be added { te.next = null; firstTimer = te; } else if (append) // appending timer to the end of the list { TimerEvent last = null; for (TimerEvent t = firstTimer; t != null; t = t.next) { if (t == te) // already inserted? get out! return; last = t; } if (last != null) last.next = te; te.next = null; } else // inserting timer to the beginning of the list { te.next = firstTimer; firstTimer = te; } setTimerInterval(1); // forces a call to _onTimerTick inside the TC Event Thread } /** * Removes the given timer from the timers queue. This method returns true if the timer was found * and removed and false if the given timer could not be found. * The <code>target</code> member is set to null. */ public boolean removeTimer(TimerEvent timer) { if (timer == null) return false; TimerEvent t = firstTimer; TimerEvent prev = null; while (t != timer) { if (t == null) return false; prev = t; t = t.next; } if (prev == null) firstTimer = t.next; else prev.next = t.next; if (timer.target != null) // not already removed? setTimerInterval(1); // forces a call to _onTimerTick inside the TC Event Thread timer.target = null; // guich@tc120_46 return true; } /** Removes any timers that belongs to this window or whose paren't is null */ void removeTimers(Window win) { boolean changed; do { changed = false; TimerEvent t = firstTimer; while (t != null) { Control c = (Control)t.target; Window w = c.getParentWindow(); if (w == null || w == win) { changed = true; if (Flick.currentFlick != null && Flick.currentFlick.timer == t) Flick.currentFlick.stop(true); else removeTimer(t); break; } t = t.next; } } while (changed); } /** Called by the VM when the application is starting. Setups a * timer that will call initUI after the event loop is started. * Never call this method directly; this method is not private * to prevent the compiler from removing it during optimization. * The timeAvail parameter is passed by the vm to show how much * time the user have to keep testing the demo vm. Even if this * value is not shown to the user, it is internally computed and * the vm will exit when the counter reaches 0. */ final public void appStarting(int timeAvail) // guich@200b4_107 - guich@tc126_46: added timeAvail parameter to show MessageBox from inside here. { mainThread = Thread.currentThread(); timeAvailable = timeAvail; gfx = new Graphics(this); // revalidate the pixels startTimer = addTimer(1); // guich@567_17 } static boolean quittingApp; /** Called by the system so we can finish things correctly. * Never call this method directly; this method is not private * to prevent the compiler from removing it during optimization. */ final public void appEnding() // guich@200final_11: fixed when switching apps not calling killThreads. { quittingApp = true; // guich@tc100: do this at device side - if (resetSipToBottom) setStatePosition(0, Window.VK_BOTTOM); // fixes a problem of the window of the sip not correctly being returned to the bottom if (initUICalled) // guich@tc126_46: don't call app's onExit if time expired, since initUI was not called. onExit(); // guich@200b4_85 if (restoreRegistry) // guich@tc115_90 try { Registry.set(Registry.HKEY_LOCAL_MACHINE, "\\System\\ErrorReporting\\DumpSettings", "UploadClient", "\\Windows\\Dw.exe"); } catch (Exception e) {} } /** * Called just before an application exits. * When this is called, all threads are already killed. * You should return from this method as soon as possible, because the OS can kill the application if * it takes too much to return. * * Note that on Windows Phone this method is NEVER called. */ public void onExit() { } /** * Called just after the application is minimized. * * If the user press the home key and then forces the application to stop (by going to the Settings / Applications), then * all Litebase tables may be corrupted (actually, no data is lost, but a TableNotClosedException will be issued). So, its a good * thing to call LitebaseConnection.closeAll in your litebase instances and recover them in the onRestore method. * <br><br> * When the onMinimize is called, the screen will only be able to be updated after it resumes (in other words, * calling repaint or repaintNow from the onMinimize method has no effect). * * On Windows Phone, the onMinimize is called and, if the user don't call the application again * within 10 seconds, the application is KILLED without notifications. So, you should save all your application's state * in this method and restore it in the onRestore method. * @see #minimize() * @since TotalCross 1.10 */ public void onMinimize() // bruno@tc110_89 - bruno@tc122_31: now supported on wince and win32 { } /** * Called just after the application is restored. * * @see #onRestore() * @since TotalCross 1.10 */ public void onRestore() // bruno@tc110_89 - bruno@tc122_31: now supported on wince and win32 { } private static class DemoBox extends MessageBox { private static String tit,msg; DemoBox() { super(tit = " TotalCross Virtual Machine "+Settings.versionStr+"."+Settings.buildNumber+" ", msg = "Copyright (c) 2008-2014\nTotalCross Mobile\nGlobal Platform\n\nDEMO VERSION\n\nTime available: "+(timeAvailable == 0 ? "EXPIRED!" : (timeAvailable/100)+"h"+Convert.zeroPad(timeAvailable%100,2)+"m"), new String[]{" Ok "}); Vm.debug(tit); Vm.debug(msg); } public void initUI() { setBackForeColors(timeAvailable == 0 ? Color.RED : Color.BLUE,Color.WHITE); setFont(font.asBold()); } public void onPopup() { super.onPopup(); needsPaint = false; // guich@210 - prevent an empty white background on startup. } public void postPressedEvent() { // do nothing } } private void startProgram() { initUICalled = Window.needsPaint = true; initUI(); Window.needsPaint = Graphics.needsUpdate = true; // required by device started = true; // guich@567_17: moved this from appStarting to here to let popup check if the screen was already painted repaintActiveWindows(); // start a robot if one is passed as parameter String cmd = getCommandLine(); if (cmd != null && cmd.endsWith(".robot")) try { new UIRobot(cmd+" (cmdline)"); } catch (Exception e) { MessageBox.showException(e,true); } } /** * Called by the VM to process timer interrupts. This method is not private * to prevent the compiler from removing it during optimization. */ final public void _onTimerTick(boolean canUpdate) { if (startTimer != null) // guich@567_17 { TimerEvent t = startTimer; startTimer = null; // removeTimer calls again onTimerTick, so we have to null out this before calling it removeTimer(t); if (!Settings.debugging && timeAvailable >= 0) // guich@tc126_46 { new DemoBox().popup(); if (timeAvailable == 0) { exit(0); return; } } else if (timeAvailable >= 0 && Settings.platform.equals(Settings.WIN32) && (Settings.romSerialNumber == null || Settings.romSerialNumber.length() == 0)) { new MessageBox("Fatal Error", "Failed to retrieve a unique device identification to activate the TotalCross VM. Please check your network settings and activate any disabled networks.").popup(); exit(0); return; } //$START:REMOVE-ON-SDK-GENERATION$ if (timeAvailable == -999998) { Settings.activationId = "NO ACTIVATION"; startProgram(); } else if (timeAvailable == -999999) try { timeAvailable = -1; Window w = (Window)Class.forName("ras.ui.ActivationWindow").newInstance(); w.addWindowListener(new WindowListener() { public void windowClosed(ControlEvent e) { startProgram(); } }); w.popupNonBlocking(); } catch (Exception e) {Vm.alert("Fatal error: "+e.getMessage()+". Exiting..."); exit(1); return;} else //$END:REMOVE-ON-SDK-GENERATION$ startProgram(); } int minInterval = 0; TimerEvent timer = firstTimer; while (timer != null) { if (timer.target == null) // aleady removed but still in the queue? { Vm.debug("removing timer since target is null"); TimerEvent t = timer.next; removeTimer(timer); timer = t != null ? t.next : null; continue; } int now = Vm.getTimeStamp(); // guich@tc100b4 int diff = now - timer.lastTick; if (diff < 0) diff += (1 << 30); // wrap around - max stamp is (1 << 30) int interval; if (diff >= timer.millis) { // post TIMER event timer.triggered = true; // guich@220_39 ((Control)timer.target).postEvent(timer); timer.triggered = false; timer.lastTick = now; interval = timer.millis; } else interval = timer.millis - diff; if (interval < minInterval || minInterval == 0) minInterval = interval; timer = timer.next; } if (minInterval > 0 || lastMinInterval > 0) // guich@tc100: call only if there's a timer to run setTimerInterval(lastMinInterval = minInterval); // run everything that needs to run on main thread Object [] runners = getRunners(); if (runners != null) { Window.enableUpdateScreen = false; // we'll update the screen below for (int i = 0; i < runners.length; i++) ((Runnable)runners[i]).run(); Window.enableUpdateScreen = true; } if (Window.needsPaint) // guich@200b4_1: corrected the infinit repaint on popup windows repaintActiveWindows(); // already calls updateScreen else if (canUpdate && Graphics.needsUpdate) // guich@tc100: make sure that any pending screen update is committed. - if not called from addTimer/removeTimer (otherwise, an open combobox will flicker) safeUpdateScreen(); } void setTimerInterval(int n) { totalcross.Launcher.instance.setTimerInterval(n); } native void setTimerInterval4D(int n); /** Returns the command line passed by the application that called this application in the Vm.exec method. * * In Android, you can start an application using adb: * <pre> * adb shell am start -a android.intent.action.MAIN -n totalcross.app.uigadgets/.UIGadgets -e cmdline "Hello world" * </pre> * In the sample above, we're starting UIGadgets. Your app should be: totalcross.app.yourMainWindowClass/.yourMainWindowClass * * Note: When you click on the application's icon, there�s no command line. */ final public static String getCommandLine() // guich@tc120_8: now is static { return totalcross.Launcher.instance.commandLine; } native public static String getCommandLine4D(); /** This method can't be called for a MainWindow */ public void setRect(int x, int y, int width, int height, Control relative, boolean screenChanged) // guich@567_19 { // no messages, please. just ignore } /** Takes a screen shot of the current screen. Since TotalCross 3.06, it uses Control.takeScreenShot. * Here's a sample: * <pre> * Image img = MainWindow.getScreenShot(); * File f = new File(Settings.onJavaSE ? "screen.png" : "/sdcard/screen.png",File.CREATE_EMPTY); * img.createPng(f); * f.close(); * </pre> * Note that the font varies from device to device and even to desktop. So, if you want to compare a device's * screen shot with one taken at desktop, be sure to set the default font in both to the same, like using * <code>setDefaultFont(Font.getFont(false,20))</code>. * * @since TotalCross 1.3 */ public static Image getScreenShot() { try { // get main window mainWindowInstance.takeScreenShot(); Image img = mainWindowInstance.offscreen; mainWindowInstance.releaseScreenShot(); // now paint other windows int lastFade = 1000; for (int j = 0,n=zStack.size(); j < n; j++) if (((Window)zStack.items[j]).fadeOtherWindows) lastFade = j; for (int i = 0, n=zStack.size(); i < n; i++) // repaints every window, from the nearest with the MainWindow size to last parent { if (i == lastFade) img.applyFade(fadeValue); if (i > 0 && zStack.items[i] != null) { Window w = (Window)zStack.items[i]; Graphics g = img.getGraphics(); g.translate(w.x,w.y); w.paint2shot(g,true); } } img.lockChanges(); return img; } catch (Throwable e) {} return null; } // stuff to let a thread update the screen private Object[] getRunners() { Object[] o = null; synchronized (runnersLock) { try { int n = runners.size(); if (n != 0) { o = runners.toObjectArray(); runners = new Vector(1); } } catch (Exception e) {} } return o; } /** The same of <code>runOnMainThread(r, true)</code>. * @see #runOnMainThread(Runnable, boolean) * Note that this * */ public void runOnMainThread(Runnable r) { runOnMainThread(r, true); } /** Runs the given code in the main thread. As of TotalCross 2.0, a thread cannot update the screen. * So, asking the code to be called in the main thread solves the problem. Of course, this call is asynchronous, ie, * the thread may run again before the screen is updated. This method is thread-safe. * If singleInstance is true and the array contains an element of this class, it is replaced by the given one. * Note that passing as false may result in memory leak. * Sample: * <pre> * new Thread() { public void run() { while (true) { Vm.sleep(1000); MainWindow.getMainWindow().runOnMainThread(new Runnable() { public void run() { log("babi "+ ++contador); } }); } } }.start(); * </pre> * @since TotalCross 2.1 */ public void runOnMainThread(Runnable r, boolean singleInstance) { synchronized (runnersLock) { try { if (singleInstance && runners.size() > 0) { String origname = r.getClass().getName()+"@"; for (int i = 0, n = runners.size(); i < n; i++) if (runners.items[i].toString().startsWith(origname)) { runners.removeElementAt(i); break; } } runners.addElement(r); setTimerInterval(1); } catch (Exception e) {} } Vm.sleep(1); } }