HTMLDialog Fill inputbox from csv data

When I create the box, the code creates a folder and saves the date as a csv.

My intent is to select the csv file and fill in the input boxes.

How can I do this?

require "sketchup.rb"
#def dave_HtmlSample()
	#load "dave_HtmlSample.rb"

	dialog = UI::HtmlDialog.new(
	{
	  :dialog_title => "Draw a box",
	  :scrollable => true,
	  :resizable => true,
	  :width => 500,
	  :height => 500,
	  :left => 200,
	  :top => 200,
	  :min_width => 150,
	  :min_height => 150,
	  :max_width =>100,
	  :max_height => 50,
	  :style => UI::HtmlDialog::STYLE_DIALOG
	})
	html = "
	<!DOCTYPE html>
	<html>
	<head>
	<title>Dave Loves You</title>
    <style>
    body{
    display:grid;
    }
    label {
  display: block;
  font:
    1rem 'Fira Sans',
    sans-serif;
}

input,
label {
  margin: 0.4rem 0;
}
input[type=number], select {
  width: 20%;
  padding: 12px 20px;
  margin: 8px 0;
  display: inline-block;
  border: 1px solid #ccc;
  border-radius: 4px;
  box-sizing: border-box;
}

input[type=submit] {
  width: 20%;
  background-color: #4CAF50;
  color: white;
  padding: 14px 20px;
  margin: 8px 0;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}

input[type=submit]:hover {
  background-color: #45a049;
}


</style>
	</head>
	<h1>Insert Box Info</h1>
	<script>
	function sendDataToSketchUp() {
		var nm = document.getElementById('nam');
		var user_input1 = document.getElementById('id1');
		var user_input2 = document.getElementById('id2');
		var user_input3 = document.getElementById('id3');
		sketchup.getUserInput(nm.value, user_input1.value, user_input2.value, user_input3.value)
	}
	</script>
	<body>
	<p>Draw Box or select an existing CSV file</p>
	<form>
  <label for='file_nm'>Choose an existing file :</label>
  <input type='file' id='file_nm' name='file_nm' accept='csv, csv' />
  <hr>
    Name: <input id='nam' type='text' name='name' value required><br>
    width: <input id='id1' type='number' name='width' value=24 required>
    length: <input id='id2' type='number' name='length' value=24 required>
    depth: <input id='id3' type='number' name='depth' value=4 required>
	</form>


	<button onclick='sendDataToSketchUp()'>Draw Box</button>
	</body>
	</html>
	"
	dialog.set_html(html)
	dialog.show
	dialog.add_action_callback("getUserInput"){|action_context,nm, user_input1, user_input2, user_input3|
        Sketchup.active_model.entities.clear!
		width = user_input1.to_f
		length = user_input2.to_f
		depth = user_input3.to_f
		model = Sketchup.active_model
		entities = model.active_entities
        UI.messagebox(nm)
		pts = []
		pts[0] = [0, 0, 0]
		pts[1] = [width, 0, 0]
		pts[2] = [width, length, 0]
		pts[3] = [0, length, 0]
		# Add the face to the entities in the model
		face = entities.add_face(pts)
    face.reverse! unless face.normal.samedirection?(Z_AXIS)
		face.pushpull depth

require 'fileutils'
FileUtils.mkdir_p("C:/Users/davem/OneDrive/Documents/testRubyFolders/#{nm}")

box_Info = [
  { Name: nm , Width: width, Length: length , Depth: depth }
]

require 'csv'

CSV.open("C:/Users/davem/OneDrive/Documents/testRubyFolders/#{nm}/testMyCsv.csv", "w") do |csv|
  csv << ["Name", "Width", "Length", "depth"]
  box_Info.each do |bx|
    csv << [bx[:Name], bx[:Width], bx[:Length], bx[:Depth]]
  end
end


	}
#end

(1) Stuffing everything into one method is somewhat clunky and hard to follow, not as easy to maintain if it were broken up, with the ruby, javascript and html in their own files.

(2) The acts of saving to disk and loading from disk are a separate task then passing to an HTML dialog and receiving changes back.

But you understand using a Ruby Hash and iterating it to write or read from CSV. This is good.

A Ruby Hash can also easily be converted to JSON passed into the dialog JavaScript creating a JS Object. This can be changed and set back to a Ruby callback automatically converted back to a Ruby Hash.


See the (collapsed) example go() method at the bottom of this post:
HTML How to delete a line with a text field and button when the button is clicked - #29 by DanRathbun

Or also a condensed general example here:
`UI::HtmlDialog`: global `sketchup` object in dialog has no callbacks when viewing page served remotely after being linked from a local html file · Issue #646 · SketchUp/api-issue-tracker · GitHub

I also discuss it at length in this topic:
Html dialog box to set and get information - Developers / Ruby API - SketchUp Community

1 Like

Thank-you,

I am separating the code by following your example from the GitHub link.

begin
  path = "C:/MySketchupDialog/"
  # Create a hash of replacement strings from dialog resource files: 
  replacements = {
    :stylesheet => File.read(File.join(path, "stylesheet.css")) ,
    :javascript => File.read(File.join(path, "javascript.js" )) ,
    :json_data  => File.read(File.join(path, "some_data.json"));
    :draw_box  => File.read(File.join(path, "drawBox.rb"))
  }
  html_text = File.read(File.join(path, "dialog.html"))
rescue => err
  # Handle IO errors
else


	@dialog = UI::HtmlDialog.new(
	{
	  :dialog_title => "Dialog Example",
	  :scrollable => true,
	  :resizable => true,
	  :width => 500,
	  :height => 300,
	  :left => 200,
	  :top => 200,
	  :min_width => 150,
	  :min_height => 150,
	  :max_width =>100,
	  :max_height => 50,
	  :style => UI::HtmlDialog::STYLE_DIALOG
	})

  # Insert the replacement file text into the HTML and pass to dialog:
  @dialog.set_html( html_text % replacements )
  @dialog.show
end

I have a separate file for the drawBox.rb but unsure where to call it.

drawBox.rb file

require "sketchup.rb"

dialog.add_action_callback("getUserInput"){|action_context,nm, user_input1, user_input2, user_input3|

Sketchup.active_model.entities.clear!

width = user_input1.to_f

length = user_input2.to_f

depth = user_input3.to_f

model = Sketchup.active_model

entities = model.active_entities

UI.messagebox(nm)

pts = []

pts[0] = [0, 0, 0]

pts[1] = [width, 0, 0]

pts[2] = [width, length, 0]

pts[3] = [0, length, 0]

# Add the face to the entities in the model

face = entities.add_face(pts)

face.reverse! unless face.normal.samedirection?(Z_AXIS)

face.pushpull depth

require 'fileutils'

FileUtils.mkdir_p("C:/MySketchupDialog/#{nm}")

box_Info = [

{ Name: nm , Width: width, Length: length , Depth: depth }

]

require 'csv'

CSV.open("C:/MySketchupDialog/#{nm}/testMyCsv.csv", "w") do |csv|

csv << ["Name", "Width", "Length", "depth"]

box_Info.each do |bx|

csv << [bx[:Name], bx[:Width], bx[:Length], bx[:Depth]]

end

end

}

javescript has this:

function sendDataToSketchUp() {
  var nm = document.getElementById('nam');
  var user_input1 = document.getElementById('id1');
  var user_input2 = document.getElementById('id2');
  var user_input3 = document.getElementById('id3');
  alert("name: " + nm.value + " width " + user_input1.value + " Length: " + user_input2.value + " height: " + user_input3.value);
  sketchup.getUserInput(nm.value, user_input1.value, user_input2.value, user_input3.value)

}

The alert does come up with the correct results.

The console does error when I click the button, saying “getUserInput” is not a function, I assume it’s because I haven’t called drawBox.rb yet.

A few comments.

(1) All of your extensions must be within a unique Ruby namespace module.

(2) Each of your extensions should be separated from each other inside an extension submodule.

 module DaveStudys
   module DrawBoxWizard
   end
 end

For example, it is not permitted to define instance variables in the top level OjectSpace. They would become global as everything in Ruby is an object, so is a subclass of Object.

(3) SketchUp extensions follow an organizational convention. An extension registrar script and a subfolder of the same name. All the main files of your extension would reside within the subfolder.

 # File: DaveStudys_DrawBoxWizard.rb
 # SketchUp Extension Registrar
 #
 module DaveStudys
   module DrawBoxWizard

     EXTENSION = SketchupExtension.new('Draw Box Wizard', 'DaveStudys_DrawBoxWizard/main')

     EXTENSION.instance_evaluate do
       self.creator   = 'Dave Studys'
       self.version   = '1.0.0'
       self.copyright = '(c)2024, by author'
       self.description = 'A nifty box wizard extension.'
     end

   end
 end

(4) When evaluated, code files in the extension subfolder can get their path location using the global __dir__() method.

 # File: DaveStudys_DrawBoxWizard/main.rb
 #
 module DaveStudys
   module DrawBoxWizard

     extend self
     PATH ||= __dir__ # local constant definition
     @dialog = nil

     # ... methods ...

   end
 end

(5) The registrar script and extension subfolder go in SketchUp’s “Plugins” folder:

  • %AppData%/SketchUp/SketchUp 2023/Plugins

This should just be the main file for the extension. It should NOT be loaded into a string value of the replacements hash.

If you wish to call it "drawBox.rb" rather than "main.rb" this is fine, except change the loader file name in the second argument of the SketchupExtension constructor call (registrar script.)

No, it is because you have not attached a “getUserInput” callback to the @dialog object.
Do this after instantiating the @dialog object, but before calling @dialog.show


I would never do this: Sketchup.active_model.entities.clear!
This would wipe out a user’s model !

Instead you should be creating your geometry within a component or group entities context.

I can’t figure out how to call the toolbar.rb and dialog.rb in the main file, it only works if I hard code it.

main.rb

# File: DaveStudys_DrawBoxWizard/main.rb
 #
 module DaveStudys
  module DrawBoxWizard



            #UI.messagebox('main rb')
            toolbar = UI::Toolbar.new "DrawBox Toolbar"
            cmd = UI::Command.new("Toolbar Button") {
              begin
                work_dir = (__dir__)


                UI.messagebox(work_dir)
                path = work_dir +"/"
                # Create a hash of replacement strings from dialog resource files: 
                replacements = {
                  :stylesheet => File.read(File.join(path, "stylesheet.css")) ,
                  :javascript => File.read(File.join(path, "javascript.js" )) ,
                  :json_data  => File.read(File.join(path, "some_data.json"))
                }
                html_text = File.read(File.join(path, "dialog.html"))
              rescue => err
                # Handle IO errors
              else
              
              
                @dialog = UI::HtmlDialog.new(
                {
                  :dialog_title => "Dialog Example",
                  :scrollable => true,
                  :resizable => true,
                  :width => 500,
                  :height => 300,
                  :left => 200,
                  :top => 200,
                  :min_width => 150,
                  :min_height => 150,
                  :max_width =>100,
                  :max_height => 50,
                  :style => UI::HtmlDialog::STYLE_DIALOG
                })
              
                # Insert the replacement file text into the HTML and pass to dialog:
                @dialog.set_html( html_text % replacements )
                @dialog.show
              end
            
          }



      icon = File.join(__dir__, 'images','test.png')
      cmd.small_icon = icon
      cmd.large_icon = icon
      cmd.tooltip = "Create A Box"
      cmd.status_bar_text = "Enter Numbers Draw Box"
      toolbar = toolbar.add_item cmd

      toolbar.show
          # ... methods ...

  end
end

box_tool.rb

require "sketchup.rb"
module Add_Toolbar
   Class ToolBar_Add

   def ToolBar_Add

      toolbar = UI::Toolbar.new "DrawBox Toolbar"
      cmd = UI::Command.new("Toolbar Button") {
      myDialog()
      }

      icon = File.join(__dir__, 'images','test.png')
      cmd.small_icon = icon
      cmd.large_icon = icon
      cmd.tooltip = "Create A Box"
      cmd.status_bar_text = "Enter Numbers Draw Box"
      toolbar = toolbar.add_item cmd

      toolbar.show

   end
end

myDialog.rb

module My_Dialog

	def myDialog()


		begin
			work_dir = (__dir__)
			UI.messagebox(work_dir)
			path = work_dir +"/"
			# Create a hash of replacement strings from dialog resource files: 
			replacements = {
			  :stylesheet => File.read(File.join(path, "stylesheet.css")) ,
			  :javascript => File.read(File.join(path, "javascript.js" )) ,
			  :json_data  => File.read(File.join(path, "some_data.json"))
			}
			html_text = File.read(File.join(path, "dialog.html"))
		  rescue => err
			# Handle IO errors
		  else
		  
		  
			@dialog = UI::HtmlDialog.new(
			{
			  :dialog_title => "Dialog Example",
			  :scrollable => true,
			  :resizable => true,
			  :width => 500,
			  :height => 300,
			  :left => 200,
			  :top => 200,
			  :min_width => 150,
			  :min_height => 150,
			  :max_width =>100,
			  :max_height => 50,
			  :style => UI::HtmlDialog::STYLE_DIALOG
			})
		  
			# Insert the replacement file text into the HTML and pass to dialog:
			@dialog.set_html( html_text % replacements )
			@dialog.show
		  end
		
	end
end

Wrong act- I use Sketchup pro on two computers, the david.morrison act is the work one.

FYI …

(a) Ruby uses 2 space indents. Please set your code editor to replace TAB with 2 spaces. If you do not the browser uses 8 space indents which make the code hard to read when copy & pasted to the forum.

(b) Ruby has language conventions.

Constants begin with capital letters.

  • Class and module identifiers are constants and use SnakeCase without underscore separating words. (Each word is capitalized.)
  • Other constants use all caps with words separated with underscore, Ie: IMAGE_PATH, PLUGIN_OPTIONS, etc.

Method names are all lowercase with words separated by underscore: add_toolbar(), save_options(), etc.


I assumed it was yourself.


(1) The "module SomeName" syntax is read by the Ruby interpreter as:

  • “Open the module “SomeName” for modification, first creating it if it does not yet exist.”

So ALL of your extension’s files need to be wrapped in the SAME namespaces:

module DaveStudys
  module DrawBoxWizard

    # Code for this file goes here

  end # extension submodule
end # top-level namespace module

(2) You need to decide upon the load order of your files.

If the main file will be referencing objects (everything in Ruby is an object including modules, classes and methods,) in the other files when it is evaluated, then the main file will need to use either core Ruby require or the API’s Sketchup::require to load the files near the top of it’s code.

# File: DaveStudys_DrawBoxWizard/main.rb
#
module DaveStudys
  module DrawBoxWizard

    extend self

    PATH ||= __dir__()

    require File.join(PATH, 'dialog.rb')
    require File.join(PATH, 'toolbar.rb')

    # ... other code ... classes, etc.

  end
end

(3) You need not do:

path = work_dir +"/"

… as File::join will concatenate its arguments (the path subparts) with the character pointed at by the core constant File::SEPARATOR (which will be correct for the platform Ruby is running upon.)

Also do not think of the extension subfolder path as a “work” path or the working directory. That is a separate concept
which you can retrieve in Ruby via Dir::pwd or it’s alias Dir::getwd.
SketchUp sets the working directory to the user’s "Documents" path. If you need to change it, do so only temporarily using the block form of Dir::chdir so that the working directory is restored when the block exits.


(4) Never put anything but a method call within the UI::Command::new block.

The reason is that UI objects can only be defined once. But methods can be redefined at any time. (This is a nifty feature of a dynamic coding language. On a related note, as each rb file is loaded, it is dynamically modifying your extension submodule. This is how multiple files for defining the same modules or classes is the norm.)

So, as you develop the extension you can reload a rb file at the console by using the global load() method. (This method requires a full absolute path and the filetype, whereas require has built-in search using the paths in the global $LOAD_PATH array.)
The block for command objects cannot be redefined by Ruby because they are thinly wrapped OS objects. But if the command block simply calls a Ruby method that implements the command code, then this method can be tweaked in the editor, saved, the reloaded, in order to test the changes without having to close and restart SketchUp each time.

Some plugin coders temporarily add in an autoloading method into their extension submodule to make loading easier.

But beware. The defining of all UI objects (submenus, menu & toolbar commands, toolbars and their buttons, context menu handlers) need to be wrapped in a load guard conditional block so they only get loaded once.
Example within your submodule:

    # RUN ONCE AT LOAD TIME
    #
    unless defined?(@gui_loaded)
      #
      # UI Objects Defined here ...
      #
      @gui_loaded = true
    end

This way if the file that defines UI objects get reloaded the OS UI objects will not be duplicated in the menus or toolbars.

2 Likes

Right on, I have the files working!

I used the sample here to write the json file: Read .json file

require 'json'

box_Info = Hash[
	"nam", nm ,
	"id1", width,
	"id2", length,
	"id3", depth
].to_json

	File.write("C:/MySketchupDialog/#{nm}/testMyJS.json", box_Info)

	}

I can populate the tags with js, so far by hard coding the json data.

function fill_input_tags() {

  var data= [{"nam":"dave m","id1":224.0,"id2":234.0,"id3":44.0}];

  Object.keys(data[0]).map(value => {
    document.getElementById(value.replace(/ /g, "")).value = data[0][value]
});

How do I select a .json file to populate the input tags?

I explained this in the other topics I linked above.

(1) Load the .json file into Ruby as a JSON String.

(2) Convert that JSON String into a Ruby Hash object using JSON.parse.

(3) Then replace the %{hash_key} parameters that you should have embedded into your HTML with the values from the data hash, using the String#% method.

In the snippet you posted above in post 3 , the line:

    :json_data  => File.read(File.join(path, "some_data.json"));

… should not be in the replacements hash (looks like it was inserted and a comma does not follow it which would be a syntax error. The semicolon is not needed anyway.)

Instead, the data json should be loaded into a Ruby hash, like:

INCORRECT CODE (corrected below)
    path = __dir__

    # Load the stylesheet and javascript into a replacement hash:
    replacements = {
      :stylesheet => File.read(File.join(path, "stylesheet.css")) ,
      :javascript => File.read(File.join(path, "javascript.js" )) ,
    }

    # Load the HTML text into a Ruby String:
    html_text = File.read(File.join(path, "dialog.html"))

    # Load the external JSON data file into a Ruby String:
    json_data = File.read(File.join(path, "some_data.json"))
    # Convert to a Ruby hash:
    data = JSON.parse(json_data)

    # Now replace the embedded %{hash_key} parameters in the HTML text.
    # In your latest edition, the parameters within the HTML text should be:
    # %{nam}, %{id1}, %{id2} and %{id3}
    html_with_data = html_text % data

    # Now replace the %{stylesheet} and %{javascript} parameters with the
    # :stylesheet and :javascript values from the replacements hash into
    # the HTML text and pass to dialog:
    @dialog.set_html( html_with_data % replacements )

    # Show the dialog:
    @dialog.show

EDIT: The above (collapsed code) is actually incorrect. You cannot do replacements twice as I showed above because the first time a KeyError exception is raised when the %{stylesheet} and %{javascript} parameters do not have matching keys in the data hash.
Instead, we must merge the two hashes together and do the String#% replacement operation ONCE.

Corrected code:

    path = __dir__

    # Load the stylesheet and javascript into a replacement hash:
    replacements = {
      :stylesheet => File.read(File.join(path, "stylesheet.css")) ,
      :javascript => File.read(File.join(path, "javascript.js" )) ,
    }

    # Load the HTML text into a Ruby String:
    html_text = File.read(File.join(path, "dialog.html"))

    # Load the external JSON data file into a Ruby String:
    json_data = File.read(File.join(path, "some_data.json"))
    # Convert to a Ruby hash:
    data = JSON.parse(json_data)

    # Combine the data hash into the replacements hash:
    replacements.merge!(data)

    # Now replace the embedded %{hash_key} parameters in the HTML text.
    # In your latest edition, the parameters within the HTML text should be:
    # %{nam}, %{id1}, %{id2} and %{id3}
    # This will also replace the %{stylesheet} and %{javascript} parameters
    # with the :stylesheet and :javascript values from the replacements hash.
    # Then pass the HTML text and to the dialog:
    @dialog.set_html( html_text % replacements )

    # Show the dialog:
    @dialog.show

So within your HTML text, lets say you are using <input type="text">

    <div id="form">
        <input type="text" id="nam" value="%{nam}" />
        <input type="text" id="id1" value="%{id1}" />
        <input type="text" id="id2" value="%{id2}" />
        <input type="text" id="id3" value="%{id3}" />
    </div>

… and after the html_with_data = html_text % data statement executes, the %{key} parameters in the HTML text should be replaced with the values from the data hash corresponding to its hash symbol keys, and the result assigned to the html_with_data string.


You should not be hardcoding paths. Your extension should be in a subfolder of the “Plugins” path which is in the user’s %AppData% path … if, …
IF the data is extension / users specific but not model specific.

plugins_path = Sketchup.find_support_file('Plugins')

Also from a file in your extensions subfolder (beneath "Plugins") you could also do:

plugins_path = File.dirname(__dir__)

If the data is model specific, either it likely should be saved to model.path or saved within the model itself as an attribute dictionary.

Going from JSON to attribute dictionary:

dict = model.attribute_dictionary('DaveStudys_DrawBoxWizard', true)
hash = JSON.parse( json_text )
hash.each { |key, value| dict[key]= value }

Going from existing attribute dictionary to JSON:

dict = model.attribute_dictionary('DaveStudys_DrawBoxWizard')
json_text = dict.to_h.to_json
1 Like

Thanks for this,
I guess I was thinking of this differently.

My idea was to click the button, it draws the box and creates a folder with the json file.

There would not be a specific sketchup file, just the json file.

If the user wants to use existing data, they would find and select the appropriate json file to populate the dialog.

After reading and processing your last post,I should save the skb file with the json file.
When opening an existing sketchup file, then when activating the dialog code, it will get the data from the json file in that specific .skb folder.

That would be better and not have to figure out how the user would select a file.

SketchUp model filetype

.skp is the filetype for a non-backup SketchUp model.

SketchUp model backup filetype

my_model.skb is a backup file on Windows, but is my_model~.skp on MacOS.


I have no idea why anyone would want to do this.

Again WHY !?

Why separate the data from the model ? The dimensions of the box in the model can be saved into the model itself as an attribute dictionary.

If the box is a group or component, then the attribute dictionary can be attached to the instance object instead of the model.

Have you looked at the “Hello Cube” tutorial example ?

1 Like

The dialog would have more than just the dimensions.

Hey, could you explain exactly what the purpose of this script is? What is the finished extension supposed to do for the User? Genuinely curious…

The above (collapsed code) is actually incorrect. You cannot do replacements twice as I showed above because the first time a KeyError exception is raised when the %{stylesheet} and %{javascript} parameters do not have matching keys in the data hash.
Instead, we must merge the two hashes together and do the String#% replacement operation ONCE.

I’ve updated the code example and collapsed the incorrect code block.

1 Like

Hello,

In the dialog
-fill the dimension input tags
-fill in lots of other input tags.

There are quite a few calculations based on the two different tags.

When the button is clicked:

  • The model is drawn-(it’s just the shape of the product with dimensions)
  • a folder is created
  • a .csv file is produced that is sent to our company server and gets processed for a quotation.
  • a .json file is created that has the input tags values(the original question was a csv file, it has since been changed to a .json file.)

The csv and json files will be saved into the new folder.

When the quote becomes an order or if the quote needs editing, the user needs to be able to select the json file to fill in the input tags.

1 Like

I can see the need to export csv (or json) data to send to a server.

But it makes no good sense to make the user go looking for a local json file in order to populate fields in a webform.
Again, please look at attaching the dialog properties directly to the geometric group or component instance (or the model) using an AttributeDictionary.

The dictionary name should be unique to your extension, so the existence of your dictionary can be the test expression for a context menu handler Edit command.

With this scenario, the user need only select the product object (assuming it is a group or component instance), then right-click and they’ll see an “Edit Box…” command on the context menu.

REF: UI::add_context_menu_handler

Ex: Say that your module nesting is per the above example DaveStudys::DrawBoxWizard, this can be used as a unique dictionary name with the scope operator (::) replace with an underscore.

module DaveStudys
  module DrawBoxWizard

    extend self

    # At the top of the submodule where constants are defined:
    DICT ||= 'DaveStudys_DrawBoxWizard'

    def edit_box_command(obj, dict)
      # Call other methods such as the dialog generator:
      result = box_dialog(dict)
      generate_box(dict) if result
    end

    # ... other methods ...

    # RUN ONCE - UI object load guard
    if !defined?(@loaded)

      UI.add_context_menu_handler do |context_menu|
        sel = Sketchup.active_model.selection
        if !sel.empty? && sel.single_object?
          obj = sel[0]
          if dict = obj.attribute_dictionary(DICT)
            # This extension dictionary exists, so add the edit command
            # passing the selected object and it's dictionary references:
            context_menu.add_item('Edit Box...') { edit_box_command(obj, dict) }
          end
        end # if selection has an object
      end # handler

      # ... other UI objects, like commands, menus, toolbars, etc.

      @loaded = true
    end # run once block

  end # submodule
end # top-level namespace module
1 Like

Ah, I am finally figuring out what you are saying.

The AttributeDictionary, it took me a while to realize those code samples were two parts.

Working on it.

I have been trying to get your above code snippet to work

I think it’s supposed to be in the main.rb although I could be wrong, I do not get the ‘Edit Box’ menu item when the code runs.

# File: DaveStudys_DrawBoxWizard/main.rb
#
module DaveStudys
  module DrawBoxWizard

    extend self
    # At the top of the submodule where constants are defined:
    DICT ||= 'DaveStudys_DrawBoxWizard'

    def edit_box_command(obj, dict)
      # Call other methods such as the dialog generator:
      result = box_dialog(dict)
      generate_box(dict) if result
    end
    # RUN ONCE - UI object load guard
    if !defined?(@loaded)

      UI.add_context_menu_handler do |context_menu|
        sel = Sketchup.active_model.selection
        if !sel.empty? && sel.single_object?
          obj = sel[0]
          if dict = obj.attribute_dictionary(DICT)
            # This extension dictionary exists, so add the edit command
            # passing the selected object and it's dictionary references:
            context_menu.add_item('Edit Box...') { edit_box_command(obj, dict) }
          end
        end # if selection has an object
      end # handler
  
      @loaded = true
    end # run once block    
    PATH ||= __dir__()
    # ... other code ... classes, etc.
    require File.join(PATH, 'myDialog.rb')
    require File.join(PATH, 'box_toolBar.rb')
    require File.join(PATH, 'DrawBox.rb')
    ToolBar_Add()

  end
end

When I run the code to show the dict results, it works

def show_dict()
	model = Sketchup.active_model
	attrdict = model.attribute_dictionaries['DaveStudys_DrawBoxWizard']
	attrdict.each { | key, value |
		puts "#{key} = #{value}"
	}
end

2024-02-10_8-31-43

Yes, see the comment at the top of the file snippet.

How are you running the code?

Look at the conditional expressions within the context menu handler creation block.

(1) The model’s selection must not be empty and only a single object must be selected.
(2) The selected object (obj) must have your extension’s dictionary attached

otherwise the context menu will not have the “Edit Box…” command added.


Re your latest main file snippet:

A. It is customary to have the definition of local constants at the top of the first file loaded ("main.rb" in this case.)

B. Also, the requiring of other support files are usually also up near the top of the first file. Very often, SketchUp extensions use a dedicated “loader” file that does nothing but require all the other files of the extension. But this is not absolutely necessary. The first file can load all the others.
But ensure that needed resources (like local constants, methods, etc.,) are defined before any of the support files code might reference them.

C. Any and all calls to create GUI objects should be within the “run once at startup” load guard conditional block.
So, this also means your call to the Toolbar_add() method, (which should be named toolbar_add().)
The main reason for this is so you can use the global load() method to reload individual files of your extension as your tweak and test them during development. Closing and restarting SketchUp after making a code change will get mighty tedious very quickly.

D. Your naming of methods and files lacks a consistent convention. In Ruby, method names are all lower case within words separated by underscore.

E. Files normally follow the method naming convention for like 97 percent of .rb files, (referring to Ruby’s standard library.) There have been some issues from time to time where files load in a weird order because upper case characters collate differently than lower case. So, it is convention that .rb filenames are all lower case, words separated with underscores.
Now the natural alpha load order (of course) does not come into play when your code is controlling the load order via explicit require statements. But does effect the load order of all the extensions in the "Plugins" folder.
Another deviation in file naming can be the SketchUp extension registrar scripts in the "Plugins" folder. They must match the extension subfolder name, and usually (for most extension authors) parrot the module namspacing names, which as SnakeCase (aka CamelCase.) But yes, some authors (notably the Trimble SketchUp team,) will downcase even the registrar file and extension folder name (and usually abbreviate their Sketchup namespace to "su_" in file and folder names.) In my opinion, using abbreviations increases the chance of conflicting extension files.

F. Your show_dict method (if it is part of the extension,) can also use the DICT constant.
Also directly using the #attribute_dictionaries method with a chaining #[] call is dangerous. If an object does not have any dictionary objects attached, then the return value is nil and the NilClass does not have a #[] method defined. So you can get a NoMethodError with a "undefined method '[]' for NilClass" message.
It is much safer to test using Entity#attribute_dictionary(), but it is also good defensive coding to always test the result for nil. You might also get a NoMethodError with a "undefined method 'each' for NilClass" message, so adding in a conditional to test for nil and output a "No such dictionary" message is “playing it safe.”

2 Likes