Sunday, June 19, 2011

Converting SWT to JFace

It has been a slow couple of weeks for my coding. I've had a lot of activities going on which have kept me away. Today I focused on making a major architectural change to PunchClock by reimplementing the view layer of PunchClock to completely use the JFace pattern instead of the pure SWT that was mostly there.

To do this I had to rewrite all of my custom SWT dialog shell code using various subclasses of the JFace Dialog class. All of my menu bar was replaced using a set of JFace MenuManagers with related Actions. Finally the main application Shell was implemented through a new class that extends ApplicationWindow and acts as the "central glue" for the new JFace implementation. As always the tutorials by Adrian Emmenis, "Using the Eclipse GUI outside the Eclipse Workbench", were my main guide in this process although was also able to use the SWT/JFace javadocs available in the Eclipse Platform API Specification reference site.

The purported benefits of using JFace is that it "is a UI toolkit that provides helper classes for developing UI features that can be tedious to implement". So one would hope that this could be quantified in terms of fewer lines of code, easier readability and/or maintainability. As PunchClock 0.2.1 is a fairly stable, simple application, using a couple of JFace dialogs, but not depending on it too much, I was hoping to see evidence of a benefit by switching over. To be honest, the experience today felt like rearranging deck chairs on a cruise ship. Generally what I needed to do was move methods around from a class that previously managed most of the SWT UI interactions and updates and put these chunks of code into other more object oriented classes, such as Dialogs, ApplicationWindow or Actions. A lot of my EventListeners, for menu items for example were easily converted to JFace Actions with a couple of constructor changes and converting the implementation of SelectionListener.widgetSelected() method to override the Action.run() method. For example:
Before old SWT code with Listener for the "Open" menu item

/**
* @param shell
*/
public OpenAction(Shell shell, TimeTrackController controller, DialogSettings settings) {
_shell = shell;
_controller = controller;
_settings = settings;
}

/*
* (non-Javadoc)
*
* @see org.eclipse.swt.events.SelectionAdapter#widgetSelected
* (org.eclipse.swt.events.SelectionEvent)
*/
public void widgetSelected(SelectionEvent e) {

// create new dialog where user enters the project name
final Shell dialog = new Shell(_shell, SWT.DIALOG_TRIM
| SWT.APPLICATION_MODAL);
dialog.setText("Open Project");
FormLayout formLayout = new FormLayout();
formLayout.marginWidth = 10;
formLayout.marginHeight = 10;
formLayout.spacing = 10;
dialog.setLayout(formLayout);
// position near parent window location
dialog.setLocation(_shell.getLocation().x + 10,
_shell.getLocation().y + 10);
// text displayed in dialog
Label openProjectLabel = new Label(dialog, SWT.NONE);
openProjectLabel.setText("Type a Project name:");
FormData data = new FormData();
openProjectLabel.setLayoutData(data);

//for combo box
String[] options = new String[]{};
try{
List availableProjects = _controller.getProjects();
options = new String[availableProjects.size()];
availableProjects.toArray(options);
}catch(IOException ioe){
ioe.printStackTrace();
}


//creates cancel button
Button cancel = new Button(dialog, SWT.PUSH);
cancel.setText("Cancel");
data = new FormData();
data.width = 60;
data.right = new FormAttachment(100, 0);
data.bottom = new FormAttachment(100, 0);
cancel.setLayoutData(data);
cancel.addSelectionListener(new SelectionAdapter() {
public void widgetSelected(SelectionEvent e) {
System.out.println("User cancelled dialog");
dialog.close();
}
});


//creates text box
//final Text textBox = new Text(dialog, SWT.BORDER);
data = new FormData();
data.width = 200;
data.left = new FormAttachment(openProjectLabel, 0, SWT.DEFAULT);
data.right = new FormAttachment(100, 0);
data.top = new FormAttachment(openProjectLabel, 0, SWT.CENTER);
data.bottom = new FormAttachment(cancel, 0, SWT.DEFAULT);
//textBox.setLayoutData(data);
//textBox.forceFocus();

final Combo combo = new Combo(dialog, SWT.NONE);
combo.setItems (options);
//set the default text to the first option, can be overridden by user
if(options.length > 0){
String selectedOption = Configuration.getSetting(_settings, Configuration.LAST_PROJECT, options[0]);
combo.setText(selectedOption);
}
combo.setLayoutData(data);
combo.forceFocus();


//creates ok button
Button ok = new Button(dialog, SWT.PUSH);
ok.setText("OK");
data = new FormData();
data.width = 60;
data.right = new FormAttachment(cancel, 0, SWT.DEFAULT);
data.bottom = new FormAttachment(100, 0);
ok.setLayoutData(data);
ok.addSelectionListener(
// nested inner class
new SelectionAdapter() {

/*
* (non-Javadoc)
*
* @see org.eclipse.swt.events.SelectionAdapter
* #widgetSelected (org.eclipse.swt.events.SelectionEvent)
*/
public void widgetSelected(SelectionEvent e) {
String selectedProject = combo.getText();
//set the project in the controller
_controller.setProject(selectedProject);
//updated the last project setting with chosen value
_settings.put(Configuration.LAST_PROJECT, selectedProject);
// close the message dialog
dialog.close();

}
});


dialog.setDefaultButton(ok);
dialog.pack();
dialog.open();
}


Now becomes just:

/**
* @param shell
*/
public OpenAction(PunchClockWindow window) {
_window = window;
setText("&Open Project...\tCTRL+O");
ImageRegistry reg = Resources.getImageRegistry();
setImageDescriptor(reg.getDescriptor(Resources.IMAGES_FOLDER_OPEN_PNG));
}


/* (non-Javadoc)
* @see org.eclipse.jface.action.Action#run()
*/
public void run() {

// create new JFace dialog where user enters the project name
OpenProjectDialog projDialog = new OpenProjectDialog(_window);
projDialog.open();
}


Now, a lot of the code above is actually still there, just contained in the new object OpenProjectDialog which extends JFace's Dialog class. The one big benefit I notice with Dialogs is that you get the OK and Cancel buttons "for free". So instead of needing to build the popup window from scratch as shown above, I just overrode and implemented the createDialogArea() and okPressed() methods from Dialog with my custom functions. That completely removes maintaining the Cancel button code from my responsibility for example.

Whether the conversion to JFace has objectively produced less code for the same exact results, I utilized a free code line counter, CLOC, to check how much improvements there were at all.


Before (v0.2.1 code pulled from SVN)
http://cloc.sourceforge.net v 1.53  T=0.5 s (64.0 files/s, 10350.0 lines/s)
-------------------------------------------------------------------------------
Language                     files          blank        comment           code
-------------------------------------------------------------------------------
Java                            32            725           1680           2770
-------------------------------------------------------------------------------
SUM:                            32            725           1680           2770
-------------------------------------------------------------------------------
After JFace conversion of view layer 
http://cloc.sourceforge.net v 1.53  T=0.5 s (66.0 files/s, 9538.0 lines/s)
-------------------------------------------------------------------------------
Language                     files          blank        comment           code
-------------------------------------------------------------------------------
Java                            33            705           1510           2554
-------------------------------------------------------------------------------
SUM:                            33            705           1510           2554
-------------------------------------------------------------------------------
There was an 8% shrinking of the code. Not too bad, I'll take it.

JFace ApplicationWindow's mysterious separator line
One quirk that I did come across after the conversion to pure JFace implemenation of the main window, was that it seems the default behavior for ApplicationWindow is to create a separator line at the top of the window's "contents" panel. This is in case you have a menu. This wasn't there before the port, so I was pretty confused. I began coloring all the widgets in my UI to try to understand what was creating this line but had no luck, because it wasn't in my code after all. See the strange extra line on the left.
JFace application window adds this extra line, shown on the left.


In my ApplicationWindow class, it turns out you need to override the ApplicationWindow#showTopSeperator() method and make sure it returns false if you don't need it, and this depends on which OS you have e.g. for Windows 7 and Mac OSX the menu bar is separate from the application panel anyways, no separator is needed. It may mean that for some OSs like Win XP for example, I might need to make the code smarter to conditionally show the separator or not.

No comments:

Post a Comment