/** * This file is part of "BBSSH" (c) 2010 Marc A. Paradise --LICENSE NOTICE-- This program is free software; you can * redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software * Foundation; either version 2 of the License, or (at your option) any later version. This program 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. See the GNU General Public License for more details. You should have received a * copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., * 675 Mass Ave, Cambridge, MA 02139, USA. --LICENSE NOTICE-- */ package org.bbssh.ui.screens; import net.rim.device.api.i18n.ResourceBundle; import net.rim.device.api.system.UnsupportedOperationException; import net.rim.device.api.ui.Field; import net.rim.device.api.ui.FieldChangeListener; import net.rim.device.api.ui.Font; import net.rim.device.api.ui.MenuItem; import net.rim.device.api.ui.UiApplication; import net.rim.device.api.ui.component.Menu; import net.rim.device.api.ui.container.FullScreen; import org.bbssh.command.CommandConstants; import org.bbssh.exceptions.FontNotFoundException; import org.bbssh.help.HelpManager; import org.bbssh.i18n.BBSSHResource; import org.bbssh.model.ConnectionProperties; import org.bbssh.model.FontSettings; import org.bbssh.model.SettingsManager; import org.bbssh.net.session.Session; import org.bbssh.platform.PlatformServicesProvider; import org.bbssh.session.RemoteSessionInstance; import org.bbssh.session.SessionManager; import org.bbssh.terminal.TerminalStateData; import org.bbssh.ui.components.HeaderBar; import org.bbssh.ui.components.TerminalField; import org.bbssh.ui.components.keybinding.CommandBindingMenuItem; import org.bbssh.ui.components.keybinding.ModalCommandBindingMenuItem; import org.bbssh.ui.components.overlay.OverlayEditField; import org.bbssh.ui.components.overlay.OverlayManager; import org.bbssh.util.Logger; import org.bbssh.util.Tools; import org.bbssh.util.Version; /** * Singleton that displays a terminal. Change between terminals by invoking setActiveSession. */ public class TerminalScreen extends FullScreen implements FieldChangeListener { ResourceBundle bundle = ResourceBundle.getBundle(BBSSHResource.BUNDLE_ID, BBSSHResource.BUNDLE_NAME); private OverlayManager overlayManager; private OverlayEditField edit; // private OverlayShortcutBar bar; protected TerminalField termField; protected int width, height; private boolean userRequestedKeyboard = false; private int oldWidth = -1; private int oldHeight = -1; private MenuItem itemDisconnect = new CommandBindingMenuItem(CommandConstants.DISCONNECT_SESSION, 0x00200000, 1); private MenuItem itemReconnect = new CommandBindingMenuItem(CommandConstants.RECONNECT_SESSION, 0x00200000, 1); private MenuItem itemMainScreen = new CommandBindingMenuItem(BBSSHResource.MENU_CONNECTION_LIST, CommandConstants.POP_TERMINAL_SCREEN, 0x00200000, 2); private MenuItem itemShowInputOverlay = new CommandBindingMenuItem(BBSSHResource.MENU_TERM_INPUT_SCREEN, CommandConstants.SHOW_OVERLAY_INPUT, 0x00300000, 1); private MenuItem itemShowShortcutBar = new CommandBindingMenuItem(BBSSHResource.MENU_TERM_SHORTCUT_BAR, CommandConstants.SHOW_OVERLAY_COMMANDS, 0x00300000, 2); private MenuItem itemToggleKeyboard; private MenuItem itemInputMode = new CommandBindingMenuItem(BBSSHResource.MENU_TERM_HYBRID_INPUT, CommandConstants.INPUT_MODE, 0x00300000, 4); private MenuItem itemShowSpecialKeys = new ModalCommandBindingMenuItem(CommandConstants.SHOW_SCREEN_SPECIAL_KEYS, 0x00300000, 5); private MenuItem itemShowSymbols = new ModalCommandBindingMenuItem(BBSSHResource.MENU_SHOW_SYMBOLS, CommandConstants.SHOW_SYMBOLS, 0x00300000, 6); private MenuItem itemSetFont = new ModalCommandBindingMenuItem(BBSSHResource.MENU_SET_FONT, CommandConstants.SHOW_SCREEN_FONT, 0x00400000, 1); private MenuItem itemRunMacro = new ModalCommandBindingMenuItem(BBSSHResource.MENU_RUN_MACRO, CommandConstants.SHOW_SCREEN_MACRO_LIST, 0x00400000, 2); private MenuItem itemShowKeyBindings = new CommandBindingMenuItem(BBSSHResource.MENU_KEYBINDINGS, CommandConstants.SHOW_SCREEN_KEYBINDINGS, 0x00400000, 3); private MenuItem itemSendFeedback = new MenuItem(bundle, BBSSHResource.MENU_SEND_FEEDBACK, 0x00500000, 1) { public void run() { UiApplication.getUiApplication().invokeAndWait(new Runnable() { public void run() { if (SessionManager.getInstance().activeSession != null) Tools.sendFeedback(SessionManager.getInstance().activeSession.backingStore); } }); } }; /** * Constructor. Note that this is kept public only for purposes of dynamic creation of paltform-specific class * instances. You should not be instantiating this class directly. */ public TerminalScreen() { super(new OverlayManager(), DEFAULT_MENU); String name = TerminalField.class.getName(); termField = (TerminalField) Version.createOSObjectInstance(name); overlayManager = (OverlayManager) getDelegate(); overlayManager.setChangeListener(this); overlayManager.setCentralField(termField); name = OverlayEditField.class.getName(); edit = (OverlayEditField) Version.createOSObjectInstance(name); // bar = (OverlayShortcutBar) Version.createOSObjectInstance(OverlayShortcutBar.class.getName()); // Apparentl there is a default change listener in OS5+ in some cases? Without setting to null first, // we get an exception because there's already a listener specified. edit.setChangeListener(null); edit.setChangeListener(this); // bar.setChangeListener(null); // bar.setChangeListener(this); edit.setChangeListener(null); edit.setChangeListener(this); // Set up our shortcut bar - this will be user configurable in hte future, // but hard-coded for now. // bar.setDetailInfoLine( // new LabelField() { // protected void paint(Graphics graphics) { // // Inverse color for the label so it stands out. // graphics.setBackgroundColor(termField.fgcolor); // graphics.setColor(termField.bgcolor); // graphics.clear(); // super.paint(graphics); // } // }); // // @todo - this will be constructed based on user preference. // bar.add(new CommandButton("I", this, new BoundCommand(new ShowOverlayInput()), "Show Input Overlay", 6)); // bar.add(new CommandButton("X", this, new BoundCommand(new ShowSpecialKeysScreen()), "Show Special Keys", 6)); // bar.add(new CommandButton("D", this, new BoundCommand(new DisconnectSession()), "Disconnect", 6)); // bar.add(new CommandButton("+F", this, new BoundCommand(new IncrDecrFontSize(), 0), "Increase Font Size", 6)); // bar.add(new CommandButton("-F", this, new BoundCommand(new IncrDecrFontSize(), 1), "Decrease Font Size", 6)); // bar.add(new CommandButton("F", this, new BoundCommand(new ShowFontPopup()), "Change Font Face / Size", 6)); // bar.add(new CommandButton("K", this, new BoundCommand(new ShowKeybindingScreen()), // "Keyboard Shortcuts", 6)); // bar.add(new CommandButton("?", this, new BoundCommand(new ShowSessionDetailScreen()), // "Session Information", 6)); // @todo bar - not closing properyl when command executed, also it's possible to navigate // off of it and have it stuck on screen until you show/hide it again. if (PlatformServicesProvider.getInstance().hasVirtualKeyboard()) { itemToggleKeyboard = new CommandBindingMenuItem(CommandConstants.SHOW_HIDE_KEYBOARD, 0x00300000, 3); } else { itemToggleKeyboard = new CommandBindingMenuItem(CommandConstants.NONE, 0x00300000, 3); } if (SettingsManager.getSettings().isTitlebarDisplayEnabled()) { if (!PlatformServicesProvider.getInstance().isEnhancedTitlebarSupported()) { // Add a standard titlebar if the enhanced isn't supported. HeaderBar title = new HeaderBar("Terminal"); getOverlayManager().setTitleBar(title); } } } /** * Override of base clase's setFont. Do not use. Throws an UnsupportedOperationExcecption exception - use * setFont(FontSettings) instead. */ public void setFont(Font font) { throw new UnsupportedOperationException(); } protected void onUiEngineAttached(boolean attached) { super.onUiEngineAttached(attached); if (attached) { SessionManager mgr = SessionManager.getInstance(); if (mgr.activeSession != null) { if (termField != null) { termField.redraw(true); } PlatformServicesProvider.getInstance().lockOrientation(mgr.activeSession.state.orientationMode); } } else { PlatformServicesProvider.getInstance().unlockOrientation(); } } protected void makeMenu(Menu menu, int instance) { super.makeMenu(menu, instance); // THis is.. not pretty, but we don't want to display the full menu if // the edit field has focus. if (edit.isVisible() && edit.isFocus()) { return; } PlatformServicesProvider psp = PlatformServicesProvider.getInstance(); boolean showKeyboardOption = !psp.hasHardwareKeyboard() || (psp.hasSlider() && !psp.isSliderExtended()); MenuItem defItem = null; RemoteSessionInstance rsi = SessionManager.getInstance().activeSession; TerminalStateData state = rsi.state; String text; if (showKeyboardOption) { // Some menu text updates dynamically based on state if (isVirtualKeyboardVisible()) { text = bundle.getString(BBSSHResource.MENU_TERM_HIDE_KEYBOARD); } else { text = bundle.getString(BBSSHResource.MENU_TERM_SHOW_KEYBOARD); } itemToggleKeyboard.setText(text); } if (psp.isReducedLayout()) { if (state.typingMode == TerminalStateData.TYPING_MODE_DIRECT) { text = bundle.getString(BBSSHResource.MENU_TERM_SCROLLING); } else { text = bundle.getString(BBSSHResource.MENU_TERM_DIRECT_INPUT); } } else { if (state.typingMode == TerminalStateData.TYPING_MODE_DIRECT) { text = bundle.getString(BBSSHResource.MENU_TERM_HYBRID_INPUT); } else if (state.typingMode == TerminalStateData.TYPING_MODE_HYBRID) { text = bundle.getString(BBSSHResource.MENU_TERM_SCROLLING); } else { text = bundle.getString(BBSSHResource.MENU_TERM_DIRECT_INPUT); } } itemInputMode.setText(text); menu.add(itemSendFeedback); menu.add(HelpManager.getHelpMenu()); menu.add(itemMainScreen); menu.add(itemShowKeyBindings); if (rsi.session.getConnectionState() == Session.CONNSTATE_CONNECTED) { // Separate screens that send data from those that do other stuff... menu.add(itemShowInputOverlay); menu.add(itemShowShortcutBar); menu.add(itemShowSpecialKeys); if (showKeyboardOption) { menu.add(itemToggleKeyboard); } // Symbols menu only works if we have an available keyboard - // otherwise our hack for displaying it fails since the menu option itself // is not present. if (!psp.hasTouchscreen() || psp.isSliderExtended()) { menu.add(itemShowSymbols); } menu.add(itemRunMacro); menu.add(itemInputMode); menu.add(itemSetFont); defItem = itemShowInputOverlay; } if (rsi.isConnected()) { menu.add(itemDisconnect); if (defItem == null) defItem = itemDisconnect; } else { menu.add(itemReconnect); defItem = itemMainScreen; } menu.setDefault(defItem); } public void toggleInputOverlay() { if (edit.isVisible()) { hideInputOverlay(); } else { // Whenever we show the input overlay, we are to show the virtual // keyboard if it's available. overlayManager.showBottomField(edit); showVirtualKeyboard(false); edit.setFocus(); edit.setCursorPosition(edit.getTextLength()); } } /** * Hide the virtual keyboard. */ public final void hideVirtualKeyboard() { userRequestedKeyboard = false; setVirtualKeyboardVisibility(false); } /** * Override this to provide platform implementation of keyboard state check. * * @return true if the keyboard is visible on -screen */ public boolean isVirtualKeyboardVisible() { return false; } /** * Show the virtual keyboard if it's available on the current platform. Note that this will not show the virtual * keyboard if the OS says it's not appropriate - such as when the physical keyboard is shown. * * @param userInitiated * - true if this was requested by the user via a command or menu; false if it was invoked by an internal * operation (such as a keyboard closing or an edit field displaying). */ public final void showVirtualKeyboard(boolean userInitiated) { userRequestedKeyboard = userInitiated; setVirtualKeyboardVisibility(true); } /** * Default implementation does nothing,. Platform specific overrides must correctly show or hide the virtual * keyboard, taking into account hardware state (eg slider keyboard extended) * * @param b */ protected void setVirtualKeyboardVisibility(boolean visible) { } public void hideInputOverlay() { if (edit.isVisible()) overlayManager.hideBottomField(); // If the user did not explicitly ask for the keyboard // hide it when the input window is hidden. if (!userRequestedKeyboard) hideVirtualKeyboard(); } public void showShortcutOverlay() { showExpiringMessage("Toolbar unavailable until 2.1 release."); // overlayManager.showTopField(bar); // bar.setFocus(); } public void hideShortcutOverlay() { // if (bar.isVisible()) // overlayManager.hideTopField(); // hideOverlayManager(false); } protected boolean isOverlayActive() { // return bar.isVisible() || edit.isVisible(); return edit.isVisible(); } protected OverlayManager getOverlayManager() { return overlayManager; } /** * Hides the overlay manager if no overlay windows are visible, or if 'force' is true. */ public void hideOverlayManager() { // if (overlayFields.isVisible()) { // if (force) { // @todo clean up of overlay manager handling. // if (bar.isVisible()) { // overlayManager.hideTopField(); // } if (edit.isVisible()) { overlayManager.hideBottomField(); if (!userRequestedKeyboard) { hideVirtualKeyboard(); } } // } else { // if (bar.isVisible() || edit.isVisible()) { // return; // } // } // } // if (overlayFields.getManager() != null) // delete(overlayFields); } /* * (non-Javadoc) * @see net.rim.device.api.ui.FieldChangeListener#fieldChanged(net.rim.device.api.ui.Field, int) */ public void fieldChanged(Field field, int context) { // @todo should the overlay hide itself when one of these events is received? // Because in all cases, we're hiding ... perhaps overlay manager should // handle these events internally, and pass through the ones of interest - // in addition to hiding self? RemoteSessionInstance rsi = SessionManager.getInstance().activeSession; if (field == edit) { switch (context) { // @todo enter/alt/focus - preference driven case OverlayEditField.CONTEXT_ENTER_PRESSED: rsi.sendTwoPartString(edit.getText() + "\n"); edit.setText(""); break; case OverlayEditField.CONTEXT_ALT_ENTER_PRESSED: rsi.sendTwoPartString(edit.getText()); edit.setText(""); break; case OverlayEditField.CONTEXT_ESCAPE_PRESSED: edit.setText(""); break; default: return; } hideInputOverlay(); // } else if ((field != null && field.getManager() == bar)) { // // one of the command buttons has been pressed. // if (context == CommandButton.CONTEXT_BUTTON_CLICKED) { // hideShortcutOverlay(); // } // } else if (field == bar) { // if (context == OverlayShortcutBar.CONTEXT_CANCEL) { // hideShortcutOverlay(); // } else if (context == CommandButton.CONTEXT_BUTTON_CLICKED) { // hideShortcutOverlay(); // } } else if (field == overlayManager) { if (context == OverlayManager.CONTEXT_CANCEL) { hideOverlayManager(); } } } // @todo this screen should not be aware of the overlay edit fields to begin with - let the // manager handle them fully? protected OverlayEditField getEditField() { return edit; } boolean forceShutdown = false; public void setForceShutdown(boolean forceShutdown) { this.forceShutdown = forceShutdown; } protected void sublayout(int width, int height) { boolean refresh = true; // Logger.debug("TerminalScreen.sublayout: " + height + " " + width); super.sublayout(width, height); if (SessionManager.getInstance().activeSession == null) return; if (width == oldWidth && height == oldHeight) { // nothing to do if nothing has changed... return; } // only do this after initialization is finished. if (oldWidth != -1 || oldHeight != -1) { if (width != oldWidth) { // width change means orientation changed this.width = width; this.height = height; termField.sizeChanged(true); } else { // We only care if height is different from the last time through - that indicates // that keyboard has been shown or hidden. if (height < oldHeight) { // Shrunk? Keyboard shown, was hidden if (userRequestedKeyboard) { // User specifically asked to show the keyboard. In this case, we'll resize // appropriately. this.width = width; this.height = height; termField.sizeChanged(true); } else { // user did not request kbd, which means we auto-displayed it with the entry field // so do not resize (@todo make this optional) and do not record the new size // so that when the size change is undone we don't unnecessarily process an update. /* * - maybe prompt for this? * if (terminal.resizeDisplayWhenEditOverlayKeyboardDisplayed) */ refresh = false; } } else if (height > oldHeight) { // Expanded? Keyboard hidden, was shown and we were shrunken... // Because of this, we know that we always want to resize our display this.width = width; this.height = height; termField.sizeChanged(true); // since the user hasn't requested the kbd if we aren't showing it userRequestedKeyboard = false; } } } if (oldWidth == -1) { // initial this.width = width; this.height = height; termField.sizeChanged(false); } if (refresh) { oldWidth = width; oldHeight = height; } } /** * Displays the specifeid message on the terminal screen, if the terminal session specified is the active session. * * @param rsi * @param message */ // @todo - move interface to SessionManager? public void showExpiringMessage(RemoteSessionInstance rsi, String message) { if (SessionManager.getInstance().activeSession == rsi) termField.showExpiringMessage(message); } public void showExpiringMessage(String message) { termField.showExpiringMessage(message); } /** * @return font settings of terminal instance currently being viewed. */ public FontSettings getFontSettings() { return termField.getFontSettings(); } /** * Refreshes font and resizes as appropriate * * @param fs * font settings to use. * @throws FontNotFoundException */ public void updateFontSettings(FontSettings fs) throws FontNotFoundException { if (SessionManager.getInstance().activeSession == null) return; SessionManager.getInstance().activeSession.state.fs = fs; if (termField.updateFontSettings(fs)) { termField.sizeChanged(true); } } public void invalidateStatusIcons() { termField.invalidateStatusIcons(); } public void showInputOverlay(String value) { if (value != null) { edit.setText(value); edit.setCursorPosition(value.length()); } if (!edit.isVisible()) { toggleInputOverlay(); showVirtualKeyboard(false); } } public void clearSession() { if (isVisible()) invalidate(); } // Switch the session which is actively displayed. Note that undisplayed sessions // are continuing to run, but are not attached to the rendering screen. public void attachSession(RemoteSessionInstance rsi) throws FontNotFoundException { if (rsi == null) { Logger.error("TerminalScreen.attachSession - unexpected null RSI"); return; } ConnectionProperties prop = rsi.session.getProperties(); edit.setColors(Tools.color[prop.getForegroundColorIndex()], prop.getBackgroundColorIndex()); // This will pre-emptively restablish the lock - BEFORE the screen is displayed and layout executed // which prevents spurious resizes. (The layout is not changed until after we are attached) PlatformServicesProvider.getInstance().lockOrientation(rsi.state.orientationMode); termField.attachInstance(rsi); Field f = getOverlayManager().getTitleBar(); if (f instanceof HeaderBar) { HeaderBar h = (HeaderBar) f; h.setTitle(rsi.state.settings.getName()); h.setBackgroundColor(prop.getBackgroundColorIndex()); h.setFontColor(prop.getForegroundColorIndex()); } } protected boolean keyDown(int keycode, int time) { return super.keyDown(keycode, time); } public void forceRefresh() { termField.redraw(true); } public void showExpiringAlertMessage(int messageId) { termField.showExpiringAlertMessage(messageId); } public Menu getMenu(int instance) { Menu menu = super.getMenu(instance); // remove item for show/hide keyboard - we have our own impementation tha we must use. if (menu == null) return menu; for (int i = menu.getSize() - 1; i >= 0; i--) { MenuItem menuItem = menu.getItem(i); // This is the ordinal used for keyboard show/hide if (menuItem.getOrdinal() == 20480) { menu.deleteItem(i); } } return menu; } // @todo remove this. public static TerminalScreen getInstance() { return SessionManager.getInstance().getTerminalScreen(); } public void redraw(boolean forceRefresh) { termField.redraw(forceRefresh); } }