Riding the Editor Life Cycle

This post looks at how a web application can integrate with and participate in the Eclipse editor life cycle.

The example, located here, takes an Orion editor like this:

Editor in Firefox

…and embeds it in Eclipse like this:

To see the example in action use the Eclipse Open With menu option to open *.js, *.css or *.java.  Choose the “Orion Embedded Editor”.  This editor can also be set as the default for these file types in the file associations preferences page.

The same Orion editor can be opened in a web browser by opening the orion/examples/embeddededitor.html from the example plug-in in a web browser.

Why Does an Embedded Editor Need Tight Integration?

Because it sucks when a user closes an editor tab and loses unsaved changes.

There are other good reasons too.  When running in  a workbench the more a web application looks like a native editor the better.  Maybe the editor status should appear in the workbench status bar instead of the page itself.  Maybe the editor should interact with other views in the workbench.  Maybe the editor would benefit from participating in the Eclipse history view for tracking and comparing changes.  But probably it is just best to avoid being chased by mobs of angry users who have lost hours of work by accidentally closing a tab.

How to Participate in the Editor Life Cycle?

There are two hows:

  1. How do Eclipse and web applications talk to each other.
  2. How to structure a web application so that the code differences required for different hosted environments is contained and manageable.

How to Communicate

The first step is to extend EditorPart and implement some key methods:

  • createPartControl(Composite)
  • init(IEditorSite, IEditorInput)
  • doSave(IProgressMonitor)
  • setDirty(boolean)
  • isDirty()

createPartControl() needs to create an org.eclipse.swt.browser.Browser widget and set its URL to the web application as shown in line 11

On Line 8 a LocationListener is added to register and deregister browser functions as pages are loaded.  What browser functions are, and why they need to be registered only for particular pages is discussed later.

public class Editor extends EditorPart {
	// Create the editor widget
	public void createPartControl(Composite parent) {
		// Use the system's default browser
		browser = new Browser(parent, SWT.NONE);

		// Add a listener to register and unregister browser functions when pages load
		browser.addLocationListener(getLocationListener());

		// Point the browser at the Orion editor hosted on orionhub.org
		browser.setUrl("http://deanoneclipse.orionhub.org:8080/examples/embeddededitor.html");

		// Create Eclipse status line contributions
		createStatusLine();
	}
}

init() is straight forward Eclipse code that stores the arguments in fields.  Likewise, getDirty() returns the value of the boolean field dirty.  The only interesting part is that setDirty() should fire a property changed event when the dirty state changes.

// Set by the web application's editor service when the dirty state changes
protected void setDirty(boolean newValue) {
	if (isDirty != newValue) {
		isDirty = newValue;
		firePropertyChange(PROP_DIRTY);
	}
}

doSave() is interesting and will be discussed later.

Communication is a two way street

There are two mechanisms which allow Eclipse and web applications to communicate.  BrowserFunctions and the Browser.evaluate() method.

The Browser.evaluate() method allows Eclipse to call into a web applicatin and receive a response.

Browser functions allow web applications to call into Eclipse and receive a response.

Object Browser.evaluate(String javaScript)

This method causes the browser widget to execute the String javaScript within the context of the currently loaded page.  If the script’s return value is a supported type, that type will be converted to an appropriate Java Object.  If the script causes a javascript error or the return value is not a supported type the method throws an SWTException.

Since the return value is the result of executing the script, the keyword “return” must be included to receive the result of a function call.

browser.evaluate(“return isSkyBlue()”);

If a return value is not required use

boolean Browser.execute(String javaScript)

instead.  This method returns true if the javaScript executed without error instead of throwing an SWTException.

org.eclipse.swt.browser.BrowserFunction

A browser function is a way to write Java code that looks like a JavaScript function to a web application.  The web application can pass arguments of supported types to the browser function, and receive a return value of a supported type.  The web application should make sure the browser function exists before it calls it.  This check looks like:

if (typeof myFunctionProvidedByEclipse === 'function') {
	return myfunctionProvidedByEclipse("arg1", 2, "arg3");
}

Defining a BrowserFunction

To create a BrowserFunction extend org.eclipse.swt.browser.BrowserFunction

public class EditorService extends BrowserFunction {
	public EditorService(Browser browser, String name) {
		super(browser, name);
	}
}

When JavaScript calls the browser function the confusingly named method “function” is executed.

Object BrowserFunction.function(Object[] arguments)

If the web application passed in function arguments of supported types they will be converted to Java Objects and passed in the Object[] arguments.  The method can return a value of supported type to the web application.  This method runs on the UI thread.

A browser function is instantiated with a reference to a browser widget, and a name.  From that point on any web application hosted in the browser widget can refer to the browser function by the provided name.

For example after executing

new EditorService(browser, “editorServiceHandler”)

when a JavaScript program loaded in “browser” makes a function call like

result = editorServiceHandler(“foo”, “bar”, 1, 3, true)

The EditorService.function(Object[] arguments) is executed.

Once a BrowserFunction is registered with a browser widget, that function is bound as a global function.  i.e. window.editorServiceHandler().  If there is a name collision between function names the browser function may override an existing JavaScript function.  The actual behaviour depends on what order pages are loaded and may depend on the browser implementation.  There is an open SWT defect about this issue.

For this reason, it is good practice to register browser functions only when the page they are intended to work on is loaded.  That is the purpose of the LocationListener that was added in the example’s createPartControl().  The LocationListener implementation would look something like

	private LocationListener getLocationListener() {
		return new LocationListener() {
			public void changing(LocationEvent event) {
				// Do nothing before the page loads
			}

			public void changed(LocationEvent event) {
				if (event.location.contains("embeddededitor.html")) {
					// When JavaScript calls the function "editorServiceHandler" this instance of EditorService()  (a BrowserFunction subclass) is called
					editorService = new EditorService(browser, "editorServiceHandler", this);
				} else {
					if (editorService != null && editorService instanceof BrowserFunction) {
						editorService.dispose();
					}
				}
			}
		};
	}

When a Browser widget is disposed, it will dispose any browser functions still associated with it. But, if you need to remove a browser function before the widget is disposed you must call dispose directly as in line 13.

How to Structure the Web Application

The javascript code talked about here is included in the example plug-in.  It is in the file static/embeddededitor.js.

The first step in structuring the web application is to identify the behaviours that should be different depending on hosted environment.

Workbench

  • Dirty marker on the editor tab
  • Cursor position in status bar
  • Editor input from the local file system
  • Saves to the local file system (for this example).

Web Browser

  • Dirty marker in page content
  • Cursor position in page content
  • Editor input from a server
  • Saves to a server

From these points a JavaScript object “EditorService” is created and defines the services required by the editor.  The EditorService is a simple function map with an empty function “slot” defined for each of action.

  • dirtyChanged notification
  • getContentName
  • getInitialContent
  • save
  • statusChanged notification
var editorService = {
	DIRTY_CHANGED : 1,
	dirtyChanged: function(dirty) {},		// Called by the editor when its dirty state changes

	GET_CONTENT_NAME : 2,
	getContentName: function() {},			// Called to get the current content name.  A file name for example

	GET_INITIAL_CONTENT : 3,
	getInitialContent: function() {},		// Called to get the initial contents for the editor

	SAVE : 4,
	save: function(editor) {},			// Called to persist the contents of the editor

	STATUS_CHANGED : 5,
	statusChanged: function(message, isError) {}	// Called by the editor to report status line changes
};

At run time, the web application can determine its hosted environment and attach the appropriate implementations to the map.  The constants in CAPS are passed to the browser function so it can perform the appropriate action.

Before the correct implementations can be attached, the hosted environment has to be determined

// Install the appropriate editorService for the current hosting environment
if (typeof editorServiceHandler === 'function') {
	installWorkbenchHooks();
} else {
	installBrowserHooks();
}

Below is some code that defines an appropriate implementation of the statusChanged action for each hosted environment.   Line 4 shows the creation for browser hosting and line 23 shows the creation for workbench hosting.

	// Install functions for servicing browser hosted applications
	function installBrowserHooks() {
		// Register an implementation to display status changes reported by the editor
		editorService.statusChanged = function(message, isError) {
			var status;
			if (isError) {
				status =  "ERROR: " + message;
			} else {
				status = message;
			}

			var dirtyIndicator = "";
			if (editorContainer.isDirty()) {
				dirtyIndicator = "*";
			}
			dojo.byId("status").innerHTML = dirtyIndicator + status;
		};
	}

	// Install functions for servicing Eclipse Workbench hosted applications
	function installWorkbenchHooks() {
		// Register an implementation that should run when the editors status changes.
		editorService.statusChanged = function(message, isError) {
			editorServiceHandler(editorService.STATUS_CHANGED, message, isError);
		};
	}

The browser implementation builds a string from information provided by the Orion editor and appends a dirty flag if required.  The string is set in an HTML section.

The workbench implementation calls a BrowserFunction that was instantiated with the name “editorServiceHandler”.  An action ID for STATUS_CHANGED is passed along with the information provided by the Orion editor.

By using action ids the implementation only defines a single BrowserFunction.  Since the ids can be represented as an int, the function() method can use a switch statement to dispatch to an appropriate method.

public Object function(Object[] arguments) {
	super.function(arguments);

	if (arguments.length == 0 || !(arguments[0] instanceof Double)) {
		return null;
	}

	int action = ((Double) arguments[0]).intValue();

	switch (action) {
		case DIRTY_CHANGED:
			return doDirtyChanged(arguments);

		case GET_CONTENT_NAME:
			return doGetContentName(arguments);

		case GET_INITIAL_CONTENT:
			return doGetInitialContent(arguments);

		case SAVE:
			return doSave(arguments);

		case STATUS_CHANGED:
			return doStatusChanged(arguments);

		default:
			return null;
	}
}

Finally, all references to the service functions such as statusChanged are made through the initialized EditorService.  Line 9 shows where the statusChanged function is passed to the Orion editor constructor.

// Create the orion editor
editorContainer = new orion.EditorContainer({
	editorFactory: editorFactory,
	undoStackFactory: new orion.UndoFactory(),
	annotationFactory: annotationFactory,
	lineNumberRulerFactory: new orion.LineNumberRulerFactory(),
	contentAssistFactory: contentAssistFactory,
	keyBindingFactory: keyBindingFactory,
	statusReporter: editorService.statusChanged,
	domNode: editorContainerDomNode
});

Performing Save

Typically only the web application knows what it means to save itself.  If the application is configuring database servers the save will be a submit to the back end that can perform the configuration.

If, as in the example, the web application is a text editor then the behaviour may be different depending on where the application is being hosted.  An editor running in a browser will have to save its contents to a server, while an editor running in a workbench may need to save to the local file system.

Since the web application will decide what save means, Eclipse will call into the web application and ask it to perform the save.

In our example, when the editor is hosted in a workbench, the web application will have Eclipse perform the save by calling back into Eclipse.

In other words, the save action is handled the same way as the statusChanged action. Since the web application will perform different behaviour depending on its hosted environment, the save function is hooked onto the EditorService function map just as the statusChanged function was.

// Register an implementation that can save the editors contents.
editorService.save = function() {
	// This is a function created in Eclipse and registered with the page.
	var result = editorServiceHandler(editorService.SAVE, editorContainer.getContents());
	if (result) {
	editorContainer.onInputChange(null, null, null, true);
	}
	return result;
};

In this example, the contents of the editor are passed to Eclipse as a string.  If the editor model was more complicated JSON could be used serialize the model to a string and deserialize it again in Eclipse. All modern browsers and Eclipse include JSON parsers.

After the workbench reports a successful save line 6 makes a JavaScript call to the Orion editor to perform some housekeeping.  In this case, it resets its undo stack.

Initiating Save

Saves can be initiated from both the workbench and the web application.  In the example the web application provides a Save link and hooks the ctrl+s accelerator key.  In addition, the Eclipse editor framework hooks accelerators, menu items and tool bar items.

In the web application it is simply a matter of passing the the save function from the EditorService to whatever binding mechanism the web application supports.

editor.getEditorWidget().setAction("save", function(){
	// The save function is called through the editorService allowing Eclipse and Browser hosted instances to behave differently
	editorService.save(editor);
	return true;
});

When save is initiated from Eclipse the editor’s doSave(IProgressMonitor) method is called.  In this method the browser.evaluate() method is used to request that the web application perform the save.  By remembering to call through the EditorService the correct save behaviour for the hosted environment is used.

public void doSave(IProgressMonitor monitor) {
	// Evaluate a JavaScript function on the loaded page.
	// The 'return' statement is required in order to receive a return value
	try {
		Object resultObj = browser.evaluate("retrun editorServiceHandler.save()");
		// If the call to the web application returns false, indicating the save failed, cancel the operation
		if (!(resultObj instanceof Boolean && (Boolean) resultObj)) {
			monitor.setCanceled(true);
		}
	} catch (SWTException e) {
		// Either the script caused a javascript error or returned an unsupported type
		e.printStackTrace();
		monitor.setCanceled(true);
	}
}

A Better Save

The save example above is somewhat naive.  While workbench saves are typically synchronous events, a web application save is typically asynchronous and may, in fact, be long running.  Occasionally to the point of not returning at all.

The Eclipse doSave(IProgressMonitor) mechanism used above is synchronous.  Luckily Eclipse does have an asynchronous mechanism using the ISaveablePart interface.  We will look at this mechanism in a future post.

Conclusion

So this post has shown how to communicate between Eclipse and a hosted web application.  It has also suggested one way to structure parts of a web application to easily support different behaviours based on hosted environment.

Hopefully this will prove helpful, or at least provide food for thought.  Please leave some comments if you have particular requirements that would not be met by this strategy or if you have some strategies of your own you have been using.

About these ads
This entry was posted in Web UI/Workbench Integration. Bookmark the permalink.

One Response to Riding the Editor Life Cycle

  1. Bill Higgins says:

    Great blog post. I worked with some IBM Eclipse guys and IBM Jazz guys on a prototype of something like this back in 2008 so it’s great to see it becoming real.

    This might make you laugh: Here’s a blog post by Steve Northover from 2008 where he shows a really funny Mac PowerPoint fail. It’s relevant to this blog post because if you’re able to read the sideways content, you’ll see it’s actually about this topic! http://inside-swt.blogspot.com/2008/10/you-gotta-love-mac.html

Comments are closed.