Customizing Komodo with the UI SDK

Komodo User Interface (UI) SDK

For the past ~3 years, we here on the Komodo Dev team have been gradually creating a new SDK for customizing and developing Komodo. As part of this process, we have also migrated legacy code into the new SDK to make it easier to access.

This is a pretty exciting time to be a developer on the Komodo team as there is so much room for growth. If we need a new feature or ready access to some legacy code, we can often create a new SDK to fill that need.

The SDK is chock full of handy functionality but I'm here to write about one major part, the ko/ui SDK, which gives you an easy way to customize your Komodo user interface by building your own UI elements.

If you haven't already, download Komodo IDE to follow along with this tutorial.

ko/ui/...

This has to be one of the more ambitious additions to our SDK. The idea came from Nathan, our lead dev, who wanted to start transitioning away from XUL whilst also improving on the limitations of needing a dedicated markup file separate from your code (think userscripts). He wanted to be able to create any UI element with a single line of code. And now you can.

Almost all UI elements created in the Komodo core as of Komodo 10 have been built using ko/ui/.... For example, the entire Startup Wizard, Project Wizards and Tutorial tool interface are dynamically generated UI elements.

On top of making elements easier to create, we've created APIs for certain elements that just made sense. For example, when you've created an new menu...


var colourMenu = require("ko/ui/menu").create()

...You can quickly add all your needed menu items using addMenuItems:


colourMenu.addMenuItems(["red","green","blue"]);

...And if you forgot one, use addMenuItem to append it:


colourMenu.addMenuItem("pink");

As we go through this blog, you can run Javascript commands in the JS Console in Komodo to see what they do and play around as we go. To get to the JS console, use View menu > Tabs & Sidebars > Console.

ko/ui... Structure

These modules utilize object inheritance, so we don't have to write a ton of duplicate code. The main base "classes" (they aren't real classes but for purposes of this blog they can be) are element & container. You don't need to ever worry about the element modules directly, but instead would use modules that inherit its functions. I just want to give you a more in-depth understanding of the tool you're about to use. The container module, on the other hand, can be used directly:


require("ko/ui/container").create() // creates the equivalent of a 'div' in XUL

Pro-tip: If you're using ko/ui/element directly you're going to have a really bad time.

ko/ui/element

Everything in ko/ui inherits from ko/ui/element. This is the module that allows Komodo ko/ui to so easily create new elements as it contains the create function, with a lot of magic inside. It in turn wraps many functions from ko/dom, so if you're familiar with that module or have used jQuery before, you'll find many useful functions in all objects you create from using ko/ui.

In the cases where we don't provide the exact function you require and need to do some custom hacking on the DOM, we provide an element property (AKA the JS DOM) and an $element property, which is the ko/dom object for your element.

Example output:


var button = require("ko/ui/button").create("click me");
console.log("SDK Object:");
console.log(button);
console.log("DOM Object:");
console.log(button.element);
console.log("ko/dom module object:");
console.log(button.$element);

ko/ui/container

This module, as the name implies, is used by anything that can contain other UI elements. Lists and Menus are an exception, but this blog is already going to be long enough...let's not dive into that. We'll just leave it at "Lists and Menus are special".

Any object that uses ko/ui/container has access to 4 basic functions:

Basic example:

For all of the following:

  • Takes a ko/ui/element object or nothing.
  • Creates a container (except add, it's just a ko/ui/element) and returns it.


// A panel is a container so let's use that
var panel = require("ko/ui/panel").create();
// Adds a row. 
var panelRow = panel.addRow(require("ko/ui/button").create());
// Adds a col.
var panelCol = panel.addColumn();
// Adds a groupbox to compartmentalize groups of elements.
var panelGBox = panel.addGroupbox();
// Just add any old element then return it
var panelElement = panel.add();

We'll be using a more robust example below so you can see what this creates in the next section.

For a great example of this code in use, see startupWizard.js:getPageUI().

Using ko/ui/...

The great thing about this module is that it can be as easy or complicated as you want. You can either quickly instantiate (eg. var panel = require("ko/ui/panel").create()) an element, then have its contents dictate its size. Alternatively, you can pass in an attributes object that contains everything from an array of menu items, width & height attrs, class lists, and any other attributes attributed to the element you are creating.

Demo Time

I'll stop talking so much now and just keep it to code and comments. You can either follow along, copy and pasting lines into the JS console, or create a new userscript, paste the code below into the snippet, then run it to see what it does. Feel free to modify it as you like:


// create your "container"
var panel = require("ko/ui/panel").create();

// Now some interactive UI elements
var checkbox = require("ko/ui/checkbox").create("Active");
var menuLabel = require("ko/ui/label").create("Colours");
var menu = require("ko/ui/menulist").create();
var listLabel = require("ko/ui/label").create("Width");
var listbox = require("ko/ui/listbox").create();
var button = require("ko/ui/button").create("Done");
var groupbox = require("ko/ui/groupbox").create({caption:"ko/UI Examples"});

// Add them to the panel in rows
panel.addRow(groupbox);
groupbox.addRow(checkbox);
groupbox.addRow([menuLabel,menu]);
groupbox.addRow([listLabel,listbox]);
groupbox.addRow(button);

// What's a menu or listbox without items?
var menuitems = ["blue","red","purple","green"];
menu.addMenuItems(menuitems);
// Set a value to be selected in the list
menu.value("blue");
var listitems = ["100","200","300","500"];
listbox.addListItems(listitems);

// It's not fun if it doesn't do anything.
// Let's write some event handlers.
var checkBoxChg = () =>
{
    if(!checkbox.checked())
    {
        panel.$element.attr("width", listbox.value());
        panel.$element.attr("style","background:"+menu.value());
    }
    else
    {
        panel.$element.attr("width", "250");
        panel.$element.attr("style","background:#28282e");
    }
}
checkbox.off("click", checkBoxChg);
checkbox.on("click", checkBoxChg);
// Close the window when we're done.
button.on("click",()=>{panel.remove()});
// Ok let's see it!
panel.open();

Why Should I Care? USERSCRIPTS

This may seem like it’s only really helpful for the development team (and OMG is it ever) but this SDK allows you to create UI elements without actually having to load XUL windows. This means you can now quickly generate custom UI elements from your userscripts! Not too long ago I wrote a blog about the userscripts that I used as a proof of concept for the Share on Slack feature. The rest of the UI built for that feature was done with the UI SDK and could all have been implemented in more userscripts. You don’t have to use AddOns for UI! This wasn’t really possible before this SDK was created.

Summary

The ko/ui SDK should be robust enough to perform the UI work you need. As I said in the intro, all new UI has been done using the the UI SDK and some legacy interfaces have been migrated.

For more information, see our SDK documentation. And download Komodo IDE to get your UI work done in whatever languages you use - fully functional free trial.

I hope you enjoyed this brief introduction to the Komodo UI SDK. There is so much more to learn in this module, as well as the rest of the Komodo SDK.

Stay tuned for more blogs about the Komodo SDK.

P.S. Licensing

Just a heads up to anyone reading this and sampling the code above: all code in this blog is licensed under the MIT license so feel free to use it how you like.