Updated Embedded Orion Editor

I’ve updated the embedded Orion editor example to use the latest version of Orion.  As well I’ve provided a P2 repository containing the pre-built plug-in for anyone that just wants to use the Orion editor in Eclipse.

This updated example should work better on Mac and Unix.

Links to the update example and the P2 repository are here.

Posted in Web UI/Workbench Integration

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.

Posted in Web UI/Workbench Integration | 1 Comment

Intercepting Links

When a web application runs inside an Eclipse workbench it would be nice if links in the application could do interesting workbench actions.  Opening a new view or a wizard, for example.  It would be nicer still, if the web application’s HTML or JavaScript could remain unchanged.

Luckily intercepting links is easy to do with:

org.eclipse.swt.browser.Browser.addLocationListener(LocationListener listener)

Here is code from a ViewPart implementation that creates an instance of org.eclipse.swt.browser.Browser and adds a new LocationListener to it:


    public void createPartControl(Composite parent) {
        browser = new Browser(parent, SWT.NONE);
        browser.setUrl(Perspective.gmailURL);

        // Hooks the link intercept code
        browser.addLocationListener(new LinkInterceptListener());
    }

The implementer must also provide an implementation for the interface:

org.eclipse.swt.browser.LocationListener

LocationListener specifies two methods:

  1. changing(LocationEvent event)
  2. changed(LocationEvent event)

changing() is called before the browser widget opens a new location and provides a mechanism for preventing the browser from doing so.  changed() is called after the browser has opened the location.

A sample implementation of a LocationListener might look like:

	/**
	 * Implement a LocationListener to intercept links and decide what to do.
	 */
	private class LinkInterceptListener implements LocationListener {
		// method called when the user clicks a link but before the link is opened.
		public void changing(LocationEvent event) {
			try {
				// Call user code to process link as desired and return
				// true if the link should be opened in place.
				boolean shouldOpenLinkInPlace = !openView(event.location);

				// Setting event.doit to false prevents the link from opening in place
				event.doit = shouldOpenLinkInPlace;
			} catch (PartInitException e) {
				e.printStackTrace();
			}
		}

		// method called after the link has been opened in place.
		public void changed(LocationEvent event) {
			// Not used in this example
		}
	}

Line 10 gets the URL the browser is attempting to open and calls the implementer’s method to determine what to do based on the link.  Line 13 sets the event field that controls whether the browser opens the location in place or not.  The implementer could decide to open their own view and let the browser open the link in place, but I suspect this is not typical.

It is also useful to remember that the LocationListener is invoked each time the browser’s location changes.  This includes when content is first loaded by a call to browser.setURL(), redirects in a loaded page and the user clicking a link.

And finally, a simple example of inspecting the links and taking an apporpriate action.  You will notice that this method is returning true when it intercepts a link and opens a workbench element.  The caller above is setting event.doit to the negation of the return value.  That is, the browser should open the link in place if the link was NOT intercepted.

	private boolean openView(String location) throws PartInitException {
		// Open a view
		if (location.equals("http://www.google.com/intl/en_CA/mobile/mail/#utm_source=en_CA-cpp-g4mc-gmhp&utm_medium=cpp&utm_campaign=en_CA")) {
			IViewPart newView = getViewSite().getPage().showView("url.link.1");
			((LinkView) newView).setURL(location);

			return false;
		// Open a wizard
		} else if (location.contains("/accounts/recovery")) {
			BasicNewFileResourceWizard wizard = new BasicNewFileResourceWizard();
			wizard.init(getSite().getWorkbenchWindow().getWorkbench(), new StructuredSelection());
			WizardDialog dialog = new WizardDialog(getSite().getShell(), wizard);
			dialog.create();
			dialog.open();

			return false;
		}

		// Do not intercept link.  Allow browser widget to open link in place
		return true;
	}

In a real implementation there would likely be a better mapping strategy than an if-else statement.

Runnable Example

There is a working example of this code can be checked out from CVS.  This page has the details.the following location:

Discussion

Going back to my first post, this is where the feedback is needed.

To me, this appears to be all that is needed for intercepting web application links in a meaningful way.  But am I right?  Without specific domain knowledge about the types of web applications the community is trying to embed in the Eclipse workbench I have no way of knowing if this simple approach is sufficient or if more is required.  Sometimes less is more, sometimes less is just … less.

For example, I made the assumption that it is beneficial if the web application does not change.  Since the implementer is creating a new workbench view, it was natural to encapsulate all the link mapping in the workbench code.

Does anybody want to make an argument for a contribution mechanism whereby a web application can make API calls to register interceptable links?  My gut tells me no, but perhaps your experience tells you yes.

Posted in Web UI/Workbench Integration | 2 Comments

Integrating Web UIs with the Workbench – Opening Salvo

Obviously this is just MY opening salvo, and not THE opening salvo.  Over the last several years, Boris and many others have posted several articles that address Web UIs in general, Web UIs in the Workbench and even Web UIs as the Workbench.  Here are just a few:

A common occurrence with these kinds of post is that the comments inevitably end up being a (heated) discussion about whether or not people want their Workbenches to be Web Apps.  Some of the discussions even get down to the thin client vs fat client debate and whether we are racing back towards the 70′s when modern-day desktop computers can outperform servers of that era.

These are, I’m sure, very good questions.  Certainly there are very smart people making very compelling arguments for both sides.  Adding my voice to that debate isn’t going to help anyone.

The fact is that people – lots of them – are building Web UIs.  Other people – lots of them — are building Eclipse based applications.  And some of these people are combining the two and want to have a better, more consistent, user experience.  Hopefully providing some tips and techniques, and a place to discuss the problems, requirements, solutions and implementation details, will help at least some people.

The next thing you should know is: I’m really not very smart.  And I don’t mean, “I’m really not very smart” in the self-deprecating “I do brain surgery in my spare time” way.  I mean it in the “frameworks confuse and scare me” way.  I like simple things, and they like me (Hi Karen).   And that is likely all you will ever see in these posts.  Simple solutions for simple people.  Or, more accurately, simple solutions for really smart people who need to get this stuff done so they can move on to more interesting/profitable/cool problems.

Over the next little while, with your help, I’m going to be trying to solve some of the Web UI Workbench integration issues I believe are important.  I really WILL need your help to get that right.  So far my career has been based around building IDEs for workstations and security stacks for embedded Java run times, so my domain knowledge in the Web UI space is limited.  I’ll be making assumptions about what is important in this space and how people are organizing their Web UIs .  Perhaps what I think is important really isn’t.  Perhaps you’ve solved the same problem I’m discussing in a better way.  Perhaps I’ll get a few things right.  I’d like to hear about all of it.

Without your help there is no way I can be relevant or helpful.  And I really do want to be helpful.  Helping you ship products and be happy makes me happy.  Win-win.

So please, comment the hell out of my posts.  Let me know what you are doing with your Web UIs and what you would like to be able to do in the Workbench … but if you want to talk about the inherent merit of Web UIs vs. Workbench UIs or if Eclipse 3.x is better than e4 you will have to find someone smarter than me.  I bet Boris or McQ would love to hear from you.

Thanks for reading this content-free post.  Now that the ground rules have been set …

Coming up soon: Intercepting Links

Posted in Web UI/Workbench Integration