|
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 |
|
|
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:
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 |
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.
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:
find_support_file() Plugins/lumberman folder via a Sketchup.find_support_file()
# /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.
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.
On your own, add another button that says something else. Click both until you're ready for Ruby to talk back.
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:
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>
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.
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 |
var pic1 = document.createElement( 'img' ); |
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:
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.
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
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.
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:
Now you have a vessel into which you can pour data.
each() method? If so, you can loop through it.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:
(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.
'["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.
execute_script() 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:
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 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.
|
|
|