|
|
Contents: |
|
|
|
Related content: |
|
|
|
Subscriptions: |
|
|
| Completing the simple file explorer application
Adrian
Van Emmenis (mailto:van@vanemmenis.com?cc=&subject=Adding
actions, menus, and toolbars) Independent consultant 4 March
2003
In this third and final article in the
series, A. O. Van Emmenis completes the file explorer example begun in
Part
1 and Part
2 by adding actions, menu bars, pop-up menus, and toolbars. He shows
how to set menu item properties, how to reuse actions in menus and
toolbars, and how to make actions context sensitive by listening to
events from viewers. The example actions use utilities to launch
programs and access the system clipboard.
Introduction Part
1 of this series began an example that subclassed the JFace
application window and used a tree viewer and a table viewer to display
folders and files. In Part
2 we did some tidying and added some icons using the JFace image
registry.
This time, we'll look at actions and you can use them in menus and
toolbars. We'll see the use of the Program
class to launch programs and the Clipboard
class to access the system clipboard. We have already used icons to
display files and folders in viewers. We'll see how they can be used in
menus and toolbars as well. Finally, we'll make actions listen to events
from viewers in order to make them context sensitive.
Installation
notes If download the source
code for the examples in this article, please note my system setup:
- Windows 2000
- Eclipse, stable build M3 (November 15, 2002)
- Eclipse installed in C:\eclipse-2.1.0
I will leave you to do any swizzling of names and file separators in
what follows, so that the programs work correctly on your system.
Build/run
instructions Ensure that these .jar files are on your class
path:
C:\eclipse-2.1.0\plugins\org.eclipse.jface_2.1.0\jface.jar
C:\eclipse-2.1.0\plugins\org.eclipse.runtime_2.1.0\runtime.jar
C:\eclipse-2.1.0\plugins\org.eclipse.swt.win32_2.1.0\ws\win32\swt.jar
C:\eclipse-2.1.0\plugins\org.eclipse.ui.workbench_2.1.0\workbench.jar
C:\eclipse-2.1.0\plugins\org.eclipse.core.runtime_2.1.0\runtime.jar
Ensure that the Java VM picks up the correct shared libraries for the
GUI you are using at runtime by running it with the following
argument:
-Djava.library.path=C:\eclipse-2.1.0\plugins\org.eclipse.swt.win32_2.1.0\os\win32\x86\
Finally, so that the examples can find the .gif files containing the
icons, run the programs from the folder that contains the icons
folder.
Picking up the example from Part 2 At the end
of the last
article, our explorer application looked like Figure 1.
Figure 1. Explorer (version 8)
Folders are displayed in the left pane using a tree viewer. When you
select a folder in the left pane, the files that it contains are shown in
a table viewer in the right pane. We are sorting the items in the right
pane so that folders appear first. We are using icons to represent files
and folders in both viewers.
Let's add a simple bar menu to the window.
Menus The JFace MenuManager
simplifies the creation and updating of SWT menus. A menu manager can
contain items, other menu managers (for sub-menus), and separators. Once
you have created a menu manager, it can be represented by either a bar
menu, a context menu (in other words, a pop-up menu), or a toolbar
drop-down menu.
Just as with viewers, a menu manager is a helper object rather than a
wrapper object, although you will generally not need to access the SWT
menu itself. Before we discuss menus, let's see what can go into a menu
manager.
Actions You add
actions to menu managers. Actually, you can add actions to buttons and
toolbars, too. The idea is that you subclass Action,
set the text that you want to appear in the menu/toolbar/button, and
implement the run() method to make it do what you want.
Let's dive straight into an example: ExitAction shown in Listing
1: Listing 1. ExitAction (version 1)
import org.eclipse.jface.action.*;
import org.eclipse.jface.window.*;
public class ExitAction extends Action
{
ApplicationWindow window;
public ExitAction(ApplicationWindow w)
{
window = w;
setText("E&xit");
}
public void run()
{
window.close();
}
}
|
All fairly simple. The & character before the x in
Exit indicates that x is to be the keyboard navigation key
(mnemonic) for this menu item. Note that this is different from a key
accelerator (hotkey). We'll see these soon ...
Subclassing
action Don't be misled by the fact that Action
defines the method getText() . You are not intended to
override it. Instead, you are meant to use
setText(String) , and Action will store it and ensure
that all the SWT controls that are currently using this action are
updated with the new text.
This applies to all the other Action properties like the tool tip
text, its enabled state, etc. -- we'll see these
later. |
Adding a bar menu to an application window We
need to configure the application window to have a bar menu using this
method in ApplicationWindow:
protected void addMenuBar()
Remember that we must do this before the SWT shell is created.
And this will call the application window method
createMenuManager() , which returns the menu manager that it
will use later to create the SWT Bar Menu. Here is our implementation in
Listing 2: Listing 2. Explorer -
createMenuManager
protected MenuManager createMenuManager()
{
MenuManager bar_menu = new MenuManager("");
MenuManager file_menu = new MenuManager("&File");
MenuManager edit_menu = new MenuManager("&Edit");
MenuManager view_menu = new MenuManager("&View");
bar_menu.add(file_menu);
bar_menu.add(edit_menu);
bar_menu.add(view_menu);
file_menu.add(new ExitAction(this));
return bar_menu;
}
|
Explorer now looks like Figure 2:
Figure 2. Explorer (Version 9)
Note that the menus that are empty are disabled. Try closing the
explorer application using the navigation keys ALT+Fx.
Let's improve the exit action a little, as shown in Listing 3: Listing 3. ExitAction (version 2)
import org.eclipse.jface.action.*;
import org.eclipse.jface.resource.*;
import org.eclipse.jface.window.*;
import org.eclipse.swt.*;
public class ExitAction extends Action
{
ApplicationWindow window;
public ExitAction(ApplicationWindow w)
{
window = w;
setText("E&xit@Ctrl+W");
setToolTipText("Exit the application");
setImageDescriptor(
ImageDescriptor.createFromURL(Util.newURL("file:icons/close.gif")));
}
public void run()
{
window.close();
}
}
|
We've added an accelerator key (hotkey), a tool tip, and an image (the
tool tip won't show up on menu items, but it will on tool bar items, which
we'll see later). There is a method to set the accelerator key directly,
but it's easier to specify it in the text after the @ character,
since this way, it gets added into the text of the menu item, as shown in
Figure 3:
Figure 3. Explorer (version 10)
The eagle-eyed reader may have spotted that we supplied an image
descriptor directly to the action. What we would really like to do is get
the image descriptor from the image registry. The problem is that the
image registry only gives out Images -- you can't ask it for an
ImageDescriptor. This is bug 23555 in
the Eclipse bug database.
Launching a program associated with a
file Running the program associated with a file is a pretty
useful thing to do. It's actually remarkably easy, too, using the Program
class, as shown in Listing 4: Listing 4.
OpenAction (version 1)
import java.io.*;
import org.eclipse.jface.action.*;
import org.eclipse.jface.resource.*;
import org.eclipse.jface.viewers.*;
import org.eclipse.swt.program.*;
public class OpenAction extends Action
{
Explorer window;
public OpenAction(Explorer w)
{
window = w;
setText("Run");
setToolTipText("Run the associated program on a file");
setImageDescriptor(
ImageDescriptor.createFromURL(Util.newURL("file:icons/run.gif")));
}
public void run()
{
IStructuredSelection selection = window.getTableSelection();
if (selection.size() != 1)
return;
File selected_file = (File) selection.getFirstElement();
if (selected_file.isFile())
{
Program.launch(selected_file.getAbsolutePath());
}
}
}
|
We get hold of the selected elements from the table using
getTableSelection() (we'll see that method in a moment) and
then check that there is exactly one element selected -- remember that the
table is now a multi-select style table -- and then get the element
itself, check it is really a file rather than a folder, and launch it.
The Program launch() method does all the work looking up
the associated program based on the file extension, and then it runs the
appropriate executable, supplying the absolute file name as the
argument.
Before we try this out, let's do one last action.
Using the
system clipboard Here's a neat little use of the system
clipboard. The action CopyFileNamesToClipboardAction copies
the absolute file names of all the selected files to the clipboard.
We transfer text to the system clipboard using the Clipboard
object.
First make the Util class lazily create a clipboard
object, as shown in Listing 5: Listing 5. Util
(version 2)
import java.net.*;
import org.eclipse.jface.resource.*;
import org.eclipse.swt.dnd.*;
import org.eclipse.swt.widgets.*;
public class Util
{
private static ImageRegistry image_registry;
private static Clipboard clipboard;
public static URL newURL(String url_name)
{
try
{
return new URL(url_name);
}
catch (MalformedURLException e)
{
throw new RuntimeException("Malformed URL " + url_name, e);
}
}
public static ImageRegistry getImageRegistry()
{
if (image_registry == null)
{
image_registry = new ImageRegistry();
image_registry.put(
"folder",
ImageDescriptor.createFromURL(newURL("file:icons/folder.gif")));
image_registry.put(
"file",
ImageDescriptor.createFromURL(newURL("file:icons/file.gif")));
}
return image_registry;
}
public static Clipboard getClipboard()
{
if (clipboard == null)
{
clipboard = new Clipboard(Display.getCurrent());
}
return clipboard;
}
}
|
To put text into the clipboard, we use this method on Clipboard:
public void setContents(Object[] data, Transfer[]
dataTypes)
Each array slot in data is associated with a transfer object that tells
the clipboard what type of data is in data. In our case we'll use a TextTransfer
object, which tells the clipboard that we are transferring plain text
(rather than, say, RTF).
The arguments to public void setContents(Object[] data,
Transfer[] dataTypes) are arrays, so that you can transfer data in
several formats at once. For example, a word processing application might
want to transfer text in RTF and plain text.
In the code that follows in Listing 6, we do this:
- Get the selection
- Check it's not empty
- Iterate though it, adding the absolute file names to a string buffer
- Transfer the string to the clipboard with a Text Transfer object
- Put the string into the status line to give us some feedback
Listing 6. CopyFileNamesToClipboardAction (version
1)
import java.io.*;
import java.util.*;
import org.eclipse.jface.action.*;
import org.eclipse.jface.resource.*;
import org.eclipse.jface.viewers.*;
import org.eclipse.swt.*;
import org.eclipse.swt.dnd.*;
public class CopyFileNamesToClipboardAction extends Action
{
Explorer window;
public CopyFileNamesToClipboardAction(Explorer w)
{
window = w;
setToolTipText("Copy absolute file names of selected files to the clipboard");
setText("Copy File &Names@Ctrl+Shift+C");
setImageDescriptor(
ImageDescriptor.createFromURL(Util.newURL("file:icons/copy.gif")));
}
public void run()
{
Clipboard clipboard = Util.getClipboard();
TextTransfer text_transfer = TextTransfer.getInstance();
IStructuredSelection selection = window.getTableSelection();
if (selection.isEmpty())
{
return;
}
StringBuffer string_buffer = new StringBuffer();
for (Iterator i = selection.iterator(); i.hasNext();)
{
File file = (File) i.next();
string_buffer.append(" ");
string_buffer.append(file.getAbsolutePath());
}
clipboard.setContents(
new Object[] { string_buffer.toString()},
new Transfer[] { text_transfer });
}
}
|
And finally, here are the changes to Explorer to add the
getTableSelection() method, along with the code to add the
two new actions to the bar menu (Listing 7): Listing
7. Explorer (version 10)
import java.io.*;
import org.eclipse.jface.action.*;
import org.eclipse.jface.viewers.*;
import org.eclipse.jface.window.*;
import org.eclipse.swt.*;
import org.eclipse.swt.custom.*;
import org.eclipse.swt.widgets.*;
public class Explorer extends ApplicationWindow
{
private TableViewer tbv;
...
public static void main(String[] args)
{
Explorer w = new Explorer();
w.setBlockOnOpen(true);
w.open();
Display.getCurrent().dispose();
Util.getClipboard().dispose();
}
protected MenuManager createMenuManager()
{
MenuManager bar_menu = new MenuManager("");
MenuManager file_menu = new MenuManager("&File");
MenuManager edit_menu = new MenuManager("&Edit");
MenuManager view_menu = new MenuManager("&View");
bar_menu.add(file_menu);
bar_menu.add(edit_menu);
bar_menu.add(view_menu);
file_menu.add(new ExitAction(this));
edit_menu.add(new CopyFileNamesToClipboardAction(this));
edit_menu.add(new OpenAction(this));
return bar_menu;
}
public IStructuredSelection getTableSelection()
{
return (IStructuredSelection) (tbv.getSelection());
}
}
|
Explorer
instance diagram So, we now have 3 actions and 8 other
classes. Let's review the instance diagram (Figure 4) for Explorer (I've
shown the classes that we implemented in a darker gray).
Figure 4. Explorer (version 10), with its
associated objects
Let's run this version and see the actions (Figure 5):
Figure 5. Explorer (version 10)
Avoiding spaghetti in GUI instance
hierarchies In my experience, when designing GUI objects,
you tend to end up building fairly large instance hierarchies. Objects at
low levels in different hierarchies need to know about each other, so
there is a temptation to create ad-hoc cross references between them,
which can lead to code resembling that famous pasta.
Even in our small example, we can see this just beginning to happen.
The action objects need to know about the currently selected item in the
table viewer. The question is: what object does the action store? Is it
the table viewer, the SWT table itself, the Window?
Figure 6. Windows, widgets, and
actions
One solution is to funnel all the access to widgets/selection objects
through the window. Have each action store the window that created it, and
then it can use the accessor methods on that window to get to the objects
it needs.
If you want to share actions between different windows where the
widgets are accessed using different method names, then you can wrap the
access to the widgets from the action inside a method, which could be
reimplemented in subclasses of the action to fetch the correct widget.
For example, to share an action that needed to access a selection, the
method getSelection() in the superclass could be implemented
as:
window.getTableSelection()
in one subclass and (say)
window.getThirdListViewerSelection()
in another subclass.
Making
actions context sensitive Another thing we can do with
actions is to make them aware of what is happening in the rest of the
window and adapt themselves accordingly. We're going to make
OpenAction listen for any changes to the current selection in
the table viewer. When it notices a change, it can look at the new
selection and change its text, tool tip, and enabled state to reflect
that.
OpenAction launches the "associated" program on the
currently selected file. If nothing is selected then, rather than letting
it run and report an error, or just mysteriously do nothing, why don't we
disable the action?
While we're thinking about being nice to the user, I often get confused
when I see menu items that are disabled and I don't understand why they
are disabled. So, here's an idea: How about changing the tool tip so that
we even tell the user why the action is disabled?
We also have to decide what to do if the selection includes several
files. Well, we could just run them all, one after another, but, since I
have painful memories of selecting 300 files and choosing the Open option
from the Windows File Explorer, let's just disable it for now.
Finally, we'll check if the selected item is a folder and disable the
open action in this case as well.
We need to make OpenAction a listener for the
SelectionChanged event from the table viewer, so we'll make it implement
ISelectionChangedListener
(Listing 8).
We'll do this in selectionChanged() :
- Set the text and tool tip text to the default
- Check the selection
- If we don't have exactly one item selected, we disable the action,
adjust the tool tip to say why it is disabled, and return.
- If the selection is a file (rather than a folder), then we adjust
the text and tool tip to reflect that file name and enable it.
Listing 8. OpenAction (version 2)
import java.io.*;
import org.eclipse.jface.action.*;
import org.eclipse.jface.resource.*;
import org.eclipse.jface.viewers.*;
import org.eclipse.swt.program.*;
public class OpenAction
extends Action
implements ISelectionChangedListener
{
Explorer window;
public OpenAction(Explorer w)
{
window = w;
setText("Run");
setToolTipText("Run the associated program on a file");
setImageDescriptor(
ImageDescriptor.createFromURL(Util.newURL("file:icons/run.gif")));
}
public void run()
{
IStructuredSelection selection = window.getTableSelection();
if (selection.size() != 1)
{
return;
}
File selected_file = (File) selection.getFirstElement();
if (selected_file.isFile())
{
Program.launch(selected_file.getAbsolutePath());
}
}
public void selectionChanged(SelectionChangedEvent event)
{
setText("Run");
setToolTipText("Run the associated program on a file");
IStructuredSelection selection = window.getTableSelection();
if (selection.size() != 1)
{
setEnabled(false);
setToolTipText(
getToolTipText() + " (Only enabled when exactly one item is selected)");
return;
}
File file = (File) selection.getFirstElement();
if (file.isFile())
{
setEnabled(true);
setText("Run the associated program on " + file.getName());
setToolTipText(
"Run the program associated with "
+ file.getName()
+ " with this file as the argument");
}
}
}
|
And now, to make the open action listen to the table viewer for the
SelectionChanged event (Listing 9). Listing 9.
Explorer (version 11); some code that hasn't changed has been
omitted
...
public class Explorer extends ApplicationWindow
{
private TableViewer tbv;
private OpenAction open_action;
...
protected Control createContents(Composite parent)
{
...
tbv.addSelectionChangedListener(open_action);
return sash_form;
}
...
protected MenuManager createMenuManager()
{
MenuManager bar_menu = new MenuManager("");
MenuManager file_menu = new MenuManager("&File");
MenuManager edit_menu = new MenuManager("&Edit");
MenuManager view_menu = new MenuManager("&View");
bar_menu.add(file_menu);
bar_menu.add(edit_menu);
bar_menu.add(view_menu);
file_menu.add(new ExitAction(this));
edit_menu.add(new CopyFileNamesToClipboardAction(this));
open_action = new OpenAction(this);
edit_menu.add(open_action);
return bar_menu;
}
...
}
|
Figure 7. Explorer (version 11), showing Edit
menu with a single file selected
Now let's see how the number of selected files changes the open action
(Figure 8):
Figure 8. Explorer (version 11), showing Edit
menu with no file selected
Tool bars and
pop-up menus Finally we'll add a toolbar and pop-up
(context) menu. Happily, there isn't really much to do since we've done
all the hard work already in our actions. The toolbar and the pop-up menu
are merely going to share those actions.
Just as with the status line and the bar menu, we configure the window
to have a tool bar, and we implement the
createToolBarManager() method to create it.
It's slightly different for the pop-up menu. We create it in the
createContents() method and add it directly to the table
widget.
We also refactor the code to have all three actions as fields (rather
than local variables) so that we can access them from three methods. Let's
see the final version of Explorer (Listing 10): Listing 10. Explorer (version 12)
import java.io.*;
import org.eclipse.jface.action.*;
import org.eclipse.jface.viewers.*;
import org.eclipse.jface.window.*;
import org.eclipse.swt.*;
import org.eclipse.swt.custom.*;
import org.eclipse.swt.widgets.*;
public class Explorer extends ApplicationWindow
{
private TableViewer tbv;
private TreeViewer tv;
private OpenAction open_action;
private ExitAction exit_action;
private CopyFileNamesToClipboardAction copy_action;
public Explorer()
{
super(null);
exit_action = new ExitAction(this);
copy_action = new CopyFileNamesToClipboardAction(this);
open_action = new OpenAction(this);
addStatusLine();
addMenuBar();
addToolBar(SWT.FLAT | SWT.WRAP);
}
protected Control createContents(Composite parent)
{
getShell().setText("JFace File Explorer");
SashForm sash_form = new SashForm(parent, SWT.HORIZONTAL | SWT.NULL);
tv = new TreeViewer(sash_form);
tv.setContentProvider(new FileTreeContentProvider());
tv.setLabelProvider(new FileTreeLabelProvider());
tv.setInput(new File("C:\\"));
tv.addFilter(new AllowOnlyFoldersFilter());
tbv =
new TableViewer(sash_form, SWT.BORDER | SWT.FULL_SELECTION | SWT.MULTI);
tbv.setContentProvider(new FileTableContentProvider());
tbv.setLabelProvider(new FileTableLabelProvider());
tbv.setSorter(new FileSorter());
TableColumn column = new TableColumn(tbv.getTable(), SWT.LEFT);
column.setText("Name");
column.setWidth(200);
column = new TableColumn(tbv.getTable(), SWT.RIGHT);
column.setText("Size");
column.setWidth(100);
tbv.getTable().setHeaderVisible(true);
tv.addSelectionChangedListener(new ISelectionChangedListener()
{
public void selectionChanged(SelectionChangedEvent event)
{
IStructuredSelection selection =
(IStructuredSelection) event.getSelection();
Object selected_file = selection.getFirstElement();
tbv.setInput(selected_file);
}
});
tbv.addSelectionChangedListener(new ISelectionChangedListener()
{
public void selectionChanged(SelectionChangedEvent event)
{
IStructuredSelection selection =
(IStructuredSelection) event.getSelection();
setStatus("Number of items selected is " + selection.size());
}
});
tbv.addSelectionChangedListener(open_action);
MenuManager menu_manager = new MenuManager();
tbv.getTable().setMenu(menu_manager.createContextMenu(tbv.getTable()));
menu_manager.add(exit_action);
menu_manager.add(copy_action);
menu_manager.add(open_action);
return sash_form;
}
public static void main(String[] args)
{
Explorer w = new Explorer();
w.setBlockOnOpen(true);
w.open();
Display.getCurrent().dispose();
Util.getClipboard().dispose();
}
protected MenuManager createMenuManager()
{
MenuManager bar_menu = new MenuManager("");
MenuManager file_menu = new MenuManager("&File");
MenuManager edit_menu = new MenuManager("&Edit");
MenuManager view_menu = new MenuManager("&View");
bar_menu.add(file_menu);
bar_menu.add(edit_menu);
bar_menu.add(view_menu);
file_menu.add(exit_action);
edit_menu.add(copy_action);
edit_menu.add(open_action);
return bar_menu;
}
public IStructuredSelection getTableSelection()
{
return (IStructuredSelection) (tbv.getSelection());
}
public void openFolder(File folder)
{
tv.setExpandedState(folder, true);
tv.setSelection(new StructuredSelection(folder), false);
}
protected ToolBarManager createToolBarManager(int style)
{
ToolBarManager tool_bar_manager = new ToolBarManager(style);
tool_bar_manager.add(exit_action);
tool_bar_manager.add(copy_action);
tool_bar_manager.add(open_action);
return tool_bar_manager;
}
}
|
And now, for the last time, let's fire up Explorer and see the tool bar
and the pop-up menu in operation (Figures 9 and 10).
Figure 9. Explorer (version 12) showing pop-up
menu with one file selected
Figure 10. Explorer (version 12) showing tool
tip with two files selected
Conclusion We've
covered quite a lot of JFace in these three articles. We've seen how the
pluggable JFace window, viewer, and menu framework can generate
great-looking user interfaces using relatively small amounts of code.
I hope that you have learned:
- How to subclass application windows
- How to use the pluggable viewers and content providers
- How to add icons using images and the image registry
- How to use the system clipboard
- How to launch programs
- How to use menus and actions
- How actions can work with different menu containers and listeners to
produce context-sensitive applications
But, of course, there's more. Check out the Resources
for more information.
Resources
- Read Part
1 and Part
2 in this series.
- Download the source
code for the examples in this article.
- See the main Eclipse Web site
for downloads, documentation, mail archives, and articles. There you can
learn more about these classes and interfaces:
Program,
Clipboard,
MenuManager,
Action,
ApplicationWindow,
TextTransfer ,
and ISelectionChangedListener .
- For a description of using a tree viewer inside the Eclipse
Workbench, see the Eclipse article "How
to use the JFace Tree Viewer".
- Image handling is discussed in the Eclipse article "Using
Images in the Eclipse UI".
- For project development plans, a FAQ, and a list of handy SWT code
snippets, check out the SWT
component development resources.
- Learn more about Eclipse in these developerWorks
articles:
- Find more Open
source resources in the developerWorks Open source projects
zone.
About the
author A. O. Van Emmenis is an independent
consultant, specializing in Java/J2EE training and consulting, based
in Cambridge, UK. Van has worked in the software industry for 20
years or so. He first started working with objects using Smalltalk
in the CAD industry and now works mainly in Java. He is particularly
interested in Agile methods and GUI design. You can contact Van at
van@vanemmenis.com. |
|