Interacting with default tools after activating custom tool

So I managed to successfully convert my Ruby UI::Command into a Sketchup::Tool, but I have a question regarding the interaction of my tool with the standard ones provided by SketchUp.

First off, my custom tool begins by presenting a UI::HtmlDialog to the user where certain parameters have to be inputted. From here the user must select a couple of entities from the 2D view (without relying on the Select tool) and then click a button on the aforementioned UI::HtmlDialog in order to generate some custom geometry with respect to those inputs. This workflow is fairly intuitive, even though it requires a bit of back and forth between the canvas and the non-modal dialog box.

That being said, my concern is that the user of my extension might want to redraw some of the 2D geometry after invoking my custom tool. For example, a problematic workflow might look like this:

  1. User activates my custom tool;
  2. User is unhappy with the geometry and activates the Line tool to make some edits without closing the dialog (which right now is designed to switch focus to the previous tool when closed: dialog.set_on_closed { Sketchup.active_model.tools.pop_tool })
  3. User clicks back on the dialog hoping to continue where they left off.

However, I don’t think there is a way to reactivate my custom tool just by clicking on the dialog that was previously out of focus. So the user has to close the pop-up and then navigate to the menu bar in order to activate the tool again, which is not quite ideal. The (not so) funny part is that if the dialog is not dismissed and the tool reactivated from the menu bar, then I will end up with two instances of UI::HtmlDialog…

Anyway, I am trying to determine if:

  1. it is possible to reactivate a custom tool after clicking on its non-dismissed and out of focus UI::HtmlDialog;
  2. I am doing the right thing keeping the dialog open when the user wants to interact with other tools.

Regarding 2), I tried calling dialog.close in the Tool#deactivate(view) method in order to give full focus to the tool that the user intends to select. However, because I put

Sketchup.active_model.tools.pop_tool

in the set_on_closed callback, this always causes the Select tool to be activated, which is not great at all.

It’s fairly obvious that I’m struggling with designing good UX within SketchUp, so any help will be greatly appreciated!

I think I will just go back to the UI::Command implementation as I find the Sketchup::Tool very awkward to work it. Despite giving me the option to “select” points, the tool makes it a lot harder to deal with interactions of standard tools, which someone is very likely to perform after activating my custom tool. With a command, on the other hand, I can keep my dialog open at all times and not worry if the user selects other tools until they are ready to invoke the command. The point selection procedure will not be as user-friendly, though.

I am still interested to know whether point 1) from my post above is feasible or not.

1 Like

See: Sketchup::Model#select_tool()

If your extension holds a persistent reference for the active model that points at the tool instance then this would be the argument to pass when the dialog is clicked or the document.body gets the focus.

@dialog.add_action_callback("reactivate_tool") {
  model.select_tool(@tool)
}

Trapping when the dialog becomes active can be problematic in CEF using Javascript. See:

What others have done is use the onMouseEnter / onMouseOut on the body or document to fire a Ruby callback that calls @dialog.bring_to_front which should give the dialog window the focus. (Was bugged until v2021.1 for the Mac.)


Normally a UI::Command would be created to activate a tool. This allows the command to be added to a menu and used for a toolbar button.

It’s best if a command’s proc only calls a method where the work is done. The reason is that GUI objects cannot be redefined during development but the method can be tweaked and redefined, and the file reloaded. The menu item or toolbar button will always call the latest edition of the method.

1 Like

I’m not using that because of this bug.

I will give onMouseEnter and onMouseOut a try later today, but I think I’ve convinced myself that I want to continue with the command approach instead.

Speaking of which, I’m not even creating a UI::Command object explicitly. My tool-based implementation is roughly along the lines of:

menu.add_item('My Item') {
  dialog = UI::HtmlDialog.new(...)
  
  tool = Tool.new(dialog)
  Sketchup.active_model.tools.push_tool(tool)

  dialog.add_action_callback('click') { |action_context, arg|
    main_func(tool, arg)
  }
  
  dialog.set_on_closed { 
    Sketchup.active_model.tools.pop_tool 
  }

  dialog.set_file('html/test.html')
  dialog.show
}

Note that I don’t want to include a toolbar button as having an item in the menu bar is just fine.

Those are not useful as they get triggered even if the user hovers over the dialog. I found that focus and blur are the events that I should be listening to instead (and they have to be bound to the window rather than the document object).

I’ve not been able to get those to fire using CEF dialogs. (See the API Issue linked above.)

I had a look at the Github issue you linked to and also at the code you attached there. I followed the same pattern as you did and everything works just fine under SU 21.1.331 on macOS. Perhaps it doesn’t work in an earlier version if I understand correctly.

In JavaScript:

window.addEventListener('focus', function() {
    sketchup.reactivateTool()
});

In Ruby:

my_tool = MyTool.new
Sketchup.active_model.tools.push_tool(my_tool)

dialog.add_action_callback('reactivateTool') { |action_context|  
  if Sketchup.active_model.tools.active_tool_name != 'RubyTool'
    Sketchup.active_model.tools.push_tool(my_tool)
  end
}

I don’t like the fact that the custom tool I wrote goes under the rather non-descriptive name of RubyTool, but I can live with that…

The other small issue I have is that if I select something like the Line tool and go back to my custom tool, the cursor is still renderer as a pencil until I click once on the canvas. How can I force the cursor to be arrow-like right from the start without having to push the Select tool to the stack?

Never mind, I solved that issue as well. Had to find (or rather guess) the id of the select tool, which is 628.

I opened that issue/feature request in February of 2020.

On Windows platform there were bad focus issues before 2021.1. I came back to the issue and posted an update 5 days after v21.1 was released.

The 21.1 release saw the CEF version updated from 64 to 88.

A long standing complaint. We have asked that if a Tool class defines a #name getter method that the API use this for Tools.active_tool_name.

The conditional is fragile as it will be the same for all Ruby tool objects.

If you are using a persistent reference like @tool[Sketchup.active_model] where @tool is a hash with model reference keys, then …

# At top of extension submodule:
@tool |= {}

def get_tool
  model = Sketchup.active_model
  @tool[model]= MyTool.new if @tool[model].nil?
  return @tool[model]
end

# Usage:
def push_tool
  Sketchup.active_model.tools.push_tool(get_tool())
end
# Define callbacks for HTML dialog.
def define_callbacks(dialog)
  dialog.add_action_callback('reactivateTool') { |action_context|
    model = Sketchup.active_model
    my_tool = get_tool()
    tool = model.tools.active_tool # Will be nil if a native tool.
    if tool.nil? || tool != my_tool
      model.tools.push_tool(my_tool)
    end
  }
  #
  # ... other callback definitions ...
  # 
end

The reason that you want to use a hash with model keys is because on the mac multiple models can be open at the same time. Each model object has it’s own toolstack which should normally have unique tool instances. (More than likely a tool will hold state data specific to the model to which it belongs.)


Also the first time your tool is activated, your code can get and store it’s own tool id.
(I wish that the API did this for us.)

# Inside your tool class:
attr_reader :id

def activate
  @id = Sketchup.active_model.tools.active_tool_id if @id.nil?
  #
  # ... other tool code ...
  #
end

Afterward you can test for that integer id.

1 Like