WebDialog / HtmlDialog Tutorial using sketchup-bridge
SketchUp has two classes for creating UI dialogs:
-
The deprecated
UI::WebDialog
using the operating system’s browser engine.
JavaScript functions can be called withdialog.execute_script(string)
and Ruby callbacks are triggered withwindow.location = 'skp:' + callbackname + '@' + parameter_string
which has limitations with maximal URL length, Unicode encoding/character loss and requires the developer to manually perform (de)serialization and parameter splitting. -
The new
UI::HtmlDialog
using an embedded Chromium browser with modern JavaScript.
JavaScript functions can be called withdialog.execute_script(string)
and Ruby callbacks are triggered withsketchup.callbackname(...parameters)
, which now allows any amount of JSON-compatible parameters.
WebDialogs had several problems that are deeply covered in the Lost Manual. With HtmlDialog, developers still face two major difficulties that cause people to spend over and over again development time to build their own solutions instead of just building extensions:
-
There is not yet a direct foreign function invocation from Ruby to JavaScript (analog to JavaScript to Ruby:
sketchup.callbackname()
). While developers can useexecute_script
, they have to take care every single time about encoding parameters properly into a valid JavaScript string. -
Continuous control flow is still broken into pieces because of asynchronicity. While it is possible to invoke a function and pass data from either side, it is not easy to communicate back and forth in a continuous manner (like synchronous code):
JavaScript→Ruby→JavaScript→…
sketchup.callbackname(...parameters, { 'onCompleted': function () {} })
allows to invoke a JavaScript function after the Ruby callback completed, but it neither transfers the Ruby return value nor does it give feedback about success/failure.
I want to propose a library that I have been using for several years and that I think has worked well enough for me that it can serve others as well. You don’t have to use it, but if you stumble across hurdles with working directly with HtmlDialog, think about it.
Idea
My idea was to use Promises. Promises help us to deal with asynchronous programming.
- Compared to the callback function pattern (callback at the end within the parameters list like
onCompleted
), callbacks are attached onto the returned promise object, which avoids clashes in the parameters list. - Promises provide two feedback channels for success and failure. So the developer can decide whether to handle errors (or some errors) on the Ruby side or JavaScript side.
- Promises work with modern JavaScript
async
andawait
.
Using WebDialogs/HtmlDialogs
If you wanted to implement JavaScript→Ruby→JavaScript
you would probably have done something similar to this:
WebDialog
function myFunction1 () {
// ...
window.location = 'skp:compute_area@' + width + ',' + length;
// You cannot get the computation result here and continue
// (like displaying it in the UI).
}
dialog.add_action_callback('compute_area') { |dlg, parameter_string|
width, length = parameter_string.split(',').map(&:to_f)
# If this fails, the error goes unnoticed.
result = compute_area(width, length)
# You could create invalid JavaScript if you
# concatenate strings "myFunction2(" + result.to_s + ")"
dlg.execute_script("myFunction2(#{json.generate(result)})")
}
function myFunction2 (result) {
// How to identify to which HTML element this result belongs to?
// This is out of the context of myFunction1,
// we cannot access variables to HTMLElements.
document.getElementById('myElement').value = result;
}
HtmlDialog
function myFunction1 () {
// ...
sketchup.compute_area(width, length);
// You cannot get the computation result here and continue
// (like displaying it in the UI).
}
dialog.add_action_callback('compute_area') { |dlg, width, length|
result = compute_area(width, length)
# You could create invalid JavaScript if you
# concatenate strings "myFunction2(" + result.to_s + ")"
dlg.execute_script("myFunction2(#{json.generate(result)})")
}
function myFunction2 (result) {
// How to identify to which HTML element this result belongs to?
// This is out of the context of myFunction1,
// we cannot access variables to HTMLElements.
document.getElementById('myElement').value = result;
}
Using the sketchup-bridge library
Instead of making your Ruby code push data to the dialog, you make the dialog fetch data from Ruby whenever it needs. That means the Ruby part acts like a server, and the dialog is the client.
function myFunction1 () {
// ...
Bridge.get('compute_area', width, length).then(function (result) {
// The callbacks have access to all variables in the scope of myFunction1.
// This makes it practical for object-oriented programming, e.g. if you have
// a whole table of elements to update.
document.getElementById('myElement').value = result;
}).catch(function (error) {
alert('The computation failed: \n' + error);
});
}
dialog.on('compute_area') { |deferred, width, length|
result = compute_area(width, length)
deferred.resolve(result)
}
Using modern JavaScript (HtmlDialog with Chromium 55+ / ECMAScript 2017) it gets simplified to:
function myFunction1 () {
// ...
let result = await Bridge.get('compute_area', width, length)
document.getElementById('myElement').value = result
}
which gets rid of all the boilerplate and looks and reads like synchronous code.