private static final ResourceBundle i18n = ResourceBundle.getBundle(TApplication.class.getName());
// ------------------------------------------------------------------------
- // Public constants -------------------------------------------------------
+ // Constants --------------------------------------------------------------
// ------------------------------------------------------------------------
/**
}
// ------------------------------------------------------------------------
- // Primary/secondary event handlers ---------------------------------------
+ // Variables --------------------------------------------------------------
// ------------------------------------------------------------------------
+ /**
+ * The primary event handler thread.
+ */
+ private volatile WidgetEventHandler primaryEventHandler;
+
+ /**
+ * The secondary event handler thread.
+ */
+ private volatile WidgetEventHandler secondaryEventHandler;
+
+ /**
+ * The widget receiving events from the secondary event handler thread.
+ */
+ private volatile TWidget secondaryEventReceiver;
+
+ /**
+ * Access to the physical screen, keyboard, and mouse.
+ */
+ private Backend backend;
+
+ /**
+ * Actual mouse coordinate X.
+ */
+ private int mouseX;
+
+ /**
+ * Actual mouse coordinate Y.
+ */
+ private int mouseY;
+
+ /**
+ * Old version of mouse coordinate X.
+ */
+ private int oldMouseX;
+
+ /**
+ * Old version mouse coordinate Y.
+ */
+ private int oldMouseY;
+
+ /**
+ * The last mouse up click time, used to determine if this is a mouse
+ * double-click.
+ */
+ private long lastMouseUpTime;
+
+ /**
+ * The amount of millis between mouse up events to assume a double-click.
+ */
+ private long doubleClickTime = 250;
+
+ /**
+ * Event queue that is filled by run().
+ */
+ private List<TInputEvent> fillEventQueue;
+
+ /**
+ * Event queue that will be drained by either primary or secondary
+ * Thread.
+ */
+ private List<TInputEvent> drainEventQueue;
+
+ /**
+ * Top-level menus in this application.
+ */
+ private List<TMenu> menus;
+
+ /**
+ * Stack of activated sub-menus in this application.
+ */
+ private List<TMenu> subMenus;
+
+ /**
+ * The currently active menu.
+ */
+ private TMenu activeMenu = null;
+
+ /**
+ * Active keyboard accelerators.
+ */
+ private Map<TKeypress, TMenuItem> accelerators;
+
+ /**
+ * All menu items.
+ */
+ private List<TMenuItem> menuItems;
+
+ /**
+ * Windows and widgets pull colors from this ColorTheme.
+ */
+ private ColorTheme theme;
+
+ /**
+ * The top-level windows (but not menus).
+ */
+ private List<TWindow> windows;
+
+ /**
+ * The currently acive window.
+ */
+ private TWindow activeWindow = null;
+
+ /**
+ * Timers that are being ticked.
+ */
+ private List<TTimer> timers;
+
+ /**
+ * When true, the application has been started.
+ */
+ private volatile boolean started = false;
+
+ /**
+ * When true, exit the application.
+ */
+ private volatile boolean quit = false;
+
+ /**
+ * When true, repaint the entire screen.
+ */
+ private volatile boolean repaint = true;
+
+ /**
+ * Y coordinate of the top edge of the desktop. For now this is a
+ * constant. Someday it would be nice to have a multi-line menu or
+ * toolbars.
+ */
+ private static final int desktopTop = 1;
+
+ /**
+ * Y coordinate of the bottom edge of the desktop.
+ */
+ private int desktopBottom;
+
+ /**
+ * An optional TDesktop background window that is drawn underneath
+ * everything else.
+ */
+ private TDesktop desktop;
+
+ /**
+ * If true, focus follows mouse: windows automatically raised if the
+ * mouse passes over them.
+ */
+ private boolean focusFollowsMouse = false;
+
/**
* WidgetEventHandler is the main event consumer loop. There are at most
* two such threads in existence: the primary for normal case and a
}
}
- /**
- * The primary event handler thread.
- */
- private volatile WidgetEventHandler primaryEventHandler;
+ // ------------------------------------------------------------------------
+ // Constructors -----------------------------------------------------------
+ // ------------------------------------------------------------------------
/**
- * The secondary event handler thread.
+ * Public constructor.
+ *
+ * @param backendType BackendType.XTERM, BackendType.ECMA48 or
+ * BackendType.SWING
+ * @param windowWidth the number of text columns to start with
+ * @param windowHeight the number of text rows to start with
+ * @param fontSize the size in points
+ * @throws UnsupportedEncodingException if an exception is thrown when
+ * creating the InputStreamReader
*/
- private volatile WidgetEventHandler secondaryEventHandler;
+ public TApplication(final BackendType backendType, final int windowWidth,
+ final int windowHeight, final int fontSize)
+ throws UnsupportedEncodingException {
- /**
- * The widget receiving events from the secondary event handler thread.
- */
- private volatile TWidget secondaryEventReceiver;
+ switch (backendType) {
+ case SWING:
+ backend = new SwingBackend(this, windowWidth, windowHeight,
+ fontSize);
+ break;
+ case XTERM:
+ // Fall through...
+ case ECMA48:
+ backend = new ECMA48Backend(this, null, null, windowWidth,
+ windowHeight, fontSize);
+ break;
+ default:
+ throw new IllegalArgumentException("Invalid backend type: "
+ + backendType);
+ }
+ TApplicationImpl();
+ }
/**
- * Wake the sleeping active event handler.
+ * Public constructor.
+ *
+ * @param backendType BackendType.XTERM, BackendType.ECMA48 or
+ * BackendType.SWING
+ * @throws UnsupportedEncodingException if an exception is thrown when
+ * creating the InputStreamReader
*/
- private void wakeEventHandler() {
- if (secondaryEventHandler != null) {
- synchronized (secondaryEventHandler) {
- secondaryEventHandler.notify();
- }
- } else {
- assert (primaryEventHandler != null);
- synchronized (primaryEventHandler) {
- primaryEventHandler.notify();
- }
+ public TApplication(final BackendType backendType)
+ throws UnsupportedEncodingException {
+
+ switch (backendType) {
+ case SWING:
+ // The default SwingBackend is 80x25, 20 pt font. If you want to
+ // change that, you can pass the extra arguments to the
+ // SwingBackend constructor here. For example, if you wanted
+ // 90x30, 16 pt font:
+ //
+ // backend = new SwingBackend(this, 90, 30, 16);
+ backend = new SwingBackend(this);
+ break;
+ case XTERM:
+ // Fall through...
+ case ECMA48:
+ backend = new ECMA48Backend(this, null, null);
+ break;
+ default:
+ throw new IllegalArgumentException("Invalid backend type: "
+ + backendType);
}
+ TApplicationImpl();
}
- // ------------------------------------------------------------------------
- // TApplication attributes ------------------------------------------------
- // ------------------------------------------------------------------------
-
/**
- * Access to the physical screen, keyboard, and mouse.
+ * Public constructor. The backend type will be BackendType.ECMA48.
+ *
+ * @param input an InputStream connected to the remote user, or null for
+ * System.in. If System.in is used, then on non-Windows systems it will
+ * be put in raw mode; shutdown() will (blindly!) put System.in in cooked
+ * mode. input is always converted to a Reader with UTF-8 encoding.
+ * @param output an OutputStream connected to the remote user, or null
+ * for System.out. output is always converted to a Writer with UTF-8
+ * encoding.
+ * @throws UnsupportedEncodingException if an exception is thrown when
+ * creating the InputStreamReader
*/
- private Backend backend;
+ public TApplication(final InputStream input,
+ final OutputStream output) throws UnsupportedEncodingException {
+
+ backend = new ECMA48Backend(this, input, output);
+ TApplicationImpl();
+ }
/**
- * Get the Backend.
+ * Public constructor. The backend type will be BackendType.ECMA48.
*
- * @return the Backend
+ * @param input the InputStream underlying 'reader'. Its available()
+ * method is used to determine if reader.read() will block or not.
+ * @param reader a Reader connected to the remote user.
+ * @param writer a PrintWriter connected to the remote user.
+ * @param setRawMode if true, set System.in into raw mode with stty.
+ * This should in general not be used. It is here solely for Demo3,
+ * which uses System.in.
+ * @throws IllegalArgumentException if input, reader, or writer are null.
*/
- public final Backend getBackend() {
- return backend;
+ public TApplication(final InputStream input, final Reader reader,
+ final PrintWriter writer, final boolean setRawMode) {
+
+ backend = new ECMA48Backend(this, input, reader, writer, setRawMode);
+ TApplicationImpl();
}
/**
- * Get the Screen.
+ * Public constructor. The backend type will be BackendType.ECMA48.
*
- * @return the Screen
+ * @param input the InputStream underlying 'reader'. Its available()
+ * method is used to determine if reader.read() will block or not.
+ * @param reader a Reader connected to the remote user.
+ * @param writer a PrintWriter connected to the remote user.
+ * @throws IllegalArgumentException if input, reader, or writer are null.
*/
- public final Screen getScreen() {
- if (backend instanceof TWindowBackend) {
- // We are being rendered to a TWindow. We can't use its
- // getScreen() method because that is how it is rendering to a
- // hardware backend somewhere. Instead use its getOtherScreen()
- // method.
- return ((TWindowBackend) backend).getOtherScreen();
- } else {
- return backend.getScreen();
- }
- }
+ public TApplication(final InputStream input, final Reader reader,
+ final PrintWriter writer) {
- /**
- * Actual mouse coordinate X.
- */
- private int mouseX;
+ this(input, reader, writer, false);
+ }
/**
- * Actual mouse coordinate Y.
+ * Public constructor. This hook enables use with new non-Jexer
+ * backends.
+ *
+ * @param backend a Backend that is already ready to go.
*/
- private int mouseY;
+ public TApplication(final Backend backend) {
+ this.backend = backend;
+ backend.setListener(this);
+ TApplicationImpl();
+ }
/**
- * Old version of mouse coordinate X.
+ * Finish construction once the backend is set.
*/
- private int oldMouseX;
+ private void TApplicationImpl() {
+ theme = new ColorTheme();
+ desktopBottom = getScreen().getHeight() - 1;
+ fillEventQueue = new ArrayList<TInputEvent>();
+ drainEventQueue = new ArrayList<TInputEvent>();
+ windows = new LinkedList<TWindow>();
+ menus = new LinkedList<TMenu>();
+ subMenus = new LinkedList<TMenu>();
+ timers = new LinkedList<TTimer>();
+ accelerators = new HashMap<TKeypress, TMenuItem>();
+ menuItems = new ArrayList<TMenuItem>();
+ desktop = new TDesktop(this);
- /**
- * Old version mouse coordinate Y.
- */
- private int oldMouseY;
+ // Special case: the Swing backend needs to have a timer to drive its
+ // blink state.
+ if ((backend instanceof SwingBackend)
+ || (backend instanceof MultiBackend)
+ ) {
+ // Default to 500 millis, unless a SwingBackend has its own
+ // value.
+ long millis = 500;
+ if (backend instanceof SwingBackend) {
+ millis = ((SwingBackend) backend).getBlinkMillis();
+ }
+ if (millis > 0) {
+ addTimer(millis, true,
+ new TAction() {
+ public void DO() {
+ TApplication.this.doRepaint();
+ }
+ }
+ );
+ }
+ }
+ }
- /**
- * The last mouse up click time, used to determine if this is a mouse
- * double-click.
- */
- private long lastMouseUpTime;
+ // ------------------------------------------------------------------------
+ // Runnable ---------------------------------------------------------------
+ // ------------------------------------------------------------------------
/**
- * The amount of millis between mouse up events to assume a double-click.
+ * Run this application until it exits.
*/
- private long doubleClickTime = 250;
+ public void run() {
+ // Start the main consumer thread
+ primaryEventHandler = new WidgetEventHandler(this, true);
+ (new Thread(primaryEventHandler)).start();
- /**
- * Event queue that is filled by run().
- */
- private List<TInputEvent> fillEventQueue;
+ started = true;
- /**
- * Event queue that will be drained by either primary or secondary
- * Thread.
- */
- private List<TInputEvent> drainEventQueue;
+ while (!quit) {
+ synchronized (this) {
+ boolean doWait = false;
- /**
- * Top-level menus in this application.
- */
- private List<TMenu> menus;
+ if (!backend.hasEvents()) {
+ synchronized (fillEventQueue) {
+ if (fillEventQueue.size() == 0) {
+ doWait = true;
+ }
+ }
+ }
- /**
- * Stack of activated sub-menus in this application.
- */
- private List<TMenu> subMenus;
+ if (doWait) {
+ // No I/O to dispatch, so wait until the backend
+ // provides new I/O.
+ try {
+ if (debugThreads) {
+ System.err.println(System.currentTimeMillis() +
+ " MAIN sleep");
+ }
- /**
- * The currently active menu.
- */
- private TMenu activeMenu = null;
+ this.wait();
- /**
- * Active keyboard accelerators.
- */
- private Map<TKeypress, TMenuItem> accelerators;
+ if (debugThreads) {
+ System.err.println(System.currentTimeMillis() +
+ " MAIN AWAKE");
+ }
+ } catch (InterruptedException e) {
+ // I'm awake and don't care why, let's see what's
+ // going on out there.
+ }
+ }
- /**
- * All menu items.
- */
- private List<TMenuItem> menuItems;
+ } // synchronized (this)
- /**
- * Windows and widgets pull colors from this ColorTheme.
- */
- private ColorTheme theme;
+ synchronized (fillEventQueue) {
+ // Pull any pending I/O events
+ backend.getEvents(fillEventQueue);
- /**
- * Get the color theme.
- *
- * @return the theme
- */
- public final ColorTheme getTheme() {
- return theme;
- }
+ // Dispatch each event to the appropriate handler, one at a
+ // time.
+ for (;;) {
+ TInputEvent event = null;
+ if (fillEventQueue.size() == 0) {
+ break;
+ }
+ event = fillEventQueue.remove(0);
+ metaHandleEvent(event);
+ }
+ }
- /**
- * The top-level windows (but not menus).
- */
- private List<TWindow> windows;
+ // Wake a consumer thread if we have any pending events.
+ if (drainEventQueue.size() > 0) {
+ wakeEventHandler();
+ }
- /**
- * The currently acive window.
- */
- private TWindow activeWindow = null;
+ } // while (!quit)
- /**
- * Timers that are being ticked.
- */
- private List<TTimer> timers;
+ // Shutdown the event consumer threads
+ if (secondaryEventHandler != null) {
+ synchronized (secondaryEventHandler) {
+ secondaryEventHandler.notify();
+ }
+ }
+ if (primaryEventHandler != null) {
+ synchronized (primaryEventHandler) {
+ primaryEventHandler.notify();
+ }
+ }
- /**
- * When true, exit the application.
- */
- private volatile boolean quit = false;
+ // Shutdown the user I/O thread(s)
+ backend.shutdown();
- /**
- * When true, repaint the entire screen.
- */
- private volatile boolean repaint = true;
+ // Close all the windows. This gives them an opportunity to release
+ // resources.
+ closeAllWindows();
- /**
- * Repaint the screen on the next update.
- */
- public void doRepaint() {
- repaint = true;
- wakeEventHandler();
}
- /**
- * Y coordinate of the top edge of the desktop. For now this is a
- * constant. Someday it would be nice to have a multi-line menu or
- * toolbars.
- */
- private static final int desktopTop = 1;
+ // ------------------------------------------------------------------------
+ // Event handlers ---------------------------------------------------------
+ // ------------------------------------------------------------------------
/**
- * Get Y coordinate of the top edge of the desktop.
+ * Method that TApplication subclasses can override to handle menu or
+ * posted command events.
*
- * @return Y coordinate of the top edge of the desktop
+ * @param command command event
+ * @return if true, this event was consumed
*/
- public final int getDesktopTop() {
- return desktopTop;
- }
+ protected boolean onCommand(final TCommandEvent command) {
+ // Default: handle cmExit
+ if (command.equals(cmExit)) {
+ if (messageBox(i18n.getString("exitDialogTitle"),
+ i18n.getString("exitDialogText"),
+ TMessageBox.Type.YESNO).getResult() == TMessageBox.Result.YES) {
+ exit();
+ }
+ return true;
+ }
- /**
- * Y coordinate of the bottom edge of the desktop.
- */
- private int desktopBottom;
+ if (command.equals(cmShell)) {
+ openTerminal(0, 0, TWindow.RESIZABLE);
+ return true;
+ }
- /**
- * Get Y coordinate of the bottom edge of the desktop.
- *
- * @return Y coordinate of the bottom edge of the desktop
- */
- public final int getDesktopBottom() {
- return desktopBottom;
- }
-
- /**
- * An optional TDesktop background window that is drawn underneath
- * everything else.
- */
- private TDesktop desktop;
+ if (command.equals(cmTile)) {
+ tileWindows();
+ return true;
+ }
+ if (command.equals(cmCascade)) {
+ cascadeWindows();
+ return true;
+ }
+ if (command.equals(cmCloseAll)) {
+ closeAllWindows();
+ return true;
+ }
- /**
- * Set the TDesktop instance.
- *
- * @param desktop a TDesktop instance, or null to remove the one that is
- * set
- */
- public final void setDesktop(final TDesktop desktop) {
- if (this.desktop != null) {
- this.desktop.onClose();
+ if (command.equals(cmMenu)) {
+ if (!modalWindowActive() && (activeMenu == null)) {
+ if (menus.size() > 0) {
+ menus.get(0).setActive(true);
+ activeMenu = menus.get(0);
+ return true;
+ }
+ }
}
- this.desktop = desktop;
- }
- /**
- * Get the TDesktop instance.
- *
- * @return the desktop, or null if it is not set
- */
- public final TDesktop getDesktop() {
- return desktop;
+ return false;
}
/**
- * Get the current active window.
+ * Method that TApplication subclasses can override to handle menu
+ * events.
*
- * @return the active window, or null if it is not set
+ * @param menu menu event
+ * @return if true, this event was consumed
*/
- public final TWindow getActiveWindow() {
- return activeWindow;
- }
+ protected boolean onMenu(final TMenuEvent menu) {
- /**
- * Get a (shallow) copy of the window list.
- *
- * @return a copy of the list of windows for this application
- */
- public final List<TWindow> getAllWindows() {
- List<TWindow> result = new LinkedList<TWindow>();
- result.addAll(windows);
- return result;
- }
+ // Default: handle MID_EXIT
+ if (menu.getId() == TMenu.MID_EXIT) {
+ if (messageBox(i18n.getString("exitDialogTitle"),
+ i18n.getString("exitDialogText"),
+ TMessageBox.Type.YESNO).getResult() == TMessageBox.Result.YES) {
+ exit();
+ }
+ return true;
+ }
- /**
- * If true, focus follows mouse: windows automatically raised if the
- * mouse passes over them.
- */
- private boolean focusFollowsMouse = false;
+ if (menu.getId() == TMenu.MID_SHELL) {
+ openTerminal(0, 0, TWindow.RESIZABLE);
+ return true;
+ }
- /**
- * Get focusFollowsMouse flag.
- *
- * @return true if focus follows mouse: windows automatically raised if
- * the mouse passes over them
- */
- public boolean getFocusFollowsMouse() {
- return focusFollowsMouse;
+ if (menu.getId() == TMenu.MID_TILE) {
+ tileWindows();
+ return true;
+ }
+ if (menu.getId() == TMenu.MID_CASCADE) {
+ cascadeWindows();
+ return true;
+ }
+ if (menu.getId() == TMenu.MID_CLOSE_ALL) {
+ closeAllWindows();
+ return true;
+ }
+ if (menu.getId() == TMenu.MID_ABOUT) {
+ showAboutDialog();
+ return true;
+ }
+ if (menu.getId() == TMenu.MID_REPAINT) {
+ doRepaint();
+ return true;
+ }
+ return false;
}
/**
- * Set focusFollowsMouse flag.
+ * Method that TApplication subclasses can override to handle keystrokes.
*
- * @param focusFollowsMouse if true, focus follows mouse: windows
- * automatically raised if the mouse passes over them
- */
- public void setFocusFollowsMouse(final boolean focusFollowsMouse) {
- this.focusFollowsMouse = focusFollowsMouse;
- }
-
- // ------------------------------------------------------------------------
- // General behavior -------------------------------------------------------
- // ------------------------------------------------------------------------
-
- /**
- * Display the about dialog.
+ * @param keypress keystroke event
+ * @return if true, this event was consumed
*/
- protected void showAboutDialog() {
- messageBox(i18n.getString("aboutDialogTitle"),
- MessageFormat.format(i18n.getString("aboutDialogText"),
- this.getClass().getPackage().getImplementationVersion()),
- TMessageBox.Type.OK);
- }
+ protected boolean onKeypress(final TKeypressEvent keypress) {
+ // Default: only menu shortcuts
- // ------------------------------------------------------------------------
- // Constructors -----------------------------------------------------------
- // ------------------------------------------------------------------------
+ // Process Alt-F, Alt-E, etc. menu shortcut keys
+ if (!keypress.getKey().isFnKey()
+ && keypress.getKey().isAlt()
+ && !keypress.getKey().isCtrl()
+ && (activeMenu == null)
+ && !modalWindowActive()
+ ) {
- /**
- * Public constructor.
- *
- * @param backendType BackendType.XTERM, BackendType.ECMA48 or
- * BackendType.SWING
- * @throws UnsupportedEncodingException if an exception is thrown when
- * creating the InputStreamReader
- */
- public TApplication(final BackendType backendType)
- throws UnsupportedEncodingException {
+ assert (subMenus.size() == 0);
- switch (backendType) {
- case SWING:
- // The default SwingBackend is 80x25, 20 pt font. If you want to
- // change that, you can pass the extra arguments to the
- // SwingBackend constructor here. For example, if you wanted
- // 90x30, 16 pt font:
- //
- // backend = new SwingBackend(this, 90, 30, 16);
- backend = new SwingBackend(this);
- break;
- case XTERM:
- // Fall through...
- case ECMA48:
- backend = new ECMA48Backend(this, null, null);
- break;
- default:
- throw new IllegalArgumentException("Invalid backend type: "
- + backendType);
+ for (TMenu menu: menus) {
+ if (Character.toLowerCase(menu.getMnemonic().getShortcut())
+ == Character.toLowerCase(keypress.getKey().getChar())
+ ) {
+ activeMenu = menu;
+ menu.setActive(true);
+ return true;
+ }
+ }
}
- TApplicationImpl();
+
+ return false;
}
/**
- * Public constructor. The backend type will be BackendType.ECMA48.
- *
- * @param input an InputStream connected to the remote user, or null for
- * System.in. If System.in is used, then on non-Windows systems it will
- * be put in raw mode; shutdown() will (blindly!) put System.in in cooked
- * mode. input is always converted to a Reader with UTF-8 encoding.
- * @param output an OutputStream connected to the remote user, or null
- * for System.out. output is always converted to a Writer with UTF-8
- * encoding.
- * @throws UnsupportedEncodingException if an exception is thrown when
- * creating the InputStreamReader
+ * Process background events, and update the screen.
*/
- public TApplication(final InputStream input,
- final OutputStream output) throws UnsupportedEncodingException {
+ private void finishEventProcessing() {
+ if (debugThreads) {
+ System.err.printf(System.currentTimeMillis() + " " +
+ Thread.currentThread() + " finishEventProcessing()\n");
+ }
- backend = new ECMA48Backend(this, input, output);
- TApplicationImpl();
- }
+ // Process timers and call doIdle()'s
+ doIdle();
- /**
- * Public constructor. The backend type will be BackendType.ECMA48.
- *
- * @param input the InputStream underlying 'reader'. Its available()
- * method is used to determine if reader.read() will block or not.
- * @param reader a Reader connected to the remote user.
- * @param writer a PrintWriter connected to the remote user.
- * @param setRawMode if true, set System.in into raw mode with stty.
- * This should in general not be used. It is here solely for Demo3,
- * which uses System.in.
- * @throws IllegalArgumentException if input, reader, or writer are null.
- */
- public TApplication(final InputStream input, final Reader reader,
- final PrintWriter writer, final boolean setRawMode) {
+ // Update the screen
+ synchronized (getScreen()) {
+ drawAll();
+ }
- backend = new ECMA48Backend(this, input, reader, writer, setRawMode);
- TApplicationImpl();
+ if (debugThreads) {
+ System.err.printf(System.currentTimeMillis() + " " +
+ Thread.currentThread() + " finishEventProcessing() END\n");
+ }
}
/**
- * Public constructor. The backend type will be BackendType.ECMA48.
+ * Peek at certain application-level events, add to eventQueue, and wake
+ * up the consuming Thread.
*
- * @param input the InputStream underlying 'reader'. Its available()
- * method is used to determine if reader.read() will block or not.
- * @param reader a Reader connected to the remote user.
- * @param writer a PrintWriter connected to the remote user.
- * @throws IllegalArgumentException if input, reader, or writer are null.
+ * @param event the input event to consume
*/
- public TApplication(final InputStream input, final Reader reader,
- final PrintWriter writer) {
+ private void metaHandleEvent(final TInputEvent event) {
- this(input, reader, writer, false);
- }
+ if (debugEvents) {
+ System.err.printf(String.format("metaHandleEvents event: %s\n",
+ event)); System.err.flush();
+ }
- /**
- * Public constructor. This hook enables use with new non-Jexer
- * backends.
- *
- * @param backend a Backend that is already ready to go.
- */
- public TApplication(final Backend backend) {
- this.backend = backend;
- backend.setListener(this);
- TApplicationImpl();
- }
+ if (quit) {
+ // Do no more processing if the application is already trying
+ // to exit.
+ return;
+ }
- /**
- * Finish construction once the backend is set.
- */
- private void TApplicationImpl() {
- theme = new ColorTheme();
- desktopBottom = getScreen().getHeight() - 1;
- fillEventQueue = new ArrayList<TInputEvent>();
- drainEventQueue = new ArrayList<TInputEvent>();
- windows = new LinkedList<TWindow>();
- menus = new LinkedList<TMenu>();
- subMenus = new LinkedList<TMenu>();
- timers = new LinkedList<TTimer>();
- accelerators = new HashMap<TKeypress, TMenuItem>();
- menuItems = new ArrayList<TMenuItem>();
- desktop = new TDesktop(this);
+ // Special application-wide events -------------------------------
- // Special case: the Swing backend needs to have a timer to drive its
- // blink state.
- if ((backend instanceof SwingBackend)
- || (backend instanceof MultiBackend)
- ) {
- // Default to 500 millis, unless a SwingBackend has its own
- // value.
- long millis = 500;
- if (backend instanceof SwingBackend) {
- millis = ((SwingBackend) backend).getBlinkMillis();
- }
- if (millis > 0) {
- addTimer(millis, true,
- new TAction() {
- public void DO() {
- TApplication.this.doRepaint();
- }
- }
- );
+ // Abort everything
+ if (event instanceof TCommandEvent) {
+ TCommandEvent command = (TCommandEvent) event;
+ if (command.getCmd().equals(cmAbort)) {
+ exit();
+ return;
}
}
- }
- // ------------------------------------------------------------------------
- // Screen refresh loop ----------------------------------------------------
- // ------------------------------------------------------------------------
+ synchronized (drainEventQueue) {
+ // Screen resize
+ if (event instanceof TResizeEvent) {
+ TResizeEvent resize = (TResizeEvent) event;
+ synchronized (getScreen()) {
+ getScreen().setDimensions(resize.getWidth(),
+ resize.getHeight());
+ desktopBottom = getScreen().getHeight() - 1;
+ mouseX = 0;
+ mouseY = 0;
+ oldMouseX = 0;
+ oldMouseY = 0;
+ }
+ if (desktop != null) {
+ desktop.setDimensions(0, 0, resize.getWidth(),
+ resize.getHeight() - 1);
+ }
- /**
- * Process background events, and update the screen.
- */
- private void finishEventProcessing() {
- if (debugThreads) {
- System.err.printf(System.currentTimeMillis() + " " +
- Thread.currentThread() + " finishEventProcessing()\n");
- }
+ // Change menu edges if needed.
+ recomputeMenuX();
- // Process timers and call doIdle()'s
- doIdle();
+ // We are dirty, redraw the screen.
+ doRepaint();
- // Update the screen
- synchronized (getScreen()) {
- drawAll();
- }
+ /*
+ System.err.println("New screen: " + resize.getWidth() +
+ " x " + resize.getHeight());
+ */
+ return;
+ }
- if (debugThreads) {
- System.err.printf(System.currentTimeMillis() + " " +
- Thread.currentThread() + " finishEventProcessing() END\n");
+ // Put into the main queue
+ drainEventQueue.add(event);
}
}
/**
- * Invert the cell color at a position. This is used to track the mouse.
+ * Dispatch one event to the appropriate widget or application-level
+ * event handler. This is the primary event handler, it has the normal
+ * application-wide event handling.
*
- * @param x column position
- * @param y row position
+ * @param event the input event to consume
+ * @see #secondaryHandleEvent(TInputEvent event)
*/
- private void invertCell(final int x, final int y) {
- if (debugThreads) {
- System.err.printf("%d %s invertCell() %d %d\n",
- System.currentTimeMillis(), Thread.currentThread(), x, y);
+ private void primaryHandleEvent(final TInputEvent event) {
+
+ if (debugEvents) {
+ System.err.printf("Handle event: %s\n", event);
}
- CellAttributes attr = getScreen().getAttrXY(x, y);
- attr.setForeColor(attr.getForeColor().invert());
- attr.setBackColor(attr.getBackColor().invert());
- getScreen().putAttrXY(x, y, attr, false);
- }
+ TMouseEvent doubleClick = null;
- /**
- * Draw everything.
- */
- private void drawAll() {
- boolean menuIsActive = false;
+ // Special application-wide events -----------------------------------
- if (debugThreads) {
- System.err.printf("%d %s drawAll() enter\n",
- System.currentTimeMillis(), Thread.currentThread());
- }
+ // Peek at the mouse position
+ if (event instanceof TMouseEvent) {
+ TMouseEvent mouse = (TMouseEvent) event;
+ if ((mouseX != mouse.getX()) || (mouseY != mouse.getY())) {
+ oldMouseX = mouseX;
+ oldMouseY = mouseY;
+ mouseX = mouse.getX();
+ mouseY = mouse.getY();
+ } else {
+ if (mouse.getType() == TMouseEvent.Type.MOUSE_UP) {
+ if ((mouse.getTime().getTime() - lastMouseUpTime) <
+ doubleClickTime) {
- if (!repaint) {
- if (debugThreads) {
- System.err.printf("%d %s drawAll() !repaint\n",
- System.currentTimeMillis(), Thread.currentThread());
- }
- synchronized (getScreen()) {
- if ((oldMouseX != mouseX) || (oldMouseY != mouseY)) {
- // The only thing that has happened is the mouse moved.
- // Clear the old position and draw the new position.
- invertCell(oldMouseX, oldMouseY);
- invertCell(mouseX, mouseY);
- oldMouseX = mouseX;
- oldMouseY = mouseY;
- }
- if (getScreen().isDirty()) {
- backend.flushScreen();
+ // This is a double-click.
+ doubleClick = new TMouseEvent(TMouseEvent.Type.
+ MOUSE_DOUBLE_CLICK,
+ mouse.getX(), mouse.getY(),
+ mouse.getAbsoluteX(), mouse.getAbsoluteY(),
+ mouse.isMouse1(), mouse.isMouse2(),
+ mouse.isMouse3(),
+ mouse.isMouseWheelUp(), mouse.isMouseWheelDown());
+
+ } else {
+ // The first click of a potential double-click.
+ lastMouseUpTime = mouse.getTime().getTime();
+ }
}
- return;
}
- }
- if (debugThreads) {
- System.err.printf("%d %s drawAll() REDRAW\n",
- System.currentTimeMillis(), Thread.currentThread());
+ // See if we need to switch focus to another window or the menu
+ checkSwitchFocus((TMouseEvent) event);
}
- // If true, the cursor is not visible
- boolean cursor = false;
+ // Handle menu events
+ if ((activeMenu != null) && !(event instanceof TCommandEvent)) {
+ TMenu menu = activeMenu;
- // Start with a clean screen
- getScreen().clear();
+ if (event instanceof TMouseEvent) {
+ TMouseEvent mouse = (TMouseEvent) event;
- // Draw the desktop
- if (desktop != null) {
- desktop.drawChildren();
- }
+ while (subMenus.size() > 0) {
+ TMenu subMenu = subMenus.get(subMenus.size() - 1);
+ if (subMenu.mouseWouldHit(mouse)) {
+ break;
+ }
+ if ((mouse.getType() == TMouseEvent.Type.MOUSE_MOTION)
+ && (!mouse.isMouse1())
+ && (!mouse.isMouse2())
+ && (!mouse.isMouse3())
+ && (!mouse.isMouseWheelUp())
+ && (!mouse.isMouseWheelDown())
+ ) {
+ break;
+ }
+ // We navigated away from a sub-menu, so close it
+ closeSubMenu();
+ }
- // Draw each window in reverse Z order
- List<TWindow> sorted = new LinkedList<TWindow>(windows);
- Collections.sort(sorted);
- TWindow topLevel = null;
- if (sorted.size() > 0) {
- topLevel = sorted.get(0);
- }
- Collections.reverse(sorted);
- for (TWindow window: sorted) {
- if (window.isShown()) {
- window.drawChildren();
+ // Convert the mouse relative x/y to menu coordinates
+ assert (mouse.getX() == mouse.getAbsoluteX());
+ assert (mouse.getY() == mouse.getAbsoluteY());
+ if (subMenus.size() > 0) {
+ menu = subMenus.get(subMenus.size() - 1);
+ }
+ mouse.setX(mouse.getX() - menu.getX());
+ mouse.setY(mouse.getY() - menu.getY());
}
+ menu.handleEvent(event);
+ return;
}
- // Draw the blank menubar line - reset the screen clipping first so
- // it won't trim it out.
- getScreen().resetClipping();
- getScreen().hLineXY(0, 0, getScreen().getWidth(), ' ',
- theme.getColor("tmenu"));
- // Now draw the menus.
- int x = 1;
- for (TMenu menu: menus) {
- CellAttributes menuColor;
- CellAttributes menuMnemonicColor;
- if (menu.isActive()) {
- menuIsActive = true;
- menuColor = theme.getColor("tmenu.highlighted");
- menuMnemonicColor = theme.getColor("tmenu.mnemonic.highlighted");
- topLevel = menu;
- } else {
- menuColor = theme.getColor("tmenu");
- menuMnemonicColor = theme.getColor("tmenu.mnemonic");
- }
- // Draw the menu title
- getScreen().hLineXY(x, 0, menu.getTitle().length() + 2, ' ',
- menuColor);
- getScreen().putStringXY(x + 1, 0, menu.getTitle(), menuColor);
- // Draw the highlight character
- getScreen().putCharXY(x + 1 + menu.getMnemonic().getShortcutIdx(),
- 0, menu.getMnemonic().getShortcut(), menuMnemonicColor);
+ if (event instanceof TKeypressEvent) {
+ TKeypressEvent keypress = (TKeypressEvent) event;
- if (menu.isActive()) {
- menu.drawChildren();
- // Reset the screen clipping so we can draw the next title.
- getScreen().resetClipping();
+ // See if this key matches an accelerator, and is not being
+ // shortcutted by the active window, and if so dispatch the menu
+ // event.
+ boolean windowWillShortcut = false;
+ if (activeWindow != null) {
+ assert (activeWindow.isShown());
+ if (activeWindow.isShortcutKeypress(keypress.getKey())) {
+ // We do not process this key, it will be passed to the
+ // window instead.
+ windowWillShortcut = true;
+ }
+ }
+
+ if (!windowWillShortcut && !modalWindowActive()) {
+ TKeypress keypressLowercase = keypress.getKey().toLowerCase();
+ TMenuItem item = null;
+ synchronized (accelerators) {
+ item = accelerators.get(keypressLowercase);
+ }
+ if (item != null) {
+ if (item.isEnabled()) {
+ // Let the menu item dispatch
+ item.dispatch();
+ return;
+ }
+ }
+
+ // Handle the keypress
+ if (onKeypress(keypress)) {
+ return;
+ }
}
- x += menu.getTitle().length() + 2;
}
- for (TMenu menu: subMenus) {
- // Reset the screen clipping so we can draw the next sub-menu.
- getScreen().resetClipping();
- menu.drawChildren();
+ if (event instanceof TCommandEvent) {
+ if (onCommand((TCommandEvent) event)) {
+ return;
+ }
}
- // Draw the status bar of the top-level window
- TStatusBar statusBar = null;
- if (topLevel != null) {
- statusBar = topLevel.getStatusBar();
+ if (event instanceof TMenuEvent) {
+ if (onMenu((TMenuEvent) event)) {
+ return;
+ }
}
- if (statusBar != null) {
- getScreen().resetClipping();
- statusBar.setWidth(getScreen().getWidth());
- statusBar.setY(getScreen().getHeight() - topLevel.getY());
- statusBar.draw();
- } else {
- CellAttributes barColor = new CellAttributes();
- barColor.setTo(getTheme().getColor("tstatusbar.text"));
- getScreen().hLineXY(0, desktopBottom, getScreen().getWidth(), ' ',
- barColor);
+
+ // Dispatch events to the active window -------------------------------
+ boolean dispatchToDesktop = true;
+ TWindow window = activeWindow;
+ if (window != null) {
+ assert (window.isActive());
+ assert (window.isShown());
+ if (event instanceof TMouseEvent) {
+ TMouseEvent mouse = (TMouseEvent) event;
+ // Convert the mouse relative x/y to window coordinates
+ assert (mouse.getX() == mouse.getAbsoluteX());
+ assert (mouse.getY() == mouse.getAbsoluteY());
+ mouse.setX(mouse.getX() - window.getX());
+ mouse.setY(mouse.getY() - window.getY());
+
+ if (doubleClick != null) {
+ doubleClick.setX(doubleClick.getX() - window.getX());
+ doubleClick.setY(doubleClick.getY() - window.getY());
+ }
+
+ if (window.mouseWouldHit(mouse)) {
+ dispatchToDesktop = false;
+ }
+ } else if (event instanceof TKeypressEvent) {
+ dispatchToDesktop = false;
+ }
+
+ if (debugEvents) {
+ System.err.printf("TApplication dispatch event: %s\n",
+ event);
+ }
+ window.handleEvent(event);
+ if (doubleClick != null) {
+ window.handleEvent(doubleClick);
+ }
+ }
+ if (dispatchToDesktop) {
+ // This event is fair game for the desktop to process.
+ if (desktop != null) {
+ desktop.handleEvent(event);
+ if (doubleClick != null) {
+ desktop.handleEvent(doubleClick);
+ }
+ }
}
+ }
- // Draw the mouse pointer
- invertCell(mouseX, mouseY);
- oldMouseX = mouseX;
- oldMouseY = mouseY;
+ /**
+ * Dispatch one event to the appropriate widget or application-level
+ * event handler. This is the secondary event handler used by certain
+ * special dialogs (currently TMessageBox and TFileOpenBox).
+ *
+ * @param event the input event to consume
+ * @see #primaryHandleEvent(TInputEvent event)
+ */
+ private void secondaryHandleEvent(final TInputEvent event) {
+ TMouseEvent doubleClick = null;
+
+ // Peek at the mouse position
+ if (event instanceof TMouseEvent) {
+ TMouseEvent mouse = (TMouseEvent) event;
+ if ((mouseX != mouse.getX()) || (mouseY != mouse.getY())) {
+ oldMouseX = mouseX;
+ oldMouseY = mouseY;
+ mouseX = mouse.getX();
+ mouseY = mouse.getY();
+ } else {
+ if (mouse.getType() == TMouseEvent.Type.MOUSE_UP) {
+ if ((mouse.getTime().getTime() - lastMouseUpTime) <
+ doubleClickTime) {
+
+ // This is a double-click.
+ doubleClick = new TMouseEvent(TMouseEvent.Type.
+ MOUSE_DOUBLE_CLICK,
+ mouse.getX(), mouse.getY(),
+ mouse.getAbsoluteX(), mouse.getAbsoluteY(),
+ mouse.isMouse1(), mouse.isMouse2(),
+ mouse.isMouse3(),
+ mouse.isMouseWheelUp(), mouse.isMouseWheelDown());
- // Place the cursor if it is visible
- if (!menuIsActive) {
- TWidget activeWidget = null;
- if (sorted.size() > 0) {
- activeWidget = sorted.get(sorted.size() - 1).getActiveChild();
- if (activeWidget.isCursorVisible()) {
- if ((activeWidget.getCursorAbsoluteY() < desktopBottom)
- && (activeWidget.getCursorAbsoluteY() > desktopTop)
- ) {
- getScreen().putCursor(true,
- activeWidget.getCursorAbsoluteX(),
- activeWidget.getCursorAbsoluteY());
- cursor = true;
} else {
- getScreen().putCursor(false,
- activeWidget.getCursorAbsoluteX(),
- activeWidget.getCursorAbsoluteY());
- cursor = false;
+ // The first click of a potential double-click.
+ lastMouseUpTime = mouse.getTime().getTime();
}
}
}
}
- // Kill the cursor
- if (!cursor) {
- getScreen().hideCursor();
+ secondaryEventReceiver.handleEvent(event);
+ // Note that it is possible for secondaryEventReceiver to be null
+ // now, because its handleEvent() might have finished out on the
+ // secondary thread. So put any extra processing inside a null
+ // check.
+ if (secondaryEventReceiver != null) {
+ if (doubleClick != null) {
+ secondaryEventReceiver.handleEvent(doubleClick);
+ }
}
+ }
- // Flush the screen contents
- if (getScreen().isDirty()) {
- backend.flushScreen();
+ /**
+ * Enable a widget to override the primary event thread.
+ *
+ * @param widget widget that will receive events
+ */
+ public final void enableSecondaryEventReceiver(final TWidget widget) {
+ if (debugThreads) {
+ System.err.println(System.currentTimeMillis() +
+ " enableSecondaryEventReceiver()");
}
- repaint = false;
- }
+ assert (secondaryEventReceiver == null);
+ assert (secondaryEventHandler == null);
+ assert ((widget instanceof TMessageBox)
+ || (widget instanceof TFileOpenBox));
+ secondaryEventReceiver = widget;
+ secondaryEventHandler = new WidgetEventHandler(this, false);
- // ------------------------------------------------------------------------
- // Main loop --------------------------------------------------------------
- // ------------------------------------------------------------------------
+ (new Thread(secondaryEventHandler)).start();
+ }
/**
- * Force this application to exit.
+ * Yield to the secondary thread.
*/
- public void exit() {
- quit = true;
- synchronized (this) {
- this.notify();
+ public final void yield() {
+ assert (secondaryEventReceiver != null);
+
+ while (secondaryEventReceiver != null) {
+ synchronized (primaryEventHandler) {
+ try {
+ primaryEventHandler.wait();
+ } catch (InterruptedException e) {
+ // SQUASH
+ }
+ }
}
}
/**
- * Run this application until it exits.
+ * Do stuff when there is no user input.
*/
- public void run() {
- // Start the main consumer thread
- primaryEventHandler = new WidgetEventHandler(this, true);
- (new Thread(primaryEventHandler)).start();
+ private void doIdle() {
+ if (debugThreads) {
+ System.err.printf(System.currentTimeMillis() + " " +
+ Thread.currentThread() + " doIdle()\n");
+ }
- while (!quit) {
- synchronized (this) {
- boolean doWait = false;
+ synchronized (timers) {
- synchronized (fillEventQueue) {
- if (fillEventQueue.size() == 0) {
- doWait = true;
+ if (debugThreads) {
+ System.err.printf(System.currentTimeMillis() + " " +
+ Thread.currentThread() + " doIdle() 2\n");
+ }
+
+ // Run any timers that have timed out
+ Date now = new Date();
+ List<TTimer> keepTimers = new LinkedList<TTimer>();
+ for (TTimer timer: timers) {
+ if (timer.getNextTick().getTime() <= now.getTime()) {
+ // Something might change, so repaint the screen.
+ repaint = true;
+ timer.tick();
+ if (timer.recurring) {
+ keepTimers.add(timer);
}
+ } else {
+ keepTimers.add(timer);
}
+ }
+ timers.clear();
+ timers.addAll(keepTimers);
+ }
- if (doWait) {
- // No I/O to dispatch, so wait until the backend
- // provides new I/O.
- try {
- if (debugThreads) {
- System.err.println(System.currentTimeMillis() +
- " MAIN sleep");
- }
-
- this.wait();
-
- if (debugThreads) {
- System.err.println(System.currentTimeMillis() +
- " MAIN AWAKE");
- }
- } catch (InterruptedException e) {
- // I'm awake and don't care why, let's see what's
- // going on out there.
- }
- }
-
- } // synchronized (this)
-
- synchronized (fillEventQueue) {
- // Pull any pending I/O events
- backend.getEvents(fillEventQueue);
-
- // Dispatch each event to the appropriate handler, one at a
- // time.
- for (;;) {
- TInputEvent event = null;
- if (fillEventQueue.size() == 0) {
- break;
- }
- event = fillEventQueue.remove(0);
- metaHandleEvent(event);
- }
- }
-
- // Wake a consumer thread if we have any pending events.
- if (drainEventQueue.size() > 0) {
- wakeEventHandler();
- }
+ // Call onIdle's
+ for (TWindow window: windows) {
+ window.onIdle();
+ }
+ if (desktop != null) {
+ desktop.onIdle();
+ }
+ }
- } // while (!quit)
+ /**
+ * Wake the sleeping active event handler.
+ */
+ private void wakeEventHandler() {
+ if (!started) {
+ return;
+ }
- // Shutdown the event consumer threads
if (secondaryEventHandler != null) {
synchronized (secondaryEventHandler) {
secondaryEventHandler.notify();
}
- }
- if (primaryEventHandler != null) {
+ } else {
+ assert (primaryEventHandler != null);
synchronized (primaryEventHandler) {
primaryEventHandler.notify();
}
}
+ }
- // Shutdown the user I/O thread(s)
- backend.shutdown();
+ // ------------------------------------------------------------------------
+ // TApplication -----------------------------------------------------------
+ // ------------------------------------------------------------------------
- // Close all the windows. This gives them an opportunity to release
- // resources.
- closeAllWindows();
+ /**
+ * Get the Backend.
+ *
+ * @return the Backend
+ */
+ public final Backend getBackend() {
+ return backend;
+ }
+ /**
+ * Get the Screen.
+ *
+ * @return the Screen
+ */
+ public final Screen getScreen() {
+ if (backend instanceof TWindowBackend) {
+ // We are being rendered to a TWindow. We can't use its
+ // getScreen() method because that is how it is rendering to a
+ // hardware backend somewhere. Instead use its getOtherScreen()
+ // method.
+ return ((TWindowBackend) backend).getOtherScreen();
+ } else {
+ return backend.getScreen();
+ }
}
/**
- * Peek at certain application-level events, add to eventQueue, and wake
- * up the consuming Thread.
+ * Get the color theme.
*
- * @param event the input event to consume
+ * @return the theme
*/
- private void metaHandleEvent(final TInputEvent event) {
+ public final ColorTheme getTheme() {
+ return theme;
+ }
- if (debugEvents) {
- System.err.printf(String.format("metaHandleEvents event: %s\n",
- event)); System.err.flush();
- }
+ /**
+ * Repaint the screen on the next update.
+ */
+ public void doRepaint() {
+ repaint = true;
+ wakeEventHandler();
+ }
- if (quit) {
- // Do no more processing if the application is already trying
- // to exit.
- return;
- }
+ /**
+ * Get Y coordinate of the top edge of the desktop.
+ *
+ * @return Y coordinate of the top edge of the desktop
+ */
+ public final int getDesktopTop() {
+ return desktopTop;
+ }
- // Special application-wide events -------------------------------
+ /**
+ * Get Y coordinate of the bottom edge of the desktop.
+ *
+ * @return Y coordinate of the bottom edge of the desktop
+ */
+ public final int getDesktopBottom() {
+ return desktopBottom;
+ }
- // Abort everything
- if (event instanceof TCommandEvent) {
- TCommandEvent command = (TCommandEvent) event;
- if (command.getCmd().equals(cmAbort)) {
- exit();
- return;
- }
+ /**
+ * Set the TDesktop instance.
+ *
+ * @param desktop a TDesktop instance, or null to remove the one that is
+ * set
+ */
+ public final void setDesktop(final TDesktop desktop) {
+ if (this.desktop != null) {
+ this.desktop.onClose();
}
+ this.desktop = desktop;
+ }
- synchronized (drainEventQueue) {
- // Screen resize
- if (event instanceof TResizeEvent) {
- TResizeEvent resize = (TResizeEvent) event;
- synchronized (getScreen()) {
- getScreen().setDimensions(resize.getWidth(),
- resize.getHeight());
- desktopBottom = getScreen().getHeight() - 1;
- mouseX = 0;
- mouseY = 0;
- oldMouseX = 0;
- oldMouseY = 0;
- }
- if (desktop != null) {
- desktop.setDimensions(0, 0, resize.getWidth(),
- resize.getHeight() - 1);
- }
-
- // Change menu edges if needed.
- recomputeMenuX();
+ /**
+ * Get the TDesktop instance.
+ *
+ * @return the desktop, or null if it is not set
+ */
+ public final TDesktop getDesktop() {
+ return desktop;
+ }
- // We are dirty, redraw the screen.
- doRepaint();
- return;
- }
+ /**
+ * Get the current active window.
+ *
+ * @return the active window, or null if it is not set
+ */
+ public final TWindow getActiveWindow() {
+ return activeWindow;
+ }
- // Put into the main queue
- drainEventQueue.add(event);
- }
+ /**
+ * Get a (shallow) copy of the window list.
+ *
+ * @return a copy of the list of windows for this application
+ */
+ public final List<TWindow> getAllWindows() {
+ List<TWindow> result = new LinkedList<TWindow>();
+ result.addAll(windows);
+ return result;
}
/**
- * Dispatch one event to the appropriate widget or application-level
- * event handler. This is the primary event handler, it has the normal
- * application-wide event handling.
+ * Get focusFollowsMouse flag.
*
- * @param event the input event to consume
- * @see #secondaryHandleEvent(TInputEvent event)
+ * @return true if focus follows mouse: windows automatically raised if
+ * the mouse passes over them
*/
- private void primaryHandleEvent(final TInputEvent event) {
+ public boolean getFocusFollowsMouse() {
+ return focusFollowsMouse;
+ }
- if (debugEvents) {
- System.err.printf("Handle event: %s\n", event);
- }
- TMouseEvent doubleClick = null;
+ /**
+ * Set focusFollowsMouse flag.
+ *
+ * @param focusFollowsMouse if true, focus follows mouse: windows
+ * automatically raised if the mouse passes over them
+ */
+ public void setFocusFollowsMouse(final boolean focusFollowsMouse) {
+ this.focusFollowsMouse = focusFollowsMouse;
+ }
- // Special application-wide events -----------------------------------
+ /**
+ * Display the about dialog.
+ */
+ protected void showAboutDialog() {
+ messageBox(i18n.getString("aboutDialogTitle"),
+ MessageFormat.format(i18n.getString("aboutDialogText"),
+ this.getClass().getPackage().getImplementationVersion()),
+ TMessageBox.Type.OK);
+ }
- // Peek at the mouse position
- if (event instanceof TMouseEvent) {
- TMouseEvent mouse = (TMouseEvent) event;
- if ((mouseX != mouse.getX()) || (mouseY != mouse.getY())) {
- oldMouseX = mouseX;
- oldMouseY = mouseY;
- mouseX = mouse.getX();
- mouseY = mouse.getY();
- } else {
- if (mouse.getType() == TMouseEvent.Type.MOUSE_UP) {
- if ((mouse.getTime().getTime() - lastMouseUpTime) <
- doubleClickTime) {
+ // ------------------------------------------------------------------------
+ // Screen refresh loop ----------------------------------------------------
+ // ------------------------------------------------------------------------
- // This is a double-click.
- doubleClick = new TMouseEvent(TMouseEvent.Type.
- MOUSE_DOUBLE_CLICK,
- mouse.getX(), mouse.getY(),
- mouse.getAbsoluteX(), mouse.getAbsoluteY(),
- mouse.isMouse1(), mouse.isMouse2(),
- mouse.isMouse3(),
- mouse.isMouseWheelUp(), mouse.isMouseWheelDown());
-
- } else {
- // The first click of a potential double-click.
- lastMouseUpTime = mouse.getTime().getTime();
- }
- }
- }
-
- // See if we need to switch focus to another window or the menu
- checkSwitchFocus((TMouseEvent) event);
+ /**
+ * Invert the cell color at a position. This is used to track the mouse.
+ *
+ * @param x column position
+ * @param y row position
+ */
+ private void invertCell(final int x, final int y) {
+ if (debugThreads) {
+ System.err.printf("%d %s invertCell() %d %d\n",
+ System.currentTimeMillis(), Thread.currentThread(), x, y);
+ }
+ CellAttributes attr = getScreen().getAttrXY(x, y);
+ if (attr.getForeColorRGB() < 0) {
+ attr.setForeColor(attr.getForeColor().invert());
+ } else {
+ attr.setForeColorRGB(attr.getForeColorRGB() ^ 0x00ffffff);
}
+ if (attr.getBackColorRGB() < 0) {
+ attr.setBackColor(attr.getBackColor().invert());
+ } else {
+ attr.setBackColorRGB(attr.getBackColorRGB() ^ 0x00ffffff);
+ }
+ getScreen().putAttrXY(x, y, attr, false);
+ }
- // Handle menu events
- if ((activeMenu != null) && !(event instanceof TCommandEvent)) {
- TMenu menu = activeMenu;
+ /**
+ * Draw everything.
+ */
+ private void drawAll() {
+ boolean menuIsActive = false;
- if (event instanceof TMouseEvent) {
- TMouseEvent mouse = (TMouseEvent) event;
+ if (debugThreads) {
+ System.err.printf("%d %s drawAll() enter\n",
+ System.currentTimeMillis(), Thread.currentThread());
+ }
- while (subMenus.size() > 0) {
- TMenu subMenu = subMenus.get(subMenus.size() - 1);
- if (subMenu.mouseWouldHit(mouse)) {
- break;
- }
- if ((mouse.getType() == TMouseEvent.Type.MOUSE_MOTION)
- && (!mouse.isMouse1())
- && (!mouse.isMouse2())
- && (!mouse.isMouse3())
- && (!mouse.isMouseWheelUp())
- && (!mouse.isMouseWheelDown())
- ) {
- break;
- }
- // We navigated away from a sub-menu, so close it
- closeSubMenu();
+ if (!repaint) {
+ if (debugThreads) {
+ System.err.printf("%d %s drawAll() !repaint\n",
+ System.currentTimeMillis(), Thread.currentThread());
+ }
+ synchronized (getScreen()) {
+ if ((oldMouseX != mouseX) || (oldMouseY != mouseY)) {
+ // The only thing that has happened is the mouse moved.
+ // Clear the old position and draw the new position.
+ invertCell(oldMouseX, oldMouseY);
+ invertCell(mouseX, mouseY);
+ oldMouseX = mouseX;
+ oldMouseY = mouseY;
}
-
- // Convert the mouse relative x/y to menu coordinates
- assert (mouse.getX() == mouse.getAbsoluteX());
- assert (mouse.getY() == mouse.getAbsoluteY());
- if (subMenus.size() > 0) {
- menu = subMenus.get(subMenus.size() - 1);
+ if (getScreen().isDirty()) {
+ backend.flushScreen();
}
- mouse.setX(mouse.getX() - menu.getX());
- mouse.setY(mouse.getY() - menu.getY());
+ return;
}
- menu.handleEvent(event);
- return;
}
- if (event instanceof TKeypressEvent) {
- TKeypressEvent keypress = (TKeypressEvent) event;
+ if (debugThreads) {
+ System.err.printf("%d %s drawAll() REDRAW\n",
+ System.currentTimeMillis(), Thread.currentThread());
+ }
- // See if this key matches an accelerator, and is not being
- // shortcutted by the active window, and if so dispatch the menu
- // event.
- boolean windowWillShortcut = false;
- if (activeWindow != null) {
- assert (activeWindow.isShown());
- if (activeWindow.isShortcutKeypress(keypress.getKey())) {
- // We do not process this key, it will be passed to the
- // window instead.
- windowWillShortcut = true;
- }
- }
+ // If true, the cursor is not visible
+ boolean cursor = false;
- if (!windowWillShortcut && !modalWindowActive()) {
- TKeypress keypressLowercase = keypress.getKey().toLowerCase();
- TMenuItem item = null;
- synchronized (accelerators) {
- item = accelerators.get(keypressLowercase);
- }
- if (item != null) {
- if (item.isEnabled()) {
- // Let the menu item dispatch
- item.dispatch();
- return;
- }
- }
+ // Start with a clean screen
+ getScreen().clear();
- // Handle the keypress
- if (onKeypress(keypress)) {
- return;
- }
- }
+ // Draw the desktop
+ if (desktop != null) {
+ desktop.drawChildren();
}
- if (event instanceof TCommandEvent) {
- if (onCommand((TCommandEvent) event)) {
- return;
- }
+ // Draw each window in reverse Z order
+ List<TWindow> sorted = new LinkedList<TWindow>(windows);
+ Collections.sort(sorted);
+ TWindow topLevel = null;
+ if (sorted.size() > 0) {
+ topLevel = sorted.get(0);
}
-
- if (event instanceof TMenuEvent) {
- if (onMenu((TMenuEvent) event)) {
- return;
+ Collections.reverse(sorted);
+ for (TWindow window: sorted) {
+ if (window.isShown()) {
+ window.drawChildren();
}
}
- // Dispatch events to the active window -------------------------------
- boolean dispatchToDesktop = true;
- TWindow window = activeWindow;
- if (window != null) {
- assert (window.isActive());
- assert (window.isShown());
- if (event instanceof TMouseEvent) {
- TMouseEvent mouse = (TMouseEvent) event;
- // Convert the mouse relative x/y to window coordinates
- assert (mouse.getX() == mouse.getAbsoluteX());
- assert (mouse.getY() == mouse.getAbsoluteY());
- mouse.setX(mouse.getX() - window.getX());
- mouse.setY(mouse.getY() - window.getY());
-
- if (doubleClick != null) {
- doubleClick.setX(doubleClick.getX() - window.getX());
- doubleClick.setY(doubleClick.getY() - window.getY());
- }
-
- if (window.mouseWouldHit(mouse)) {
- dispatchToDesktop = false;
- }
- } else if (event instanceof TKeypressEvent) {
- dispatchToDesktop = false;
+ // Draw the blank menubar line - reset the screen clipping first so
+ // it won't trim it out.
+ getScreen().resetClipping();
+ getScreen().hLineXY(0, 0, getScreen().getWidth(), ' ',
+ theme.getColor("tmenu"));
+ // Now draw the menus.
+ int x = 1;
+ for (TMenu menu: menus) {
+ CellAttributes menuColor;
+ CellAttributes menuMnemonicColor;
+ if (menu.isActive()) {
+ menuIsActive = true;
+ menuColor = theme.getColor("tmenu.highlighted");
+ menuMnemonicColor = theme.getColor("tmenu.mnemonic.highlighted");
+ topLevel = menu;
+ } else {
+ menuColor = theme.getColor("tmenu");
+ menuMnemonicColor = theme.getColor("tmenu.mnemonic");
}
+ // Draw the menu title
+ getScreen().hLineXY(x, 0, menu.getTitle().length() + 2, ' ',
+ menuColor);
+ getScreen().putStringXY(x + 1, 0, menu.getTitle(), menuColor);
+ // Draw the highlight character
+ getScreen().putCharXY(x + 1 + menu.getMnemonic().getShortcutIdx(),
+ 0, menu.getMnemonic().getShortcut(), menuMnemonicColor);
- if (debugEvents) {
- System.err.printf("TApplication dispatch event: %s\n",
- event);
- }
- window.handleEvent(event);
- if (doubleClick != null) {
- window.handleEvent(doubleClick);
- }
- }
- if (dispatchToDesktop) {
- // This event is fair game for the desktop to process.
- if (desktop != null) {
- desktop.handleEvent(event);
- if (doubleClick != null) {
- desktop.handleEvent(doubleClick);
- }
+ if (menu.isActive()) {
+ menu.drawChildren();
+ // Reset the screen clipping so we can draw the next title.
+ getScreen().resetClipping();
}
+ x += menu.getTitle().length() + 2;
}
- }
- /**
- * Dispatch one event to the appropriate widget or application-level
- * event handler. This is the secondary event handler used by certain
- * special dialogs (currently TMessageBox and TFileOpenBox).
- *
- * @param event the input event to consume
- * @see #primaryHandleEvent(TInputEvent event)
- */
- private void secondaryHandleEvent(final TInputEvent event) {
- TMouseEvent doubleClick = null;
+ for (TMenu menu: subMenus) {
+ // Reset the screen clipping so we can draw the next sub-menu.
+ getScreen().resetClipping();
+ menu.drawChildren();
+ }
- // Peek at the mouse position
- if (event instanceof TMouseEvent) {
- TMouseEvent mouse = (TMouseEvent) event;
- if ((mouseX != mouse.getX()) || (mouseY != mouse.getY())) {
- oldMouseX = mouseX;
- oldMouseY = mouseY;
- mouseX = mouse.getX();
- mouseY = mouse.getY();
- } else {
- if (mouse.getType() == TMouseEvent.Type.MOUSE_UP) {
- if ((mouse.getTime().getTime() - lastMouseUpTime) <
- doubleClickTime) {
+ // Draw the status bar of the top-level window
+ TStatusBar statusBar = null;
+ if (topLevel != null) {
+ statusBar = topLevel.getStatusBar();
+ }
+ if (statusBar != null) {
+ getScreen().resetClipping();
+ statusBar.setWidth(getScreen().getWidth());
+ statusBar.setY(getScreen().getHeight() - topLevel.getY());
+ statusBar.draw();
+ } else {
+ CellAttributes barColor = new CellAttributes();
+ barColor.setTo(getTheme().getColor("tstatusbar.text"));
+ getScreen().hLineXY(0, desktopBottom, getScreen().getWidth(), ' ',
+ barColor);
+ }
- // This is a double-click.
- doubleClick = new TMouseEvent(TMouseEvent.Type.
- MOUSE_DOUBLE_CLICK,
- mouse.getX(), mouse.getY(),
- mouse.getAbsoluteX(), mouse.getAbsoluteY(),
- mouse.isMouse1(), mouse.isMouse2(),
- mouse.isMouse3(),
- mouse.isMouseWheelUp(), mouse.isMouseWheelDown());
+ // Draw the mouse pointer
+ invertCell(mouseX, mouseY);
+ oldMouseX = mouseX;
+ oldMouseY = mouseY;
+ // Place the cursor if it is visible
+ if (!menuIsActive) {
+ TWidget activeWidget = null;
+ if (sorted.size() > 0) {
+ activeWidget = sorted.get(sorted.size() - 1).getActiveChild();
+ if (activeWidget.isCursorVisible()) {
+ if ((activeWidget.getCursorAbsoluteY() < desktopBottom)
+ && (activeWidget.getCursorAbsoluteY() > desktopTop)
+ ) {
+ getScreen().putCursor(true,
+ activeWidget.getCursorAbsoluteX(),
+ activeWidget.getCursorAbsoluteY());
+ cursor = true;
} else {
- // The first click of a potential double-click.
- lastMouseUpTime = mouse.getTime().getTime();
+ getScreen().putCursor(false,
+ activeWidget.getCursorAbsoluteX(),
+ activeWidget.getCursorAbsoluteY());
+ cursor = false;
}
}
}
}
- secondaryEventReceiver.handleEvent(event);
- if (doubleClick != null) {
- secondaryEventReceiver.handleEvent(doubleClick);
+ // Kill the cursor
+ if (!cursor) {
+ getScreen().hideCursor();
}
- }
- /**
- * Enable a widget to override the primary event thread.
- *
- * @param widget widget that will receive events
- */
- public final void enableSecondaryEventReceiver(final TWidget widget) {
- if (debugThreads) {
- System.err.println(System.currentTimeMillis() +
- " enableSecondaryEventReceiver()");
+ // Flush the screen contents
+ if (getScreen().isDirty()) {
+ backend.flushScreen();
}
- assert (secondaryEventReceiver == null);
- assert (secondaryEventHandler == null);
- assert ((widget instanceof TMessageBox)
- || (widget instanceof TFileOpenBox));
- secondaryEventReceiver = widget;
- secondaryEventHandler = new WidgetEventHandler(this, false);
-
- (new Thread(secondaryEventHandler)).start();
- }
-
- /**
- * Yield to the secondary thread.
- */
- public final void yield() {
- assert (secondaryEventReceiver != null);
-
- while (secondaryEventReceiver != null) {
- synchronized (primaryEventHandler) {
- try {
- primaryEventHandler.wait();
- } catch (InterruptedException e) {
- // SQUASH
- }
- }
- }
+ repaint = false;
}
/**
- * Do stuff when there is no user input.
+ * Force this application to exit.
*/
- private void doIdle() {
- if (debugThreads) {
- System.err.printf(System.currentTimeMillis() + " " +
- Thread.currentThread() + " doIdle()\n");
- }
-
- synchronized (timers) {
-
- if (debugThreads) {
- System.err.printf(System.currentTimeMillis() + " " +
- Thread.currentThread() + " doIdle() 2\n");
- }
-
- // Run any timers that have timed out
- Date now = new Date();
- List<TTimer> keepTimers = new LinkedList<TTimer>();
- for (TTimer timer: timers) {
- if (timer.getNextTick().getTime() <= now.getTime()) {
- // Something might change, so repaint the screen.
- repaint = true;
- timer.tick();
- if (timer.recurring) {
- keepTimers.add(timer);
- }
- } else {
- keepTimers.add(timer);
- }
- }
- timers = keepTimers;
- }
-
- // Call onIdle's
- for (TWindow window: windows) {
- window.onIdle();
- }
- if (desktop != null) {
- desktop.onIdle();
+ public void exit() {
+ quit = true;
+ synchronized (this) {
+ this.notify();
}
}
windows.remove(0);
activeWindow = null;
for (TWindow w: windows) {
+
+ // Do not activate a hidden window.
+ if (w.isHidden()) {
+ continue;
+ }
+
if (w.getZ() > z) {
w.setZ(w.getZ() - 1);
if (w.getZ() == 0) {
}
/**
- * Add a window to my window list and make it active.
+ * Add a window to my window list and make it active. Note package
+ * private access.
*
* @param window new window to add
*/
- public final void addWindow(final TWindow window) {
+ final void addWindowToApplication(final TWindow window) {
// Do not add menu windows to the window list.
if (window instanceof TMenu) {
}
synchronized (windows) {
+ if (windows.contains(window)) {
+ throw new IllegalArgumentException("Window " + window +
+ " is already in window list");
+ }
+
// Whatever window might be moving/dragging, stop it now.
for (TWindow w: windows) {
if (w.inMovements()) {
}
if (((window.flags & TWindow.CENTERED) == 0)
- && smartWindowPlacement) {
+ && ((window.flags & TWindow.ABSOLUTEXY) == 0)
+ && (smartWindowPlacement == true)
+ ) {
doSmartPlacement(window);
}
continue;
}
for (int x = w.getX(); x < w.getX() + w.getWidth(); x++) {
+ if (x < 0) {
+ continue;
+ }
if (x >= width) {
continue;
}
for (int y = w.getY(); y < w.getY() + w.getHeight(); y++) {
+ if (y < 0) {
+ continue;
+ }
if (y >= height) {
continue;
}
}
}
+ /**
+ * Post an event to process.
+ *
+ * @param event new event to add to the queue
+ */
+ public final void postEvent(final TInputEvent event) {
+ synchronized (this) {
+ synchronized (fillEventQueue) {
+ fillEventQueue.add(event);
+ }
+ if (debugThreads) {
+ System.err.println(System.currentTimeMillis() + " " +
+ Thread.currentThread() + " postEvent() wake up main");
+ }
+ this.notify();
+ }
+ }
+
/**
* Post an event to process and turn off the menu.
*
return helpMenu;
}
- // ------------------------------------------------------------------------
- // Event handlers ---------------------------------------------------------
- // ------------------------------------------------------------------------
-
- /**
- * Method that TApplication subclasses can override to handle menu or
- * posted command events.
- *
- * @param command command event
- * @return if true, this event was consumed
- */
- protected boolean onCommand(final TCommandEvent command) {
- // Default: handle cmExit
- if (command.equals(cmExit)) {
- if (messageBox(i18n.getString("exitDialogTitle"),
- i18n.getString("exitDialogText"),
- TMessageBox.Type.YESNO).getResult() == TMessageBox.Result.YES) {
- exit();
- }
- return true;
- }
-
- if (command.equals(cmShell)) {
- openTerminal(0, 0, TWindow.RESIZABLE);
- return true;
- }
-
- if (command.equals(cmTile)) {
- tileWindows();
- return true;
- }
- if (command.equals(cmCascade)) {
- cascadeWindows();
- return true;
- }
- if (command.equals(cmCloseAll)) {
- closeAllWindows();
- return true;
- }
-
- if (command.equals(cmMenu)) {
- if (!modalWindowActive() && (activeMenu == null)) {
- if (menus.size() > 0) {
- menus.get(0).setActive(true);
- activeMenu = menus.get(0);
- return true;
- }
- }
- }
-
- return false;
- }
-
- /**
- * Method that TApplication subclasses can override to handle menu
- * events.
- *
- * @param menu menu event
- * @return if true, this event was consumed
- */
- protected boolean onMenu(final TMenuEvent menu) {
-
- // Default: handle MID_EXIT
- if (menu.getId() == TMenu.MID_EXIT) {
- if (messageBox(i18n.getString("exitDialogTitle"),
- i18n.getString("exitDialogText"),
- TMessageBox.Type.YESNO).getResult() == TMessageBox.Result.YES) {
- exit();
- }
- return true;
- }
-
- if (menu.getId() == TMenu.MID_SHELL) {
- openTerminal(0, 0, TWindow.RESIZABLE);
- return true;
- }
-
- if (menu.getId() == TMenu.MID_TILE) {
- tileWindows();
- return true;
- }
- if (menu.getId() == TMenu.MID_CASCADE) {
- cascadeWindows();
- return true;
- }
- if (menu.getId() == TMenu.MID_CLOSE_ALL) {
- closeAllWindows();
- return true;
- }
- if (menu.getId() == TMenu.MID_ABOUT) {
- showAboutDialog();
- return true;
- }
- if (menu.getId() == TMenu.MID_REPAINT) {
- doRepaint();
- return true;
- }
- return false;
- }
-
- /**
- * Method that TApplication subclasses can override to handle keystrokes.
- *
- * @param keypress keystroke event
- * @return if true, this event was consumed
- */
- protected boolean onKeypress(final TKeypressEvent keypress) {
- // Default: only menu shortcuts
-
- // Process Alt-F, Alt-E, etc. menu shortcut keys
- if (!keypress.getKey().isFnKey()
- && keypress.getKey().isAlt()
- && !keypress.getKey().isCtrl()
- && (activeMenu == null)
- && !modalWindowActive()
- ) {
-
- assert (subMenus.size() == 0);
-
- for (TMenu menu: menus) {
- if (Character.toLowerCase(menu.getMnemonic().getShortcut())
- == Character.toLowerCase(keypress.getKey().getChar())
- ) {
- activeMenu = menu;
- menu.setActive(true);
- return true;
- }
- }
- }
-
- return false;
- }
-
// ------------------------------------------------------------------------
// TTimer management ------------------------------------------------------
// ------------------------------------------------------------------------
return openTerminal(x, y, TWindow.RESIZABLE, commandLine);
}
+ /**
+ * Convenience function to open a terminal window and execute a custom
+ * command line inside it.
+ *
+ * @param x column relative to parent
+ * @param y row relative to parent
+ * @param flags mask of CENTERED, MODAL, or RESIZABLE
+ * @param command the command line to execute
+ * @return the terminal new window
+ */
+ public final TTerminalWindow openTerminal(final int x, final int y,
+ final int flags, final String [] command) {
+
+ return new TTerminalWindow(this, x, y, flags, command);
+ }
+
/**
* Convenience function to open a terminal window and execute a custom
* command line inside it.
public final TTerminalWindow openTerminal(final int x, final int y,
final int flags, final String commandLine) {
- return new TTerminalWindow(this, x, y, flags, commandLine);
+ return new TTerminalWindow(this, x, y, flags, commandLine.split("\\s"));
}
/**
* @param title window title, will be centered along the top border
* @param width width of window
* @param height height of window
+ * @return the new window
*/
public final TWindow addWindow(final String title, final int width,
final int height) {
* @param width width of window
* @param height height of window
* @param flags bitmask of RESIZABLE, CENTERED, or MODAL
+ * @return the new window
*/
public final TWindow addWindow(final String title,
final int width, final int height, final int flags) {
* @param y row relative to parent
* @param width width of window
* @param height height of window
+ * @return the new window
*/
public final TWindow addWindow(final String title,
final int x, final int y, final int width, final int height) {
* @param width width of window
* @param height height of window
* @param flags mask of RESIZABLE, CENTERED, or MODAL
+ * @return the new window
*/
public final TWindow addWindow(final String title,
final int x, final int y, final int width, final int height,
* active.
*
* @param file the file to open
+ * @return the new editor window
* @throws IOException if a java.io operation throws
*/
public final TEditorWindow addEditor(final File file) throws IOException {