package org.bbssh.ui.components.keybinding;
import java.io.IOException;
import javax.microedition.lcdui.Font;
import net.rim.device.api.i18n.ResourceBundle;
import net.rim.device.api.i18n.ResourceBundleFamily;
import net.rim.device.api.ui.Field;
import net.rim.device.api.ui.FieldChangeListener;
import net.rim.device.api.ui.component.BasicEditField;
import net.rim.device.api.ui.component.Dialog;
import net.rim.device.api.ui.component.LabelField;
import net.rim.device.api.ui.component.ObjectChoiceField;
import net.rim.device.api.ui.component.SeparatorField;
import net.rim.device.api.ui.container.HorizontalFieldManager;
import net.rim.device.api.ui.container.PopupScreen;
import net.rim.device.api.ui.container.VerticalFieldManager;
import org.bbssh.command.CommandConstants;
import org.bbssh.i18n.BBSSHResource;
import org.bbssh.keybinding.ExecutableCommand;
import org.bbssh.keybinding.KeyBindingHelper;
import org.bbssh.keybinding.TerminalKey;
import org.bbssh.model.KeyBindingManager;
import org.bbssh.model.MacroManager;
import org.bbssh.ui.components.ClickableButtonField;
import org.bbssh.util.Tools;
/**
* This popup screen allows the caller to select a command to bind to a key. It will display the help text for the
* command and allow the user to enter a parameter if required.
*/
public class CommandBindingPopup extends PopupScreen implements FieldChangeListener {
private ResourceBundleFamily res = ResourceBundle.getBundle(BBSSHResource.BUNDLE_ID, BBSSHResource.BUNDLE_NAME);
/**
* This member is intended to accurately reflect true if we've made a change <i>during the current edit</i> while
* this dialog is displayed. Conversely, member state.changesSaved tracks whether changes need to be committed to
* the persistent store, and is managed at the screen level.
*/
// during the current edit; while state.changesSaved will reflect if the binding has changed at all
// since the last save of key bindings.
private boolean changeSaved;
boolean firstRun;
Object[] macros;
ObjectChoiceField availCommands;
private int titleId = 0;
private Field currentParamField;
private LabelField helpField = new LabelField();
private BasicEditField paramEditField = new BasicEditField(res.getString(BBSSHResource.KEYBIND_LBL_PARAMETER), "");
private BasicEditField paramNumericField = new BasicEditField(res.getString(BBSSHResource.KEYBIND_LBL_PARAMETER),
"", 32, BasicEditField.FILTER_NUMERIC);
private ObjectChoiceField paramChoiceField = new ObjectChoiceField(res
.getString(BBSSHResource.KEYBIND_LBL_PARAMETER), new Object[] { "" });
private ClickableButtonField saveChanges = new ClickableButtonField(res.getString(BBSSHResource.GENERAL_LBL_OK));
private ClickableButtonField cancelChanges = new ClickableButtonField(res
.getString(BBSSHResource.GENERAL_LBL_CANCEL));
private KeybindState state;
HorizontalFieldManager okCancelHFM;
HorizontalFieldManager parameterContainer = new HorizontalFieldManager();
// private boolean macroMode;
public CommandBindingPopup(boolean macroMode) {
super(new VerticalFieldManager(VERTICAL_SCROLL | VERTICAL_SCROLLBAR), PopupScreen.DEFAULT_CLOSE);
// This title text is used whenever this screen is displayed.
titleId = macroMode ? BBSSHResource.KEYBIND_MACRO_EDIT_TITLE : BBSSHResource.KEYBIND_SHORTCUT_EDIT_TITLE;
setFont(getFont().derive(Font.STYLE_PLAIN, 17));
// this.macroMode = macroMode;
ExecutableCommand[] commands;
if (macroMode) {
commands = KeyBindingManager.getInstance().getMacroActionCommands();
} else {
commands = KeyBindingManager.getInstance().getBindableCommands();
}
availCommands = new ObjectChoiceField(res.getString(BBSSHResource.KEYBIND_LBL_ACTION), commands);
availCommands.setChangeListener(this);
// @todo - use menus instead? Or replace with our standard component?
okCancelHFM = new HorizontalFieldManager(HorizontalFieldManager.FIELD_HCENTER);
okCancelHFM.add(saveChanges);
okCancelHFM.add(cancelChanges);
saveChanges.setChangeListener(this);
cancelChanges.setChangeListener(this);
currentParamField = null;
}
/*
* (non-Javadoc)
*
* @see net.rim.device.api.ui.FieldChangeListener#fieldChanged(net.rim.device.api.ui.Field, int)
*/
public void fieldChanged(Field field, int context) {
if (field == availCommands) {
int sel = availCommands.getSelectedIndex();
if (sel == -1)
return;
state.command = (ExecutableCommand) availCommands.getChoice(sel);
// @todo - handling for command.isParameterOptional
helpField.setText(res.getString(state.command.getDescriptionResId()));
if (state.command.isParameterRequired()) {
setupParameterField();
} else { //if (currentParamField != null && currentParamField.getManager() != null) {
parameterContainer.deleteAll();
}
} else if (field == saveChanges) {
if (isDataValid()) {
try {
save();
} catch (IOException e) {
}
close();
}
} else if (field == cancelChanges) {
setDirty(false);
changeSaved = false;
close();
}
}
/**
* This method creates and appropriately populates the required parameter for teh currently active command. At the
* moment, this is more than a little ugly - this screen is manually mapping each command to its permitted values
* and ranges. In the longer term, we will need to set up a proper object model around executablecommand.
*
*/
private void setupParameterField() {
parameterContainer.deleteAll();
currentParamField = null;
// @todo optional parameter support! case CommandConstants.INPUT_MODE:
// note that we already filtered out commands that don't support params,
// so those won't be listed here in this switch
switch (state.command.getId()) {
case CommandConstants.MOVEMENT_KEY:
currentParamField = paramChoiceField;
paramChoiceField.setChoices(KeyBindingHelper.getDirectionalTerminalKeys());
if (firstRun && state.parameter != null && state.parameter instanceof Integer) {
int key = ((Integer) state.parameter).intValue();
int idx = KeyBindingHelper.getMovementTerminalKeyIndex(key);
if (idx > -1) {
paramChoiceField.setSelectedIndex(idx);
}
}
break;
case CommandConstants.RUN_MACRO:
currentParamField = paramChoiceField;
macros = Tools.vectorToArray(MacroManager.getInstance().getMacros());
paramChoiceField.setChoices(macros);
if (macros.length > 0 && firstRun && state.parameter != null && state.parameter instanceof String) {
// @todo - this SHOULD work if .equals is being used ...
paramChoiceField.setSelectedIndex(state.parameter);
}
break;
case CommandConstants.SHOW_DEBUG_MESSAGE:
case CommandConstants.SCROLL_DOWN_LINES:
case CommandConstants.SCROLL_UP_LINES:
case CommandConstants.WAIT:
case CommandConstants.WAIT_FOR_ACTIVITY: // @todo support to have a separate numeric field?
currentParamField = paramNumericField;
if (firstRun && state.parameter != null) {
paramNumericField.setText(state.parameter.toString());
}
break;
case CommandConstants.SEND_TEXT:
currentParamField = paramEditField;
if (firstRun && state.parameter != null) {
paramEditField.setText(state.parameter.toString());
}
break;
case CommandConstants.SEND_TERMINAL_KEY:
// @todo this should be modified to sendStandardKeyAtIndex...
currentParamField = paramChoiceField;
paramChoiceField.setChoices(KeyBindingHelper.getTerminalKeys());
if (firstRun && state.parameter != null && state.parameter instanceof Integer) {
int key = ((Integer) state.parameter).intValue();
int idx = KeyBindingHelper.getTerminalKeyIndex(key);
if (idx > -1) {
paramChoiceField.setSelectedIndex(idx);
}
}
break;
case CommandConstants.INCDEC_FONT_SIZE:
currentParamField = paramChoiceField;
paramChoiceField.setChoices(KeyBindingHelper.getFontChangeChoices());
if (firstRun && state.parameter != null && state.parameter instanceof Integer) {
int action = ((Integer) state.parameter).intValue();
if (action > -1) {
paramChoiceField.setSelectedIndex(action);
} else {
paramChoiceField.setSelectedIndex(0);
}
}
break;
default:
currentParamField = null;
break;
}
// If the param field we're needing now isn't the same as the last time we displayed,
// replace the old one with the correct field.
if (currentParamField != null) {
parameterContainer.add(currentParamField);
}
state.parameter = null;
}
/**
* Returns state tracking for the the key-binding being managed by this field.
*
* @return state instance for key being managed
*/
public KeybindState getKeybindState() {
return this.state;
}
// @todo - freeform text is a problem here: fucking virtual kbd in horizontal
// mode makes it unusable.
/**
* Updates this popup to reflect the specific key binding state provided. Invoking this will cause fields to
* refresh/repaint, so it is up to the caller to ensure that it's invoked from the UI thread.
*
* @param state
*/
public void setKeybindState(KeybindState state) {
this.state = state;
LabelField title = new LabelField(res.getString(titleId) + state.bindingDescription);
changeSaved = false;
firstRun = true;
// @todo - do we really need to deleteAll? Or just refresh values...
deleteAll();
add(title); // 0
add(availCommands); // 1
add(parameterContainer);
currentParamField = null;
paramChoiceField.setChoices(new Object[] {});
paramEditField.setText("");
paramNumericField.setText("");
// 2 - we are inserting the parameter field here, when parameter field is needed.
add(new SeparatorField()); // 3
add(helpField);// 4
add(okCancelHFM); // 5
if (state.command == null) {
availCommands.setSelectedIndex(0);
} else {
// this will trigger fieldChanged, which populates the rest of the screen as needed.
// Hmm. Unless it's already set from a previous display.
int old = availCommands.getSelectedIndex();
availCommands.setSelectedIndex(state.command);
// If the command hasn't changed from last time we displayed, then the change event won't
// fire. Let's fire it manually.
if (old == availCommands.getSelectedIndex()) {
fieldChanged(availCommands, 1);
}
}
firstRun = false;
}
/**
* Invoked by framework when there are chanegs to be saved, this implementation simply updates local member objects
* to reflect the new parameter. Owning screen is responsible for determining whether the actual changes are to be
* persisted.
*
* also @see net.rim.device.api.ui.Screen#save()
*/
public void save() throws IOException {
changeSaved = true;
if (currentParamField instanceof ObjectChoiceField) {
// Note tha we're not checking for -1 -- because isDataValid guarantees that
// if we reach this point, we have a valid selection
int index = paramChoiceField.getSelectedIndex();
// Now we have to do the same mess as when the field changes - we need to map the
// enterd value to the real underlying type.
switch (state.command.getId()) {
case CommandConstants.MOVEMENT_KEY:
case CommandConstants.SEND_TERMINAL_KEY:
if (index > -1) {
state.parameter = ((TerminalKey) paramChoiceField.getChoice(index)).getValue();
}
break;
case CommandConstants.RUN_MACRO:
// Here our parameter is the name of the macro - so we can use the string object in the list.
if (index > -1) {
state.parameter = paramChoiceField.getChoice(index).toString();
}
break;
case CommandConstants.INCDEC_FONT_SIZE:
state.parameter = new Integer(index);
break;
}
} else {
switch (state.command.getId()) {
case CommandConstants.SCROLL_DOWN_LINES:
case CommandConstants.SCROLL_UP_LINES:
case CommandConstants.WAIT:
case CommandConstants.WAIT_FOR_ACTIVITY:
state.parameter = Integer.valueOf(paramNumericField.getText());
break;
default:
state.parameter = paramEditField.getText();
break;
}
paramEditField.setText("");
paramNumericField.setText("");
}
super.save();
}
/**
* returns true if a change has been made on this screen and it was not canceled.
*
* @return true if change was saved.
*/
public boolean isChangeSaved() {
return this.changeSaved;
}
/*
* (non-Javadoc)
*
* @see net.rim.device.api.ui.Screen#isDataValid()
*/
public boolean isDataValid() {
if (state.command != null && !state.command.isParameterRequired()) {
return true;
}
int messageId = 0;
int len1 = paramEditField.getTextLength();
int len2 = paramNumericField.getTextLength();
int len = len1 + len2;
if (state.command == null) {
messageId = BBSSHResource.MSG_KEYBIND_NO_SELECTION;
} else if (currentParamField instanceof BasicEditField) {
// currently the only special handling in place is for "WAIT" -
// this value must be numeric.
if (len == 0) {
messageId = BBSSHResource.KEYBIND_MSG_NO_PARAM;
} else {
int minValue = 0;
switch (state.command.getId()) {
case CommandConstants.WAIT:
minValue = 1;
// no break intentional - both fields use numeric validation.
case CommandConstants.WAIT_FOR_ACTIVITY:
try {
if (Integer.parseInt(paramNumericField.getText()) < minValue) {
throw new NumberFormatException();
}
} catch (NumberFormatException e) {
// might happen if the number is too big for an int...
messageId = BBSSHResource.KEYBIND_MSG_INVALID_MILLISECONDS;
}
}
}
} else {
if (paramChoiceField.getSelectedIndex() < 0) {
messageId = BBSSHResource.KEYBIND_MSG_NO_PARAM;
}
}
if (messageId > 0) {
Dialog.ask(Dialog.D_OK, res.getString(messageId));
currentParamField.setFocus();
return false;
}
return super.isDataValid();
}
}