Sunday, October 27, 2013

AngularJS in a Desktop app pt. 1

Why AngularJS+FX?

(I am very sorry for formatting. Will search for another blogging platform)

Bear has an UI to monitor tasks being run on the hosts and write bear script snippets. When choosing a UI framework for the Bear, I considered several options which were JavaFX, AngularJS app with Play! framework backend or an AngularJS for Desktop via the WebView in JavaFX.

Why AngularJS+FX

  • Compared to the JavaFX app AngularJS+FX has Twitter Bootstrap (including many themes) and AngularJS. HTML provides a more rapid developing experience than FXML.
  • The most rapid development
  • No need to study yet another framework (JavaFX)
  • No external dependencies on the backend
  • Migrating to a server app should not be a pain
  • A nice experience being a pioneer :-) 
Now, after having a working prototype of an AngularJS Desktop app I'm thinking - why on Earth Oracle created JavaFX 2 why didn't Oracle create nice Java bindings for the WebView. Hiring a web developer to create UI for your desktop app could be cheaper and the result would be of better quality.

Binding your JS app to Java


Prerequisites: JDK 8 ea109+. Avoid using JDK 7 for the WebViews as it has Font rendering problems.

Below is the Hello World for JavaFX which is also demo of a bug in JavaFX (at Github). 

public class TestOverloadingApp extends Application {

    private WebEngine webEngine;

    @Override
    public void start(Stage stage) throws Exception {
        final WebView webView = new WebView();
        webEngine = webView.getEngine();

        Scene scene = new Scene(webView);

        stage.setScene(scene);
        stage.setWidth(1200);
        stage.setHeight(600);
        stage.show();

        // this is a proper way to load a page from your resources
        // if your testOverloading.html references an image with <img src="images/test.jpg">
        // then your test.jpg must be placed in /javafx/overloading1/images/test.jpg     
        webEngine.load(TestOverloadingApp.class.getResource("/javafx/overloading1/testOverloading.html").toURI().toURL().toString());

        webEngine.setOnAlert(new EventHandler<WebEvent<String>>() {
            @Override
            public void handle(WebEvent<String> stringWebEvent) {
                // this is a simple way to debug your app - call alert('this will into your Java log') on the JS side
                System.out.println("alert: " + stringWebEvent.getData());
            }
        });

        webEngine.getLoadWorker().stateProperty().addListener(new ChangeListener<Worker.State>() {
            @Override
            public void changed(ObservableValue<? extends Worker.State> ov, Worker.State t, Worker.State t1) {
                System.out.println("[JAVA INIT] setting...");
                if (t1 == Worker.State.SUCCEEDED) {
                    // get the window object is a global variable in JS
                    JSObject window = (JSObject) webEngine.executeScript("window");

                    // bind our Java objects as fields in the window object
                    // in JS this would look like
                    // window.foo = new Foo() 
                    window.setMember("fooWhichIsOK", new FooWhichIsOk());
                    window.setMember("foo", new Foo());
                }
            }
        });
    }

    public static void main(String[] args) {
        launch(args);
    }   
}


HTML

In HTML you would call your provided Java instances:

<script>
function testOk(){
    // this invokes a method on a Java bean registered previously.
    window.foo.foo('test');
    // log an error
    alert('have called a method');
}
</script>

<body>
    <div id="okTest" onclick="testOk()">Run OK Test</div>
</body>

Current bindings limitations

After a number of tries I must say that the binding usage is quite limited. Below are the results of my work with JDK 8 ea109

Calling Java from JavaScript

  • Consider using only primitive types as parameters of java methods (i.e. int, char, String, arrays, String[])
  • For anything more complex parse JSON String
  • Overridden methods don't work
For JSON conversion I used facade beans which receive calls from JS and convert data to Java. It's similar to Controllers in frameworks like Grails or AngularJS.

Calling JavaScript from Java

This is pretty straight-forward. Just call it like this

webEngine.evaluate(window.yourObject.receiveJSON('{your:"JSON object here", with: "your params"}'))

Firebug Lite

You will soon notice that WebView lacks the development tools. To enable FirebugLite, add this script to your HTML:

<script src='http://getfirebug.com/releases/lite/1.2/firebug-lite-compressed.js'>

This is the only version which worked for me.

Init order

Init order is the following.
  1. Document is loaded into the WebView.
  2. $(document).ready(...) is called inside the WebEngine
  3. JavaFX change listener is called.
So your JavaFX init code is always called the last. Below is an example of how this could done.

var Java = {};

JS:
Java.init = function(window){
    Java.log("initializing Java...");
    Java.OpenBean = window.OpenBean;  //registers the bindings
};

Java.mode = navigator.userAgent.match(/Chrome\/\d\d/) ?
    'Chrome' :
    (navigator.userAgent.match(/Firefox\/\d\d/) ? 'FF' : 'FX');

Java.isFX = Java.mode === 'FX';

// this one could be merged into the previous
Java.initApp = function(){
    if(this.initialized){
        return;
    }
 
    console.log("started Java.initApp...");

    this.initialized = true;

    // further app initialisation
}

// One can mock this initApp call with jQuery.ready
$(document).ready(function(){
    Java.initApp();
});

Java:
webEngine.getLoadWorker().stateProperty().addListener(new ChangeListener() {
    @Override
    public void changed(ObservableValue ov, Worker.State t, Worker.State t1) {
    logger.info("[JAVA INIT] setting...");

    if (t1 == Worker.State.SUCCEEDED) {
        logger.info("ok");
        JSObject window = (JSObject) webEngine.executeScript("window");
        window.setMember("bearFX", bearFX);
        window.setMember("Bindings", bindings);

        logger.info("[JAVA INIT] calling bindings JS initializer...");
        webEngine.executeScript("Java.init(window);");
        logger.info("[JAVA INIT] calling app JS initializer...");
        webEngine.executeScript("Java.initApp();");
    }
    }
});


Source is available at Github

BearFX.java

Update for Kostya:

I'm being blocked by my russian censorship firewall on blogspot.ru (and I'm editing at blogspot.com), so I'll answer here - not, I haven't tried that.

6 comments:

  1. Hi, Andrey! I tried to follow your steps in such javabased-editor and i got a serious problem:
    copy and paste don't work at all.
    I simplified all code to the:

    WebView webView = new WebView;
    webView.getEngine().load("http://ace.c9.io/build/kitchen-sink.html");

    and even such a simple stuff doen't allow to do copy-paste thing.

    I'm working on MacOs with jdk8 build 109.

    Have you ever had this kind of problem in your app?

    ReplyDelete
    Replies
    1. I tested this editor http://codemirror.net/demo/complete.html in the same way and it works as expected, but i need ace because of groovy support.

      Delete
    2. Hey. Yep, I did - copy-paste is broken indeed. So I fixed it manually, by adding keyboard shortcuts:

      editor.commands.addCommand({
      name: "copyShortcut",
      bindKey: {win: "Ctrl-C", mac: "Command-C"},
      exec: function(editor) {
      //this is a call to Java
      window.bear.call('conf', 'copyToClipboard', editor.getCopyText());
      }
      });

      editor.commands.addCommand({
      name: "pasteShortcut",
      bindKey: {win: "Ctrl-V", mac: "Command-V"},
      exec: function(editor) {
      var r = window.bear.call('conf', 'pasteFromClipboard');
      editor.insert(r);
      }
      });


      So I just delegated clipboard operations to the Clipboard of JavaFX.

      Ace Editor is awesome and very popular, but it's API is not well documented - may be because it's very dynamic, so I have to google a lot to find answers. Hope this helps.

      Delete
    3. Yes, it works very well!
      I have one more question: have you resolved copy-past thing in context menu too? How to bind jafafx functions calling to the context menu?

      Delete
    4. I got it -- just disable webkit context menu and create one custom:

      webView.setContextMenuEnabled(false);
      webView.setOnMouseClicked(new EventHandler() {
      @Override
      public void handle(MouseEvent mouse) {
      if (mouse.getButton() == MouseButton.SECONDARY) {
      if (menu != null) {
      menu.hide();
      menu.show(webView, mouse.getScreenX(), mouse.getScreenY());
      }
      } else {
      if (menu != null) {
      menu.hide();
      }
      }
      }
      });

      And one more code:
      ContextMenu menu = new ContextMenu();
      MenuItem cut = new MenuItem("Cut");
      cut.setOnAction(new EventHandler() {
      @Override
      public void handle(ActionEvent arg0) {
      webView.getEngine().executeScript("copy(editor.getCopyText());editor.remove();");
      }
      });

      menu.getItems().addAll(cut, copy, paste);

      where copy is my js-proxy function to javafx.

      Delete
  2. Yep, thanks. Will use it someday before release. :-)

    ReplyDelete