package org.bbssh.ui.screens;
import java.io.IOException;
import java.util.Vector;
import net.rim.device.api.i18n.ResourceBundle;
import net.rim.device.api.i18n.ResourceBundleFamily;
import net.rim.device.api.system.KeypadListener;
import net.rim.device.api.ui.Color;
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.Graphics;
import net.rim.device.api.ui.Keypad;
import net.rim.device.api.ui.Manager;
import net.rim.device.api.ui.MenuItem;
import net.rim.device.api.ui.Screen;
import net.rim.device.api.ui.UiApplication;
import net.rim.device.api.ui.component.Dialog;
import net.rim.device.api.ui.component.Menu;
import net.rim.device.api.ui.component.ObjectChoiceField;
import net.rim.device.api.ui.component.SeparatorField;
import net.rim.device.api.ui.component.Status;
import net.rim.device.api.ui.component.TreeField;
import net.rim.device.api.ui.component.TreeFieldCallback;
import net.rim.device.api.ui.container.MainScreen;
import net.rim.device.api.ui.container.VerticalFieldManager;
import net.rim.device.api.util.IntEnumeration;
import net.rim.device.api.util.IntIntHashtable;
import net.rim.device.api.util.IntLongHashtable;
import net.rim.device.api.util.IntVector;
import net.rim.device.api.util.LongEnumeration;
import net.rim.device.api.util.LongHashtable;
import org.bbssh.BBSSHApp;
import org.bbssh.command.CommandConstants;
import org.bbssh.i18n.BBSSHResource;
import org.bbssh.keybinding.BoundCommand;
import org.bbssh.keybinding.ExecutableCommand;
import org.bbssh.keybinding.KeyBindingHelper;
import org.bbssh.model.KeyBindingManager;
import org.bbssh.platform.PlatformServicesProvider;
import org.bbssh.ui.components.IndexedListFieldItem;
import org.bbssh.ui.components.keybinding.CommandBindingPopup;
import org.bbssh.ui.components.keybinding.KeybindState;
import org.bbssh.util.Logger;
import org.bbssh.util.Tools;
/**
* Screen that displays a list of keyboard shortcuts and the commands they are bound to.
*
* @author marc
*
*/
public class KeybindingScreen extends MainScreen implements FieldChangeListener {
public static final int FILTER_SHOW_BOUND = 0;
public static final int FILTER_SHOW_MODIFIED = 1;
private ResourceBundleFamily res = ResourceBundle.getBundle(BBSSHResource.BUNDLE_ID, BBSSHResource.BUNDLE_NAME);
private CommandBindingPopup popup = new CommandBindingPopup(false);
private ObjectChoiceField filterList;
private TreeField eventTree;
private IndexedListFieldItem[] categoryData = null;
private IntLongHashtable treeNodeMapping;
private int lastRootNodeId = -1;
private Font fontBase;
private Font line1Font;
private Font line2Font;
// List of items which need to be updated and saved.
private LongHashtable saveList = new LongHashtable(128);
private Font line2FontAlt;
/**
* create a BoundCommandList instance and initializes the filter to the selection
*/
public KeybindingScreen() {
super(DEFAULT_CLOSE | Screen.NO_VERTICAL_SCROLL | Screen.NO_VERTICAL_SCROLLBAR);
setTitle(res.getString(BBSSHResource.KEYBIND_TITLE));
treeNodeMapping = new IntLongHashtable(128);
fontBase = getFont();
line1Font = fontBase.derive(Font.BOLD, (fontBase.getHeight() / 4) * 3);
line2Font = fontBase.derive(Font.PLAIN, (fontBase.getHeight() / 4) * 3);
line2FontAlt = line2Font.derive(Font.ITALIC);
fontBase = getFont().derive(Font.BOLD);
// Font font = orig.derive(Font.PLAIN, 12);
// setFont(font);
setupCategoryList();
filterList = new ObjectChoiceField(res.getString(BBSSHResource.KEYBIND_LBL_FILTER), categoryData);
eventTree = new TreeField(callback, TreeField.FOCUSABLE);
eventTree.setDefaultExpanded(false);
eventTree.setRowHeight(line1Font.getHeight() + line2Font.getHeight() + 4);
add(filterList);
add(new SeparatorField());
VerticalFieldManager scroller = new VerticalFieldManager(Manager.VERTICAL_SCROLL | Manager.VERTICAL_SCROLLBAR);
scroller.add(eventTree);
add(scroller);
// For some reason in 6.0 ObjectChoiceFields and ObjectListFields somtimes come with a fieldChangeListener
// already set.
filterList.setChangeListener(null); // clear existing one to avoid exception
filterList.setChangeListener(this);
// This will also cause the initial list population to complete... unless default index is 0,
filterList.setSelectedIndex(0);
eventTree.setChangeListener(null);
eventTree.setChangeListener(this);
refreshTable();
}
private void addCategoryItemIfAvailable(Vector v, int catId, int resId) {
IntVector temp = PlatformServicesProvider.getInstance().getEventsForCategory(catId);
if (temp != null && temp.size() > 0)
v.addElement(new IndexedListFieldItem(res.getString(resId), catId));
}
private void setupCategoryList() {
Vector v = new Vector(16);
v.addElement(new IndexedListFieldItem(res.getString(BBSSHResource.KEYBIND_FILTER_LBL_BOUND), -1));
// v.addElement(new IndexedListFieldItem(res.getString(BBSSHResource.KEYBIND_FILTER_LBL_MODIFIED), -1));
addCategoryItemIfAvailable(v, KeyBindingHelper.CAT_KEYBOARD, BBSSHResource.BIND_CATEGORY_KEYBOARD);
addCategoryItemIfAvailable(v, KeyBindingHelper.CAT_PHONE, BBSSHResource.BIND_CATEGORY_PHONE);
addCategoryItemIfAvailable(v, KeyBindingHelper.CAT_NAV, BBSSHResource.BIND_CATEGORY_NAV);
addCategoryItemIfAvailable(v, KeyBindingHelper.CAT_MEDIA, BBSSHResource.BIND_CATEGORY_MEDIA);
addCategoryItemIfAvailable(v, KeyBindingHelper.CAT_TOUCH_GESTURE, BBSSHResource.BIND_CATEGORY_TOUCH_OTHER);
addCategoryItemIfAvailable(v, KeyBindingHelper.CAT_TOUCH_CLICK, BBSSHResource.BIND_CATEGORY_TOUCH_CLICK);
addCategoryItemIfAvailable(v, KeyBindingHelper.CAT_TOUCH_DOUBLE_CLICK,
BBSSHResource.BIND_CATEGORY_TOUCH_DOUBLE_CLICK);
addCategoryItemIfAvailable(v, KeyBindingHelper.CAT_TOUCH_TAP, BBSSHResource.BIND_CATEGORY_TOUCH_TAP);
addCategoryItemIfAvailable(v, KeyBindingHelper.CAT_TOUCH_DOUBLE_TAP, BBSSHResource.BIND_CATEGORY_TOUCH_DOUBLE_TAP);
addCategoryItemIfAvailable(v, KeyBindingHelper.CAT_TOUCH_HOVER, BBSSHResource.BIND_CATEGORY_TOUCH_HOVER);
categoryData = new IndexedListFieldItem[v.size()];
v.copyInto(categoryData);
}
/**
* Adds entry to the table for the specified key code and modifier.
*
* @param line
*
* @param keyCode
* @param modifier
*/
private String getNodeText(int nodeId, int line) {
long packed = treeNodeMapping.get(nodeId);
boolean boundOnly = filterList.getSelectedIndex() == FILTER_SHOW_BOUND;
// Shouldn't actually happen, this is just a sanity check...
if (packed == -1 && !boundOnly)
return "Invalid Node";
StringBuffer bindingDesc = new StringBuffer(20);
if (boundOnly) {
int parent = eventTree.getParent(nodeId);
if (parent == 0) {
if (line > 0) {
return "";
}
return ((IndexedListFieldItem) eventTree.getCookie(nodeId)).getName();
}
}
// Line 0 is the binding name / name+modifier
if (line == 0) {
String mod = KeyBindingHelper.getModifierFriendlyName((int) (packed & 0xFFFFFFFF));
boolean hasmod = (mod != null && mod.length() > 0);
if (hasmod) {
bindingDesc.append(mod);
bindingDesc.append(" + ");
}
// if (boundOnly && hasmod) {
// bindingDesc.append(" + ");
// }
// All one line for filter 0 (bound only) - no separately liens for modifiers.
// if (boundOnly || !hasmod) {
int resId = PlatformServicesProvider.getInstance().getEventResourceId((int) (packed >> 32));
if (resId == -1) {
// with our existing filters in populating the tree this should not happen, but again
// just a sanity check
bindingDesc.append("Unknown event: " + (int) (packed >> 32));
} else
bindingDesc.append(res.getString(resId));
// }
} else {
// Line 1 is the command that's bound to this.
// int filter = filterList.getSelectedIndex();
KeyBindingManager man = KeyBindingManager.getInstance();
// First make sure we don't have a locally modified version of the binding.
KeybindState state = (KeybindState) saveList.get(packed);
Object param = null;
ExecutableCommand ecmd = null;
if (state == null) {
BoundCommand cmd = man.getKeyBinding(packed);
if (cmd != null) {
ecmd = cmd.getCommand();
param = cmd.getParam();
}
} else {
ecmd = state.command;
param = state.parameter;
}
if (ecmd != null) {
bindingDesc.append(" ");
bindingDesc.append(res.getString(ecmd.getNameResId()));
if (ecmd.isParameterRequired() || (ecmd.isParameterOptional() && param != null)) {
bindingDesc.append(": ");
bindingDesc.append(ecmd.translateParameter(param));
}
}
}
return bindingDesc.toString();
}
/**
* Populates key binding table based on selected filter; if filters are not set, then all bindable keys are added.
*/
private void populateKeybindTable() {
PlatformServicesProvider psp = PlatformServicesProvider.getInstance();
int filter = filterList.getSelectedIndex();
if (filter == -1)
return;
filter = categoryData[filter].getIndex();
treeNodeMapping.clear();
if (filter > 0) {
eventTree.setDefaultExpanded(false);
populateEvents(filter, (IntVector) psp.getEventsForCategory(filter));
} else {
eventTree.setDefaultExpanded(true);
populateBoundEvents();
}
}
// private int getFilterIndexFromCategory(int categoryId) {
// for (int x = 0; x < categoryData.length; x++) {
// if (categoryData[x].getIndex() == categoryId) {
// return x;
// }
// }
// return -1;
// }
/**
* Populates the event tree with all bound events, within a tree structure by type.
*/
private void populateBoundEvents() {
// Create a node for each category. Go through all bindings and assign to appropriate nodes.
// Finally, remove those categories that are empty.
int nodeid = -1;
IntIntHashtable rootNodes = new IntIntHashtable(categoryData.length); // simple tracking of category odes.
for (int x = 0; x < categoryData.length; x++) {
nodeid = (nodeid == -1 ? eventTree.addChildNode(0, categoryData[x]) : eventTree.addSiblingNode(nodeid,
categoryData[x]));
rootNodes.put(categoryData[x].getIndex(), nodeid);
}
nodeid = -1;
KeyBindingManager mgr = KeyBindingManager.getInstance();
PlatformServicesProvider psp = PlatformServicesProvider.getInstance();
LongEnumeration enm = mgr.getBindingFactories().keys();
long next;
int cat, addnode, evt;
while (enm.hasMoreElements()) {
next = enm.nextElement();
evt = (int) (next >> 32);
// Some default bindings will be for hardware that this device does not have
// Ignore these.
if (!psp.isEventValidForDevice(evt)) {
continue;
}
// We may have some default bindings not available on this platform - their
// category won't be found. We'll ignore them.
cat = rootNodes.get(psp.getEventCategory(evt));
if (cat == -1)
continue;
addnode = eventTree.getLastNode(cat, true);
nodeid = (addnode == cat ? eventTree.addChildNode(cat, null) : eventTree.addSiblingNode(addnode, null));
treeNodeMapping.put(nodeid, next);
}
// Finally, clean up the nodes we created that don't have children.
IntEnumeration en = rootNodes.elements();
while (en.hasMoreElements()) {
int id = en.nextElement();
if (eventTree.getFirstChild(id) == -1) {
eventTree.deleteSubtree(id);
}
}
}
private void populateEvents(int catId, IntVector events) {
PlatformServicesProvider psp = PlatformServicesProvider.getInstance();
boolean hasLeftShift = psp.hasLeftShift();
boolean hasRightShift = psp.hasRightShift();
boolean hasShiftX = psp.hasShiftX();
int nextid = -1;
int lshift = KeypadListener.STATUS_SHIFT | KeypadListener.STATUS_SHIFT_LEFT;
int rshift = KeypadListener.STATUS_SHIFT | KeypadListener.STATUS_SHIFT_RIGHT;
int eventId;
int size = events.size();
for (int x = 0; x < size; x++) {
eventId = events.elementAt(x);
lastRootNodeId = (lastRootNodeId == -1 ? eventTree.addChildNode(0, null) : eventTree.addSiblingNode(
lastRootNodeId, null));
treeNodeMapping.put(lastRootNodeId, Tools.packToLong(eventId, 0));
// One ugly exception: ALT Escape can't be bound - though shift can.
if (eventId != Keypad.KEY_ESCAPE) {
nextid = eventTree.addChildNode(lastRootNodeId, null); // ALT
treeNodeMapping.put(nextid, Tools.packToLong(eventId, KeypadListener.STATUS_ALT));
}
// ShiftX or LShift depending on keyboard.
if (hasLeftShift || hasShiftX) {
// can happen if keypad.escape ...
if (nextid == -1) {
nextid = eventTree.addChildNode(lastRootNodeId, null); // ALT
} else {
nextid = eventTree.addSiblingNode(nextid, null);
}
treeNodeMapping.put(nextid, Tools.packToLong(eventId, lshift));
}
if (hasRightShift) {
nextid = eventTree.addSiblingNode(nextid, null); // RShift
treeNodeMapping.put(nextid, Tools.packToLong(eventId, rshift));
}
}
}
public void save() throws IOException {
KeyBindingManager manager = KeyBindingManager.getInstance();
LongEnumeration en = saveList.keys();
while (en.hasMoreElements()) {
KeybindState state = (KeybindState) saveList.get(en.nextElement());
if (state.changed) {
if (state.command == null) {
manager.unbindKey(state.combinedKey);
} else {
// Some cleanup - don't store a valu eif it's not used.
manager.bindKey(state.combinedKey, state.command, state.parameter);
if (!state.command.isParameterRequired() && !state.command.isParameterOptional()) {
state.parameter = null;
}
}
}
}
Logger.info("Saving changes to key binding manager.");
KeyBindingManager.getInstance().commitData();
super.save();
}
/*
* (non-Javadoc)
*
* @see net.rim.device.api.ui.container.MainScreen#makeMenu(net.rim.device.api.ui.component.Menu, int)
*/
protected void makeMenu(Menu menu, int instance) {
super.makeMenu(menu, instance);
if (getNodeMappedKey(eventTree.getCurrentNode()) != -1) {
BoundCommand cmd = KeyBindingManager.getInstance().getKeyBinding(
getNodeMappedKey(eventTree.getCurrentNode()));
if (cmd != null)
menu.add(itemClear);
menu.add(itemEdit);
KeybindState state = getSelectedBindingState(false);
if (state != null && state.changed == true)
menu.add(itemRevert);
menu.setDefault(itemEdit);
}
if (instance != Menu.INSTANCE_CONTEXT) {
menu.add(itemResetToDefault);
}
}
/**
* Clear and repopulate the table based on current filter.
*/
private void refreshTable() {
BBSSHApp.inst().suspendPainting(true);
lastRootNodeId = -1;
eventTree.deleteAll();
populateKeybindTable();
BBSSHApp.inst().suspendPainting(false);
// delete(table);
// table.deleteAll();
// add(table);
}
/**
* get the binding state for the current selection. If the selection has been modified, it will return the modified
* keybind state. This will create the state if it does not already exist.
*
* @param createIfNotPresent if a valid node is selected but no state object exists, create the state object.
*
* @return the keybind state assosciated with the current selection, or null if no selection/selection is not a
* keybinding
*/
private KeybindState getSelectedBindingState(boolean createIfNotPresent) {
return getBindingState(eventTree.getCurrentNode(), createIfNotPresent);
}
private KeybindState getBindingState(int node, boolean createIfNotPresent) {
long key = getNodeMappedKey(node);
if (key == -1)
return null;
if (saveList.containsKey(key))
return (KeybindState) saveList.get(key);
KeyBindingManager mgr = KeyBindingManager.getInstance();
BoundCommand cmd = mgr.getKeyBinding(key);
if (createIfNotPresent) {
// @todo - we can just use boundcommand for this, can't we?
KeybindState state;
if (cmd == null)
state = new KeybindState(key, getNodeText(node, 0), null, null);
else
state = new KeybindState(key, getNodeText(node, 0), cmd.getCommand(), cmd.getParam());
saveList.put(key, state);
return state;
}
return null;
}
private long getNodeMappedKey(int node) {
if (node == -1)
return -1;
if (eventTree.getCookie(node) != null)
return -1;
long key = treeNodeMapping.get(node);
return key;
}
public void fieldChanged(Field field, int context) {
refreshTable();
}
/**
* Clear any key binding associated with selection; sets flag to indicate change completed if the item wasn't
* originally clear.
*/
private MenuItem itemClear = new MenuItem(res, BBSSHResource.KEYBIND_MENU_ITEM_CLEAR, 0x00200000, 1) {
public void run() {
clearSelectedBinding();
}
};
/**
* Revert any changes made to the selected key binding.
*/
private MenuItem itemRevert = new MenuItem(res, BBSSHResource.KEYBIND_MENU_ITEM_REVERT, 0x00200000, 1) {
public void run() {
int node = eventTree.getCurrentNode();
long key = getNodeMappedKey(node);
if (key == -1)
return;
KeybindState state = getBindingState(node, false);
if (state == null)
return; // no changes
if (!state.changed)
return; // no changes
if (saveList.containsKey(key)) {
saveList.remove(key);
}
eventTree.invalidateNode(node);
}
};
/**
* Edit the selected key binding.
*/
private MenuItem itemEdit = new MenuItem(res, BBSSHResource.KEYBIND_MENU_ITEM_EDIT, 0x00200000, 10) {
public void run() {
KeybindState state = getSelectedBindingState(true);
if (state == null)
return;
popup.setKeybindState(state);
UiApplication.getUiApplication().pushModalScreen(popup);
if (popup.isChangeSaved()) {
if (state.command != null && state.command.getId() == CommandConstants.NONE) {
clearSelectedBinding();
return;
}
int node = eventTree.getCurrentNode();
long key = getNodeMappedKey(node);
BoundCommand cmd = KeyBindingManager.getInstance().getKeyBinding(key);
if (cmd == null) {
state.changed = true;
} else {
if (cmd.getCommand() != state.command || cmd.getParam() != state.parameter) {
state.changed = true;
} else {
state.changed = false;
}
}
eventTree.invalidateNode(node);
}
}
};
/**
* Reset ALL keybindings to their default values.
*/
private MenuItem itemResetToDefault = new MenuItem(res, BBSSHResource.KEYBIND_MENU_ITEM_RESET, 0x00300000, 10) {
public void run() {
if (Dialog.ask(Dialog.D_YES_NO, res.getString(BBSSHResource.MSG_KEYBIND_RESET_CONFIRM)) == Dialog.YES) {
saveList.clear();
KeyBindingManager.getInstance().resetDefaults();
Status.show(res.getString(BBSSHResource.MSG_KEYBIND_RESET_COMPLETE));
setDirty(true);
refreshTable();
}
}
};
private void clearSelectedBinding() {
int node = eventTree.getCurrentNode();
BoundCommand cmd = KeyBindingManager.getInstance().getKeyBinding(getNodeMappedKey(node));
KeybindState state = getSelectedBindingState(true);
if (state == null)
return;
// Should not happene - no binding to clear.
if (cmd == null)
return;
state.command = null;
state.changed = true;
eventTree.invalidateNode(node);
}
private boolean isNodeBindingCleared(int node) {
KeybindState state = getBindingState(node, false);
return (state != null && state.changed == true && state.command == null);
}
private boolean isNodeBindingChanged(int node) {
KeybindState state = getBindingState(node, false);
return (state != null && state.changed);
}
private TreeFieldCallback callback = new TreeFieldCallback() {
// private int savedIndent;
public void drawTreeItem(TreeField treeField, Graphics graphics, int node, int y, int width, int indent) {
graphics.setColor(Color.BLACK);
// If there's a cookie we don't draw a second line - bceause only "category" nodes have cookies, and
// they
// consist only of the category text
if (treeField.getCookie(node) == null) {
graphics.setFont(line1Font);
graphics.drawText(getNodeText(node, 0), indent, y, 0, width);
// If we're drawing a leaf node, then we want to set it to the same indent level
// as its parent.
// @todo - shading/box to make this group of nodes look like part of hte same block visually?
// if (treeField.getFirstChild(node) == -1) {
// indent = savedIndent;
// } else {
// savedIndent = indent;
// }
String text;
if (isNodeBindingCleared(node)) {
graphics.setColor(Color.RED);
graphics.setFont(line2FontAlt);
text = res.getString(BBSSHResource.KEYBIND_LBL_CLEARED);
} else {
text = getNodeText(node, 1);
if (text.length() == 0) {
graphics.setColor(Color.DARKGRAY);
graphics.setFont(line2FontAlt);
text = " " + res.getString(BBSSHResource.KEYBIND_LBL_UNBOUND);
} else {
graphics.setColor(isNodeBindingChanged(node) ? Color.RED : Color.BLUE);
}
}
graphics.drawText(text, indent, y + (treeField.getRowHeight() / 2), 0, width);
} else {
graphics.setFont(fontBase);
graphics.drawText(getNodeText(node, 0), indent, y + (treeField.getRowHeight() / 2)
- (fontBase.getHeight() / 2), 0, width);
}
}
};
public boolean isDirty() {
LongEnumeration en = saveList.keys();
while (en.hasMoreElements()) {
if (((KeybindState) saveList.get(en.nextElement())).changed)
return true;
}
return false;
}
}