Has a best practices alternative method been set for transferring values from a dialog to an extension? I was storing values in hidden inputs and moving those to the extension with the get_element_value method on the WebDialog.
Yes.
In the UI::HtmlDialog
class, the add_action_callback()
instance method will convert between Js and Ruby types automatically.
In the old UI::WebDialog
class, your webpages could only send String data via a protocol handler.
The UI::HtmlDialog
class does not use the protocol handler to send data to Ruby.
That said, you could still likely do it, thus defining a singleton method upon your HtmlDialog instance:
EDIT: Proven not to work! The UI::HtmlDialog
classâ execute_script()
method returns nil
.
Non-working attempt ...
@dlg.define_singleton_method(:get_element_value) {|id|
self.execute_script("document.getElementById('#{id}').value")
}
Or, you can create your own subclass of UI::HtmlDialog
, thus:
module Archetris
class NiftyDialog < UI::HtmlDialog
def get_element_value( id )
self.execute_script(
"document.getElementById('#{id}').value"
)
end
end
end
EDIT: For a working example see âŚ
Thanks, Dan. Thatâs incredibly helpful.
@Dan: question is though: is it best practice to continue storing values in hidden elements OR just use the callback to send data from JS to Ruby instead?
The storing of values, and whether they are hidden or visible is not really the issue anymore. (You can still have hidden values, stored in hidden html elements, or even better stored in Javascript objects.)
It is the passing of values to the Ruby side is the more important concept.
So yes, best practice going forward will be to use the new sketchup
Javascript object, which will convert on the Ruby side to the correct class of Ruby object.
No more chopping up big strings into little strings and then convert the strings to arrays and numbers, etc.
I just showed the above code as a workaround for getting old code to work without a large overhaul.
the method #execute_script is defined to return nil
Yes, and the API docs are never wrong âŚ
If the above does not work,⌠then try something like this:
EDIT: This does not work either, See below.
(Collapsed as this example will also not work.)
module AuthorTest
@@loaded ||= false
class NiftyDialog < UI::HtmlDialog
def initialize(*args)
super(*args)
@ready ||= {}
@returned_values ||= {}
add_action_callback('returned_value') {|ac,id,value|
@returned_values[id]= value
}
add_action_callback('element_value_ready') {|ac,id|
@ready[id]= true
}
end
def get_element_value( id )
@ready[id]= false
self.execute_script(
"sketchup.returned_value('#{id}',document.getElementById('#{id}').value,
{ onCompleted: function() {
sketchup.element_value_ready('#{id}');
}
});"
)
until @ready[id]
sleep(0.1)
end
@returned_values[id]
end
end # custom HtmlDialog subclass
def self::open
self::reset()
@dlg = NiftyDialog::new(dialog_title: "Test")
@dlg.set_html('
<!DOCTYPE html>
<html>
<head>
</head>
<body>
<div id="DataField">
Testing 1, 2, 3, ...
</div>
</body>
</html>
'
)
@dlg.show
sleep(1.0)
end
def self::test
msg = "Getting the 'DataField' element's value ..."
msg<< " value is: \"#{@dlg.get_element_value('DataField')}\""
puts msg
UI.messagebox(msg,MB_OK)
end
def self::reset
@dlg.close if @dlg && @dlg.visible?
@dlg = nil
end
if !@@loaded
sm = UI.menu("Plugins").add_submenu("Get Element Value Test")
sm.add_item('Open Dialog') { self::open }
sm.add_item('Test') { self::test }
sm.add_item('Reset') { self::reset }
@@loaded = true
end
end
EDIT: For a working example see âŚ
Yep. This is true for the UI::HtmlDialog
class. Seems like it used to work for the old UI::WebDialog
class.
I just tested and it seems now (with the UI::HtmlDialog
class,) there is no way to get an immediate synchronous value of a HtmlDialog element. Control must return to the SketchUp engine before the JS to Ruby callbacks will fire. So then there is no way, that a custom get_element_value()
method could ever itself return the value, if it is the instigator of the JS call to send the value to Ruby.
get_element_value_test.rb (2.6 KB)
This is unfortunate as that olâ get_element_value()
method came in very handy.
Whatâs the workaround ?
It seems that youâll need to make a call to fire a method on the JS side to collect ALL values that you might wish to test on the Ruby-side, and pass them en masse to Ruby as a array, etc. Or,⌠you move this kind of immediate comparison expression to the JS side of your code.
EDIT: For an asynchronous working example see âŚ
Hi Dan. I have been able to get the UI::htmlDialog to work using your pattern. And I get the appropriate information coming back from the javascript Sketchup.receiveValue(arg1, arg2). That works. The objective of this routine for now is to return a selected directory path. Just before process_value Iâm attempting to get a list of directories/files within the path returned, but no matter what method/block/proc/ whatever, it is always returning an error stating that it undefined method.
Iâm trying to call a method within the Module namespace that contains the Dir(path). These methods exist outside of the Class which contains the UI.htmlDialog. So how do I reference methods outside of the class methods defined by your example that exist outside of the htmlDialog instance?
Thanks,
Scott.
class MyToolsDialog < UI::HtmlDialog
def attach_callbacks
add_action_callback('receiveCmd') do |not_used,id,val|
receiveCmd(id,val)
UI.messagebox("Inside add_action_callback(receiveCmd, directly from MyTools_Jscript.js")
end
end
def get_element_value(id)
#return unless id.is_a?(String)
execute_script("sendValue('#{id}');")
end
def receiveCmd(htmlCmd,htmlParam)
UI.messagebox("Inside receiveCmd, directly from add_action_callback()")
process_value(htmlCmd)
process_value(htmlParam)
UI.messagebox("Inside receiveCmd, just before get_dir_list(#{htmlParam})")
THIS IS WHERE IT ERRORS OUT
get_dir_list(htmlParam)
end
def process_value(id)
# Do something with @values[id]
puts id
end
def show(*args)
attach_callbacks()
super
end
def show_modal(*args)
attach_callbacks()
super
end
end # custom HtmlDialog subclass
def get_dir_list(x_dir)
UI.messagebox(âInside get_dir_list #{x_dir}â)
puts listFiles(x_dir)
end
def listFiles(x_dir)
UI.messagebox(âInside listFiles(#{x_dir})â)
xDirList = ââ
UI.messagebox(âxDirList initializedâŚâ)
xList = Dir.entries(x_dir)
UI.messagebox(âDir.entries executed⌠#{xList}â)
xList.each {|fname|
if (xDirList.length() > 0) then
xDirList = xDirList + â,â
end
xDirList = xDirList + â#{fname}â unless fname.start_with?(â.â)
}
UI.messagebox(âlistFiles: returning xDirList = #{xDirList}â)
return xDirList
end
The first 2 puts prints the values returned from the javascript function.
I was elated to get the communications back into Sketchup
DirList
------------ is the command that I receive the htmlDialog javascript routines
------------ that I want to trigger the Directory List for the returned path
D:/my source directory name/Business Projects/SketchupProjects
Error: #<NoMethodError: undefined method get_dir_list' for MyTools:Module> C:/Users/Owner/AppData/Roaming/SketchUp/SketchUp 2017/SketchUp/Plugins/myRubyTools.rb:62:in
receiveCmdâ
C:/Users/Owner/AppData/Roaming/SketchUp/SketchUp 2017/SketchUp/Plugins/myRubyTools.rb:47:in block in attach_callbacks' SketchUp:1:in
callâ
Can you explain whatâs happening and why it is not calling the appropriate method?
Please reformat (edit) the above post, following the instructions on posting code in the forum.
(And have your editor replace tabs with 2 space characters.)
Do not use UI.messagebox
to debug code.
- It can fail silently if the evaluation of the string fails.
- It is modal and freezes SketchUpâs Ruby, and this causes weird behavior especially in event driven callbacks.
Instead, use puts()
to output info to the Ruby Console.
Some random tidbits âŚ
xDirList = xDirList + "#{fname}"
⌠is a silly way of making Ruby do unneeded work.
fname
is already a String
object. It evaluates the same as âŚ
xDirList = xDirList + fname
xDirList = xDirList + ","
# ... and ...
xDirList = xDirList + "#{fname}"
Just use the String#<<
(append) method ⌠if changing the original string is what you want to do.
xDirList << ","
# ... and ...
xDirList << fname
unless fname.start_with?('.')
If you do not want the '.'
and '..'
files, then use Dir.glob
or Dir[]
instead âŚ
xList = Dir["*.*"]
# ... or ...
xList = Dir::glob("*.*")
The return from these two Dir
class methods, is an Array of pathnames.
If you want one long string of the names separated by the Locale list separator character, try the Array#join
method âŚ
listing = xList.join(Sketchup::RegionalSettings.list_separator)
The separator will NOT be a comma where commas are used as the decimal separator character.
Lastly are you assuming any certain current working directory ?
Yes it appears you havenât yet learned basic Ruby.
(1) ALL your code needs to be within a top level author / company namespace module.
This means the class and your library module, and your plugins submodules.
(2) To call a module method in another module you need to qualify the method call.
MyNamespace::MyTools::get_dir_list(htmlParam)
⌠and you must make the module method available from outside by either declaring it as a module method âŚ
module MyNamespace
module MyTools
def self.get_dir_list(x_dir)
puts "Inside get_dir_list #{x_dir}"
puts listFiles(x_dir)
end
end
end
⌠OR extend the module with itself, creating module method copies of instance methods âŚ
module MyNamespace
module MyTools
extend self
def get_dir_list(x_dir)
puts "Inside get_dir_list #{x_dir}"
puts listFiles(x_dir)
end
end
end
I would recommend against implementing something like the above get_element_value
for HtmlDialog â because it does not do what its name says, it does not get and return anything. Moreover the whole construct with callback is split over three methods, hardly understandable and maintainable. Actually you would rather want to have the callback together with get_element_value
than separate (either as callback function parameter or as callback that can be registered on a returned promise).
Better get used to the new concept of asynchronous dialogs. Even the web is asynchronous and works well. Think of the dialog (client) sending to or requesting data from SketchUp (server).
And please put your code in code blocks (edit, </> button). That the code turns out so messy (bold, different size, characters missing) is a direct consequence from that the forum software thinks it can interprete it is markup.
I was thinking along the same lines yesterday, that the method is misnamed, as itâs not a âgetterâ method.
It is more of a âasynchronous triggerâ method. Ie âŚ
In the linked example, perhaps it should be named request_element_value(id)
?
(EDIT: The above method rename was done in the example.)
However, what this was aimed at, was to have old code work for both UI::WebDialog
and UI::HtmlDialog
.
BUT, it seems this is not really possible unless the code was designed previously to work asynchronously.
So really this was a testing / proof of concept, that proved the concept incompatible with UI::HtmlDialog
.
Hi Dan. I was able to get the entire communications between Sketchup and HtmlDialog to work seemlessly. Thanks your assistance.
One more minor detail that has got me stumped. I now have an active HtmlDialog. As I create the new instance of the dialog with HtmlDialog.show, I would like to close it when one of the options in the file passes back something to Sketchup, then automatically close the dialog box.
I have tried the close (where @dlg is assigned to the new instance for the HtmlDialog class) but none of these options are working.
I have a select statement with the command option to close that gets processed upon the return from the Html file.
when "Close"
UI.messagebox("Close was sent...")
@dlg.close
end
This snippet is within the HtmlDialog class
def close
@dlg = nil
end
What Iâm needing is a way to close the HtmlDialog window within the Ruby code. Everything that I have searched always brings me back to the .close, and this isnât working for me.
Thanks,
Scott.
HtmlDialog closing by itself
I think that threadâs problem was a different one (it was about an undesired closing because of a non-persisting local variable).
but none of these options are working
That is not helpful. âNot workingâ can mean everything and nothing. It should close the dialog, what exactly are you doing and what exactly happens instead of what you expect?
Also, in any callback you should (if not to say must) wrap the whole callback in a
@dlg.add_action_callback("callbackName"){ |action_context, *args|
begin
...
rescue Exception => error
$stderr.write(error + "\n")
end
}
clause because since not invoked from the console, exceptions wonât be automatically redirected to the console. Possibly your calback just throws an exception that you donât know about.
FYI, this doesnât actually create the Ruby-side dialog instance (the ::new
constructor method does this,) âŚ
⌠the show
method creates the platform C-side window object, that hosts the CEF process.
(1) I do not think it is wise to use the same identifier for the callback and a local instance method.
In the original post, youâre using "receiveCmd"
as the identifier for both an action callback and an instance method called from that same callback.
Also camelcase on the Ruby-side is for modules and classes. Methods are all lower case, with words separated by underscores.
(2) Again, you are not following the instructions given above, and still do not understand basic ruby.
If the above snippet, is within a add_action_callback
block, that is within the attach_callbacks
method in the subclass definition (for the MyToolsDialog
subclass,) ⌠then self
is implied as the receiver object, not @dlg
(which is external to the subclass definition.)
SO , âŚ
# within MyToolsDialog subclass definition ...
def attach_callbacks
add_action_callback('receiveCmd') do |not_used,id,val|
puts("Inside add_action_callback(receiveCmd, directly from MyTools_Jscript.js")
receive_command(id,val)
end
end
def receive_command(id,val)
case val
when "This"
do_this(id,val)
when "That"
do_that(id,val)
when "Close"
puts("Close was sent...")
close() # <<<<----<<<<< self is implied as the receiver object
else
# whatever ...
end
end
NO, it is not.
But if you meant within the MyToolsDialog
subclass âŚ
Then do not ever override super class methods without calling super
from the subclass override method.
(Refer to the subclass override methods in my example, where I make sure to call the superclassâ method within the subclass override.)
If you wish to mark the current Ruby-side instance of the MyToolsDialog
dialog object as ready for garbage collection, then âŚ
externally (from your subclass definition) add a block to the @dlg
instanceâs set_on_closed
callback âŚ
@dlg = MyToolsDialog.new(options)
@dlg.set_on_closed { @dlg = nil }
@dlg.show