Command validation proc not called on OS X


#21

This could be another useful pattern, especially when using MF_DISABLED.

I prefer to to disable toolbar buttons when they can’t be used due to the selection not supporting them, e.g. like Make Component in the Lareg Toolset being disabled for empty selections. However this causes issues on Mac as the command often gets stuck in the disabled state.

# Set command state if supported.
#
# Only use when the command state isn't crucial for the functionality of the
# extension as it isn't supported by on Mac prior to SketchUp 2018.
#
# @param command [UI::Command]
#
# @yield_returns [Integer] `MF_ENABLED`, `MF_DISABLED`, `MF_CHECKED`,
#   `MF_UNCHECKED`, or `MF_GRAYED`.
#
# @return [UI::Command]
def set_validation_proc(command, &block)
  if Sketchup.platform == :platform_win || Sketchup.version.to_i >= 18
    command.set_validation_proc(&block)
  end

  command
end

This example adds the commands Edge Selected and Face Selected to the Extensions menu and a floating toolbar. The commands should be checked when an edge or a face respectively is selected, and otherwise be unchecked.

Could anyone with a Mac confirm that this works on SU2018?

2018-04-02_11h49_25

menu = UI.menu("Plugins")
tb = UI::Toolbar.new("State Test")
tb.show

cmd = UI::Command.new("Edge Selected") { UI.messagebox("Lorem ipsum.") }
cmd.tooltip = "Edge Selected"
cmd.status_bar_text = "Checked if edge is selected (if supported by platform)."
set_validation_proc(cmd) {
  ss = Sketchup.active_model.selection
  ss.grep(Sketchup::Edge).any? ? MF_CHECKED : MF_UNCHECKED
}
menu.add_item(cmd)
tb.add_item(cmd)

cmd = UI::Command.new("Face Selected") { UI.messagebox("Lorem ipsum.") }
cmd.tooltip = "Face Selected"
cmd.status_bar_text = "Checked if face is selected (if supported by platform)."
set_validation_proc(cmd) {
  ss = Sketchup.active_model.selection
  ss.grep(Sketchup::Face).any? ? MF_CHECKED : MF_UNCHECKED
}
menu.add_item(cmd)
tb.add_item(cmd)

#22

error:

'Error: #<NoMethodError: undefined method set_validation_proc' for main:Object>
<main>:7:in'SketchUp:1:in eval'

edit: forgot to paste the proc first…testing again…

ok, restarted SketchUp, the toolbar pops up, but does not respond on the selected items (edge, face or both)
However, when reinstated via View>toolbar and with something in the selection, it displays the icon bounded.


#23

Ouch. That would mean the issue isn’t fixed in SU2018 for Mac :frowning: .

Maybe I misinterpreted the documentation but I got the impression commands were now updated on Mac too.


#24

Actually upon further thought, … you want a method like this to be as fast as possible.
So, you need to eliminate the conditionals and anything that will slow it down.
This means you conditionally define the correct method for the platform and version …

if OS_IS_WIN

  def refresh_toolbars
    true # Return if Windows
  end

else # Mac
  
  # Use new method if available; older versions
  # will use the push/pop tool workaround.
  if ::UI.respond_to?(:refresh_toolbars)
    def refresh_toolbars
      ::UI.refresh_toolbars
    end
  else
    def refresh_toolbars
      if (model = ::Sketchup.active_model)
        model.tools.push_tool(nil)
        model.tools.pop_tool
      end
    end
  end

end

#25

Perhaps adding a selection observer to a model and calling refersh_toolbars every time a bulk selection is made could be the way to go on OS X. But of course, that could be performance consuming unless we use an efficient function as Dan described.


#26

Yea, that would be a bit of a overkill. Especially if multiple extensions start doing that.
We looked at it, but toolbar works very differently under OSX than Windows in terms of implementation. It’s not trivial to address.

What I ended up doing for my own extensions is to not rely on the button state on Mac - making sure to have logic that will make the buttons noop if they should have been disabled. Then just have the button state as a bonus on Windows (which is the major platform.)

We do have the issue for Mac open though - it’s not abandoned.


#27

My exact thought.

Do you use a method something similar to this?

def set_validation_proc(command, &block)
  # If the Mac bug is fixed, change this condition to include the supported versions.
  if Sketchup.platform == :platform_win
    command.set_validation_proc(&block)
  end

  command
end

#28

Something like that, yes.

My variant is:

    # OSX has a bug where the validation procs for toolbar buttons doesn't
    # update correctly.
    USE_VALIDATIONS = Sketchup.platform != :platform_osx


    cmd = UICommand.new(S.tr('Subdivided')) {
      Command.toggle_subdivision
    }
    cmd.set_validation_proc {
      Validate.toggle_subdivision
    } if USE_VALIDATIONS
    cmd.tooltip = S.tr('Toggle Subdivision On/Off')
    cmd.status_bar_text = S.tr('Toggle between control mesh and subdivided mesh.')
    cmd.small_icon = File.join(PATH_IMAGES, 'subdivide-16.png')
    cmd.large_icon = File.join(PATH_IMAGES, 'subdivide-24.png')
    @toggle_subdivision = cmd

Note that UICommand is a wrapper around UI::Command. I could have (and probably should have) put the Mac validation logic check in there.

For reference, my UI::Command wrapper:

#-------------------------------------------------------------------------------
#
# Thomas Thomassen
# thomas[at]thomthom[dot]net
#
#-------------------------------------------------------------------------------

Sketchup.require 'TT_SUbD/resource'


module TT::Plugins::SUbD
  module UICommand

    # SketchUp allocate the object by implementing `new` - probably part of
    # older legacy implementation when that was the norm. Because of that the
    # class cannot be sub-classed directly. This module simulates the interface
    # for how UI::Command is created. `new` will create an instance of
    # UI::Command but mix itself into the instance - effectively subclassing it.
    # (yuck!)
    def self.new(title, &block)
      command = UI::Command.new(title) {
        begin
          block.call
        rescue Exception => exception
          ERROR_REPORTER.handle(exception)
        end
      }
      command.extend(self)
      command
    end

    # Sets the large icon for the command. Provide the full path to the raster
    # image and the method will look for a vector variant in the same folder
    # with the same basename.
    #
    # @param [String] path
    def large_icon=(path)
      # noinspection RubySuperCallWithoutSuperclassInspection
      super(Resource.get_icon_path(path))
    end

    # @see #large_icon
    #
    # @param [String] path
    def small_icon=(path)
      # noinspection RubySuperCallWithoutSuperclassInspection
      super(Resource.get_icon_path(path))
    end

  end # module
end # module