Creating a Custom Editor inside FemtoIDE - Tutorial Part 1

After a lot of trial and error using an example provided by @FManga I have successfully figured out how to create a custom editor for a project. This example allows you to create some files purely within your project’s directory and doesn’t require modding FemtoIDE in any way (ie. you don’t need to have people copy stuff into the plugins directory for this to work).

Step 1: Determine a custom file extension for your data

The first trick to this template is to determine what file extension you want to register for your custom editor. This example uses the extension “CE” (ie. somefile.ce) but you can choose any extension you’d like.

Step 2: Create the script that hooks your editor into FemtoIDE

Create a “scripts” folder inside your project folder
Create a new javascript file CustomEditor.js inside the scripts folder (you can replace “CustomEditor” with a name for each specific editor you wish to create).
Add the following javascript to this file:

CustomEditor.js
//!APP-HOOK:addMenu
//!MENU-ENTRY:Custom Editor

if(!APP.customEditorInstalled()){
  APP.log("Adding custom editor");
  
  // list of file extensions this view can edit
  const extensions = ["CE"];
  
  // path to the html to load
  // file path will be concatenated after the "?"
  const prefix = `file://${DATA.projectPath}/editor/editor.html?`;
  
  // add extensions for binary files here
  Object.assign(encoding, {
    "CE":null
  });
  
  class CustomEditorView {
    
    // gets called when the tab is activated
    attach(){
      if( this.DOM.src != prefix + this.buffer.path )
        this.DOM.src = prefix + this.buffer.path;
      this.DOM.contentWindow.readFile = this.readFile;
      this.DOM.contentWindow.saveFile = this.saveFile;
    }
    
    // file was renamed, update iframe
    onRenameBuffer( buffer ){
      if( buffer == this.buffer ){
        this.DOM.src = prefix + this.buffer.path;
      }
    }
    
    readFile(filePath, mode)
    {
      return window.require("fs").readFileSync(filePath, mode);
    }
    
    saveFile(filePath, data)
    {
      window.require("fs").writeFileSync(filePath, data);
    }
    
    constructor( frame, buffer ){
      this.buffer = buffer;
      this.DOM = DOC.create( frame, "iframe", {
        className:"CustomEditorView",
        src: prefix + buffer.path,
        style:{
          border: "0px none",
          width: "100%",
          height: "100%",
          margin: 0,
          position: "absolute"
        },
        load:function(){
        }
      });
    }
  }
  
  APP.add(new class CustomEditor{
    
    customEditorInstalled(){ return true; }
    
    pollViewForBuffer( buffer, vf ){
      if( extensions.indexOf(buffer.type) != -1 && vf.priority < 2 ){
        vf.view = CustomEditorView;
        vf.priority = 2;
      }
    }
    
  }());
  
}

On line 8: const extensions = ["CE"]; replace "CE" with a list of extensions you want your editor to handle.

On line 16: "CE":null replace “CE” with any extensions that are binary files (additional ones can be added with a as standard json type array.

On line 12: const prefix = file://${DATA.projectPath}/editor/editor.html?; replace /editor/editor.html with the location of your editor’s html file relative to the project path (don’t forget to leave the ? at the end as the path of the file to edit will be appended here)

You can define additional custom functions by adding them after the saveFile function definition
This is only necessary if the function requires access to the node.js routines (such as file i/o)
Example:

myFunction(myParameter1, ...) // functions can have zero or more parameters
{
  //Do stuff here
}

Then hook it to your editor by adding this.DOM.contentWindow.myFunction = myFunction; after the line: this.DOM.contentWindow.saveFile = this.saveFile;

Step 3: Create your custom editor

Create an “editor” folder in the root of your project (you can name it something else just remember to adjust the above script to point to the correct location)

Add the following template html file to the “editor” folder

editor.html
<!DOCTYPE html>
<html lang="en">
  <head>
  </head>
  <body onload="">
    <form>
      <button type="button" onclick="load()">Load</button>
      <button type="button" onclick="save()">Save</button><br/>
      <input type="text" id="testText" value="">
    </form>
    <script src="editor.js"></script>
  </body>
</html>

Add the following template javascript to the “editors” folder:

editor.js
function load()
{
  var filePath = window.location.search.substr(1);
  document.getElementById("testText").value = window.readFile(filePath, "utf-8");
}

function save()
{
  var filePath = window.location.search.substr(1);
  window.saveFile(filePath, document.getElementById("testText").value);
}

Step 4: Create some files to edit

For this demo you can create the following text file at the root of your project:

bacon.ce

Need more bacon!

Step 5: Open the file inside FemtoIDE

Clicking the “bacon.ce” file from inside FemtoIDE should open it with the custom editor which currently looks like this:


Clicking the “Load” button should read the contents of the “bacon.ce” file into the text field below
Clicking the “Save” button should save the contents of the text input to the “bacon.ce” file (you can verify this by opening the “bacon.ce” file in an external text editor)

Step 6: Create your own custom web-based editor

From here it’s just a matter of using whatever html5 magic you like to create a full custom editor and making calls to window.readFile(filePath, mode) to read files and window.saveFile(filePath, data) to write files.

How to actually use things like html5 canvas to make a custom editor is beyond the scope of this part of the tutorial, but from here on out there’s nothing more you need to worry about with utilizing FemtoIDE as everything else can be done with pure HTML5, CSS, and JavaScript.

EDIT: If you want to read the contents of a binary file simply call window.readFile(filePath) without providing a mode or pass null for mode.

EDIT 2: Modified the instructions to change where to hook the readFile and saveFile functions to the contentWindow (previous method only works the first time the editor is opened).

7 Likes

An excellent tutorial. This has been a missing piece of information in Femto: How to make UI plug-ins. Good work!

2 Likes

Thanks a lot! This will definitely be extremely valuable

@tuxinator2009 Good work! You might want to give this a try, I haven’t tested it. It should allow you to call require("fs") from within the iframe.

this.DOM.contentWindow.require = requireWrapped;

function requireWrapped(file) {
    return wrap(require(file, null));
    function wrap(obj, that){
        if(typeof obj == "function") return (...args) => obj.apply(that, args);
        if(!obj || typeof obj != "object") return obj;
        let w = Array.isArray(obj) ? [] : {};
        if(Array.isArray(obj)){
            for(let value of obj)
                w.push(wrap(value, obj));
        } else {
            for(let key in obj)
                w[key] = wrap(obj[key], obj);
        }
        return w;
    }
}
1 Like

Haven’t tested this yet, but one thing I like about the above approach is it’s easy enough to test for window.readFile and window.saveFile and if they’re undefined then revert to the normal Open File Dialog and Download File routines supported in the browsers (though I guess you could do the same with window.require. The benefit there is the editors can be very easily setup to work as a plugin within FemtoIDE or as standalone editors using any web browser. As an example my Battle Animation Editor can be used within FemtoIDE now, but can also be used standalone in a normal web browser (which in some ways can be easier to debug and test without having to setup the NWJS SDK).

On that note though setting up the NWJS SDK version wasn’t terribly difficult and allows for better debugging of custom editors, but the browser method requires less setup so it might be easier for others.