//++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
// J2ME-Lib source file
// Copyright (c) 2006 Elmar Sonnenschein / esoco GmbH
// Last Change: 17.10.2006 by eso
//
// J2ME-Lib is free software; you can redistribute it and/or modify it under
// the terms of the GNU Lesser General Public License as published by the Free
// Software Foundation; either version 2.1 of the License, or (at your option)
// any later version.
//
// J2ME-Lib 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 Lesser General Public License for more
// details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with J2ME-Lib; if not, write to the Free Software Foundation, Inc.,
// 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA or use the contact
// information from the GNU website http://www.gnu.org
//++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
package de.esoco.j2me.ui;
import de.esoco.j2me.model.MutableHierarchyNode;
import de.esoco.j2me.model.HierarchyNode;
import de.esoco.j2me.model.NodeAccessException;
import de.esoco.j2me.resource.ResourceBundle;
import de.esoco.j2me.storage.StorageException;
import de.esoco.j2me.storage.StorageFullException;
import de.esoco.j2me.util.Executable;
import de.esoco.j2me.util.Log;
import de.esoco.j2me.util.TextUtil;
import de.esoco.j2me.util.UserNotificationException;
import java.util.Stack;
import javax.microedition.lcdui.Choice;
import javax.microedition.lcdui.ChoiceGroup;
import javax.microedition.lcdui.Command;
import javax.microedition.lcdui.Displayable;
import javax.microedition.lcdui.Image;
import javax.microedition.lcdui.Item;
import javax.microedition.lcdui.List;
import javax.microedition.lcdui.TextField;
/********************************************************************
* A view to display hierarchical data in J2ME lcdui List screens. To use this
* class it is mandatory that MIDlets are subclassed from the BasicMidlet class
* from this library because it provides the necessary infrastructure for
* resource and display access.
*
* @author eso
*/
public class HierarchyView
{
//~ Static fields/initializers ---------------------------------------------
/** The maximum length of node title strings */
public static final int MAX_TITLE_SIZE = 128;
//~ Instance fields --------------------------------------------------------
/** This node reference stores entries that are cut or copied from a list */
private HierarchyNode aClipboard = null;
/** Image for new item at end of list */
private Image aNewItemImage = null;
/** Stack of the currently displayed Displayables (hierarchy levels) */
private Stack aViewStack = new Stack();
/** The currently displayed Displayable object */
private Displayable rCurrentView;
/** The root node of the data to be displayed (which itself is not shown) */
private HierarchyNode rModel;
/** The controller for the view */
private HierarchyViewController rViewController;
//~ Constructors -----------------------------------------------------------
/***************************************
* Constructor of a new HierarchyView for a specific set of data. It will
* create a list for the root data but will not display it on the screen
* already. This needs to be done by the application by calling the method
* setCurrentView().
*
* @param rDataRoot The root of the hierarchical data to display in
* the view; this must always be a group node!
* @param rNodeController The controller for accessing and editing of nodes
* @param rGeneralCommands A command array containing the general commands
* that will always be availabe in all screens. This
* array must contain either ExecutableCommand or
* NodeCommand entries which can be performed
* directly through their execute methods. If other
* commands are set they will appear on the screen
* but they <b>WILL NOT RESULT IN ANY ACTION BEING
* PERFORMED!</b>
*/
public HierarchyView(HierarchyNode rDataRoot,
NodeController rNodeController,
Command[] rGeneralCommands)
{
rModel = rDataRoot;
rNodeController.setView(this);
rViewController = createViewController(rNodeController,
rGeneralCommands);
aNewItemImage = getImage("JL_NewNodeI");
init();
}
//~ Methods ----------------------------------------------------------------
/***************************************
* Returns the currently visisble Node, i.e. the node the children of which
* are displayed.
*
* @return The currently active parent HierarchyNode instance
*/
public HierarchyNode getCurrentNode()
{
if (rCurrentView instanceof HierarchyNodeDisplay)
{
return ((HierarchyNodeDisplay) rCurrentView).getRoot();
}
else
{
return getCurrentDisplay().getSelectedNode();
}
}
/***************************************
* Returns the currently shown Displayable object of the HierarchyView.
*
* @return The current Displayable object
*/
public Displayable getCurrentView()
{
return rCurrentView;
}
/***************************************
* Returns the currently selected Node, i.e. the child node of the current
* node which is currently highlighted. If the currently displayed Screen is
* not a List, the current Node will be returned instead (because in this
* case either a leaf editor or viewer is visible).
*
* @return The currently selected HierarchyNode instance or NULL if the"New
* Item" entry is selected
*/
public HierarchyNode getSelectedNode()
{
if (rCurrentView instanceof HierarchyNodeDisplay)
{
return ((HierarchyNodeDisplay) rCurrentView).getSelectedNode();
}
else
{
return getCurrentNode();
}
}
/***************************************
* Initializes and re-displays the view so that it shows the same state as
* if it had just been created. This will also empty the clipboard.
*/
public void reset()
{
aClipboard = null;
init();
show();
}
/***************************************
* Displays the hierarchy view on the screen. This method should normally be
* used by the application to display the first HierarchyView screen.
* invokes <code>ScreenManager.setCurrent()</code> and therefore replaces
* the currently visible screen on the screen stack.
*/
public void show()
{
if (rCurrentView != ScreenManager.getCurrent())
{
ScreenManager.setCurrent(rCurrentView);
}
}
/***************************************
* Re-displays the current view from the internal view stack. This method is
* used internally during navigation but it can also be used by applications
* and subclasses to restore a HierarchyView's last screen if they have
* displayed other screens in front of it.
*/
public void updateCurrentView()
{
rCurrentView = (Displayable) aViewStack.peek();
ScreenManager.setCurrent(rCurrentView);
}
/***************************************
* Updates the list item for a certain node with new values for the title
* string and/or the item image. The given node must be a child node of the
* currently displayed parent node.
*
* @param rNode The node for which the item shall be updated
* @param sNewTitle The new title or NULL to keep the old string
* @param rNewImage The new image or NULL for the default image (queried
* from the node controller)
*/
public void updateNodeItem(HierarchyNode rNode,
String sNewTitle,
Image rNewImage)
{
if (rNode != null)
{
HierarchyNodeDisplay rDisplay = getCurrentDisplay();
int nIndex = rDisplay.getRoot().getChildIndex(rNode);
if (nIndex >= 0)
{
if (sNewTitle == null)
{
sNewTitle = rDisplay.getListView().getString(nIndex);
}
if (rNewImage == null)
{
rNewImage = rViewController.getNodeController()
.getNodeImage(rNode
.getNodeType(),
rNode);
}
rDisplay.getListView().set(nIndex, sNewTitle, rNewImage);
}
}
}
/***************************************
* Internal tool method to recursively add a command to all list views on
* the stack.
*
* @param rCmd The command to add
* @param bEditableOnly If TRUE the command will only be added to Lists
* which display an MutableHierarchyNode
*/
protected void addListCommand(Command rCmd, boolean bEditableOnly)
{
for (int i = aViewStack.size(); i > 0;)
{
Object rView = aViewStack.elementAt(--i);
if (rView instanceof HierarchyNodeDisplay)
{
HierarchyNodeDisplay rDisplay = (HierarchyNodeDisplay) rView;
HierarchyNode rNode = rDisplay.getRoot();
if (!bEditableOnly || (rNode instanceof MutableHierarchyNode))
{
((Displayable) rDisplay).addCommand(rCmd);
}
}
}
}
/***************************************
* To go back one level in the hierarchical data (if not already on top).
*/
protected void back()
{
if (aViewStack.size() > 1)
{
aViewStack.pop();
updateCurrentView();
}
}
/***************************************
* To copy the child node at the position currently selected into the
* internal clipboard. The list will not be changed by this action.
*
* @param rNode The Node to cut to the clipboard
*/
protected void copyNode(HierarchyNode rNode)
{
aClipboard = rNode.deepCopy();
addListCommand(rViewController.getPasteCommand(), true);
}
/***************************************
* Creates a new node display instance that displays a list of the node's
* children. This method may be overwritten by subclasses to return a
* different kind of node display. The returned object must also be a
* subclass of {@link Displayable}, else the invoking code will throw a
* ClassCastException.
*
* <p>This default implementation returns an instance of the inner class
* {@link DefaultNodeDisplay}.</p>
*
* @param rRoot The root node to display the children of
* @param rTitles The list item titles
* @param rImages The list item images
*
* @return A displayable instance
*/
protected HierarchyNodeDisplay createNodeDisplay(HierarchyNode rRoot,
String[] rTitles,
Image[] rImages)
{
return new DefaultNodeDisplay(rRoot, rTitles, rImages);
}
/***************************************
* Creates an instance of the view controller that is used by this view.
*
* @param rNodeController The node controller to use for node access
* @param rGeneralCommands The commands that are always available
*
* @return A HierarchyViewController instance
*/
protected HierarchyViewController createViewController(NodeController rNodeController,
Command[] rGeneralCommands)
{
return new HierarchyViewController(this, rNodeController,
rGeneralCommands);
}
/***************************************
* To copy the child node at the position currently selected to the internal
* clipboard and to delete it from the displayed parent node afterwards. All
* nodes after that position will be shifted one place up.
*
* @param rNode The Node to cut to the clipboard
*
* @throws UserNotificationException If the operations fails
*/
protected void cutNode(HierarchyNode rNode) throws UserNotificationException
{
aClipboard = rNode;
deleteNode(rNode);
addListCommand(rViewController.getPasteCommand(), true);
}
/***************************************
* To delete the child node at the position currently selected from the
* displayed parent node. All nodes after that position will be shifted one
* place up.
*
* @param rNode The Node to delete
*
* @throws UserNotificationException If the operations fails
*/
protected void deleteNode(HierarchyNode rNode)
throws UserNotificationException
{
HierarchyNodeDisplay rDisplay = (HierarchyNodeDisplay) rCurrentView;
HierarchyNode rParent = rDisplay.getRoot();
int nIndex = rParent.getChildIndex(rNode);
try
{
((MutableHierarchyNode) rParent).removeChild(nIndex);
rDisplay.getListView().delete(nIndex);
}
catch (StorageException eStorage)
{
errorException(eStorage, "JL_MtFile", "JL_MsStorErr",
eStorage.getMessage());
}
catch (NodeAccessException eNodeAccess)
{
errorException(eNodeAccess, "JL_MtError", "JL_MsNodeErr",
eNodeAccess.getMessage());
}
}
/***************************************
* To end the editing of a leaf node's data.
*
* @param rNode TRUE if editing has been cancelled, Ok otherwise.
*
* @throws UserNotificationException If the operations fails
*/
protected void endLeafEditor(MutableHierarchyNode rNode)
throws UserNotificationException
{
try
{
rViewController.getNodeController().editFinished(rCurrentView,
rNode);
}
catch (StorageFullException eFull)
{
errorException(eFull, "JL_MtFile", "JL_MsStorFul",
eFull.getMessage());
}
catch (StorageException eStorage)
{
errorException(eStorage, "JL_MtFile", "JL_MsStorErr",
eStorage.getMessage());
}
back();
rViewController.getNodeController().updateLeafViewer(rCurrentView,
rNode);
}
/***************************************
* Internal method to create and throw a new UserNotificationException based
* on several parameters. The original exception which caused the error
* condition can be queried as the causing exception of the new
* UserNotificationException which will be thrown.
*
* @param eCause The original exception that caused the error
* @param sTitleKey The resource key for the error message's title
* @param sMessageKey The resource key for the message of the new exception
* @param rMessageArg An optional argument for the message that will be
* used in a call to TextFormat.formatMessage() or NULL
*
* @throws UserNotificationException This method will always throw a new
* exception
*/
protected void errorException(Exception eCause,
String sTitleKey,
String sMessageKey,
Object rMessageArg)
throws UserNotificationException
{
String sMessage = TextUtil.formatMessage(getString(sMessageKey),
new Object[] { rMessageArg });
Log.error(eCause);
throw new UserNotificationException(getString(sTitleKey), sMessage,
eCause);
}
/***************************************
* Returns the current List of the HierarchyView. This will scan over all
* other displayable objects on the view stack until it finds the topmost
* List object (there must always be at least one).
*
* @return The topmost List on the view stack
*/
protected HierarchyNodeDisplay getCurrentDisplay()
{
Displayable rCheck;
int nPos = aViewStack.size();
do
{
rCheck = (Displayable) aViewStack.elementAt(--nPos);
}
while (!(rCheck instanceof HierarchyNodeDisplay));
return (HierarchyNodeDisplay) rCheck;
}
/***************************************
* Internal shortcut method for {@link ResourceBundle#getImage(String)}.
*
* @see ResourceBundle#getImage(String)
*/
protected Image getImage(String sKey)
{
return ResourceBundle.getCurrent().getImage(sKey);
}
/***************************************
* Internal shortcut method for {@link ResourceBundle#getString(String)}.
*
* @see ResourceBundle#getString(String)
*/
protected String getString(String sKey)
{
return ResourceBundle.getCurrent().getString(sKey);
}
/***************************************
* Initializes the view screen without displaying it.
*/
protected void init()
{
Displayable aDisplay = newNodeDisplay(rModel, false);
aViewStack.removeAllElements();
aViewStack.push(aDisplay);
rCurrentView = aDisplay;
}
/***************************************
* To insert a node into the displayed parent node at the position currently
* selected. The node currently at that position and all after it will be
* shifted one place further down. To append the node to the end of the list
* there is a special "new item" element at the end.
*
* @param rNode The node to insert
*
* @throws UserNotificationException If inserting the node fails
*/
protected void insertNode(HierarchyNode rNode)
throws UserNotificationException
{
HierarchyNodeDisplay rDisplay = (HierarchyNodeDisplay) rCurrentView;
HierarchyNode rParent = rDisplay.getRoot();
int nIndex = rDisplay.getListView()
.getSelectedIndex();
try
{
((MutableHierarchyNode) rParent).insertChild(rNode, nIndex);
rDisplay.getListView().insert(nIndex, rNode.getTitle(),
rViewController.getNodeController()
.getNodeImage(rNode.getNodeType(),
rNode));
}
catch (StorageFullException eFull)
{
errorException(eFull, "JL_MtFile", "JL_MsStorFul",
eFull.getMessage());
}
catch (StorageException eStorage)
{
errorException(eStorage, "JL_MtFile", "JL_MsStorErr",
eStorage.getMessage());
}
catch (NodeAccessException eNodeAccess)
{
errorException(eNodeAccess, "JL_MtError", "JL_MsNodeErr",
eNodeAccess.getMessage());
}
}
/***************************************
* To create a new node as a child of the currently displayed node at the
* position currently selected. The node currently at that position and all
* after it will be shifted one place further down. To append a node to the
* end of the list there is a special "new item" element at the end.
*
* @param sType The type of node to create as defined by the
* getChildCreateOptions() of MutableHierarchyNode
* @param sTitle The title of the new node
*
* @see MutableHierarchyNode#getChildCreateOptions()
*/
protected void newNode(String sType, String sTitle)
throws UserNotificationException
{
try
{
MutableHierarchyNode rParent = (MutableHierarchyNode)
getCurrentNode();
HierarchyNode rNode = rParent.createChild(sType, sTitle);
insertNode(rNode);
}
catch (NodeAccessException eNodeAccess)
{
errorException(eNodeAccess, "JL_MtError", "JL_MsNodeErr",
eNodeAccess.getMessage());
}
}
/***************************************
* Returns a new Displayable instance that will display a list of the
* children of a particular Node.
*
* @param rParent The data node to create a new list for it's
* children
* @param bWithBackCommand TRUE if the list shall have a "Back" command
*
* @return A new List object for the current hierarchy level
*/
protected Displayable newNodeDisplay(HierarchyNode rParent,
boolean bWithBackCommand)
{
boolean bEditable = (rParent instanceof MutableHierarchyNode);
int nCount = rParent.getChildCount();
int nSize = nCount;
// increase size to reserve space for "New" command on editable nodes
if (bEditable)
{
nSize++;
}
String[] aTitles = new String[nSize];
Image[] aImages = new Image[nSize];
for (int i = 0; i < nCount; i++)
{
HierarchyNode rChild = rParent.getChild(i);
aTitles[i] = rChild.getTitle();
aImages[i] = rViewController.getNodeController().getNodeImage(rChild
.getNodeType(),
rChild);
}
if (bEditable)
{
aTitles[nCount] = getString("JL_NewItem");
aImages[nCount] = aNewItemImage;
}
Displayable aDisplay = (Displayable) createNodeDisplay(rParent, aTitles,
aImages);
rViewController.setListCommands(aDisplay, rParent, bEditable,
aClipboard != null, bWithBackCommand);
return aDisplay;
}
/***************************************
* To copy the node currently in the clipboard into the displayed parent
* node at the position currently selected. It uses the method insertNode()
* to perform the insertion.
*
* @see #insertNode(HierarchyNode)
*/
protected void pasteNode() throws UserNotificationException
{
// insert a copy to prevent side effects with the original node that
// is still kept in the clipboard
insertNode(aClipboard.deepCopy());
}
/***************************************
* To query the parameters for the creation of a new node. This method
* displays a custom message box to query the new node's type and name and
* invokes the newNode() method if the user confirms this dialog (i. e.
* selects Ok).
*
* @param rParentNode The parent node to create the new node in
*
* @see #newNode(String, String)
*/
protected void queryNewNode(MutableHierarchyNode rParentNode)
{
final String[] aTypeKeys = rParentNode.getChildCreateOptions();
String[] aTypes = new String[aTypeKeys.length];
Image[] aImages = new Image[aTypeKeys.length];
for (int i = 0; i < aTypes.length; i++)
{
String sType = aTypeKeys[i];
aImages[i] = rViewController.getNodeController().getNodeImage(sType,
null);
aTypes[i] = getString(sType);
}
final ChoiceGroup aChoice = new ChoiceGroup(getString("JL_LNodeType"),
Choice.EXCLUSIVE, aTypes,
aImages);
final TextField aInput = new TextField(getString("JL_LNodeName"),
null, MAX_TITLE_SIZE,
TextField.ANY);
// Use a MessageScreen to query the new title and, if confirmed, callback
// to method newNode(); params null, null: no Image, empty textfield
MessageScreen.showMessageBox(getString("JL_MtNewNode"), null, null,
new Item[] { aChoice, aInput }, false,
new Executable()
{
public void execute(Object rArg)
throws UserNotificationException
{
// this callback will only be invoked if the user selects Ok
newNode(aTypeKeys[aChoice
.getSelectedIndex()],
aInput.getString());
}
});
}
/***************************************
* To query the new name for renaming a particular node. This method
* displays a input message box and invokes the renameNode() method if the
* user confirms this dialog (i.e. selects Ok).
*
* @param rNode The Node to delete
*
* @see #renameNode(MutableHierarchyNode, String)
*/
protected void queryRenameNode(final MutableHierarchyNode rNode)
{
// Use a MessageScreen to query the new title, if confirmed callback
// to method renameNode()
MessageScreen.showInput(getString("JL_MtRenNode"),
getString("JL_LNodeName"), rNode.getTitle(),
MAX_TITLE_SIZE, TextField.ANY, false,
new Executable()
{
public void execute(Object rArg)
throws UserNotificationException
{ // this callback will only be invoked if the user selects Ok
renameNode(rNode,
(String) ((Dialog) rArg)
.getResult());
}
});
}
/***************************************
* To change the title of a node in the current list.
*
* @param rNode The node to be renamed
* @param sNewTitle The new title for the node
*
* @throws UserNotificationException If the operations fails
*/
protected void renameNode(MutableHierarchyNode rNode, String sNewTitle)
throws UserNotificationException
{
try
{
rNode.setTitle(sNewTitle);
updateNodeItem(rNode, sNewTitle, null);
}
catch (StorageFullException eFull)
{
errorException(eFull, "JL_MtFile", "JL_MsStorFul",
eFull.getMessage());
}
catch (StorageException eStorage)
{
errorException(eStorage, "JL_MtFile", "JL_MsStorErr",
eStorage.getMessage());
}
}
/***************************************
* To set a new Displayable object to be displayed on the screen.
*
* @param rView The object to display next
*/
protected void setNextView(Displayable rView)
{
aViewStack.push(rView);
updateCurrentView();
}
//~ Inner Classes ----------------------------------------------------------
/********************************************************************
* Extended List class that manages the associated parent node and
* additional state needed for navigating the node hierarchy.
*/
protected class DefaultNodeDisplay extends List
implements HierarchyNodeDisplay
{
//~ Instance fields ----------------------------------------------------
private HierarchyNode rRoot;
//~ Constructors -------------------------------------------------------
// private int nLastSelection = 0;
/***************************************
* Constructor for a list that displays the titles of the nodes of a
* certain parent node.
*
* @param rRoot The parent node of the listed nodes
* @param rTitles The title strings of the listed nodes
* @param rImages The (optional) images for the listed nodes
*/
public DefaultNodeDisplay(HierarchyNode rRoot,
String[] rTitles,
Image[] rImages)
{
super(rRoot.getTitle(), List.IMPLICIT, rTitles, rImages);
this.rRoot = rRoot;
}
//~ Methods ------------------------------------------------------------
/***************************************
* @see HierarchyNodeDisplay#getListView()
*/
public Choice getListView()
{
return this;
}
/***************************************
* @see HierarchyNodeDisplay#getRoot()
*/
public HierarchyNode getRoot()
{
return rRoot;
}
/***************************************
* @see HierarchyNodeDisplay#getSelectedNode()
*/
public HierarchyNode getSelectedNode()
{
int nIndex = getSelectedIndex();
// The index may be out of range because of the <New Item> entry
// at the end of lists that does not represent an existing Node
if (nIndex < rRoot.getChildCount())
{
return rRoot.getChild(nIndex);
}
else
{
return null;
}
}
}
}