Saturday, June 4, 2011

Punch Clock 0.2.1 and clickable edit button for SWT tables

After the release of Punch Clock 0.2.0 I observed a few issues that needed to be solved. I've just posted a new version, Punch Clock 0.2.1 which has these minor fixes and a new dialog to view the "audit" data in the UI.

One of the new features I added since 0.2.0 which I wanted to write about is the "Fix Session" button I have added to each row of the main table of data in PunchClock. This lets you go back after the fact and modify the times or other details for a record that has been saved. This was a true life-saver for me in practice as you often forget to start the timer at the right time, or want to put clarifying comments in later, etc. I implemented this with a click-able SWT button on every row with a wrench style icon.
Clicking on that button brings up a simple form in a dialog for editing the data in that row. Since I couldn't find an example that was exactly like this, I came up with an approach, still not sure if this is the most efficient way to do it. Here is how this was accomplished.

Using TableEditor to add the Buttons
I started from Snippet126 from the Eclipse SWT snippets site: "Table example snippet: place arbitrary controls in a table".

Using a class called org.eclipse.swt.custom.TableEditor which you construct with your Table, you specify a new control and place it against a particular TableItem in that Table, and even specify which column that control should be over. One way to picture this is that the TableEditor as floating above the Table as an invisible layer which you can put controls on that overlap your Table.

For example in my case, where table is an instance of org.eclipse.swt.widgets.Table


//get all the current items (rows) from your SWT table
TableItem [] items = table.getItems ();
for (int i=0; i<items.length; i++) {

//create the editor
TableEditor editor = new TableEditor(table);

//get the row
TableItem item = items[i];
//create my button
Button button = new Button (table, SWT.PUSH);
//my code to set the button icon image from an JFace ImageRegistry
button.setImage(Resources.getImageRegistry().get(Resources.IMAGES_EDIT_PNG));
button.setSize(16, 16);
button.setToolTipText("Fix this session");
button.pack();

//not sure if necessary, prevent the editor from being smaller than button
editor.minimumWidth = button.getSize ().x;
editor.horizontalAlignment = SWT.RIGHT;
//set the button to float over column 5 for this item
editor.setEditor (button, item, 5);
}


And that is it for the set up. Note regarding the JFace ImageRegistry, I have begun to use an excellent tutorial/article called Using the Eclipse GUI outside the Eclipse Workbench which has three parts. Part 2: Using the JFace image registry, describes the approach to creating an org.eclipse.jface.resource.ImageRegistry that is then referred to whenever you need to display the same image repeatedly in the UI, as I do in the case for the little wrench icon.

Handling Button Clicks
While the above code drops the buttons on the tables, I need to then hook them up to display the edit dialog. This is done through adding a Selection Listener to the button being added to each row.

Again, I am not sure if this is the greatest way to implement this code, but I created a class called EditButtonListener which implemented the org.eclipse.swt.widgets.Listener interface, specifically calling my new dialog from the handleEvent() method. Something like this (with my form logic simplified to pseudo code).


public class EditButtonListener implements Listener {

private TableItem _tableItem;

/**
* @param tableItem - dependency injection, keeps track of the row
*/
public EditButtonListener(TableItem tableItem) {
_tableItem = tableItem;
}

/* (non-Javadoc)
* @see org.eclipse.swt.widgets.Listener#handleEvent(org.eclipse.swt.widgets.Event)
*/
@Override
public void handleEvent(Event event) {
//get the parent shell for my table, needed to launch my new SWT dialog
Shell lShell = _tableItem.getParent().getShell();
//in my table the first column is a unique id
String sessionIdString = _tableItem.getText(0);
System.out.println("Button click for session id: "+sessionIdString);

final TimeSession selectedSession;
try{
//retrieve the selected time session from the back end
//...
}catch(Exception e){
throw new IllegalStateException(e);
}

// create new dialog where user enters the project name
final Shell dialog = new Shell(lShell, SWT.DIALOG_TRIM
| SWT.APPLICATION_MODAL);
dialog.setText("Fix this session");
FormLayout formLayout = new FormLayout();
formLayout.marginWidth = 10;
formLayout.marginHeight = 10;
formLayout.spacing = 10;
dialog.setLayout(formLayout);
// position near parent window location
dialog.setLocation(lShell.getLocation().x + 10,
lShell.getLocation().y + 10);
dialog.setSize(400, 400);
// text displayed in dialog i.e. field labels
//...

// create combo box drop down selection
//final Combo combo = new Combo(dialog, SWT.NONE);
//combo.setItems (options);
//set current value to selected session's value
//...
// text displayed in dialog i.e. field labels
//...
//next text box field
//final Text startTimeBox = new Text(dialog, SWT.SINGLE | SWTSWT.BORDER );
//...
//etc...

//create cancel button
//Button cancel = new Button(dialog, SWT.PUSH);
//cancel.setText("Cancel");
//layout stuff...
//add listener for when cancel is pressed:
//cancel.addSelectionListener(new SelectionAdapter() {
// public void widgetSelected(SelectionEvent e) {
// System.out.println("User cancelled dialog");
// dialog.close();
// }
//});

//creates ok button
//Button ok = new Button(dialog, SWT.PUSH);
//ok.setText("OK");
//layout stuff...
//add listener when OK button pressed
//ok.addSelectionListener(
// nested anonymous inner class for listener, could be separate class too
//new SelectionAdapter() {

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

//when OK is pressed save the current changes to the backend
//...

// close the message dialog
//dialog.close();

//}

//});

//finish set up of dialog and call open()
//...
//dialog.setDefaultButton(ok);
//dialog.pack();
//dialog.open();
}

}



To link the EditButtonListener back to the button I created above you would modify the button creation above slightly to include adding the Listener:

button.addListener(SWT.Selection, new EditButtonListener(item));

Now when clicked the form pops up



Improvements
One way that I would like to try to improve on this design is to be able to reuse the same EditButtonListener, so instead of creating a new listener instance for each TableEditor button, I would reuse the same instance and it would determine which button was pressed from the event.item passed in to handleEvent(). I also want to remove all the dialog code from the EditButtonListener and implement that using a JFace dialog in its own class.

1 comment:

  1. Since I wrote this post I've actually implemented a number of the improvements I've written about. It certainly is easier to have a single listener, rather than one per button. It seems to help the performance as well.

    ReplyDelete