Showing posts with label Scripting. Show all posts
Showing posts with label Scripting. Show all posts

Monday, July 8, 2019

Building UIs with EASE

You probably used EASE before to automate daily tasks in your IDE or to augment toolbars and menus with custom functionality. But so far scripts could not be used to build UIs. This changed with the recent contribution of the UI Builder module.

What it is all about
The UI Builder module allows to create views and dialogs by pure script code in your IDE. It is great for custom views that developers do not want to put into their products, for rapid prototyping and even for mocking.

The aim of EASE is to hide layout complexity as much as possible and provide a simple, yet flexible way to implement typical UI tasks.

Example 1: Input Form
We will start by creating a simple input form for address data.

loadModule("/System/UI Builder");
createView("Create Contact");

setColumnCount(2);
createLabel("First Name:");
var txtFirstName = createText();
createLabel("Last Name:");
var txtLastName = createText();
This snippet will create a dynamic view as shown below:
The renderer used will apply a GridLayout. By setting the columnCount to 2 we may simply add our elements without providing any additional layout information - a simple way to create basic layouts.

If needed EASE provides more control by providing layout information when creating components:

createView("Create Contact");
createLabel("First Name:", "1/1 >x");
var txtFirstName = createText("2-4/1 o!");
createLabel("Last Name:", "1/2 >x");
var txtLastName = createText("2-4/2 o!");
Creates the same view as above, but now with detailed layout information.
As an example "1/2 >x" means: first column, second row, horizontal align right, vertical align middle. A full documentation on the syntax is provided in the module documentation (Hover over the UI Builder module in the Modules Explorer view).

Now lets create a combo viewer to select a country for the address:
cmbCountry = createComboViewer(["Austria", "Germany", "India", "USA"])
Simple, isn't it?

So far we did not need to react on any of our UI elements. Next step is to create a button which needs some kind of callback action:
createButton("Save 1", 'print("you hit the save button")')
createButton("Save 2", saveAddress)

function saveAddress() {
 print("This is the save method");
}
The first version of a button adds the callback code as string argument. When the button gets pressed, the callback code will be executed. You may call any script code that the engine is capable of interpreting.

The second version looks a bit cleaner, as it defines a function saveAddress() which is called on a button click. Note that we provide a function reference to createButton().

View the full example of this script on our script repository. In addition to some more layouting it also contains a working implementation of the save action to store addresses as JSON data files.

Interacting with SWT controls

The saveAddress() method needs to read data from the input fields of our form. This could be done using
var firstName = txtFirstName.getText();
Unfortunately SWT Controls can only be queried in the UI thread, while the script engine is executed in its own thread. To move code execution to the UI thread, the UI module provides a function executeUI(). By default this functionality is disabled as a bad script executed in the UI thread might stall your Eclipse IDE. To enable it you need to set a checkbox in Preferences/Scripting. The full call then looks like this:
loadModule("/System/UI")
var firstName = executeUI('txtFirstName.getText();');

Example 2: A viewer for our phone numbers

Now that we are able to create some sample data, we also need a viewer for our phone numbers. Say we are able to load all our addresses into an array, the only thing we need is a table viewer to visualize our entries. Following 2 lines will do the job:
var addresses = readAddresses();
var tableViewer = createTableViewer(addresses)
Where readAddresses() collects our *.address files and stores them into an array.

So the viewer works, however we need to define how our columns shall be rendered.
createViewerColumn(tableViewer, "Name", createLabelProvider("getProviderElement().firstName + ' ' + getProviderElement().lastName"))
createViewerColumn(tableViewer, "Phone", createLabelProvider("getProviderElement().phone"))
Whenever a callback needs a viewer element, getProviderElement() holds the actual element.
We are done! 3 lines of code for a TableViewer does not sound too bad, right? Again a full example is available on our script repository. It automatically loads *.address files from your workspace and displays them in the view.

Example 3: A workspace viewer

We had a TableViewer before, now lets try a TreeViewer. As a tree needs structure, we need to provide a callback to calculate child elements from a given parent:
var viewer = createTreeViewer(getWorkspace().getProjects(), getChildren);

function getChildren() {
 if (getProviderElement() instanceof org.eclipse.core.resources.IContainer)
  return getProviderElement().members();
 
 return null;
}
So simple! The full example looks like this:
Example 4: Math function viewer

The last example demonstrates how to add a custom Control to a view.
For plotting we use the Charting module that is shipped with EASE. The source code should be pretty much self explanatory.

Some Tips & Tricks

  • Layouting is dynamic.
    Unlike the Java GridLayout you do not need to fill all cells of your layout. The EASE renderer takes care to automatically fill empty cells with placeholders
  • Elements can be replaced.
    If you use coordinates when creating controls, you may easily replace a given control by another one. This simplifies the process of layouting (eg if you experience with alignments) and even allows a view to dynamically change its components depending on some external data/events
  • Full control.
    While some methods from SWT do not have a corresponding script function, still all SWT calls may be used as the create* methods expose the underlying SWT instances.
  • Layout help.
    To simplify layouting use the showGrid() function. It displays cell borders that help you to see row/column borders.


Wednesday, December 16, 2015

A new interpreter for EASE (4): Provide modules support

EASE is shipped with modules: libraries written in Java, made available for script interpreters. The intention of such modules is to write them once and use them in all available script languages.

Read all tutorials from this series.

Source code for this tutorial is available on github as a single zip archive, as a Team Project Set or you can browse the files online. 


Introduction

Typically modules are plain java classes. We could easily create an instance in BeanShell and then call instance.method(). While this is perfectly ok for the average programmer, some of our user might expect a more simple, functional interface. What module loading does is:
  • create an instance of the module
  • inject the instance into the interpreter
  • create dynamic script code to wrap method calls
  • execute the created code
For BeanShell we would create code like
method(param1, param2) { 

result = __MOD_module_instance.method(param1, param2);

return result;

}
After loading this fragment we could access method() without the knowledge of modules, instances and object orientation at all.

Step 1: The code factory

To dynamically create code EASE needs an ICodeFactory implementation. To start you may look at existing implementations, basically we just need to build strings containing script code.
package org.eclipse.ease.lang.beanshell;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.List;

import org.eclipse.ease.Logger;
import org.eclipse.ease.modules.AbstractCodeFactory;
import org.eclipse.ease.modules.IEnvironment;
import org.eclipse.ease.modules.IScriptFunctionModifier;
import org.eclipse.ease.modules.ModuleHelper;

public class BeanShellCodeFactory extends AbstractCodeFactory {

 public static final List<String> RESERVED_KEYWORDS = Arrays.asList("abstract", "continue", "for", "new", "switch", "assert", "default", "goto", "package",
   "synchronized", "boolean", "do", "if", "private", "this", "break", "double", "implements", "protected", "throw", "byte", "else", "import", "public",
   "throws", "case", "enum", "instanceof", "return", "transient", "catch", "extends", "int", "short", "try", "char", "final", "interface", "static",
   "void", "class", "finally", "long", "strictfp", "volatile", "const", "float", "native", "super", "while");

 private static boolean isValidMethodName(final String methodName) {
  return BeanShellHelper.isSaveName(methodName) && !RESERVED_KEYWORDS.contains(methodName);
 }

 @Override
 public String getSaveVariableName(final String variableName) {
  return BeanShellHelper.getSaveName(variableName);
 }

 @Override
 public String createFunctionWrapper(final IEnvironment environment, final String moduleVariable, final Method method) {

  final StringBuilder scriptCode = new StringBuilder();

  // parse parameters
  final List<Parameter> parameters = ModuleHelper.getParameters(method);

  // build parameter string
  final StringBuilder parameterList = new StringBuilder();
  for (final Parameter parameter : parameters)
   parameterList.append(", ").append(parameter.getName());

  if (parameterList.length() > 2)
   parameterList.delete(0, 2);

  final StringBuilder body = new StringBuilder();

  // insert hooked pre execution code
  body.append(getPreExecutionCode(environment, method));

  // insert deprecation warnings
  if (ModuleHelper.isDeprecated(method))
   body.append("\tprintError('" + method.getName() + "() is deprecated. Consider updating your code.');\n");

  // insert method call
  body.append("\t ");
  if (!method.getReturnType().equals(Void.TYPE))
   body.append(IScriptFunctionModifier.RESULT_NAME).append(" = ");

  body.append(moduleVariable).append('.').append(method.getName()).append('(');
  body.append(parameterList);
  body.append(");\n");

  // insert hooked post execution code
  body.append(getPostExecutionCode(environment, method));

  // insert return statement
  if (!method.getReturnType().equals(Void.TYPE))
   body.append("\treturn ").append(IScriptFunctionModifier.RESULT_NAME).append(";\n");

  // build function declarations
  for (final String name : getMethodNames(method)) {
   if (!isValidMethodName(name)) {
    Logger.error(IPluginConstants.PLUGIN_ID,
      "The method name \"" + name + "\" from the module \"" + moduleVariable + "\" can not be wrapped because it's name is reserved");

   } else if (!name.isEmpty()) {
    scriptCode.append(name).append("(").append(parameterList).append(") {\n");
    scriptCode.append(body);
    scriptCode.append("}\n");
   }
  }

  return scriptCode.toString();
 }

 @Override
 public String classInstantiation(final Class<?> clazz, final String[] parameters) {
  final StringBuilder code = new StringBuilder();
  code.append("new ").append(clazz.getName()).append("(");

  if (parameters != null) {
   for (final String parameter : parameters)
    code.append(parameter).append(", ");

   if (parameters.length > 0)
    code.delete(code.length() - 2, code.length());
  }

  code.append(")");

  return code.toString();
 }

 @Override
 public String createFinalFieldWrapper(final IEnvironment environment, final String moduleVariable, final Field field) {
  return getSaveVariableName(field.getName()) + " = " + moduleVariable + "." + field.getName() + ";\n";
 }

 @Override
 protected String getNullString() {
  return "null";
 }
}
createFunctionWrapper() is called for each exposed method, createFinalFieldWrapper() is called for each exposed final field. The function wrapper injects preExecutionCode and postExecutionCode. This is interesting for modules implementing IScriptFunctionModifier. If such a module is loaded, it may influence code generation for all other modules. Use cases are creation of log files or unit testing.

Step 2: Register code factory

Now we need to register our code factory, so EASE is able to find it. Open the Extensions tab of org.eclipse.ease.lang.beanshell/plugin.xml. Navigate to the scriptType extension and add the code factory class to the BeanShell type.
Step 3: Bootstrapping

There exists a dedicated Environment module that provides basic commands like loadModule() to load further modules. Lets try to load it in a BeanShell:

run EASE and open a bean shell in the Script Shell View. We will create an instance of EnvironmentModule and advise it to load itself:
new org.eclipse.ease.modules.EnvironmentModule().loadModule("/System/Environment");
If your code factory works as expected, commands like print() and loadModule() should be available.

To automate this task we will add a launch extension to our BeanShell language definition.
Switch to the plugin.xml and add a launchExtension to the org.eclipse.ease.language node.We bind it to the engineID of BeanShell and need to provide an implementation:
public class BootStrapper implements IScriptEngineLaunchExtension {

 @Override
 public void createEngine(final IScriptEngine engine) {
  ICodeFactory codeFactory = ScriptService.getCodeFactory(engine);
  if (codeFactory != null) {
   StringBuilder stringBuilder = new StringBuilder();
   stringBuilder.append(codeFactory.classInstantiation(EnvironmentModule.class, new String[0]));
   stringBuilder.append(".loadModule(\"");
   stringBuilder.append(EnvironmentModule.MODULE_NAME);
   stringBuilder.append("\");\n");

   engine.executeAsync(new Script("Bootloader", stringBuilder.toString()));
  }
 }
}
Of course we could directly use the string we tried manually before, but using the code factory is somewhat nicer. The implementation given above is already available from the BootStrapper class from EASE. If you do want to add more stuff to the bootloader you need to provide your own implementation.

With that in place you should be able to load all available modules into BeanShell and use its functions.

Optional: Loading external jars

We added support to register URLs to the classloader in our previous tutorial. Now as we have modules working we may try this out by calling
loadJar("http://central.maven.org/maven2/com/github/lalyos/jfiglet/0.0.7/jfiglet-0.0.7.jar")
 true
com.github.lalyos.jfiglet.FigletFont.convertOneLine("I      love      scripting");
  ___        _                                             _         _    _               
 |_ _|      | |  ___  __   __  ___        ___   ___  _ __ (_) _ __  | |_ (_) _ __    __ _ 
  | |       | | / _ \ \ \ / / / _ \      / __| / __|| '__|| || '_ \ | __|| || '_ \  / _` |
  | |       | || (_) | \ V / |  __/      \__ \| (__ | |   | || |_) || |_ | || | | || (_| |
 |___|      |_| \___/   \_/   \___|      |___/ \___||_|   |_|| .__/  \__||_||_| |_| \__, |
                                                             |_|                    |___/ 


Wednesday, April 8, 2015

Live charting with EASE

Recently we added charting support to EASE using the Nebula XY chart widget. Say you have a script acquiring some data you now may plot them as your measurement advances.

Step 1: Installation

For this tutorial you need to register the nebula update site:
http://download.eclipse.org/technology/nebula/snapshot
in Preferences/Install/Update/Available Software Sites.

Afterwards install EASE and add EASE Charting Support to install all required modules. This component depends on Nebula Visualization Widgets, therefore we added the nebula update site before. Currently you have to stick to the nighty update site of EASE as charting is not part of the current release.

Step 2: Some example plots

 
Try out this little script to get these sample plots:
loadModule('/Charting');

figure("Simple Charts");
clear();

series("linear", "m+");
for (x = 0; x <= 20; x += 1)
 plotPoint(x, x / 10 - 1);

series("1/x", "ko:");
for (x = 1; x <= 20; x += 0.2)
 plotPoint(x, 4/x);

series("sine", "bx");
for (x = 0; x < 20; x += 0.05)
 plotPoint(x, Math.sin(x) * Math.sin(3 * x));

figure("Advanced");
clear();

series("drop", "gd");
for (t = 0; t < 2; t += 0.01)
 plotPoint(t, Math.pow(Math.E, t * -3) * Math.cos(t * 30));

series("upper limit", "rf--");
plot([0, 2], [0.4, 0.4]);

series("lower limit", "rf--");
plot([0, 2], [-0.4, -0.4]);
Run the script either in the Script Shell or execute it using launch targets.

Step 3: Charting Module API

The API of the charting module tries to keep close to MatLab style, so the format string for a series is almost identical. You may even omit to create figures or series and start plotting right away and we create everything necessary on the fly.

For a better user experience we extended the XYChart a bit by allowing to zoom directly using the scroll wheel either over the plot or its axis. Use a double click for an auto-zoom.

Sunday, February 22, 2015

Reformatting multiple source files

Recently I had to apply a new code formatter template to a project. Changing 50+ files by hand seemed to be too much monkey work. Writing a custom plug-in seemed to be too much work. Fortunately there is this great scripting framework to accomplish this task quite easily.

Step 1: Detecting affected source files

I have already blogged about EASE and how to install it. So I went directly to the script shell to fetch all java files form a certan project:
loadModule('/System/Resources');

 org.eclipse.ease.modules.platform.ResourcesModule@5f8466b4
files = findFiles("*.java", getProject("org.eclipse.some.dedicated.project"), true)
 [Ljava.lang.Object;@1b61effc
Now we have an array of IFile instances. Easy, isn't it?

Step 2: The formatter script

Ideas how to format source files can be found in the forum. Taking the script and porting it to JS is simple:
function formatUnitSourceCode(file) {
 unit = org.eclipse.jdt.core.JavaCore.create(file);

 unit.becomeWorkingCopy(null);

 formatter = org.eclipse.jdt.core.ToolFactory.createCodeFormatter(null);
 range = unit.getSourceRange();
 formatEdit = formatter
   .format(
     org.eclipse.jdt.core.formatter.CodeFormatter.K_COMPILATION_UNIT
       | org.eclipse.jdt.core.formatter.CodeFormatter.F_INCLUDE_COMMENTS,
     unit.getSource(), 0, unit.getSource().length(), 0, null);
 if (formatEdit.hasChildren()) {
  unit.applyTextEdit(formatEdit, null);
  unit.reconcile(org.eclipse.jdt.core.dom.AST.JLS4, false, null, null);
 }

 unit.commitWorkingCopy(true, null);
}

Step 3: glueing it all together

Load the function into your script shell, fetch the list of files as in step 1 and then process all files in a loop:
for each (file in files) formatUnitSourceCode(file);
That's it. This short script uses your current source formatter settings and applies it to all selected files.

This little helper is available online in the EASE script repository.

Thursday, August 21, 2014

EASE - Eclipse Advanced Scripting Environment launched

It's been a while since I last wrote about EASE. Not writing about it does not mean that we were lazy. We applied for an official eclipse project and were busy setting up the infrastructure.

So please welcome the new Eclipse citizen: EASE.


What it does for you

EASE allows to write, maintain and execute scripts right within your IDE. Executed scripts run in the context of the Eclipse JRE, allowing to access all Java classes in your Eclipse universe. Thus you can manipulate and extend your IDE without the need to write plugins, pack them into features, export them into a p2 repository, install, restart, ...

As accessing Java code from script languages is typically an annoying task ("If I could write Java code I would do it in Java, why scripting?") EASE provides extension points to encapsulate typical actions into simple script commands. Basically it allows to create wrappers in the target script language to access Java methods. We already started writing some useful modules. The Modules Explorer view gives a short overview of the available commands (hint: try DND).


You already have a nice API to use? Great, just wrap() it from your script or register it via extension point.

Scripts may include other scripts using URIs. You could even access your scripts using http.To register script locations check your preferences in Preferences/Scripting.

Current UI integration gives access to a nice interactive shell to play around with, script recording and launch support.



Right, where do I get it from?

You find the update site locations on our webpage. We are focusing on Eclipse Luna, so if you are using something older and things do not work as expected, let us know.


Contribute? It's easier than you thought

Contribution is not only about writing code. Just test EASE and let us know what you think of it. Fill the bugtracker with ideas, design icons, write help, provide sample scripts - there are so many things that need a little care.


What's next

This summer we received a great contribution via Google Summer of Code. Martin Kloesch developed a Jython debugger which will be available on the update sites soon.

UI integration to bind scripts to menus and toolbars is basically working, but needs some tweaking.

A set of core modules needs to evolve. Currently we are playing around with the functions, but we need a stable script API for those modules rather soon.

... and I know we have to write lots of help and tutorials to make this project useful for you out there.

Friday, October 25, 2013

Eclipse scripting on steroids

A few months ago I released eScript, a scripting framework for the Eclipse IDE. Since then a lot of things happened. eScript was chosen by Polarsys to be used as a scripting layer for their products. So we joined forces and the project flourished and learned new tricks every day.

Now we have jython integration working, a full featured JavaScript debugger (which allows to access native Java classes used in your scripts) and an awesome UI integration. We not only have launchers for workspace files, but the possibility to attach scripts directly to context menus.



To add a new feature to eclipse you now write a short script, hook it into the UI and use it immediately without even restarting. It has never been so easy to extend Eclipse functionality.



Furthermore we pushed the project to eclipse and found a home at the e4 incubator. If you ever were interested in scripting your IDE or RCP this is your chance to contribute, propose features or simply try it out and amaze us with what you can do with EASE!


Interested? See





Or even better:

attend my "Scripting, baby!" session at EclipseCon Europe next Wednesday at 5pm.

Wednesday, July 31, 2013

Introducing EScript - scripting for your IDE

During the last years I was playing around with a scripting engine supporting JavaScript that is directly integrated in Eclipse. This engine is used as a basis for my RCPs to allow users to create their own workflow.

I thought this might come in handy for others too, so I raised bug 365133. In the meantime I was able to extend the framework and added other interpreters as well. Currently Rhino, JRuby and Jython are supported (some better, some worse).

Hoping to get feedback from the community (you!) I release this project into the wild. Check out the project homepage, or dive directly into the source at github.


"What can I do with such an engine?", you may ask.
At my company we use it to automate test equipment, create test patterns and accumulate test results. Therefore we need to commuinicate with lots of lab equipment like oscilloscopes, RFID readers and so on. As our work flows are very dynamic, we provide a script environment along with command sets to easily access these devices. Our users may then create their dedicated tests by implementing their own scripts.

By creating libraries (which are independent of the used interpreter!)  you may extend the script languages with your own functionality. Find an example at the EScript homepage.

You may also access any class available in your IDE and directly call methods on it.


All source is available under EPL terms. Drop me a line if you find it useful or need some support.
Please be aware that despite the Rhino implementation is available for quite some time, other interpreters were added just recently. So be prepared for some surprises :)