+ /**
+ * 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 {
+
+ 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();
+ }
+
+ /**
+ * 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
+ */
+ public TApplication(final InputStream input,
+ final OutputStream output) throws UnsupportedEncodingException {
+
+ backend = new ECMA48Backend(this, input, output);
+ TApplicationImpl();
+ }
+
+ /**
+ * 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) {
+
+ backend = new ECMA48Backend(this, input, reader, writer, setRawMode);
+ TApplicationImpl();
+ }
+
+ /**
+ * 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.
+ * @throws IllegalArgumentException if input, reader, or writer are null.
+ */
+ public TApplication(final InputStream input, final Reader reader,
+ final PrintWriter writer) {
+
+ this(input, reader, writer, false);
+ }
+
+ /**
+ * 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();
+ }
+
+ /**
+ * Finish construction once the backend is set.
+ */
+ private void TApplicationImpl() {
+ // Text block mouse option
+ if (System.getProperty("jexer.textMouse", "true").equals("false")) {
+ textMouse = false;
+ }
+
+ // Hide mouse when typing option
+ if (System.getProperty("jexer.hideMouseWhenTyping",
+ "false").equals("true")) {
+
+ hideMouseWhenTyping = true;
+ }
+
+ // Hide status bar option
+ if (System.getProperty("jexer.hideStatusBar",
+ "false").equals("true")) {
+ hideStatusBar = true;
+ }
+
+ // Hide menu bar option
+ if (System.getProperty("jexer.hideMenuBar", "false").equals("true")) {
+ hideMenuBar = true;
+ }
+
+ theme = new ColorTheme();
+ desktopTop = (hideMenuBar ? 0 : 1);
+ desktopBottom = getScreen().getHeight() - 1 + (hideStatusBar ? 1 : 0);
+ fillEventQueue = new LinkedList<TInputEvent>();
+ drainEventQueue = new LinkedList<TInputEvent>();
+ windows = new LinkedList<TWindow>();
+ menus = new ArrayList<TMenu>();
+ subMenus = new ArrayList<TMenu>();
+ timers = new LinkedList<TTimer>();
+ accelerators = new HashMap<TKeypress, TMenuItem>();
+ menuItems = new LinkedList<TMenuItem>();
+ desktop = new TDesktop(this);
+
+ // 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();
+ }
+ }
+ );
+ }
+ }
+
+ // Load the help system
+ invokeLater(new Runnable() {
+ /*
+ * This isn't the best solution. But basically if a TApplication
+ * subclass constructor throws and needs to use TExceptionDialog,
+ * it may end up at the bottom of the window stack with a bunch
+ * of modal windows on top of it if said constructors spawn their
+ * windows also via invokeLater(). But if they don't do that,
+ * and instead just conventionally construct their windows, then
+ * this exception dialog will end up on top where it should be.
+ */
+ public void run() {
+ try {
+ ClassLoader loader = Thread.currentThread().getContextClassLoader();
+ helpFile = new HelpFile();
+ helpFile.load(loader.getResourceAsStream("help.xml"));
+ } catch (Exception e) {
+ new TExceptionDialog(TApplication.this, e);
+ }
+ }
+ });
+ }
+
+ // ------------------------------------------------------------------------
+ // Runnable ---------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Run this application until it exits.
+ */
+ public void run() {
+ // System.err.println("*** TApplication.run() begins ***");
+
+ // Start the screen updater thread
+ screenHandler = new ScreenHandler(this);
+ (new Thread(screenHandler)).start();
+
+ // Start the main consumer thread
+ primaryEventHandler = new WidgetEventHandler(this, true);
+ (new Thread(primaryEventHandler)).start();
+
+ started = true;
+
+ while (!quit) {
+ synchronized (this) {
+ boolean doWait = false;
+
+ if (!backend.hasEvents()) {
+ synchronized (fillEventQueue) {
+ if (fillEventQueue.size() == 0) {
+ doWait = true;
+ }
+ }
+ }
+
+ if (doWait) {
+ // No I/O to dispatch, so wait until the backend
+ // provides new I/O.
+ try {
+ if (debugThreads) {
+ System.err.println(System.currentTimeMillis() +
+ " " + Thread.currentThread() + " MAIN sleep");
+ }
+
+ this.wait();
+
+ if (debugThreads) {
+ System.err.println(System.currentTimeMillis() +
+ " " + Thread.currentThread() + " 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();
+ }
+
+ } // while (!quit)
+
+ // Shutdown the event consumer threads
+ if (secondaryEventHandler != null) {
+ synchronized (secondaryEventHandler) {
+ secondaryEventHandler.notify();
+ }
+ }
+ if (primaryEventHandler != null) {
+ synchronized (primaryEventHandler) {
+ primaryEventHandler.notify();
+ }
+ }
+
+ // Close all the windows. This gives them an opportunity to release
+ // resources.
+ closeAllWindows();
+
+ // Close the desktop.
+ if (desktop != null) {
+ setDesktop(null);
+ }
+
+ // Give the overarching application an opportunity to release
+ // resources.
+ onExit();
+
+ // System.err.println("*** TApplication.run() exits ***");
+ }
+
+ // ------------------------------------------------------------------------
+ // 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).isYes()) {
+
+ exit();
+ }
+ return true;
+ }
+
+ if (command.equals(cmHelp)) {
+ if (getActiveWindow() != null) {
+ new THelpWindow(this, getActiveWindow().getHelpTopic());
+ } else {
+ new THelpWindow(this);
+ }
+ 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) && (hideMenuBar == false)) {
+ 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).isYes()) {
+
+ exit();
+ }
+ return true;
+ }
+
+ if (menu.getId() == TMenu.MID_HELP_HELP) {
+ new THelpWindow(this, THelpWindow.HELP_HELP);
+ return true;
+ }
+
+ if (menu.getId() == TMenu.MID_HELP_CONTENTS) {
+ new THelpWindow(this, helpFile.getTableOfContents());
+ return true;
+ }
+
+ if (menu.getId() == TMenu.MID_HELP_INDEX) {
+ new THelpWindow(this, helpFile.getIndex());
+ return true;
+ }
+
+ if (menu.getId() == TMenu.MID_HELP_SEARCH) {
+ TInputBox inputBox = inputBox(i18n.
+ getString("searchHelpInputBoxTitle"),
+ i18n.getString("searchHelpInputBoxCaption"), "",
+ TInputBox.Type.OKCANCEL);
+ if (inputBox.isOk()) {
+ new THelpWindow(this,
+ helpFile.getSearchResults(inputBox.getText()));
+ }
+ return true;
+ }
+
+ if (menu.getId() == TMenu.MID_HELP_PREVIOUS) {
+ if (helpTopics.size() > 1) {
+ Topic previous = helpTopics.remove(helpTopics.size() - 2);
+ helpTopics.remove(helpTopics.size() - 1);
+ new THelpWindow(this, previous);
+ } else {
+ new THelpWindow(this, helpFile.getTableOfContents());
+ }
+ return true;
+ }
+
+ if (menu.getId() == TMenu.MID_HELP_ACTIVE_FILE) {
+ try {
+ List<String> filters = new ArrayList<String>();
+ filters.add("^.*\\.[Xx][Mm][Ll]$");
+ String filename = fileOpenBox(".", TFileOpenBox.Type.OPEN,
+ filters);
+ if (filename != null) {
+ helpTopics = new ArrayList<Topic>();
+ helpFile = new HelpFile();
+ helpFile.load(new FileInputStream(filename));
+ }
+ } catch (Exception e) {
+ // Show this exception to the user.
+ new TExceptionDialog(this, e);
+ }
+ 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) {
+ getScreen().clearPhysical();
+ doRepaint();
+ return true;
+ }
+ if (menu.getId() == TMenu.MID_VIEW_IMAGE) {
+ openImage();
+ return true;
+ }
+ if (menu.getId() == TMenu.MID_SCREEN_OPTIONS) {
+ new TFontChooserWindow(this);
+ return true;
+ }
+
+ if (menu.getId() == TMenu.MID_CUT) {
+ postMenuEvent(new TCommandEvent(cmCut));
+ return true;
+ }
+ if (menu.getId() == TMenu.MID_COPY) {
+ postMenuEvent(new TCommandEvent(cmCopy));
+ return true;
+ }
+ if (menu.getId() == TMenu.MID_PASTE) {
+ postMenuEvent(new TCommandEvent(cmPaste));
+ return true;
+ }
+ if (menu.getId() == TMenu.MID_CLEAR) {
+ postMenuEvent(new TCommandEvent(cmClear));
+ 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()
+ && (hideMenuBar == false)
+ ) {
+
+ 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;
+ }
+
+ /**
+ * Process background events, and update the screen.
+ */
+ private void finishEventProcessing() {
+ if (debugThreads) {
+ System.err.printf(System.currentTimeMillis() + " " +
+ Thread.currentThread() + " finishEventProcessing()\n");
+ }
+
+ // See if we need to enable/disable the edit menu.
+ EditMenuUser widget = null;
+ if (activeMenu == null) {
+ TWindow activeWindow = getActiveWindow();
+ if (activeWindow != null) {
+ if (activeWindow.getActiveChild() instanceof EditMenuUser) {
+ widget = (EditMenuUser) activeWindow.getActiveChild();
+ }
+ } else if (desktop != null) {
+ if (desktop.getActiveChild() instanceof EditMenuUser) {
+ widget = (EditMenuUser) desktop.getActiveChild();
+ }
+ }
+ if (widget == null) {
+ disableMenuItem(TMenu.MID_CUT);
+ disableMenuItem(TMenu.MID_COPY);
+ disableMenuItem(TMenu.MID_PASTE);
+ disableMenuItem(TMenu.MID_CLEAR);
+ } else {
+ if (widget.isEditMenuCut()) {
+ enableMenuItem(TMenu.MID_CUT);
+ } else {
+ disableMenuItem(TMenu.MID_CUT);
+ }
+ if (widget.isEditMenuCopy()) {
+ enableMenuItem(TMenu.MID_COPY);
+ } else {
+ disableMenuItem(TMenu.MID_COPY);
+ }
+ if (widget.isEditMenuPaste()) {
+ enableMenuItem(TMenu.MID_PASTE);
+ } else {
+ disableMenuItem(TMenu.MID_PASTE);
+ }
+ if (widget.isEditMenuClear()) {
+ enableMenuItem(TMenu.MID_CLEAR);
+ } else {
+ disableMenuItem(TMenu.MID_CLEAR);
+ }
+ }
+ }
+
+ // Process timers and call doIdle()'s
+ doIdle();
+
+ // Update the screen
+ synchronized (getScreen()) {
+ drawAll();
+ }
+
+ // Wake up the screen repainter
+ wakeScreenHandler();
+
+ if (debugThreads) {
+ System.err.printf(System.currentTimeMillis() + " " +
+ Thread.currentThread() + " finishEventProcessing() END\n");
+ }
+ }
+
+ /**
+ * Peek at certain application-level events, add to eventQueue, and wake
+ * up the consuming Thread.
+ *
+ * @param event the input event to consume
+ */
+ private void metaHandleEvent(final TInputEvent event) {
+
+ if (debugEvents) {
+ System.err.printf(String.format("metaHandleEvents event: %s\n",
+ event)); System.err.flush();
+ }
+
+ if (quit) {
+ // Do no more processing if the application is already trying
+ // to exit.
+ return;
+ }