[Example] Quiet DC Interact Tool (subclass with tooltip toggle)

[Code Example]

Quiet DC Interact Tool (subclass with tooltip toggle)


This is an example of tool subclassing. It subclasses the DCInteractTool to wipe out any tooltips that it’s onMouseMove callback may set. But the hover messages will still show up on the status bar.

  • By default the “Show tooltips” toggle is set false (unchecked.)
  • Users can change this by checking the option in the submenu.
  • The submenu appears at the bottom of the main “Tools” menu.

This example tool came by request for allowing toggling the tootips that might be annoying whilst recording anmiated tutorials and hovering over DCs.

License: MIT License

su_quiet_dc_interact_tool_1.0.3.rbz (2.8 KB) UPDATED 2021-02-12

It isn’t a full extension example, if installed it will just be a lone file in the “Plugins” menu.
It will not load unless the Dynamic Components extension is switched on.

A good exercise ...

A good exercise might be to wrap this example into a full extension within an author module and an extension submodule, then put it in an extension subfolder and lastly create a registrar script for the “Plugins” folder.


# encoding: UTF-8
#
# File:    "su_quiet_dc_interact_tool.rb"
# Author:  Dan Rathbun, AUG 2020.
# License: MIT License
#
# This is an example of tool subclassing. It subclasses the DCInteractTool
# to wipe out any tooltips that it's onMouseMove callback may set.
# But the hover messages will still show up on the status bar.
#
# By default the "Show tooltips" toggle is set false (unchecked.)
# Users can change this by checking the option in the submenu.
# The submenu appears at the bottom of the main "Tools" menu.
#
# This example tool came by request for allowing toggling the tootips
# that might be annoying whilst recording anmiated tutorials and
# hovering over DCs.
#
# Revisions:
#
# 1.0.0   2020-08-20  initial
# 1.0.1   2020-08-21  (no feature or compatability change)
#   Corrected class docstring: "We will only" was "Will Will only".
# 1.0.2   2020-08-25  Replaced lazy boolean check of instance var @loaded
#   with test using the defined? operator. Added menu item validation
#   proc for the tool reference (to avoid unused warning.)
# 1.0.3   2021-02-12  Define constants for localized UI strings.
#   Define persistent saving of user's tooltip toggle setting.

# Request that the Dynamic Components extension be loaded:
require "su_dynamiccomponents.rb"

# Users might switch it off via the Extension Manager, so we test if
# it was loaded, specifically the superclass tool that we need to
# subclass for this example:
if !defined? DCInteractTool

  # WARN THE USER IF DC EXTENSION IS NOT LOADED: (translate if not English.)
  warn("\nWARNING: Dynamic Components extension is switched off.\n"+
      "         QuietDCInteractTool could not be loaded.\n")

else

  module Example

    # We first declare a subclass of DCInteractTool.
    #
    # There is no reason to modify how the tool is instantiated, so
    # we will not be overriding the class constructor :new, nor the
    # instance method :initialize. The superclass will handle this.
    #
    # We will need to hold the state of tooltip display in a variable
    # and define a getter and a toggle method for this state variable.
    #
    # We will only need to override the onMouseMove callback method,
    # first calling the superclass' implementation, then wiping out the
    # tooltip text if our tooltip display state variable is not true.
    #
    # Lastly, we will setup a submenu with items to activate the tool
    # and another to toggle the tooltip display state.
    #
    class QuietDCInteractTool < DCInteractTool

      VERSION ||= '1.0.3'

      OPTS_KEY ||= Module.nesting[0].name.gsub('::','_')

      # Text for user interface (translate if not English.)
      TEXT ||= Hash[
        :submenu,  "Quiet DC Interact",
        :activate, "Activate tool",
        :tooltips, "Show tooltips",
      ]


      ### CLASS METHODS
      #

      # This method saves the DC tooltip display settings so
      # it can be restored when SketchUp is started again.
      def self.save_toggle_setting
        Sketchup.write_default(OPTS_KEY,'Tooltip',@@tooltip)
      end

      if !defined? @loaded
        # Declare a class variable to hold tooltip display state,
        # and read it's value from stored preference files:
        @@tooltip = Sketchup.read_default(OPTS_KEY,'Tooltip',nil)
        if @@tooltip.nil? # First load of extension per user:
          @@tooltip = false
          self.save_toggle_setting() # Save it the 1st time
        end
      end

      # This method returns the tooltip display state.
      def self.tooltip?
        @@tooltip
      end

      # This method toggles the tooltip display state.
      def self.toggle_tooltip
        @@tooltip = !@@tooltip
        self.save_toggle_setting()
      end

      private

      # This method gets the tool id.
      def self.tool_id
        @tool_id
      end

      # This method sets the tool id.
      def self.tool_id=(id)
        @tool_id = id
      end


      ### INSTANCE METHODS
      #

      # Override the class initializer so we can initialize a set of instance
      # variables (whose referencing before assignment) is causing repeated
      # warnings to be output to $stdout (and so the console) when $VERBOSE is
      # true. (This is not part of the example, just something necessary.) 
      def initialize(*args)
        # Pass the singleton DCObservers instance to superclass constructor:
        super($dc_observers)
        # Add this tool as a ToolsObserver watching the tools collection:
        @model = Sketchup.active_model
        @model.tools.add_observer(self)
        # Initialize a few superclass instance variables that were not:
        @dc = @dcobservers.get_latest_class
        @timer = nil
        @onclick_action = ''
        @is_in_timer_handler = false
      end

      public

      # This a ToolsObserver callback method that fires when the active
      # model's tool changes. We use it to get this tool's ID for use in
      # the tool activation menu item's validation proc.
      def onActiveToolChanged(tools, tool_name, tool_id)
        self.class.tool_id= tool_id
      end

      # Called when the user first deactivates the tool. We override this so
      # we can remove the tool as a ToolsObserver watching the tools collection.
      def deactivate(view)
        super
        # Remove this tool as a ToolsObserver watching the tools collection:
        @model.tools.remove_observer(self)
        self.class.tool_id= 21022 # Set to a safe Selection Tool ID
      end

      # Override the onMouseMove callback method inherited from superclass.
      # In the superclass determinations are made about what is below the
      # cursor, and if it is a DC, then it retreives translated help text
      # to display as a tooltip and on the status bar.
      def onMouseMove(flags, x, y, view)
        # Call the superclass' method passing along ALL arguments:
        super
        # Conditionally, wipe out any tooltip set in the superclass:
        view.tooltip = '' unless @@tooltip
      end

      # MENU SETUP
      if !defined? @loaded
        @loaded = true # so this block only evals once at startup:

        tools = UI.menu("Tools")

        # Add a submenu for our new tool:
        quiet_menu = tools.add_submenu(TEXT[:submenu])

        # Add an item to activate our new "quieter" tool:
        tool = quiet_menu.add_item(TEXT[:activate]) {
          Sketchup.active_model.select_tool(self.new)
        }
        quiet_menu.set_validation_proc(tool) {
          if Sketchup.active_model.tools.active_tool_id == self.tool_id()
            MF_CHECKED | MF_DISABLED | MF_GRAYED
          else
            MF_ENABLED
          end
        }

        # Add an item to toggle the tooltip display state:
        toggle = quiet_menu.add_item(TEXT[:tooltips]) {
          self.toggle_tooltip()
        }
        quiet_menu.set_validation_proc(toggle) {
          self.tooltip?() ? MF_CHECKED : MF_UNCHECKED
        }

      end

    end # class

  end # extension module


end # if defined? DCInteractTool
4 Likes

Hey Dan,
Just realised you created this for me - thanks again - it’s saved me alot of video editing!!

2 Likes

Here’s hoping they just add a tooltip toggle to the native DCInteractTool …

:crossed_fingers:

@DanRathbun

I saw the code posted, and if !@loaded will throw an ‘undefined’ warning. Today’s warnings may be tomorrows errors. Some CI test suites switch warnings on, so it’s something I’m used to thinking about…

Lot’s of ways to fix it, I’ll leave that to you. As to using both instance and class variables defined at module/class scope, that’s a big discussion, especially if a class can be used as a ‘super’ for other classes…

In what Ruby version? It does not seem to in Ruby 2.5.5 (w/ SU2020.)

Instance variables were always nil if not specifically assigned to reference something else.

I can sum it up in this sentence:

“A module (or a class) is an instance of class Module (or class Class respectively.)”

… so instance variables are quite at home there.

I realize that module and class variables have always needed to be defined before use.

As to the class variable, in this case, it is not expected that this would need subclasses.
It is just a temporary fix for a toggle that we hope is added to the DC extension.

If you think it a poor example, then don’t call it one. I can always remove it if the community of Ruby gurus think it so darn bad ? :roll_eyes:

@MSP_Greg I tried even setting $WARN to true (within SketchUp,) but did not see a warning.

There is no need for Travis CI as this simple file is all pure Ruby and needs no building.

And, I wouldn’t worry about subjective warnings that a build or package tool might spit out, unless it is a Ruby core warning as well. (Yea, I do believe in being prepared for future breaking changes.)

Adding to the previous statement, this example is so short and simple, that anyone needing other (or added) functionality can simply copy and paste, change the subclass name, and add the “new stuff” to the new “sibling” subclass, rather than create a grandchild subclass.

Yes, (I know) the use of a class variable to hold the toggle state would be common to all subclasses.

I actually first had it as an instance variable but ran into an issue of access.

This was the simplest way. Yes, I could change it to a class attribute, … or I could nest it inside the tool instance as a true instance attribute, but then I’d need to use the singleton class paradigm … which wouldn’t be as simple a implementation. (Ie, it gets convoluted quickly and doesn’t serve well as a simple example.)


Anyway, Thomas and I Pm’d … and he’d rather that the toggle be added into the native DC Interact Tool rather than promote this as an example. Main reason is … that the Dynamic Components is still closed source and a proprietary Trimble extension. There are too many extensions already “hanging” on undocumented DC interfaces (discovered through introspection.)

If the DC tool gets a tooltip toggle, I’ll have Admins remove this thread.
(The guy who needed it to move on with his animations is temporarily satisfied.)

Dan,

I made a couple of suggestions, that was all.

As to the warnings - once the Ruby console is open, type $VERBOSE = true, then load your file. Three warning are shown. SU 2019 w/ Ruby 2.5.5.

1 Like

Okay I do see the warnings … and more from the Trimble DC code itself.
Which means I will need to override the initializer to assign initial values to a few more @vars.

These warnings are also present in Ruby 2.0.0 which began with SketchUp 2014.
(I don’t worry about [write for] older versions running Ruby 1.8.x.)


Which is preferred, … check using a method, or the core defined? operator ?

if !instance_variable_defined?(:@loaded)
  # do code

… OR:

if !defined? @loaded
  # do code

… OR:

if (defined? @loaded).nil?
  # do code

I tend to lean toward the middle pattern, and I see it suggested often on stackoverflow.

But I also saw some guru promoting the boolean method for a boolean use,
… rather than the operator which returns String names or nil.

Dan,

As to what way is most common, remember that re-loading a file is somewhat rare outside of SU. Even in SU, it’s probably used mostly by dev’s writing/debugging code.

But, in ‘Ruby world’, with more and more of the std-lib becoming gems (default or bundled gems), RubyGems and/or Bundler may load one of them, but the running app loads a different version. The SU issue is re-loading the same file, the issue in ‘Ruby world’ is re-loading the same class/module, but a different file. Regardless, both will often throw warnings.

Anyway, one pattern I use is something like:

class WhatEver
  unless const_defined? :INIT, false
    # do class init stuff
    INIT = true
  end
end

Using a constant will throw a warning if it’s changed elsewhere, which is often helpful…

This is true, which is why many extension devs put the UI building code on a separate file that doesn’t tend to need to get reloaded during testing much. :wink:

And then when testing the UI code, we have to reload SketchUp anyway as the UI objects get “set in stone” for the session.

But a small file like this … I don’t see the need. And I didn’t want to make it full blown extension package.

Uh huh, but it seems a bit much to define a constant to track whether 1 lil’ ol’ @var is defined.
I’ve used a similar pattern when I have a bunch of stuff to init at the top of a module.


With $VERBOSE set true … there are many of these "instance variable not initialized" warnings coming from many extensions (native and 3rd party.)

Many of us learning from the ol’ Pick Axe book (originally written for Ruby 1.6,) that told us we didn’t need to initialize them because they would just return nil.

We could use a simple method for class Module:

def attr_init(*args)
  val = args.first.class != Symbol ? args.shift : nil
  args.each do |var|
    instance_variable_set("@#{var}",val)
  end
end

Any Ruby Core roadmap on forced initialization of @vars ?

Haven’t really looked. Other languages (and their linters) often throw errors on uninitialized or unused variables, so I consider initializing them to be reasonable. As to the future, if it makes a speed difference or simplifies things, especially with JIT, it may change from a warning to an error.

See attached. There’s only one ‘load conditional’ vs two. Also, I tend to use variables for values that change. The INIT constant is defined by the normal load/require, and isn’t changed with additional loads…

dan.rb (3.6 KB)

Yea I see this. But I don’t have an issue with 2 conditionals.

I always define / declare constants and class variables at the top of the file, or sometimes for a large project in their own file (which loads first.)

Probably the reason why I used a variable as assigning a constant would not seem “right” to me down at the bottom of a file.

Hi Dan,
can you help me with a fully functional plugin?
Sorry, I can´t write ruby scripts
Joerg

UPDATE - v1.0.3 2021-02-12

  • Defined constants for localized UI strings.
  • Defined persistent saving of user’s tooltip toggle setting (by request.)

REMINDER : This example tool is separate from the normal DC Interact Tool.
It will not effect the normal interact tool as distributed with SketchUp !
(So this means that the normal DC Interact tool activated from the DC tollbar “hand” button will STILL be noisy showing tooltips.)

To activate this “quiet” tool, you must activate it via it’s own submenu beneath the “Tools” menu.