We opened the second section of this tutorial with the pre-Craftsman Honeymoon House, a bungalow built in what would become the Craftsman four-square style.

We close with this Craftsman four-square built in the Craftsman bungalow style.

Middletown, NY, c. 1920

City four-square built in bungalow style.

Edges to Rubies The Complete SketchUp Tutorial


Corner of VisMap Ruby plugin.

Chapter 20—Model-Dependent WebDialogs

This is a WebDialog from my VisMap Ruby plugin. It makes it easy handle visibility in models that have lots of layers and scenes. VisMap launches two WebDialog windows. The first shows which layers are visible in which scenes:

VisMap layer/scene visibility map.

VisMap scene list.  

The second window lists the scene's names.

These are examples of WebDialogs that cannot be prepared until the Ruby API is used to get the names of all the layers and scenes, as well as the visibility data.

VisMap was my first non-trivial Ruby plugin. Among the things that I learned while writing it was that your first Ruby plugin should not be a model-dependent one. There was no tutorial, like this one, to guide you through the minefield. We'll take this one small step at a time and, hopefully, you'll be happily coding your model-dependent dialogs without having to call the medics.

Before you get started,

Read About
and Install
VisMap


Getting Started

In the last chapter, our lumberman dialog gathered facts and sent them to Ruby when the user asked for a board. Let's go into this in a bit more detail. First, you want JavaScript to start the conversation. When your Ruby says wd.show() or wd.show_modal() you launch a new thread (or process) that goes about firing up a browser and displaying an HTML page. This will take a while.

If Ruby attempts to execute a script in your JavaScript before your HTML page is ready, the attempt will fail and no error will be thrown. Trying to debug this has been known to lead to hair loss as frustrated developers tear theirs out. The only safe way to initiate a conversation is to do so from the JavaScript end.

When we get to VisMap, you'll see that the first thing the HTML displays is a big button saying "Get Data from Model". That's a bit dumb. Why doesn't the Ruby just send the data? The reason is that waiting for a button click means that Ruby cannot send data until the JavaScript in the HTML page is ready. JavaScript says, "Data please." Ruby says, "OK, here's the data." Ruby must never initiate the conversation.

The "conversation" consists of JavaScript calling an action_callback routine in the Ruby code. Ruby then executes a script in the HTML page. Let's take a look at some code. Type a complex program like the following. Save it in your convenience directory. Load it in your console.

# /r/chatterbox.rb

require 'sketchup'

UI.messagebox( 'Ruby here' )

Got that running? Add a web page. Open it in your favorite browser.

<!-- chatterbox.html -->

<html> <body>

<h1> Chatterbox Here! </h1>

</body> </html>

Now you've got some very simple Ruby and HTML, not working together. Next, we want the Ruby to launch the HTML as a WebDialog. Lose the messagebox and add this (substituting your own folder path).

# /r/chatterbox.rb

require 'sketchup'

wd = UI::WebDialog.new( 'Chatterbox', true, 'chatterbox',
    500, 300, 0, 0, true )
    
wd.set_file( 'c:/r/chatterbox.html' )

wd.show_modal()

You should get something like this:

Dialog saying "Chatterbox Here!".
This is fine, but hard-coding the path will get you a dialog on your machine only. Let's fix that.

Without find_support_file()

Last chapter we got HTML from the Plugins/lumberman folder via a Sketchup.find_support_file() call. This time you've got the Ruby and HTML in the same folder. Setting a file that's in the same folder as your Ruby is done this way in your Ruby:

# /r/chatterbox.rb

require 'sketchup'

wd = UI::WebDialog.new( 'Chatterbox', true, 'chatterbox',
    500, 300, 0, 0, true )
    
pathname = File.expand_path( File.dirname(__FILE__) )    
pathname = File.join( pathname, 'chatterbox.html' )
wd.set_file( pathname )

wd.show_modal()

__FILE__ is the string you type after the load call in your console. File.dirname() gets just the path. (That may be relative or absolute. It may or may not include a drive specification on a PC.) File.expand_path() expands relative paths to absolute ones and, on a PC, prefixes with a drive specification. File.join() combines pathname pieces with the correct separator for your system.

JavaScript to Ruby Conversation

To get chatting, we need an action callback in the Ruby, and we need to call it from JavaScript in the HTML page. The Ruby requires a code block, which you saw without explanation in the last chapter. I'm leaving it without explanation in this chapter, too. Just do it this way and trust that it will work.

# /r/chatterbox.rb

require 'sketchup'

wd = UI::WebDialog.new( 'Chatterbox', true, 'chatterbox',
    500, 300, 0, 0, true )
    
pathname = File.expand_path( File.dirname(__FILE__) )    
pathname = File.join( pathname, 'chatterbox.html' )
wd.set_file( pathname )

wd.add_action_callback( "ruby_ac" ) do |dlg, msg|
    puts msg
end

wd.show_modal()

Now we add a button and a function that the button will call into JavaScript in the HTML:

<!-- chatterbox.html -->

<html> <body>

<h1> Chatterbox Here! </h1>

<button onclick='call_ruby( "ruby_ac", "Hi, Ruby!" )'> Say Hi! </button>

<script type='text/JavaScript'>

function call_ruby( callback_name, message ) {
    location = 'skp:' + callback_name + '@' + message
}

</script>

</body> </html>

That will get this fancy dialog, that reports "Hi, Ruby!" in your console when you click its button.

Dialog with "Say Hi!" button.

On your own, add another button that says something else. Click both until you're ready for Ruby to talk back.

Ruby Talks to JavaScript

In practice, Ruby will call a function in the JavaScript, passing it some data. We can get started with something simpler. Let's just call an alert to display a message.

wd.add_action_callback( "ruby_ac" ) do |dlg, msg|
    message = 'JavaScript said, "' + msg + '."'
    script = "alert( '" + message + "' );"
    puts script
    wd.execute_script( script )
end

From experience, if your call to execute_script() is more complex than the one shown here, debugging will be impossible. The puts script above will show you the exact script that you are trying to execute, which will help you correct your code. These messages within messages things are just about impossible to get right the first time. Your quotes won't properly match. Worse, MSIE will give you a response like this:

MSIE bogus error message.

Do you like that line number? I do wish Google had picked a better browser.

Now let's go on to send some data from Ruby to a JavaScript function. This means a simpler action_callback.

wd.add_action_callback( "ruby_ac" ) do |dlg, msg|
    script = 'from_ruby( "1, 2, 3" );'
    puts script
    wd.execute_script( script )
end

And add this little addition to your JavaScript:

function from_ruby( data ) {
    alert( 'Ruby sent: ' + data );
}

</script>

JavaScript reports on data it received.  

With that, JavaScript reports on the data it received.

Congratulations. Two-way conversation is a Good Thing. Now we're on to the next part of the problem which is writing JavaScript that, given data from Ruby, creates your web page.

Building the DOM with JavaScript

DOM, Document Object Model, is the standard specified for interfacing your JavaScript and the page(s) you want to manipulate. It is also the specific structure of your page. For instance, you have a heading and two buttons, if you've not added more on your own. Let's use HTML and JavaScript to write Ruby's data into our page.

Generally, you use document.createElement( 'tag name' ) to create an HTML element in JavaScript. The JavaScript name is one of the tags you would add to fixed HTML as <tag>. After you create the element, you add attributes. Last, when you are ready to go, you append the element as child of a parent. If it doesn't otherwise have a parent (it would just be part of the document's <body> section you append it as a child of document.body.

Here's a sample:

HTML JavaScript
<img
    id='pic1'
    alt='Alternate text here.'
    border=0
    src='images/pic1.jpg'
>
var pic1 = document.createElement( 'img' );
    pic1.id = 'pic1';
    pic1.alt = 'Alternate text here.';
    pic1.border = 0;
    pic1.src = 'images/pic1.jpg';
document.body.appendChild( pic1 );

Got the idea? Let's put it into practice. Add the changes here to create a new div on your chatterbox page. Note: a call to document.getElementById() returns null if there isn't an element with the given ID.

function from_ruby( data ) {

    var data_div = document.getElementById( 'data_div' );

    if ( data_div == null ) {
        data_div = document.createElement( 'div' );
        data_div.id = 'data_div';
        data_div.style.backgroundColor = '#e0ffff';
        data_div.innerHTML = 
            '<center><h1> New Div Here! </h1> </center>';
        document.body.appendChild( data_div );
    }
    
    data_div.innerHTML += data + '<br>';

} // end of from_Ruby()

</script>

With those changes, you get this:

Chatterbox with new div added in JavaScript.

You now know enough to build anything, even tables, in JavaScript. (Well, you would if I told you that you can't add rows to your table element. Technically, you need a <tbody> inside the table and you add your rows to the <tbody>. Fortunately, there are table-specific methods that you can use to simplify table building.

First, though, a word of warning: actually building elements in JavaScript tends to be tedious. Look closely at the center table.

Center table in simple VisMap display.

Note that it switches color and draws a line every three rows. It also draws a line every five columns. (Did you know that you can specify borders separately for top, left, bottom and right? Seldom used, but just right here.) Here's pseudo-code for the main portion of the table:

for each layer
    insert a "whole layer" checkbox
    insert the layer name
    for each scene
        insert a checkbox

Here's the actual code:

for ( rw = 0; rw < nrows; rw++ ) { // array rows

    row = itbl.insertRow(rw+2);
    cell = row.insertCell(0); // layer checkbox
    cell.style.background = colors[ rc ];
    cell.style.borderLeft = '1px solid #000000';
    add_checkbox( cell, rw, 'layers_cb', handle_layer_click );
    if ( (rw%3) == 0 ) { cell.style.borderTop='1px solid #000000'; }
    if ( rw == (nrows-1) ) { 
        cell.style.borderBottom='1px solid #000000'; }

    cell = row.insertCell(1); // layer name
    cell.align = 'left';
    cell.id = rw + '_layerName';
    cell.style.fontSize = (size_cb + 2) + 'px';
    cell.style.background = colors[ rc ];
    if ( (rw%3) == 0 ) { cell.style.borderTop='1px solid #000000'; }
    cell.innerHTML = top.vismap.model.sortedLayers[rw].name;
    if ( rw == (nrows-1) ) { 
        cell.style.borderBottom='1px solid #000000'; }

    for ( cl = 0; cl < ncols; cl++ ) { // each scene
        cell = row.insertCell(cl+2);
        cell.style.background = colors[ rc ]; 
        if ( cl == 0 ) { cell.style.borderLeft = '1px solid #000000'; }
        if ( (rw%3) == 0 ) { cell.style.borderTop='1px solid #000000'; }
        add_checkbox( cell, rw*ncols + cl, 'rc_cb',	handle_click );
        if ( (cl%5) == 4 ) { cell.style.borderRight='1px solid #000000'; }
        if ( rw == (nrows-1) ) 
            { cell.style.borderBottom = '1px solid #000000'; }
        if ( cl == (ncols-1) ) 
            { cell.style.borderRight = '1px solid #000000'; }
    }

    if ( (rw%3) == 2 ) { rc = (rc==0) ? 1 : 0; } 

} // end of loop over layers

Simple enough? I thought not.

Building Tables with JavaScript

The original VisMap was built with a table that held header, inner table (the actual checkbox table) and footer. Second generation the outer table became a frameset. (Advantage to frames: the user can drag the border to shrink header and footer to almost nothing, using all possible space for the checkbox array.)

The original inner table is still called itbl although now it's the one and only checkbox table. You create a table as if it were any other HTML element. Here's how VisMap creates itbl:

    itbl = document.createElement( 'table' );
        itbl.align = 'center';
        itbl.style.backgroundColor = '#f0f0ff';
        itbl.border=0;
        itbl.cellPadding=0;
        itbl.cellSpacing=0;
        itbl.style.height='100%'; itbl.style.width='100%';
    
    ... table populated here, then
    
    div = document.getElementById( 'middle_row' );
    while ( div.hasChildNodes() ) { // empty the div
        div.removeChild( div.firstChild );
    }
    div.appendChild( itbl );

Note the two lines near the bottom that empty the div. There are two new methods and one new property there that all do exactly what their names promise. You'll need them if the user can change the model in ways that will invalidate your UI. (Here, the user could add new layers, for one example.)

Two more methods, table.insertRow( index ) and row.insertCell( index ) are all you need to build tables (and forever after forget that the rows are really attached to the <tbody>). These methods also do just what their names promise. If you insert rows and cells in sequential order, starting with index = 0 you will get along well with table building. (Yes, you could build out of sequence, too. You're on your own for that one.)

Now let's revisit VisMap's code, stripping out all the messy details. Row zero holds a checkbox for each scene. Row one shows a scene number. Rows 2 and higher are the layer rows.

    for ( rw = 0; rw < nrows; rw++ ) { // array rows

        row = itbl.insertRow(rw+2);

        cell = row.insertCell(0); // layer checkbox
        add_checkbox( cell, rw, 'layers_cb', handle_layer_click );

        cell = row.insertCell(1); // layer name
        cell.innerHTML = ... // layer name;

        for ( cl = 0; cl < ncols; cl++ ) { // each scene
            cell = row.insertCell(cl+2);
            add_checkbox( cell, rw*ncols + cl, 'rc_cb',	handle_click );
        }
        
        if ( (rw%3) == 2 ) { rc = (rc==0) ? 1 : 0; } 

    } // end of loop over layers

As the code shows, if you want to add text it's easiest to just write innerHTML. But what about adding something more complicated, like a checkbox? Here's the add_checkbox() method that hides those details.

/* In HTML:
    <input id='42_cell' type='checkbox' style='height:12px; width:12px;'
        onclick='JavaScript:...'>
*/

function add_checkbox( cell, i, id_suffix, handler ) {
    var cb = document.createElement( 'input' );
    cb.id = i + '_' + id_suffix;
    cb.type = 'checkbox';
    cb.style.height = size_cb + 'px';
    cb.style.width = size_cb + 'px';
    cell.appendChild( cb );
    cb.onclick = handler;
    

I've highlighted the non-obvious line: the checkbox is appended as a child of the cell.

Writing Some HTML

Ready to write some code? To make life simple, keep on with the chatterbox ruby and HTML. Let's get a list of layer names from the model and display them in your WebDialog.

For the next step, hard-code a div id='data_div'. (That code that does it in JavaScript? Creating divs in JavaScript is the hard way, unless your WebDialog's divs depend on model data.) Otherwise, don't worry about the JavaScript. We'll rewrite it to deal with real data.

Begin by editing your HTML so that it will look something like this:

Beginning HTML for showing layer names.

Now you have a vessel into which you can pour data.

Writing Some Ruby

Your next challenge is, in Ruby, to create an array of all the layers' names. Use your reference browser to ferret the needed information out of the API, using these hints. Open any model that has multiple layers. Then, in your console, drill down until you have the right collection. Get the first element with myLayer = ...[0]. (If I weren't trying to explain this to you, I wouldn't call it myLayer. Something much shorter, say a single letter, would work. Can you read the difference between letter l and numeral 1 in your browser?)

In your Ruby code, add a small function that loops through the layers, pushing layer names onto an array of names. Empty the code part of your action_callback. In your action_callback, call this function. puts names into your console. Mine says:

Console with list of layer names.

(Why do my names have a digit prefix? Just a little trick to force them into the order I want.) Now we're ready to do some JSON.

Writing Some Json

If we could turn these names into a valid JSON array—a valid JSON array is a valid JavaScript array, JSON is JavaScript—life would be very easy on the JavaScript end. Using my names, this is what we want:

'["Layer0", "0_basement", "1_first_floor",... ]'

Write a little utility that converts a list of names into a JSON array. Your grade is "A" if it works. ("F" if you put a comma after the last name. I'd cut you more slack, but JavaScript won't.) An "A+" if you escape embedded quotes. ("F" if you don't but your names actually have embedded quotes.) Call that utility and puts the result in your action_callback. It should look like this:

load '/r/chatterbox.rb'
[ "Layer0", "0_basement", "1_first_floor", "2_second_floor", "3_attic" ]

Give this your best beady-eyed, skeptical squint. If you prefix it with var names = will it be valid JavaScript? If so, congratulations. You're turning a list of names into valid JSON. (If not, fix it!) Don't go telling everyone that this is pretty unsophisticated stuff. Let 'em think that it's a work of brilliance. Thank Doug Crockford for noticing JSON and thank JavaScript's inventor Brendan Eich for the actual notation.

Writing Some JavaScript to execute_script()

Now back to your JavaScript. Empty the from_ruby() method. Make it report the message that your Ruby will send. Mine looks like this:

function from_ruby( data ) {

    alert( data );
    
} // end of from_Ruby()

In your action_callback, turn your names into a script (prefix the array with from_ruby( ' and suffix it with ' );. puts your script to check it; then wd.execute_script() to run it. This is the result you want:

JavaScript alert box showing the correct JSON.

Ready to write a JSON interpreter in your JavaScript? I've built mine inside the from_ruby() method. It looks like this:

function from_ruby( data ) {

    var names;
    eval( 'names = ' + data );
    alert( names );
    
} // end of from_Ruby()

That should give you this:

The array of layers in JavaScript.

The eval() method accepts and executes JavaScript statements. Its use is very much frowned on, so don't overdo it. However, right here it's perfect. If the data is valid JSON, eval( 'some_var = ' + data ) is your whole JSON interpreter. Please don't tell anyone how simple this is. I should also mention that Ruby has an eval() method that makes JavaScript to Ruby data just as easy.

And that wraps up this chapter, and this tutorial. Congratulations. I hope you've enjoyed it.

"What's that in back?" The question is, "We're not done. What do we do next?" Hey, I'm done. You should be able to write a list into a div. You get a "B" for making it work, writing innerHTML, separating layer names as text lines with "<br>" tags. You get an "A" for writing the text in input boxes; an "A+" for a nice dropdown list.

Good luck!

Oh yeah. One more thing before I forget. Our JavaScript calls Ruby and then it's done. Later on, Ruby will call back with an execute_script() and JavaScript will go back to work. That's the only way to do it. On a PC, calling Ruby is synchronous—it waits for a response. On a Mac it's async—the JavaScript keeps right on truckin'. The only safe thing to do after calling Ruby is to wait for Ruby to call back. You want a well-mannered conversation, not one where both parties are talking at once.

See you in the SketchUcation Developer's Forum.


Screenshot of JavaScript Console in operation. View of apartment contents. Transformation matrix diagram.