5 posts
Tired of JavaFX Scene Builder being run in a separate process? Fed up with no real integration between your favorite IDE and JavaFX Scene Builder? There may be a solution heading towards you. Follow this small series of blog entries to join me on my journey towards an embedded JavaFX Scene Builder in NetBeans. Welcome back (you did read the first three parts of this series?)! Ok, having done a bit of magic to Scene Builder 2.0 in the first two part of this series, let's go for some more integration. Goal of this part is to get change detection and undo/redo support working. Change detection is important to get the "Save" feature of NetBeans to work. Because the integration is actually based on reusing the fxml as a FXMLDataObject it is necessary to detect a change done in the Scene Builder and apply the result back to the FXMLDataObject. But how to detect the change. This can be done using the observable property revision from the JobManager which in turn can be retrieved from the EditorController. The JobManager tracks every action applied to the scene as a "Job" and increments the revision with each change. With this change detection can be done with the following code fragment editorController.getJobManager().revisionProperty().addListener((ov, oldValue, newValue) -> {     try {         String updatedFXMLText = editorController.getFxmlText();         EditorCookie editorCookie = dao.getLookup().lookup(EditorCookie.class);         editorCookie.getDocument().remove(0, editorCookie.getDocument().getLength());         editorCookie.getDocument().insertString(0, updatedFXMLText, null);     } catch (BadLocationException ex) {         Exceptions.printStackTrace(ex);     } }); This just reads the complete FXML as a pure text from Scene Builder and puts it back into the document managed by NetBeans. This triggers a change on the FXMLDataObject side of the world and the save logic is active. Knowing about the JobManager makes undo/redo integration quite simple. What we need from NetBeans perspective is an UndoRedo implementation, which is returned from the appropriate method of our SBFxmlMultiViewElement     @Override     public UndoRedo getUndoRedo() {         return null != undoRedo ? undoRedo : UndoRedo.NONE;     } What about the required implementation? Looking at the API's of UndoRedo and JobManager they are quite similiar. So just creating a SceneBuilderUndoRedoBridge should be fairly simple: public class SceneBuilderUndoRedoBridge implements UndoRedo {     private final JobManager manager;     private final ChangeSupport changeSupport = new ChangeSupport(this);     SceneBuilderUndoRedoBridge(JobManager manager) {         this.manager = manager;         this.manager.revisionProperty().addListener((observable) -> {             EventQueue.invokeLater(() -> {                 changeSupport.fireChange();             });         });     }     @Override     public boolean canUndo() {         return manager.canUndo();     }     @Override     public boolean canRedo() {         return manager.canRedo();     }     @Override     public void undo() throws CannotUndoException {         Platform.runLater(()-> manager.undo());     }     @Override     public void redo() throws CannotRedoException {         Platform.runLater(()-> manager.redo());     }     @Override     public void addChangeListener(ChangeListener cl) {         changeSupport.addChangeListener(cl);     }     @Override     public void removeChangeListener(ChangeListener cl) {         changeSupport.removeChangeListener(cl);     }     @Override     public String getUndoPresentationName() {         return manager.getUndoDescription();     }     @Override     public String getRedoPresentationName() {         return manager.getRedoDescription();     } } Now just make sure that the new UndoRedo support is availble by adding just one more line of code to the getVisualRepresentation()method of SBFxmlMultiViewElement     undoRedo = new SceneBuilderUndoRedoBridge(editorController.getJobManager()); That's it - change detection and undo/redo integration working! I am sure the code can be improved - just let me know and create a pull request ;-) Now you ask - can I download this magic plugin somewhere? The answer is simple: Get it from the NetBeans Plugin Portal by downloading it from here or install it via your Plugin Manager from inside NetBeans. What you will need in addition to the plugin is a fairly recent (latest) JDK8_u20 ea build. There are some issues with DnD and JavaFX-Swing-Integration which have been fixed in latest builds. Stay tuned for the next part of this series, showing .. whatever I come up with next. Any feature requests? Head over to NbSceneBuilder and let me know.
Tired of JavaFX Scene Builder being run in a separate process? Fed up with no real integration between your favorite IDE and JavaFX Scene Builder? There may be a solution heading towards you. Follow this small series of blog entries to join me on my journey towards an embedded JavaFX Scene Builder in NetBeans. Finally, with the new developer preview builds of JavaFX Scene Builder 2.0 a new important change was introduced. The new version contains the so called "JavaFX Scene Builder Kit". The Release notes state that "JavaFX Scene Builder Kit is an API that allows the integration of Scene Builder panels and functionalities directly into the GUI of a larger application, or a Java IDE, such as NetBeans, IntelliJ, and Eclipse." Having read this during Devoxx, I started looking into the  documentation how to get started with the integration. Really excited about the possibilities I fired up my IDE and started throwing together some lines of code to just open up the main view of JavaFX SceneBuilder inside NetBeans. After fiddling a bit with the threading (a lot of things still have to be executed on the JavaFX Thread) I finally got to the point where the view should open. Yes should - bummer some exception "No such Method" was blocking me from seeing the result. I asked about this problem on the OpenJFX mailing list (remember to join, if you are interested in JavaFX - it is a great source for information on what is going on). I got the confirmation that my JDK 8 ea build was too new and due to a small API change incompatible with Scene Builder 2.0 Early Access. So some more waiting.... Finally, I  got pinged a couple of days ago, that with a new build of the developer peview this problem should now be gone. Great! So back to where I left the prototype, some more fiddling and I had my first TopComponent showing the JavaFX Scene Builder main view. But somehow this still was a bit of a mess, so I decided to start going the full nine yards. NetBeans has some nice feature, it is called a MultiView. It allows you to register special views called MultiViewElement's for the same file type. So I registered my completely rewritten dirty prototype code as a MultiViewElement - and (drumroll) - see the result
The necessary code is quite simple (once you figure out some of the details ;-) ) @MultiViewElement.Registration(displayName = "#LBL_SceneBuilder",         mimeType = "text/x-fxml+xml",         persistenceType = TopComponent.PERSISTENCE_NEVER,         preferredID = "SceneBuilder",         position = 3000) @NbBundle.Messages({     "LBL_SceneBuilder=Visual" }) public class SBFxmlMultiViewElement implements MultiViewElement {     private MultiViewElementCallback callback;     private DataObject dao;     public SBFxmlMultiViewElement(Lookup lkp) {         dao = lkp.lookup(DataObject.class);         assert dao != null;     }     @Override     public JComponent getVisualRepresentation() {         final JFXPanel jfxPanel = new JFXPanel();         try {             final File fxmlFile = FileUtil.toFile(dao.getPrimaryFile());             final String fxmlText = readContentFromFile(fxmlFile);             final URL fxmlLocation = fxmlFile.toURI().toURL();             Platform.runLater(new Runnable() {                 @Override                 public void run() {                     try {                         final EditorController editorController = new EditorController();                         ContentPanelController contentPanelController = new ContentPanelController(editorController);                         final BorderPane pane = new BorderPane();                         pane.setCenter(contentPanelController.getPanelRoot());                         Scene scene = new Scene(pane);                         jfxPanel.setScene(scene);                         editorController.setFxmlTextAndLocation(fxmlText, fxmlLocation);                     } catch (IOException ex) {                         Exceptions.printStackTrace(ex);                     }                 }             });         } catch (IOException ex) {             Exceptions.printStackTrace(ex);         }         return jfxPanel;     }     @Override     public JComponent getToolbarRepresentation() {         return new JPanel();     }     @Override     public Action[] getActions() {         return new Action[]{};     }     @Override     public Lookup getLookup() {         return Lookup.EMPTY;     }     @Override     public void componentOpened() {     }     @Override     public void componentClosed() {     }     @Override     public void componentShowing() {     }     @Override     public void componentHidden() {     }     @Override     public void componentActivated() {     }     @Override     public void componentDeactivated() {     }     @Override     public UndoRedo getUndoRedo() {         return UndoRedo.NONE;     }     @Override     public void setMultiViewCallback(MultiViewElementCallback callback) {         this.callback = callback;     }     @Override     public CloseOperationState canCloseElement() {         return CloseOperationState.STATE_OK;     }     private static String readContentFromFile(File file) throws IOException {         final byte[] buffer;         buffer = new byte[(int) file.length()];         try (DataInputStream is = new DataInputStream(new FileInputStream(file))) {             is.readFully(buffer);         }         return new String(buffer, Charset.forName("UTF-8"));     } } Now you ask - can I download this magic plugin somewhere? The legal disclaimer from Oracle keeps me from redistributing the necessary SceneBuilderKit.jar. Sorry, you have to do it on your own. Get the sources from Bitbucket and try it out. The one thing you have to do is  get Scene Builder 2.0 Early Access and put SceneBuilderKit.jar into the right directory (hint there is a file indicating, which one it is). Stay tuned for the next part of this series, showing how to get the navigation integrated.