Level: Intermediate Adrian Emmenis (van@vanemmenis.com), Independent consultant
04 Mar 2003 In this third and final article in this "Using the Eclipse GUI outside the Eclipse Workbench" series, A.O. Van Emmenis completes the file explorer example 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 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
Download the source
code from Part 1, but take my system setup into account:
- Windows® 2000
- Eclipse, stable build M3 (15 Nov 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
You need these JAR files on your classpath:
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 run time 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, run the programs from the folder that contains the icons folder, so that the
examples can find the GIFs containing the icons.
Picking up the example from Part 2
At the end of Part 2, 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 submenus), 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.
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 toolbar 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 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 multiselect-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, 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 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, we:
- Get the selection
- Check that 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 three actions and eight 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, 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, 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
Toolbars 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
toolbar, 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 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 toolbar 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 Learn
-
Read Part 1 and Part 2 of this "Using the Eclipse GUI outside the Eclipse Workbench" series.
-
Image handling is discussed in the Eclipse article
"Using
Images in the Eclipse UI."
-
Visit Eclipse.org for downloads, documentation, mail
archives, and articles.
-
Learn more about the Standard Widget Toolkit.
-
For a description of using a tree viewer inside the Eclipse Workbench, see the Eclipse
article "How
to use the JFace Tree Viewer."
-
Read "Developing Eclipse plug-ins."
-
Check out "Plug a Swing-based development tool into Eclipse."
-
Be sure to read "Internationalizing your Eclipse plug-in."
-
And "Testing your internationalized Eclipse plug-in" also has information
that can help you.
-
Image handling is discussed in the Eclipse article "Using
Images in the Eclipse UI (Revised for V2.0)."
-
For project development plans, a FAQ and a list of handy SWT code snippets, check out the
SWT
component development resources.
-
Check out the "Recommended Eclipse reading list."
-
Browse all the Eclipse content on developerWorks.
-
New to Eclipse? Read the developerWorks article "Get started with Eclipse Platform" to learn its origin and architecture, and how to extend Eclipse with plug-ins.
-
Expand your Eclipse skills by checking out IBM developerWorks' Eclipse project resources.
-
To listen to interesting interviews and discussions for software developers, check out check out developerWorks podcasts.
-
For an introduction to the Eclipse platform, see "Getting started with the Eclipse Platform."
-
Stay current with developerWorks' Technical events and webcasts.
-
Watch and learn about IBM and open source technologies and product functions with the no-cost developerWorks On demand demos.
-
Check out upcoming conferences, trade shows, webcasts, and other Events around the world that are of interest to IBM open source developers.
-
Visit the developerWorks Open source zone for extensive how-to information, tools, and project updates to help you develop with open source technologies and use them with IBM's products.
Get products and technologies
Discuss
-
The Eclipse Platform newsgroups should be your first stop to discuss questions regarding Eclipse. (Selecting this will launch your default Usenet news reader application and open eclipse.platform.)
-
The Eclipse newsgroups has many resources for people interested in using and extending Eclipse.
-
Participate in developerWorks blogs and get involved in the developerWorks community.
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. |
Rate this page
|