Command validation proc not called on OS X


#1

I’m porting a plugin from Windows to OS X and I have a very basic problem with UI::Commands. The validation proc callbacks aren’t called regularly. They are only called when I click on one of the default SketchUp toolbar buttons, not when I click on any of my own buttons.
Here’s a minimal example that exhibits the problem on SketchUp 2015 and 2016 on OS X El Capitan 10.11.3:

require 'sketchup.rb'

show_ruby_panel()

$toolbar_test_bool = false
cmdtext = "Toolbar Test"
cmd  = UI::Command.new(cmdtext) {
    $toolbar_test_bool = !$toolbar_test_bool
    puts("bool == #{$toolbar_test_bool}")
}

cmd.set_validation_proc {
   puts("validation proc") 
    if $toolbar_test_bool
        puts("validation proc true") 
        MF_CHECKED
    else
        puts("validation proc false") 
        MF_UNCHECKED
    end
}

toolbar = UI::Toolbar.new("Toolbar Test")
toolbar.add_item(cmd)
toolbar.show()

What Mac specific bugs are there to SketchUp (that I as a developer may need to know)?
#2

it works without the globals [that you shouldn’t use]…

@toolbar_test_bool = false
cmdtext = "Toolbar Test"
cmd  = UI::Command.new(cmdtext) {
    @toolbar_test_bool = !@toolbar_test_bool
    puts("bool == #{@toolbar_test_bool}")
}

cmd.set_validation_proc {
   puts("validation proc") 
    if @toolbar_test_bool
        puts("validation proc true") 
        MF_CHECKED
    else
        puts("validation proc false") 
        MF_UNCHECKED
    end
}

toolbar = UI::Toolbar.new("Toolbar Test")
toolbar.add_item(cmd)
toolbar.show()

#3

No, this doesn’t change anything. And the global was just used in the minimal example.


#4

This is a known issue. Under OSX they update only when the active tool updates. This means you cannot rely on changing checked or disabled state for toolbar buttons under OSX. They should work for menus though.


#5

this appears to do what the code is asking on my mac…

how does it differ on a PC?


#6

Oh, I see. Is there some kind of work-around? We have lots of toolbar buttons that aren’t tools in that sense.


#7

The validation proc should be called at least as often as the button is clicked. Not just when the active tool is changed. Because clicking the button may change the state of the button. And the visible state is only updated when the validation proc is called. We have a toolbar where the user can select one of ten alternatives that are represented by ten buttons in a row. Like radio buttons. They don’t change the active tool, though.


#8

Afraid not. I have some extensions where I disable the buttons on Windows - under OSX I have to take into account they button might always be pressed.


#9

Not even UI.refresh_inspectors() ? … view.invalidate ? (anything to get the UI redrawn.)

Hmm… first I’ve heard of it. A new bug? A note box for the API doc UI::Command class would be nice.


#10

No, we’ve seen (and reported) this for a while. The plain vs greyed look of an extension’s buttons depends only on how many times they have been pressed, and does not necessarily reflect the current active/deactivated status of the tool.

Another example of how the UI on Mac has been neglected for some time now…


#11

Those two methods don’t help, but the following code does trigger a call of the validation procs:

tools = Sketchup.active_model.tools
tools.push_tool(nil)
tools.pop_tool()

This does actually trigger the procs twice, because both push and pop trigger them.
I’m adding this code to my toolbar button click callbacks. I hope this doesn’t lead to any problems with the active tool.

Edit: it’s looking good so far.


#12

Strange. In the past these methods have caused BugSplats!, especially if the toolstack is empty. And the only way to check that is to test the tool id for a 0 value:

if tools.active_tool_id != 0
  tools.push_tool(nil)
  tools.pop_tool()
end

They are only supposed to work with Ruby tools, not native tools. So, it is strange that the nil argument does not raise a TypeError exception. But then, the Tool class is abstract (a class without a specified superclass, other than the implied Object.)

Calling Sketchup::select_tool(nil) activates the SelectTool.
Does calling tools.push_tool(nil) ?


#13

I was looking at the source for this the other day and as far as I can tell it’s been like that forever.

UI.refresh_inspectors() refresh dialogs. view.invalidate refresh the viewport. None of them trigger toolbars to update their state - I found no other code paths other than when tools change.

Under Windows it’s different as the update is done continuously by the main UI thread - but that’s all in OS specific land so it doesn’t translate to OSX.


#14

Do you really need to pop the tool afterwards? I would have thought nil would be an invalid argument. Either raising an error - or silently ignore. But I’ve not tested.


#15

Yes, I’ve tested it, and we need to pop the invalid tool to get back to the previous tool. It looks like pushing nil activates an invalid “non-tool”, which does nothing. It’s not even the select tool.


#16

Are there any plans to fix this issue on Mac? Being able to changing the state of the button is quite important to design a intuitive UI and make the plugin fit in to SketchUp (match Make Component, Face Style, Display Section Cut/Plane etc). For developers like me who don’t have a Mac to play with things like this can be quite problematic.


#17

Was this fixed in SU2018 Mac OS X and was UI.refresh_toolbars added for this purpose?

Thanks


#18

Referring to the documentation …

http://ruby.sketchup.com/UI.html#refresh_toolbars-class_method

Yes it was added with the release of v2018.

… and its says …

Tells SketchUp to refresh all floating toolbars. This is useful when you need to manually force a refresh after you’ve made a change to the document via Ruby.

Generally, SketchUp will keep these in sync for you, but occasionally it does not, such as when Sketchup::Model#start_operation has disabled UI updates.

This only affects macOS, on Windows the toolbars are always refreshing.

… so, yes to the second question.


#19

Thank you, Dan.

This is how a utility refresh function might look (ignore the namespacing as this is a sample):

def refresh_toolbars
  # Return if Windows
  return if RUBY_PLATFORM =~ /mswin|mingw/i
  # For SU2018 use new function; for others use the push/pop tool workaround.
  if ::Sketchup.version.to_i >= 18
    ::UI.refresh_toolbars
  else
    model = ::Sketchup.active_model
    return unless model # Make sure there is a model as can be nil
    model.tools.push_tool(nil)
    model.tools.pop_tool
  end
end

#20

I think I’d prefer a pattern like …


# At top of module:
OS_IS_WIN =( Sketchup.platform == :platform_win rescue RUBY_PLATFORM !~ /darwin/i )

def refresh_toolbars
  # Return if Windows
  return if OS_IS_WIN
  # Use new method if available; older versions
  # will use the push/pop tool workaround.
  if ::UI.respond_to?(:refresh_toolbars)
    ::UI.refresh_toolbars
  else
    model = ::Sketchup.active_model
    # Make sure there is a model as can be nil
    return unless model
    model.tools.push_tool(nil)
    model.tools.pop_tool
  end
end

Ie, …

if ::Sketchup.version.to_i >= 18

… needs to call 3 methods before doing the boolean evaluation.
And it makes you try to remember version numbers matched to features.
Better to just test for functionality.