If you followed my blog closely you already know that I invested some time into makeing Eclipse Databinding available for GWT (as part of the Eclipse UFaceKit project) and because I couldn’t find a GWT-UI-Library who suited my needs I started on writing a GWT wrapper for Qooxdoo.
In my last blog entry I show a very minimal example of a UFaceKit-TableViewer. One of the major problem of the code base by then was that I had to load myriads of small tiny JavaScript files which made the start up extremely slow and unusable in a none local environment.
Though I haven’t solved the problem completely I improved the situation by using the YUI-Compressor and merging the all necessary files into as few as possible (I ended up with 6 yet). This still makes the download around 1MB (which is cached by the browser once loaded) so still a “bit” too high but for Intranet Applications acceptable.
Besiding doing this necessary downsizing of the download size I also improved the UFaceKit-Viewer support and implemented some databinding properties to observe some standard widget attributes (currently only text attributes of various widgets).
Example code
So time to show of some demo and the accompanying code. Let’s at first take a look on the following screencast I created:
It’s a simply application but it the important thing is to see how it is created.
The first important thing when it comes to databinding an GWT is that we need a domain object which informs us about attribute changes. In case of Java and Eclipse one has 2 main choices:
- JavaBeans(tm) with reflection and PropertyChangeSupport
- EMF with it’s reflective EObject-API and Adapter
where I would preferr the latter.
The problem in GWT is that one can’t use the JavaBeans because of the (out of the box) missing reflection support nor one can use EMF because there’s no port available yet (long ago I worked on such a thing but its simply a very huge task) but there’s a very minimal EMF-like domain model implementation named UBean (part of the UFaceKit project) which provides a similar reflective API than EObject does.
Here’s the example of a domain model object when using UBean:
import java.util.HashSet; import java.util.List; import java.util.Set; import org.eclipse.ufacekit.core.ubean.UArrayBean; public class MailFolder extends UArrayBean { public static final int MAILS = 0; public static final int SUBFOLDERS = 1; public static final int LABEL = 2; public static final int LAST_SELECTION = 3; private static final HashSet<Integer> SET = new HashSet<Integer>(); static { SET.add(MAILS); SET.add(SUBFOLDERS); SET.add(LABEL); } public MailFolder(String label) { set(LABEL, label); } @Override public Set<Integer> getSupportedFeatureIds() { return SET; } @SuppressWarnings("unchecked") @Override public <T> T getValueType(int featureId) { switch (featureId) { case MAILS: return (T) List.class; case LAST_SELECTION: return (T) Mail.class; case SUBFOLDERS: return (T) List.class; default: return (T) String.class; } } }
At the moment one has to hand craft those UBean classes but in future I want to provide a JET-Template to generate them from your Ecore-Model. The complete domain model has 2 more classes named Account and Mail.
The UI itself is made up from 3 classes until now:
- UMail: Which is simply the Application with its main start point
- MailFolderPart: Representing the Accounts and the folders as a Tree
- MailDetailPart: Representing the MailFolder contents and displaying the mail content
Let’s start with UMail class which is fairly simple:
public class UMail extends QooxdooApp { @Override protected void run(final QxAbstractGui application) { Realm.runWithDefault(QxObservables.getRealm(), new Runnable() { public void run() { QxPane pane = new QxPane(Orientation.HORIZONTAL); MailFolderPart folderPart = new MailFolderPart(); MailDetailPart detailPart = new MailDetailPart( folderPart.getSelection() ); application.getRoot().add( pane, QxOption.top(0), QxOption.left(0), QxOption.bottom(0), QxOption.right(0) ); pane.add(folderPart, 0); pane.add(detailPart, 1); } }); } }
I think it’s not really a miracle what the code above this. It simply splits the main window area into 2 parts so let’s take a look at the second class which makes up the left hand side folder structure.
public class MailFolderPart extends QxComposite { private WritableValue value = new WritableValue(); public MailFolderPart() { super(); setWidth(200); setLayout(new QxGrow()); TreeViewer<UBean, Collection<UBean>> viewer = new TreeViewer<UBean, Collection<UBean>>(); viewer.setLabelConverter(new LabelConvertImpl()); viewer.setContentProvider(new ContentProviderImpl()); viewer.setInput(getAccounts()); viewer.addSelectionChangedListener(new ISelectionChangedListener<UBean>() { public void selectionChanged(SelectionChangedEvent<UBean> event) { if( ! event.getSelection().isEmpty() ) { if( event.getSelection().getElements().get(0) instanceof MailFolder ) { value.setValue(event.getSelection().getElements().get(0)); } else { value.setValue(null); } } else { value.setValue(null); } } }); add(viewer.getWidget()); } public IObservableValue getSelection() { return value; } private Collection<UBean> getAccounts() { Collection<UBean> accounts = new ArrayList<UBean>(); Account account = new Account("tom.schindl@ufacekit.org"); MailFolder folder = new MailFolder("Inbox"); account.add(Account.FOLDERS, folder); Mail mail = new Mail( "QxWT example", "examples@ufacekit.org", "tom.schindl@ufacekit.org", new Date() ); /* CREATE TEST MAIL CONTENT */ folder.add(MailFolder.MAILS, mail); mail = new Mail( "Qt for UFaceKit", "tom.schindl@bestsolution.at", "tom.schindl@ufacekit.org", new Date(new Date().getTime()+60*60*1000*10) ); /* CREATE TEST MAIL CONTENT */ folder.add(MailFolder.MAILS, mail); MailFolder subfolder = new MailFolder("QxWT"); folder.add(MailFolder.SUBFOLDERS, subfolder); subfolder = new MailFolder("UFaceKit"); folder.add(MailFolder.SUBFOLDERS, subfolder); folder = new MailFolder("Templates"); account.add(Account.FOLDERS, folder); folder = new MailFolder("Sent"); account.add(Account.FOLDERS, folder); folder = new MailFolder("Trash"); account.add(Account.FOLDERS, folder); accounts.add(account); account = new Account("info@ufacekit.org"); accounts.add(account); return accounts; } private class LabelConvertImpl extends LabelConverter<UBean> { @Override public String getText(UBean element) { if( element instanceof Account ) { return element.get(Account.LABEL); } else { return element.get(MailFolder.LABEL); } } } private class ContentProviderImpl implements ITreeContentProvider<UBean, Collection<UBean>> { public Collection<UBean> getChildren(UBean parentElement) { if (parentElement instanceof Account) { return ((Account) parentElement).get(Account.FOLDERS); } else if (parentElement instanceof MailFolder) { return ((MailFolder) parentElement).get(MailFolder.SUBFOLDERS); } return Collections.emptyList(); } public UBean getParent(UBean element) { return null; } public boolean hasChildren(UBean element) { if (element instanceof Account) { List<MailFolder> rv = ((Account) element).get(Account.FOLDERS); return rv != null && rv.size() > 0; } else if (element instanceof MailFolder) { List<MailFolder> rv = ((MailFolder) element).get(MailFolder.SUBFOLDERS); return rv != null && rv.size() > 0; } return false; } public void dispose() { // TODO Auto-generated method stub } public Collection<UBean> getElements(Collection<UBean> inputElement) { return inputElement; } public void inputChanged(IViewer<UBean> viewer, Collection<UBean> oldInput, Collection<UBean> newInput) { // TODO Auto-generated method stub } } }
This code is a bit more interesting because in:
- Lines 9 – 12: Standard setting up of a TreeViewer
- Lines 13 – 25: Modifying the observable value to use in Master-Detail-Observables
- Lines 78 – 87: Setting up of a LabelConverter
- Lines 89 – 125: Setting up of a standard ITreeContentProvider
And in the end the last class:
public class MailDetailPart extends QxComposite { private IObservableValue mailObservable; public MailDetailPart(IObservableValue master) { super(); setLayout(new QxGrow()); QxPane pane = new QxPane(Orientation.VERTICAL); QxComposite selectionList = createList(master); QxComposite messageDetail = createDetail(); pane.add(selectionList, 0); pane.add(messageDetail, 1); add(pane); } private QxComposite createList(final IObservableValue master) { QxComposite comp = new QxComposite(new QxGrow()); TableViewer<Mail, IObservableList> table = new TableViewer<Mail, IObservableList>(); table.setContentProvider(new ObservableListContentProvider<Mail, IObservableList>()); TableViewerColumn<Mail> subject = new TableViewerColumn<Mail>(table,"Subject"); subject.setLabelConverter(new LabelConverter<Mail>() { @Override public String getText(Mail element) { return element.get(Mail.SUBJECT); } }); subject.setWidth(300); TableViewerColumn<Mail> from = new TableViewerColumn<Mail>(table,"From"); from.setLabelConverter(new LabelConverter<Mail>() { @Override public String getText(Mail element) { return element.get(Mail.FROM); } }); from.setWidth(200); TableViewerColumn<Mail> date = new TableViewerColumn<Mail>(table,"Date"); date.setLabelConverter(new LabelConverter<Mail>() { @Override public String getText(Mail element) { return DateTimeFormat.getShortDateTimeFormat().format( (Date) element.get(Mail.DATE)); } }); date.setWidth(200); table.setInput( UBeansObservables.observeDetailList( Realm.getDefault(), master, MailFolder.MAILS, Mail.class ) ); mailObservable = ViewersObservables.observeSingleSelection(Realm.getDefault(), table); comp.add(table.getTable()); comp.setHeight(200); return comp; } private QxComposite createDetail() { DataBindingContext ctx = new DataBindingContext(); QxDock dock = new QxDock(); dock.setSeparatorY("separator-vertical"); QxComposite comp = new QxComposite(); comp.setPadding(10); comp.setLayout(dock); comp.setBackgroundColor("white"); QxGrid layout = new QxGrid(10); layout.setColumnAlign(0, HAlign.RIGHT, VAlign.MIDDLE); layout.setSpacing(3); QxComposite header = new QxComposite(layout); header.setAllowGrowY(true); header.setMarginLeft(10); header.setMarginBottom(10); header.setMarginTop(10); comp.add(header, QxOption.edge(Edge.NORTH)); QxFont font = QxFont.fromString("12px sans-serif bold"); IQxValueProperty textProperty = QxWidgetProperties.text(); QxLabel label = new QxLabel(); label.setContent("Subject: "); label.setFont(font); label.setTextColor("#a0a0a0"); header.add(label, QxOption.rowColumn(0, 0)); label = new QxLabel(); label.setFont(font); label.setMarginLeft(10); ctx.bindValue( textProperty.observe(label), UBeansObservables.observeDetailValue( Realm.getDefault(), mailObservable, Mail.SUBJECT, String.class ) ); header.add(label, QxOption.rowColumn(0, 1)); // ------------------------------------------------ label = new QxLabel(); label.setContent("From: "); label.setFont(font); label.setTextColor("#a0a0a0"); header.add(label, QxOption.rowColumn(1, 0)); label = new QxLabel(); label.setMarginLeft(10); ctx.bindValue( textProperty.observe(label), UBeansObservables.observeDetailValue( Realm.getDefault(), mailObservable, Mail.FROM, String.class ) ); header.add(label, QxOption.rowColumn(1, 1)); // ------------------------------------------------ label = new QxLabel(); label.setContent("Date: "); label.setFont(font); label.setTextColor("#a0a0a0"); header.add(label, QxOption.rowColumn(2, 0)); label = new QxLabel(); label.setMarginLeft(10); Converter date2string = new Converter(Date.class, String.class) { public Object convert(Object fromObject) { return fromObject == null ? "" : DateTimeFormat.getShortDateTimeFormat().format((Date) fromObject); } }; ctx.bindValue( textProperty.observe(label), UBeansObservables.observeDetailValue( Realm.getDefault(), mailObservable, Mail.DATE, Date.class ), null, new UpdateValueStrategy().setConverter(date2string) ); header.add(label, QxOption.rowColumn(2, 1)); // ------------------------------------------------ label = new QxLabel(); label.setContent("To: "); label.setFont(font); label.setTextColor("#a0a0a0"); header.add(label, QxOption.rowColumn(3, 0)); label = new QxLabel(); label.setMarginLeft(10); ctx.bindValue( textProperty.observe(label), UBeansObservables.observeDetailValue( Realm.getDefault(), mailObservable, Mail.TO, String.class ) ); header.add(label, QxOption.rowColumn(3, 1)); // ------------------------------------------------ QxHtml html = new QxHtml(""); html.setOverflow(Overflow.AUTO, Overflow.AUTO); ctx.bindValue( textProperty.observe(html), UBeansObservables.observeDetailValue( Realm.getDefault(), mailObservable, Mail.TEXT, String.class ) ); comp.add(html, QxOption.edge(Edge.CENTER)); return comp; } }
This code is a bit longer but in the end for those familiar with Eclipse Databinding and JFace it should look quite straight forward:
- Line 18 – 66: Creates a TableViewer and binds the detail list (=list of mails) of the selected folder to it and observes the current selection to bind their detail values to the controls afterwards
- Line 68 – 196: Binds labels and widgets using QxWidgetProperties and UBeanObservables
UFaceKit Development
A new homepage is available which is going to example backgrounds and more about UFaceKit.
Licenses
I was not sure about this in my last blog so here’s the current status:
- Qooxdoo-Wrapper: Released under EPL
- Eclipse Databinding, Viewer and UFaceKit-API: Released under GPLv3 and UFaceKit.org Commercial License
The decision not to release everything under EPL made is necessary to setup my own server with SVN and a bug tracker. Beside the current code available already yet there’s much more we are going to provide which is currently on my workstation.
Give a try yourself
All code shown is available from the above mentionned Subversion repository but if you are not familiar with setting this up you can give it a try your own here (Please note that as mentionned above there’s still ~1 MB JS-code so application startup could be better)