Tuesday, February 19, 2008

Sharing data between SketchUp Ruby and Javascript

Posted by Scott Lininger, SketchUp Software Engineer

If you are building rich WebDialogs in your SketchUp Plugin, then you are probably using Javascript + DHTML (Dynamic HTML) to show the UI and handle user input. In practical terms, this means that you could easily have as much Javascript code in your plugin as Ruby code! But the SketchUp API documentation doesn't give much guidance on using Javascript to talk to SketchUp. What's a coder to do?

Sharing data between Javascript and Ruby can be a bit of a black art. The Ruby API provides two mechanisms that we can leverage:

1. WebDialog.add_action_callback() allows us to define a Ruby method that a WebDialog can call. This allows us to send data DOWN from Javascript to Ruby.

2. WebDialog.execute_script() allows us to run a bit of Javascript code from inside Ruby. This allows us to send data UP from Ruby to Javascript.

By using these two mechanisms together, we can create a nice, generic way for Javascript to get whatever it needs from Ruby.

Step 1: Using add_action_callback to request some data
For our example, let's imagine we have two files inside our Plugins directory: selectionInfo.rb and selectionInfo.html. Together, these files provide a new feature to SketchUp: a floating window that shows us how many objects are selected inside SketchUp.

Our first step is to show a WebDialog and establish our action callback. Here's the minimal code required to make this work:

selectionInfo.rb
# Create the WebDialog instance
my_dialog = UI::WebDialog.new("Selection Info", false, "Selection Info", 200, 200, 200, 200, true)

# Attach an action callback
my_dialog.add_action_callback("get_data") do |web_dialog,action_name|
UI.messagebox("Ruby says: Your javascript has asked for " + action_name.to_s)
end

# Find and show our html file
html_path = Sketchup.find_support_file "selectionInfo.html" ,"Plugins"
my_dialog.set_file(html_path)
my_dialog.show()


selectionInfo.html
<html>
<script>
function callRuby(actionName) {
query = 'skp:get_data@' + actionName;
window.location.href = query;
}
</script>
<body>
<h3 id="output">You have 0 things selected.</h3>
<input type="button" onclick="callRuby('pull_selection_count')" value="Refresh">
</body>
</html>


If you create these two files and save them into your Plugins directory, you'll see a very simple WebDialog the next time you restart SketchUp. Click on the "Refresh" button and you'll see a messagebox that was generated by our Ruby (but controlled based on data from the Javascript.)

Make sense? We now have a mechanism for sending data down to Ruby, which is a way for Javascript to ask Ruby a question. Now we just need Ruby to provide an answer...

Step 2: Using execute_script to send some data
The next step in our data sharing scheme requires us to respond. Let's add a few lines of code to our files... (Changed code is highlighted in yellow.)

selectionInfo.rb
# Create the WebDialog instance
my_dialog = UI::WebDialog.new("Selection Info", false, "Selection Info", 200, 200, 200, 200, true)

# Attach an action callback
my_dialog.add_action_callback("get_data") do |web_dialog,action_name|
if action_name=="pull_selection_count"
total_selected = Sketchup.active_model.selection.length
js_command = "passFromRubyToJavascript("+ total_selected.to_s + ")"
web_dialog.execute_script(js_command)
end

end

# Find and show our html file
html_path = Sketchup.find_support_file "selectionInfo.html" ,"Plugins"
my_dialog.set_file(html_path)
my_dialog.show()


selectionInfo.html
<html>
<script>
function callRuby(actionName) {
query = 'skp:get_data@' + actionName;
window.location.href = query;
}

function passFromRubyToJavascript(value) {
var message = "You have " + value + " items selected.";
document.getElementById('output').innerHTML = message;
}

</script>
<body>
<h3 id="output">You have 0 things selected.</h3>
<input type="button" onclick="callRuby('pull_selection_count')" value="Refresh">
</body>
</html>


Save these changes, and then restart SketchUp. Now when you click on the "Refresh" button, you get an updated web page showing the number of objects selected.

Looking ahead: Using JSON for more complex data
We've been working with an extremely simple example where we're passing a single number. In a real world application you would probably be sending more complex data. In this case, JSON (Javascript Object Notation) is the perfect mechanism. It's a way where you can pass nested objects and arrays in a single command.

We'll explore more about JSON in a future blog post. But for now, consider the following example.

# Passing lots of data about a component
js_command = "passFromRubyToJavascript({typename:'ComponentInstance',name:'Bryce',x:10.5,y:13.5,z:0.0})"
web_dialog.execute_script(js_command)


This kind of approach is a great way to allow Javascript to get large, structured data sets out of SketchUp whenever your application needs them. Stay tuned for a deeper exploration of this idea.

9 comments:

Anonymous said...

It's nearly impossible to read the yellow text.

Todd Burch said...
This comment has been removed by the author.
Todd Burch said...

Thanks Scott for taking the time to post an example.

Todd

J said...

Speaking technically, you can only pass String objects back and forth, right? You would pass an "array" as a delimited string, and recreate the array on the other end, if need be.

Unknown said...

Yes, that is correct. At the end of the day, you are passing strings back and forth.

J said...

I've looked at the json parser for Ruby on RubyForge. It appears even the 'pure' version of the json parser requires 'strscan', which is a binary library and is not shipped with SketchUp, plus it's not platform-independent. I'm sure your thinking about this and not planning on hacking together a makeshift parser using String#split. :)

I look forward to your next article.

J said...

Hello,

I am curious about the difference between creating a WebDialog by instantiation, as opposed to sub-classing? Pros and cons?

Thanks.

Anonymous said...

This article really helped me to give birth to Namesets - see my Google web site - many thanks, Chris

Unknown said...

Dear Sketchup engineers,

WebDialogs works fine on the Windows/IE platform.

However on Mac, it seems that the mechanism of communication between Ruby and Javascript is ASYNCHRONOUS, so that in many cases, some scripts are not executed from Ruby (via dlg.execute_script), some call backs are not triggered from Javascript.

Am I right?

And is there a way to make sure that no communication (script and call backs) is lost between Ruby and javascript, and executed in the right order.

again, this works fine on Windows, and should work the same on Mac.