SketchUp Ruby Interface to JavaScript


© 2009, Martin Rinehart

My first serious Ruby was one that used the WebDialog class to display a user interface. I learned a lot from this. The first thing I learned was that this is a very bad choice for your first Ruby! This is not the place for a beginner. Here's some other things that I learned.

Introduction

If you found this page, it's probably true that you want to know how those SketchUp WebDialogs (HTML and JavaScript) communicate with the Ruby scripts that SketchUp supports.

If you don't already know this, SketchUp software engineer Scott Lininger wrote the original explanation: Sharing Data Between SketchUp Ruby and HTML . I learned from Scott's article and you may want to try that, too. (Scott's article is much shorter than this one. If you grok the matter after reading Scott's, go straight to the gotchas at the end of this article.)

Beginning with Sample HTML

<!-- sample.html - beginning HTML for the ruby2javascript piece -->

<html>
	<head>
		<title>Sample HTML Page</title>
	</head>
	<body>

<center>

    <h1 id='heading'> Sample HTML Page </h1>
    <p>
    <button 
    	style='font-size:18pt'
    	onclick='button_clicked()'
    > Click Me! </button>

</center>

<script type='text/javascript'>
	function button_clicked() {
		alert( 'Hello, World! (from JavaScript)' );
	}
</script>

	</body>
</html>

<!-- end of sample.html -->

At this point, pick a convenient directory, copy this HTML into a file named "sample.html" in that directory and open it with your favorite browser. Click the button. If it says "Hello, World! (from JavaScript)" you are in business.

Let me draw your attention to the location of the JavaScript. Former Yahoo guru, currently Google guru Steve Souders, author of High Performance Web Sites: Essential Knowledge for Front-End Engineers, says this is where it belongs. Steve is probably the world's leading expert on this subject, so listen to him. (If that's not reason enough, I am the world's leading expert on The Bug from Hell!. Put your script elsewhere and you could be visited by The Bug from Hell!. More on this later.)

Hello from Ruby

The Sketchup Ruby API has some UI components. They are simple to use and they allow a surprisingly large range of successful Rubies. Sooner or later, however, you'll find that they don't meet every need. The general-purpose Ruby UI is a WebDialog—a combination of HTML and JavaScript that runs in a browser window.

Let's begin with a very simple Ruby.

# sample.rb

require 'sketchup.rb'

UI::messagebox( 'Hello World! (from Ruby)' )

# end of sample.rb

Copy that bit of code and save it as "sample.rb" in the directory you chose for the HTML. In SketchUp, click Window/Ruby Console. In the Ruby Console:

load '/where/you/chose/sample.rb'

Windows folk: directory separators are forward slashes in Ruby.

If it reports "Hello World! (from Ruby)" you are in business. Remember that a simple up arrow/Enter combination will reload after you make changes.

Creating a WebDialog

Creating a WebDialog is simple.
  1. Use wd = UI::WebDialog.new to create the WebDialog object
  2. Use wd.set_file to tell it where you've put its HTML
  3. Call wd.show
The WebDialog.new docs aren't quite right. The second parameter, scrollable, only works on Mac. You get scrollbars on a PC whether you want them or not.

On the PC, always supply the key argument (except for throwaways). It is catenated to "HKEY_CURRENT_USER/Software/Google/SketchUp7/WebDialog_". If it is nil or an empty string, nothing is catenated and you are sharing size/location with everyone else who omits this parameter.

Following the key there are four integer parameters: width, height, left and top.

Last is another boolean: resizeable. This should be "true" unless you have some real need to take this away from the user.

The very first time you launch your WebDialog, its size and location are entered into the registry. As your user (that's you, initially) moves/resizes the dialog, the data is updated in the registry. (This also means that you don't really care about the initial location/size params you supply to launch the dialog the very first time, as these are first-time-only values.)

That said, replace the call to UI::messagebox with the new code you see highlighted here:

# sample.rb

require 'sketchup.rb'

wd=UI::WebDialog.new( 
	"My First WebDialog", true, "", 
	400, 300, 100, 100, false )

wd.set_file( "c:/where/you/chose/sample.html" )

wd.show()

# end of sample.rb

Note that there's no registry key value here. Don't omit it when you write your first keeper.

Keep after any problems (file not found? get the typos out of the path and/or name of the HTML file) until you have successfully launched your first WebDialog. Congratulations!

You are now closing in on communication.

JavaScript to Ruby Comes First

Your JavaScript has to talk to Ruby before Ruby can talk to JavaScript. This is reasonable. Your WebDialog's UI is the one where the user will be typing or clicking to signal that something needs to be processed. JavaScript will call Ruby with a "Give me some data" or a "Change the model" message. Ruby will reply with a "Here's the data" or a "Done!" message.

We need a function in the Ruby that JavaScript can call. Before showing the code, let me explain the name "first_ac". This is your first action callback. Your final exam in this subject is writing "second_ac", for which I will show no code. Ready?

Add the new method you see here. A word about the parameters. "js_wd" is a reference to a webdialog provided by JavaScript. We'll need it when we send data from Ruby to JavaScript. "message" is any string (up to about 2kB). Omit "message" and you get an instant Bug Splat!

Here's the new code:

# sample.rb

require 'sketchup.rb'

wd=UI::WebDialog.new( 
	"My First WebDialog", true, "", 
	400, 300, 100, 100, false )

wd.add_action_callback("first_ac") do |js_wd, message|
	UI::messagebox( 'I\'m Ruby. JavaScript said: "' + message + '"' )
end

wd.set_file( "c:/models/rubies/sample.html" )

wd.show()

# end of sample.rb

Of course, having a callback isn't much fun until someone actually calls back. We'll spruce up the button click handling in the HTML's JavaScript to do this callback. The trick is to create a phony URL-like thing and drop it into the browser's location bar (which you can't see, but SketchUp can).

Here's the new JavaScript.

<script type='text/javascript'>
	function button_clicked() {
		rubyCalled( 'first_ac', 'Hi, Ruby!' );
	}

	function rubyCalled( cb_name, msg ) {
		fake_url = 'skp:' + cb_name + '@' + msg
		window.location.href = fake_url
	}
</script>

When you run this, you'll see that you've achieved communication from JavaScript to Ruby. Your messagebox will report "I'm Ruby. JavaScript said: "Hi, Ruby!". You're half-way there.

Ruby to JavaScript Completes the Discussion

That JavaScript dialog that was passed to the Ruby callback has one magic trick that completes the loop. It can execute JavaScript inside your HTML page. We'll use it to send a message back to JavaScript that will prove conclusively that we have a two-way conversation going. First, add a return function to the JavaScript:

<script type='text/javascript'>
	function button_clicked() {
		rubyCalled( 'first_ac', 'Hi, Ruby!' );
	}

	function rubyCalled( cb_name, msg ) {
		fake_url = 'skp:' + cb_name + '@' + msg
		window.location.href = fake_url
	}

	function rubyReturned( msg ) {
		alert( 'Ruby returned "' + msg + '"' );
	}
</script>

That wasn't too hard, was it? Now that you've got a function to call, use Ruby to create a script that calls it and then use the JavaScript's WebDialog object to execute it. Replace your Ruby action callback with this one:

wd.add_action_callback("first_ac") do |js_wd, message|
	reply = 'Hi, JavaScript. Thanks for saying, \"' + message + '\"!'
	script = 'rubyReturned( "' + reply + '" );'
	puts script
	js_wd.execute_script( script )
end

Why "puts script"? Maybe you get everything right on the first try. I don't. Once your code is otherwise perfect, delete that line.

Run this one and your JavaScript sends a message to Ruby. Your Ruby sends a message back to JavaScript that includes JavaScript's message and an alert box confirms that you have, in fact, achieved two-way conversation. Again, congratulations.

Finding the File

So far, you've hard-coded the file location. That won't work if you want other people to run your Rubies. My VisMap Ruby installs a trivial bit of code in the "Plugins/" subdirectory. Everything else (HTML, JavaScript, Ruby) is in "Plugins/vismap/". This is the soft-coded way of loading HTML into the WebDialog:

	
    pathname = Sketchup.find_support_file( 'vismap.html', 'Plugins/vismap/' )
    wd.set_file( pathname  )

This is the program (yes, the entire program!) that actually sits in the Plugins/ subdirectory:

	
# vismap_menu.rb - the one little bit of vismap in the PlugIns directory
# Copyright 2009, Martin Rinehart

# everything else is in Plugins/vismap/

require 'sketchup'

unless file_loaded?( "vismap_menu.rb" )
	vismap = Sketchup.find_support_file( "vismap.rb", "Plugins/vismap/" )
	UI.menu("Plugins").add_item( "VisMap" ) { load( vismap ) }
	file_loaded( "vismap_menu.rb" )
end

Gotchas

"skp:" in the pseudo-URL is hardwired. Between the ":" and the "@" is the callback name. I misread Lininger, at first thinking "skp:get_data@" was hardwired. Not so.

The WebDialog that you instantiate in your Ruby cannot execute JavaScript. The web dialog that JavaScript passes to your Ruby callback is the one that can execute JavaScript.

This one I call, with no trace of affection, The Bug from Hell. You can read all about my trials and tribulations, here. Or you can just remember that you have to fully load your HTML's body before you start chatting with Ruby. If the conversation is initiated via the body's onload() method, you will be fine on a PC (see below). If the script comes at the very end of the body, where it should be, you will be fine. Start chatting while the body's loading and your malformed JavaScript web dialog will not be able to talk to your HTML page.

PC JavaScript is synchronous; Mac JavaScript is asynch. Calling Ruby from PC JavaScript is just like a subroutine call. The script continues after Ruby returns. Calling Ruby from the Mac is just like spawning a thread. The script continues while Ruby does its thing. Consider this code:

// in JavaScript
function synchronous( args ) {
	1) do some stuff
	2) call Ruby
	3) do some other stuff that depends on Ruby finishing its work
}

That will work on a PC; fail on the Mac, as it's asynch. This will work on both:

// in JavaScript
function synch( args ) {
	1) do some stuff
	2) call Ruby
}

function ronous( args ) { // Ruby calls this one!
	3) do some other stuff that depends on Ruby finishing its work
}

The Mac requires a complete <body> section before it will do anything. If your actual body depends on getting data from Ruby, this is a problem. The trick is to have bogus HTML that displays a "Start" button. Call the Ruby when the button is pressed. (And wait for it to call back, as above.)

If you'd like to chat about this, I'm MartinRinehart at gmail dot com. Good luck on the final.

Final Exam

Add another callback ("second_ac" if your imagination's not running) to the Ruby. Add another button ("Click Me, Too!", ditto) to the HTML. Talk from JavaScript to Ruby and back. (Pure NYC-cabbie rudeness might be an amusing contrast to the first example's very nice manners.)