package org.bbssh.ui.components; /** * Modified from the BB samples * @author marc * */ import java.util.Enumeration; import java.util.Vector; import javax.microedition.io.Connector; import javax.microedition.io.file.FileConnection; import javax.microedition.io.file.FileSystemRegistry; import net.rim.device.api.applicationcontrol.ApplicationPermissions; import net.rim.device.api.i18n.ResourceBundle; import net.rim.device.api.system.Characters; import net.rim.device.api.ui.Field; import net.rim.device.api.ui.FieldChangeListener; import net.rim.device.api.ui.Graphics; import net.rim.device.api.ui.UiApplication; 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.ListField; import net.rim.device.api.ui.component.ObjectListField; import net.rim.device.api.ui.container.FlowFieldManager; 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.BBSSHApp; import org.bbssh.i18n.BBSSHResource; import org.bbssh.util.Tools; /** * A PopupScreen with a file browser allowing for file selection. * * @todo refactor a base class which displays three rows - top, list, bottom w/ OK/Cancel buttons. */ public class FileSelectorPopupScreen extends PopupScreen implements BBSSHResource, FieldChangeListener { public static final int TYPE_OPEN = 0; public static final int TYPE_SAVE_AS = 1; public static final int TYPE_SELECT_FOLDER = 2; private ResourceBundle res = ResourceBundle.getBundle(BUNDLE_ID, BUNDLE_NAME); private String currentPath; // The current path; private ObjectListField list; // Lists fields and directories. private BasicEditField fileNameField; // @todo - simple UI component, OKCancelPair - b/c this comes up a LOT... private ClickableButtonField okButton = new ClickableButtonField(res.getString(GENERAL_LBL_OK)); private ClickableButtonField cancelButton = new ClickableButtonField(res.getString(GENERAL_LBL_CANCEL)); int type = TYPE_OPEN; HorizontalFieldManager topRowManager; FlowFieldManager listFieldManager; VerticalFieldManager bottomRowManager; private String startPath; private String defaultSaveAsFileName; /** * Create a new list field that notifies us any time the selected line changes. * * @return */ private ObjectListField createListField() { // @todo make this into a custom component/list field type. return new ObjectListField() { // Some extra pains here - we want to make sure our listbox // doesn't expand to push the buttons off the bottom of the page, // so we need to contain it in its own field manager that is bounded // by our top and bottom managers. // @todo idea: by using gridfieldmanager in the middle, that will give us an // effective BorderLayout... // Customizing the list field so that it notifies on selection change -- and also // changes focus on left/right movement. int oldSel = -1; public void drawListRow(ListField listField, Graphics graphics, int index, int y, int width) { // Sloppy and not 100% accurate, but we'll be using this to indicate to the // selection has changed. int x = getSelectedIndex(); if (x != oldSel) { oldSel = x; FieldChangeListener l = getChangeListener(); if (l != null) { l.fieldChanged(this, 10); } } super.drawListRow(listField, graphics, index, y, width); } protected boolean navigationMovement(int dx, int dy, int status, int time) { // We need to perform any movement required first, so that our selection is updated appropriately. // @todo - do we also need to override touch event in the Storm version? That'll get messy... if (dx > 0) { FileSelectorPopupScreen.this.bottomRowManager.getField(0).setFocus(); return true; } return super.navigationMovement(dx, dy, status, time); }; }; } public FileSelectorPopupScreen() { super(new VerticalFieldManager(VERTICAL_SCROLL | VERTICAL_SCROLLBAR)); } private void initialize() { list = createListField(); topRowManager = new HorizontalFieldManager(); listFieldManager = new FlowFieldManager() { protected void sublayout(int w, int h) { super.sublayout(w, h); setExtent(w, (list.getRowHeight() * 6)); // 6 visible rows } }; bottomRowManager = new VerticalFieldManager(); listFieldManager.add(list); fileNameField = new BasicEditField(res.getString(FILE_SELECTION_LBL_SAVE_AS), "", 32, BasicEditField.NO_NEWLINE | BasicEditField.NON_SPELLCHECKABLE | BasicEditField.FILTER_FILENAME) { protected void onFocus(int direction) { // passing value < 0 will force cursor to end of line. super.onFocus(-1); } }; if (defaultSaveAsFileName != null) fileNameField.setText(defaultSaveAsFileName); prepScreen(startPath); } /** * Display the screen, prompting the user to pick a file. * * @return current directory if the user is still browsing for a file, the selected file if the user has chosen one * or null if the user dismissed the screen. */ public String pickFile() { initialize(); customizeUI(); if (!BBSSHApp.inst().requestPermission( ApplicationPermissions.PERMISSION_FILE_API, BBSSHResource.MSG_PERMISSIONS_MISSING_FILE_ACCESS_USER)) { return ""; } UiApplication.getUiApplication().pushModalScreen(this); if (currentPath == null) { return ""; } if (type == TYPE_SAVE_AS) { return "file:///" + currentPath + fileNameField.getText(); } return "file:///" + currentPath; } /** * Override this method in os-specific implementations to further customize the UI. */ protected void customizeUI() { } protected ObjectListField getListField() { return list; } /** * Set up the screens initial fields, and populate the list using the given path. * * @param path initial path. If null, */ private void prepScreen(String path) { int title = FILE_SELECTION_TITLE_OPEN; if (type == TYPE_SAVE_AS) { title = FILE_SELECTION_TITLE_SAVE; } else if (type == TYPE_SELECT_FOLDER) { title = FILE_SELECTION_TITLE_FOLDER; } topRowManager.add(new LabelField(res.getString(title))); if (type == TYPE_SAVE_AS) { bottomRowManager.add(fileNameField); } // @todo use the new OKCancel control. HorizontalFieldManager hfm = new HorizontalFieldManager(HorizontalFieldManager.FIELD_HCENTER); hfm.add(okButton); hfm.add(cancelButton); bottomRowManager.add(hfm); add(topRowManager); add(listFieldManager); add(bottomRowManager); updateList(path); list.setChangeListener(null); list.setChangeListener(this); okButton.setChangeListener(this); cancelButton.setChangeListener(this); } private Vector getFilesystemRoots() { Vector filesVector = new Vector(); Enumeration fileEnum = FileSystemRegistry.listRoots(); while (fileEnum.hasMoreElements()) { filesVector.addElement((Object) fileEnum.nextElement()); } return filesVector; } /** * Reads all of the files and directories in a given path, applying any extension or directory filter if provided * and based on current mode. * * @param path * @return vector of all matching files */ private Vector readFiles(String path) { Enumeration fileEnum; Vector filesVector = new Vector(); currentPath = path; if (path == null) { currentPath = null; return getFilesystemRoots(); } else { // Read the files and directories for the current path. try { FileConnection fc = (FileConnection) Connector.open("file:///" + path); fileEnum = fc.list(); String currentFile; while (fileEnum.hasMoreElements()) { currentFile = (String) fileEnum.nextElement(); switch (type) { case TYPE_OPEN: case TYPE_SAVE_AS: filesVector.addElement(currentFile); break; case TYPE_SELECT_FOLDER: if (currentFile.endsWith("/")) { filesVector.addElement(currentFile); } break; } } fc.close(); } catch (Throwable ex) { // When all else fails... currentPath = null; return getFilesystemRoots(); } } return filesVector; } /** * Invoked when the user picks an entry in the list field. */ private void doSelection() { Object focus = getLeafFieldWithFocus(); if (focus == cancelButton) { doCancelProcessing(); return; } else if (focus == okButton) { doOKProcessing(); return; } else if (focus != list) { return; } // Determine the current path. String path = buildPath(); if (path != null && path.equals("*?*")) { if (type != TYPE_SELECT_FOLDER) { this.close(); return; } } if (path == null || path.endsWith("/")) { updateList(path); } } /** * Updates the entries in the ObjectListField. * * @param path */ private void updateList(String path) { // Read all files and directories in the path. Vector fileList = readFiles(path); // Create an array from the Vector. Object fileArray[] = fileVectorToArray(fileList); // Update the field with the new files. list.setChangeListener(null); list.set(fileArray); list.setChangeListener(this); } // Build a String that contains the full path of the user's selection. // If a file has been selected, close this screen. // Returns *?* if the user has selected a file. private String buildPath() { String newPath = (String) list.get(list, list.getSelectedIndex()); if (newPath.equals("..")) { // Go up one level (if we can) by removing the trailing directory. newPath = currentPath.substring(0, currentPath.length() - 1); int lastSlash = newPath.lastIndexOf('/'); if (lastSlash == -1) { newPath = null; } else { newPath = newPath.substring(0, lastSlash + 1); } } else if (newPath.endsWith("/")) { // If the path ends with /, a directory was selected. // Prefix the _currentPath if it is not null (not in the // root directory). if (currentPath != null) { newPath = currentPath + newPath; } } else { // A file was selected. currentPath += newPath; // Return *?* to stop the screen update process. newPath = "*?*"; } return newPath; } /** * Saves the files and directories listed in vector format into an object array. * * @param filesVector vector of files. * @return object arra of the files, properly padded with "parent" directory indicator if required. */ private Object[] fileVectorToArray(Vector filesVector) { Object[] files; // If not in the root, add ".." to the top of the array. if (currentPath == null || currentPath.length() == 0) { files = Tools.vectorToArray(filesVector); } else { files = Tools.vectorToArray(filesVector, 1); files[0] = (Object) (".."); } return files; } // Handle trackball clicks. protected boolean navigationClick(int status, int time) { doSelection(); return true; } protected boolean keyChar(char c, int status, int time) { // Close this screen if escape is selected. if (c == Characters.ESCAPE) { doCancelProcessing(); return true; } else if (c == Characters.ENTER) { doSelection(); return true; } return super.keyChar(c, status, time); } private void doOKProcessing() { // @todo we also need to make sure they aren't selecting a directory that's // at the top level... or that they haven't write access to. // @todo prompt for confirmation if they select any existing file. if (currentPath == null || currentPath.length() == 0 || (type != TYPE_OPEN && !currentPath.endsWith("/"))) { Dialog.ask(Dialog.D_OK, res.getString(FILE_SELECTION_MSG_INVALID_DIR)); return; } if (type == TYPE_SAVE_AS) { if (fileNameField.getTextLength() == 0) { Dialog.ask(Dialog.D_OK, res.getString(FILE_SELECTION_MSG_NO_FILE)); return; } if (fileNameField.getText().indexOf('/') > -1) { Dialog.ask(Dialog.D_OK, res.getString(FILE_SELECTION_MSG_INVALID_FILE)); return; } } this.close(); } private void doSelectionChangedProcessing() { if (type != TYPE_SAVE_AS) { return; } int idx = list.getSelectedIndex(); if (idx == -1) { return; } String name = (String) list.get(list, idx); if (name.equals("..")) { return; } if (name.endsWith("/")) { return; } fileNameField.setText(name); } private void doCancelProcessing() { currentPath = null; this.close(); } public void fieldChanged(Field field, int context) { if (field == list && context == 10) { doSelectionChangedProcessing(); } else if (field == okButton) { doOKProcessing(); } else if (field == cancelButton) { doCancelProcessing(); } } /** * @param type dialog type, one of the TYPE_ consts. */ public void setType(int type) { this.type = type; } /** * @param startPath What initial path to use when displaying the dialog. */ public void setStartPath(String startPath) { this.startPath = startPath; } /** * @param defaultSaveAsFileName if type is TYPE_SAVE_AS, then the default file name to use. */ public void setDefaultSaveAsFileName(String defaultSaveAsFileName) { this.defaultSaveAsFileName = defaultSaveAsFileName; } /** * * @return the location that was navigated to (if any) */ public String getLocation() { if (currentPath == null) return null; int pos = currentPath.lastIndexOf('/'); if (pos == -1) return null; if (pos == currentPath.length() - 1) return currentPath; return currentPath.substring(0, pos); } } /* * If in file open mode: -- simple selection. Drill down/up until you find what you want. If in save file mode: -- drill * down/up to find what you want -- type in file name * * If in folder selection mode: -- see only folders -- drill down until you see what you want -- press button */