Example html dialog (suite)

ui::htmldialog

#1

A few months ago Camlaman wrote an example to help write extensions using html dialog. ([Example]An example to help you how to write SketchUp Ruby code with HtmlDialog) Topic now closed.
Well it did help me mainly because it was reduce to most simple expression.
Of course this is not the way a rb file should be written (it was not intended to be such).
So as an exercice, I wrote it as an extension. And to further help, following his intention, I post the result.

module Htmlexample
  def self.box_dialog
	html_file =  File.join(__dir__, 'htmlF', 'box.html')
	  options = {
	  :dialog_title => 'Box',
	  :style => UI::HtmlDialog::STYLE_DIALOG
	  }
	dialog = UI::HtmlDialog.new(options)
	dialog.set_file(html_file)
	dialog.center
	dialog
  end
  
  def self.show_dialog
	dialog ||= box_dialog
	if dialog.visible?
	  dialog.bring_to_front
	else
	dialog.show
	end #if
	
	dialog.add_action_callback("getUserInput"){|action_context, user_input1, user_input2, user_input3|
		puts("JavaScript said user_input1 is #{user_input1}, user_input2 is #{user_input2} and user_input3 is #{user_input3}.")
		@width = user_input1.to_f
		@breadth = user_input2.to_f
		@depth = user_input3.to_f
		makebox
		dialog.close
	}
	end
	
	def self.makebox
		model = Sketchup.active_model
		entities = model.active_entities
		pts = []
		pts[0] = [0, 0, 0]
		pts[1] = [@width, 0, 0]
		pts[2] = [@width, @breadth, 0]
		pts[3] = [0, @breadth, 0]
		# Add the face to the entities in the model
		face = entities.add_face(pts)
		face.pushpull -@depth
	end

show_dialog
end #module

The referred html file, box.html, must be in a folder named htmlF, on same level as the rb file.
box.html.zip (1.1 KB)
If you put these in the plugins folder, the script will run at SU launch.

Now my next quest is html localisation.


#2

What exactly is the question?


Stylewise, some comments:

  • In Ruby, people prefer to write method names like showDialog in lowercase snake_case: show_dialog
  • HtmlDialogs/WebDialogs silently absorb exceptions because they are triggered outside of the main Ruby evaluation thread. You will not know about when the callback raises an exception (e.g. if makebox raises an exception). If you want to debug an issue why something does not work as expected, you can wrap the content of the callback in a begin rescue clause and print the exception to stderr or stdout:
dialog.add_action_callback("callbackName"){ |*args|
  begin
    # .. callback
  rescue Exception => error
    puts(error)
    puts(error.backtrace.join($/)
  end
}
  • Variables that start with @ are a special type of variables and should only be used for that purpose. These instance variables (or here module variables) stay persistently within an instance of a class or within the module, even when the method terminates. This can cause a lot of problems:

    • When you call a method again, the instance variables may already hold a value from previous invocations, leading to unpredictable behavior (hard to debug) depending on the sequence of operations before this method.
    • It is unobvious for a reader from where the value was assigned and how data flows through your application.

    For beginners it is better to keep the amount of state in classes and global state at a minimum.

    In your example, @entities should be entities. The parameters (@width, @breadth, @depth) can be passed into the method as method parameters to make the data flow more explicit:
    makebox(width, breadth, depth)
    However if using the dialog is optional, and all other times when the method is called it should default to the last used parameters, you could leave them as module variables.


#3

Thanks a lot Aerillius. Corrected.
Yes the 3 @ parameters will be ‘last used’ by default.
Wrt localization, my extensions have all used languagehandler to date (no html in them). You saw that I am a beginner with html dialog. One of my worries is what is a good way to deal with localization of rb using html.


#4

Continuing @Aerilius discussion,

I have noticed that misuse/abuse of @ and @@ variables is one of the most common flaws in code written by newbies (and sometimes by “experienced” Ruby programmers). I speculate that this misuse is for lack of adequate training in Object Oriented Programming, hence lack of understanding of what these variables are for.

These kinds of variables are meant to capture the “state” of an object. That is, the minimum amount of data required for an object to remember what has been done to it over its lifetime so as to be able to respond correctly to subsequent method calls by other objects.

I put those last words in italics, because they are key to understanding the difference between state variables and regular variables or parameters. State variables should be reserved for things that another object can’t be assumed to know but which will affect the behavior of the object receiving a method call. As @Aerilius pointed out, when non-essential data is improperly preserved in state variables, it can lead to unpredictable behavior later.

Aside from simple overuse due to lack of understanding, the most common misuse is as a means to reduce the parameter list of a method by passing some of the required data “around the side” when an object invokes one of its own methods. The fact that the object is talking to itself is not a valid justification for putting transient data into state variables instead of a method’s parameter list. They are remembered long after that transient usage! And one never knows how that method may be used later, in a context where even the current object doesn’t know when or why the value of each state variable was set.


#5

I usually prefer to insert defaults at the place of invocation of the method, not within the method (so that the method remains more generalized, predictable without side-effects, testable):
e.g. in the menu handler:

UI.menu('extensions').add_item('Make box'){
  makebox(@width, @breadth, @depth)
}

Regarding localization of HtmlDialogs/WebDialogs, LanguageHandler lacks tools to localize dialogs. This has been heavily criticized when LanguageHandler was overhastily promoted into the API without the needed rework being done.

  • You can translate static dialogs by providing html resource files for every language that you want to support and store them in your_plugin/Resources/<language code>. When you set the html to the dialog, choose the html file depending on Sketchup.get_locale
  • You can translate the dialog content dynamically with a JavaScript analog of LanguageHandler, only that you need to push the translation data to the dialog first (which you can get with LanguageHandler#strings).
    • To translate dynamic JavaScript apps, you would query every string from your JavaScript version of LanguageHandler with a method like get(string).
    • You could also translate all static html at once by traversing all text nodes and querying their translation. As an example, my translate.rb has a method Translate#webdialog(dialog) which does that.

#6

Very Interesting. I have a quite a bit to explore.
Thanks again.
Ha, and also for your Ruby Console+ extension which I find quite useful.


#7

ah… but here the defaults have to come from the html file. They are in the input form. Still have to figure that out. Maybe with an external file.


#8

Thought I’d share my study on units formats in HtmlDialog.
I did quite a bit of research and found few infos and what I found was very complicated codes (for me).
So here’s …
In Html, to get user input dimension values, first I naturally tried input of type ‘number’.
Like so. Heigh <input id='idH' type='number' value="">;

But this can only use dot as decimal separator.
We want many formats: comma, feet ’
So I use input type ‘text’ instead.
Interesting point is that if I enter letter chars before the numeric, they are not accepted and the dialog stays open having deleted them. If letters are entered after is not a problem, they will get deleted in rb.

Then, in rb,
get the decimal separator; @decim=Sketchup::RegionalSettings::decimal_separator
after callback of var, I need 3 lines per var to format it right.
@var = var
@var.sub(’.’, ‘,’) if @decim == ‘,’
@var = var.to_l # to model units

The main code can now be run.
After that I need to get the format back to be readable for the HtmlDialog since last used values
are wanted displayed on the next run and so are stored in a file.

First turn off units display, just in case, storing its state to be set back after.
@unitDispl=Sketchup.model.options["UnitsOptions"]["SuppressUnitsDisplay"]
Sketchup.model.options["UnitsOptions"]["SuppressUnitsDisplay"]=true
then
if Sketchup.model.options[‘UnitsOptions’][‘LengthFormat’] == 0
@var = var.to_s.sub(’,’, ‘.’)
else
@var = @var.to_f
end

now write var to file (defaults / last used) to be read and sent to the dialog at the beginning.
Works fine, any format any separator. Maybe there is an even simpler way ?


#9

If you are working with lengths you don’t need to care about regional decimal separator or unit display. Just use the to_s instance method on the Length object to format a String according to all the rules SketchUp follows, and use the to_l instance method on the String object to parse it back to a Length, again following all of SketchUp’s rules. You only need to care about decimal separators for non-length fractional numeric values, which in my experience are very rare in practice.


#10

Yes Julia, understood.
But the problem is not with lengths, it’s with Html reading them back (from rb) in input text. Feet (’) are giving me a lot of trouble.
Fractions are ok. like 8 1/2" .
But 2’ 8 1/2", gets truncated to 2.
I’m considering writing a method to sub '.


#11

You can pass to your HtmlDialog the lengths as string instead of number, if you want to preserve the exact textual representation (decimal separator, unit), but then the form input cannot be of type number (with buttons and scrollable).


#12

No, one line per variable calling the same method (DRY):

@var = convert_to_length(var, Sketchup::RegionalSettings::decimal_separator)
# ...
def convert_to_length(string_value, decimal_separator)
  string_value.sub('.', ',') if decimal_separator == ','
  return string_value.to_l # to model units
end

#13

Yes, that is what I did. Text input.
But a string with a ’ feet symbol in it gets truncated.
So to avoid the ', i tried writing to file as float. Surprise the float decimal is dot even if the system’s one is set to comma.


#14

No, one line per variable calling the same method

Thanks a lot, this has been on my To do list for a while!


#15

WebDialog or HtmlDialog? WebDialog’s skp: callbacks have always been bugged limited, all the reason for so many libraries that tried to properly work-around that.

Problems with quotes in strings can often be solved by escaping them:

input_value = input_value.replace(/'/, "\\'");

and maybe

encodeURIComponent(input_value)

#16

I put my data into 1 long string. I use a caret as a delimiter for key value pairs. The keys are the control’s id.
Then I replace all occurrences of single quote with a back quote. I then use the callback (WebDialog or HtmlDialog - makes no real difference the way I handle it)

args = "^adjust_panel_wd=#{door.rec.adjust_panel_wd}" <<                    # strings and lengths and integers
       "^half_height_rail=#{door.rec.half_height_rail ? '1' : '0'}" <<      # boolean
       "^stile_profile=#{@@stile_profiles_t.join('|')}=#{stile_id}"         # filling drop down lists

I put it back on the other side and then process the string updating all the controls.

I support all data input with or without units in the same format that the user wants to use.

Sending the data back is pretty much the same way. Controls are traversed using a JQuery class selector. Then I build up a string and replace all instances of a single quote to a back quote and send the data across using callback. Finally I replace backquotes with single quotes, split the string and process. I have a get_length function which converts the strings back into lengths etc.

Works on Mac and Windows and pretty much all versions of Sketchup.


#17

This is a problem that has been solved endless times. And while SketchUp fixed the shortcomings of WebDialog with the new HtmlDialog, they failed to provide a really good, complete callback API and also no compatibility library for WebDialog. The design of HtmlDialog totally lacks a complete bidirectional messaging support:

  • the callback mechanism is incomplete (JS→Ruby→JS onCompleted without return value)
  • the ability to let Ruby fetch data from the HTML dialog has been removed (on which many people rely, which leads to non-trained programmers implement horrible patterns and unmaintainable code).

The problem is: If SketchUp don’t provide an official, simple, fail-safe solution, nobody is going to reuse other developers’ solutions but building their own.

All the data passing between Ruby and HTML should be transparent to the user/developer without any extra action to do, because otherwise that would mean a source of errors.

I guess you have abstracted window.location = "skp:callbackname" and sketchup.callbackname() in a function. Very good!

But otherwise, string replacement is very error-prone, and needs all kinds of escaping if you wanted to use it for the general case.


#18

This small plugin is done (but for fine tuning)
I promise not to do another one. :slight_smile:


#19

I am using it for the general case - without any issues at all. I’m also using string replacement for my language dictionary to translate Parameter captions.

I am using htmldialog and passing large blocks of text back and forth. I essentially treat it like a token ring network and after I pass data I send back a response token.


#20

I would strongly recommend using JSON for serializing data. It allows you to get the same data back later, without any characters replaced. Also it automatically escapes all symbols that could be mistaken for its syntax, so you never need to worry about the user writing whatever symbol is used as delimiter.