Monday, April 7, 2008

Deploy simple swt application in eclipse

Once you have your program running in Eclipse, at some point your going to want to share it. It is easy to do - despite all the tutorials about Java Web Start and jnlp .xml files - you don't need a web server. You can build an old fashioned platform specific .jar file very easily.

This tutorial is for SWT v3.3 (and above? I only test on Windows & Linux)

Under your workspace/myprojectname folder create a folder called "build"
under that make a folder for each OS you want to distribute, for example:
/home/clayg/workspace/LeftTabs/build/LeftTabs-linux
/home/clayg/workspace/LeftTabs/build/LeftTabs-winxp

Get your hands on the latest SWT release for the OS you're building for:
Linux - swt-3.3.1.1-gtk-linux-x86.zip
Windows - swt-3.3.1.1-win32-win32-x86.zip

Once you extract that archive you'll see a file called "swt.jar" right in the root - that's your platform specific implementation of all the SWT classes your neat little app is using. It has to go in the working directory of the .jar you're going to create for your application.

So copy the swt.jar from the linux release into:
/home/clayg/workspace/myprojectname/build/myprojectname-linux
and copy the swt.jar from the windows release into:
/home/clayg/workspace/myprojectname/build/myprojectname-winxp

(C:\Documents and Settings\clayg\workspace\myprojectname\build\myprojectname-[platform])

Then go into eclipse. Open your project, we're going to create a manifest for your application's java archive. The manifest tells the java runtime which class to execute and what classes it depends on (I'm pretty sure that's basically what it's doing?).

Right click on your project in the Package Explorer and select New -> File. Name it myprojectname-manifest.txt (LeftTabs-manifest.txt). Fill it with the following goodness:

Manifest-Version: 1.0
Class-Path: swt.jar
Main-Class: myprojectname
[blank line]

Replace myprojectname with the name of the class that contains your main method, which is hopefully also the name of the project, and also more than likely your one and only source file? Replace [blank line] with you guessed - a blank line. I tried it without the blank line, and you really do need it - LAME.

Now right click on your project again, and this time selected Export. In the wizard, under Java - pick "JAR File" and click Next. Your project should already be selected, under "Select the export destination:" click Browse next to "JAR File"

You want to create a file called myprojectname-[platform].jar under the folder:
/home/clayg/workspace/myprojectname/build/myprojectname-[platform]
so the first time I did this for the linux platform, I ended up with
/home/clayg/workspace/LeftTabs/build/LeftTabs-linux/LeftTabs-linux.jar
the second time, when I did this again for windows, I got:
/home/clayg/workspace/LeftTabs/build/LeftTabs-winxp/LeftTabs-winxp.jar

Click Next, make sure "Export class files with warnings" is selected, click Next again.

Now choose "Use existing manifest from workspace" and click Browse. Select the myprojectname-manifest.txt, click OK. Now just hit Finish to build your application.

You need the whole myprojectname-[platform] directory to run your app.
In windows:
double click the myprojectname-winxp.jar file
In linux open a console in the myprojectname-linux folder, and run:
$java -jar myprojectname-linux.jar
from the command prompt

In the process of building my LeftTabs application for windows, I discovered a bug that didn't show up when I was building it under Linux. I believe the end result is actually a more elegant solution.

Line 374:
text.addListener(SWT.FocusOut, new Listener()
{ ...

became:
text.addListener(SWT.Deactivate, new Listener()
{ ...

The purpose of this Listener was to trigger the Modify Event for the view Widget which would tell the main application that the data in the view should be written back to the selected item in the tree. I was doing this when Text item in the view LOST FOCUS. But apparently in windows this happens after the new item is selected in the Tree - which caused the data in the tree to loose sync with the data in the view.

Anyway, vivir es aprender, I didn't know an event SWT.Deactivate existed. Works great.

Wednesday, April 2, 2008

SWT Java - Left Hand Tabs - with Drag and Drop custom treeItem.setData()

I'm a month behind my planned development because I had to build the foundation of my application before I could even start.

Here's hoping someone else gets a head start.

LeftTabs.java is a simple expandable application based on the SWT Java framework that has a "list" of "items" on the left, and on the right a "view" that will display their data.


The list is implemented by a tree. You can assign your own custom data type to each treeItem. You can create new items with a right-click pop-up menu. You can delete or drag and drop any item in the tree. Your custom data associated with the treeItem will be meticulously preserved. You can update the data in the view on the right and the changes will be synced back to your custom data type in the selected treeItem - automatically.

It's not all that surprising that more templates like this aren't available. It was rather difficult to but together. But there was lots of resources that went into making this possible. Many elements in this code are based on examples taken from: www.java2s.com


/*
* Basic template for app with left hand tabs
* and custom pane on right
* by clay.dot.gerrard.at.gmail.dot.com
*/
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;

import org.eclipse.swt.SWT;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.swt.widgets.Menu;
import org.eclipse.swt.widgets.MenuItem;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.Text;
import org.eclipse.swt.widgets.Tree;
import org.eclipse.swt.widgets.TreeItem;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.widgets.MessageBox;
import org.eclipse.swt.dnd.*;
import org.eclipse.swt.layout.*;

public class LeftTabs {

// left hand "tab" tree
static Tree tree;
// right hand viewer
static CompositePane view;
// temporary TreeItem used to hold data during Drag & Drop
static TreeItem sourceTreeItem;
// keep track of number of treeItems created
static int count = 1;

public static void main(String[] args) {
// setup the display & shell & layout
final Display display = new Display();
final Shell shell = new Shell(display);
FillLayout fillLayout = new FillLayout();
// columns
fillLayout.type = SWT.HORIZONTAL;
// break 'em up a little
fillLayout.spacing = 3;
fillLayout.marginHeight = 3;
shell.setLayout(fillLayout);

// And here's the tree
tree = new Tree(shell, SWT.BORDER);
// filled with some data
for (int i = 1; i <= 3; i++) {
TreeItemData myData = new TreeItemData();
myData.Name = "Item" + i;
myData.field1 = myData.Name + " Data";
TreeItem item = new TreeItem(tree, SWT.NONE);
count++;
item.setText(myData.Name);
item.setData(myData);
}

// And here's the right hand pane
view = new CompositePane(shell);

// right click menu on the tree
tree.addListener(SWT.MenuDetect, new Listener() {
public void handleEvent(Event event) {
Menu menu = new Menu(shell, SWT.POP_UP);
// NEW ITEM
MenuItem item_new = new MenuItem(menu, SWT.PUSH);
item_new.setText("New Item");
// when click "New Item", add an item to tree
item_new.addListener(SWT.Selection, new Listener() {
public void handleEvent(Event e) {
TreeItem[] selection = tree.getSelection();
int index;
if (selection.length != 0)
index = tree.indexOf(selection[0]) + 1;
else
index = 0;
TreeItem item = new TreeItem(tree, SWT.NONE, index);
TreeItemData myData = new TreeItemData();
myData.Name = "Item" + count++;
myData.field1 = myData.Name + " Data";
item.setText(myData.Name);
item.setData(myData);
view.setData((TreeItemData) item.getData());
tree.setSelection(item);
}
}); // end item_new event

// DELETE ITEM
MenuItem item_delete = new MenuItem(menu, SWT.PUSH);
item_delete.setText("Delete Item");
// when click "Delete Item", delete selected item
item_delete.addListener(SWT.Selection, new Listener() {
public void handleEvent(Event e) {
TreeItem[] selection = tree.getSelection();
int index = tree.indexOf(selection[0]);
for (int i=0; i < selection.length; i++){
selection[i].dispose();
}
// item deleted need new selection
TreeItem[] items = tree.getItems();
if (items.length != 0) {
if (index > items.length-1) {
// it would be better to select an item closer to the deleted item?
view.setData((TreeItemData) items[items.length-1].getData());
tree.setSelection(items[items.length-1]);
} else {
view.setData((TreeItemData) items[index].getData());
tree.setSelection(items[index]);
}
}
}
}); // end item_delete

menu.setLocation(event.x, event.y);
menu.setVisible(true);
while (!menu.isDisposed() && menu.isVisible()) {
if (!display.readAndDispatch())
display.sleep();
}
menu.dispose();
}
}); // end right click menu

// when a new treeItem is selected, update the view
tree.addListener(SWT.Selection, new Listener() {
public void handleEvent(Event event)
{
view.setData((TreeItemData) event.item.getData());
}
}); // end update view with tree item data

// when the tree looses focus make sure things are in order
tree.addListener(SWT.FocusOut, new Listener() {
public void handleEvent(Event event) {
TreeItem[] selection = tree.getSelection();
// no item selected - view data is ambigous, possible data loss
if (selection.length == 0) {
MessageBox messageBox = new MessageBox(shell, SWT.ICON_ERROR);
messageBox.setText("Possible Loss of Data.");
messageBox.setMessage("Think about what you did to cause the view data and the tree selection to get out of sync, and decide how you want your program to behave!");
messageBox.open();
//*/
TreeItem[] items = tree.getItems();
if (items.length != 0) {
view.setData((TreeItemData) items[0].getData());
tree.setSelection(items[0]);
} else {
// no items in view?! you can't delete the last item...
TreeItemData myData = new TreeItemData();
myData.Name = "Item" + count++;
myData.field1 = myData.Name + " Data";
TreeItem item = new TreeItem(tree, SWT.NONE);
item.setText(myData.Name);
item.setData(myData);
view.setData((TreeItemData) item.getData());
tree.setSelection(item);
}
//*/
}
}
}); // end tree loose focus

// setup DragSource
DragSource source = new DragSource(tree, DND.DROP_COPY);
source.setTransfer(new Transfer[] { TreeItemDataTransfer.getInstance() });

source.addDragListener(new DragSourceAdapter() {
public void dragStart(DragSourceEvent event) {
TreeItem[] selection = tree.getSelection();
if (selection.length > 0 && selection[0].getData() != null) {
event.doit = true;
sourceTreeItem = selection[0];
} else {
event.doit = false;
}
} // end dargStart

public void dragSetData(DragSourceEvent event) {
if (TreeItemDataTransfer.getInstance().isSupportedType(event.dataType))
event.data = sourceTreeItem.getData();
} // end dargSetData

public void dragFinished(DragSourceEvent event) {
if (event.doit) {
sourceTreeItem.dispose();
}
sourceTreeItem = null;
} // end dragFinished
}); // end DragSource

// setup DropTarget
DropTarget target = new DropTarget(tree, DND.DROP_COPY);
target.setTransfer(new Transfer[] {TreeItemDataTransfer.getInstance() });

target.addDropListener(new DropTargetAdapter() {
public void dragEnter(DropTargetEvent event) {
event.detail = DND.DROP_COPY;
} // end dragEnter

public void dragOver(DropTargetEvent event) {
event.feedback = DND.FEEDBACK_SCROLL;
// if the drop target is a specific item in the tree
if (event.item != null) {
TreeItem item = (TreeItem) event.item;
Point pt = display.map(null, tree, event.x, event.y);
Rectangle bounds = item.getBounds();
// give visual cue of drop location to user
if (pt.y < bounds.y + bounds.height/2) {
event.feedback |= DND.FEEDBACK_INSERT_BEFORE;
} else {
event.feedback |= DND.FEEDBACK_INSERT_AFTER;
}
} else {
// set event.item to last item in list & set feedback to after
}
} // end dragOver

public void drop(DropTargetEvent event) {
try {
if (event.data == null) {
event.detail = DND.DROP_NONE;
return;
}

TreeItem newItem;
// if the dropTarget is a specific item in the tree
if (event.item != null) {
TreeItem selection = (TreeItem) event.item;
Point pt = display.map(null, tree, event.x, event.y);
Rectangle bounds = selection.getBounds();
//TreeItem[] items = tree.getItems();
// find index of selection
int index = tree.indexOf(selection);
// insert newItem at index
if (pt.y < bounds.y + bounds.height/2) {
// insert before
newItem = new TreeItem(tree, SWT.NONE, index);
} else {
// insert after
newItem = new TreeItem(tree, SWT.NONE, index+1);
}
} else {
// no specific item selected, drop at the end
newItem = new TreeItem(tree, SWT.NONE);
}

TreeItemData myType = (TreeItemData) event.data;
newItem.setText(myType.Name);
newItem.setData(myType);
// set selection otherwise view data gets out of sync
tree.setSelection(newItem);
} catch (RuntimeException e) {
e.printStackTrace();
}
} // end drop
}); // end DropTarget

// write view data back to tree - see CompositePane.Update()
tree.addListener(SWT.Arm, new Listener() {
public void handleEvent(Event event) {
tree.getSelection()[0].setData((TreeItemData) view.getData());
}
});

// wrap it up
shell.open();
while (!shell.isDisposed()) {
if (!display.readAndDispatch())
display.sleep();
}
display.dispose();
}

} // class LeftTabs

class TreeItemData {
// all the data to be held by each treeItem
String Name;
String field1;
} // new members must be explicitly handled in nativeToJava & javaToNative

// Transfer class for handling DND of TreeItemData
class TreeItemDataTransfer extends ByteArrayTransfer {
private static final String TreeItemData_TRANSFER_NAME = "TreeItemData_TRANSFER";
private static final int TreeItemData_TRANSFER_ID = registerType (TreeItemData_TRANSFER_NAME);
private static TreeItemDataTransfer instance = new TreeItemDataTransfer();

public static TreeItemDataTransfer getInstance() {
return instance;
}

protected String[] getTypeNames() {
return new String[] { TreeItemData_TRANSFER_NAME };
}

protected int[] getTypeIds() {
return new int[] {TreeItemData_TRANSFER_ID};
}

public void javaToNative (Object object, TransferData transferData) {
if (object == null || !(object instanceof TreeItemData))
return;

TreeItemData myType = (TreeItemData) object;

if (isSupportedType(transferData)) {
try {
ByteArrayOutputStream stream = new ByteArrayOutputStream();
DataOutputStream out = new DataOutputStream(stream);
// write out each member in your custom dataType
out.writeUTF(myType.Name);
out.writeUTF(myType.field1);
out.close();

super.javaToNative(stream.toByteArray(), transferData);
} catch (IOException e) {
e.printStackTrace();
}
}
} // end javaToNative

public Object nativeToJava (TransferData transferData) {
if (isSupportedType(transferData)) {
byte[] buffer = (byte[]) super.nativeToJava(transferData);
if (buffer == null)
return null;

TreeItemData myType = new TreeItemData();

try {
ByteArrayInputStream stream = new ByteArrayInputStream(buffer);
DataInputStream in = new DataInputStream(stream);
// read in each member in your custom dataType
myType.Name = in.readUTF();
myType.field1 = in.readUTF();
in.close();
} catch (IOException e) {
e.printStackTrace();
return null;
}
return myType;
} else {
return null;
}
} // end nativetoJava

} // end TreeItemDataTransfer

// Composite widget for defining the right hand "view"
class CompositePane extends Composite {

// local TreeItemData, to keep track of changes
static TreeItemData viewData;

// declarations of pane view widgets
static Text text;

public CompositePane (Composite c) {
// setup layout of the pane
super(c, SWT.NONE);
this.setLayout(new FillLayout());

// put widgets in the pane
text = new Text(this, SWT.BORDER | SWT.MULTI | SWT.WRAP);

// add a loose focus listener to EVERY *EDITABLE* widget
text.addListener(SWT.Deactivate, new Listener()
{
public void handleEvent(Event event)
{
// sync changes in view back to dataum
Update();
}
});
} // CompositePane constructor

public void setData (TreeItemData n)
{
viewData = n;
// populate the pane's elements with the viewData
text.setText(viewData.field1);
}

// update viewData, then notify listener that it should sync to the tree
public void Update() {
viewData.field1 = text.getText();
this.notifyListeners(SWT.Modify, new Event());
}

public TreeItemData getData()
{
return viewData;
}

} // class CompositePane

I'm not happy with my options for displaying code in google blogger, and more than a little disappointed in this temporary solution. Expect a blog post to come out of that. I'm also trying to work on a secure crossplatform Snergy config that's giving me trouble so it may be worth a post.