How to pass variable to a tool object without using globals?


#1

Basic Ruby question:
Up to now I’ve been using @@variables within a module to share a very few module wide variables between methods.
But now I’m developing a simple tool. which is of course a class (the first time I’ve had to define a class).
How can I pass a value to a tool without using a dreaded global?

Is this what attr_accessor if for?


#2

Well, don’t know if I understand what you gonna need… but i’m usually do this (example to use a tool):

module MyModule
  class MyTool
    def initialize(model)
      @model = model
    end
  end

  def self.change
    model = Sketchup.active_model
    tool = Change.new(model)
  end
end

Attr_accessor: http://stackoverflow.com/questions/4370960/what-is-attr-accessor-in-ruby


#3

If you have some code about what you gonna need, would be better.


#4

Yes. Also referred to as “getter” and “setter” methods in other languages. They get or set instance variables. The attr_* family of methods are syntactic sugar and create basic getter and setter methods for instance variables (e 1). If you need to validate the values passed to the setter, you should write your own setter methods (ex 2.)

Ex 1

class Ticket
  attr_accessor :price # automaticaly creates @price instance variable
end

t = Ticket.new
t.price = 29.99 # creates a @price instance variable even when you did not explicity create it.
puts t.price # => 29.99

Ex 2

class Ticket
  def initialize
    @price = 19.99 # default price
  end
  def price
    @price
  end
  def price=(amount)
    # validate amount
    @price = amount
  end
end

See Module[1] for documentation on `attr_ methods.

[1] http://ruby-doc.org/core-2.0.0/Module.html


#5

Thanks, I’ll try that out once I understand it.


#6

That are two perfect answers.

To add some details about attributes: Many object oriented languages (Java) allow to make attributes public, and if the attribute would ever be renamed/type changed, external code would break. That’s why attribute getter/setter methods are recommended, because you can implement extra behavior (validation, access logging) or compensate for internal implementation changes, while keeping the public API continous without change.

That’s where Ruby is different and beautiful, it makes attributes private by default, and defining getters/setters with attr_reader / attr_writer / attr_accessor is so much easier.


#7

I’m just not seeing how I use attributes in this situation.
Here’s a test using a single global variable.
How do I do the same thing using attributes?

module Testmodule

$defaultangle = 90
puts "Original default angle = " + $defaultangle.to_s

class Simpleobject

def initialize
	puts "Simpleobject: initialize method"
	#here is where need to know the default angle:
	puts "Simple object sees default angle is " + $defaultangle.to_s
	#here is where much calculation is done (not shown)
	#to arrive at new default angle"
	$defaultangle = 45
end

end # class

simpleobject1 = Simpleobject.new

puts "new default angle is " + $defaultangle.to_s

end # module


#8

If Simpleobject is the class that will implement the Tool protocol, why not make the default angle a class variable (@@) and pass the default value as an argument to new when you create an instance of the Tool to activate?

SketchUp.active_model.select_tool(Simpleobject.new(90))

class Simpleobject
def initialize(defangle)
  @@defangle =defangle
 # other stuff
end
end

#9

It is a different way of thinking:

When you let your method access a global (or external) object, you have a dependency to that object and rely on a) that it exists and b) that it has a valid state, a correct value. Someone else (or even some other part of your code) could have changed it, and then your method fails.

Instead of letting your method fetch external data, the other way round is you pass to your method all the data it will need. Advantage is that all dependencies must be available before calling your method. By keeping the data within your object, you have it under your control.

The common way to Initialize attributes in the constructor:

class SomeObject
  def initialize(attribute1, attribute2)
    @attribute1 = attribute1
    @attribute2 = attribute2
    # …
  end
  attr_reader :attribute1, :attribute2
end

# Reading an attribute:
object1 = SomeObject.new(45, 90)
# Somewhere else someone wants to red the attribute…
object1.attribute1 # ==> 45
# If there is only a getter (attr_reader) defined, nobody else can change the attribute:
object1.attribute1 = 0 # ==> NoMethodError: undefined method `attribute1='

If it should be (like your global) shared for all instances of the class, use a class variable instead, as Steve wrote:
(In case you would have several instances; that means if one instance changes it, other instances will also work with the changed value.)

class SomeObject
  @@defangle = 0
  def self.defangle
    return @@defangle
  end
end

# Reading the class variable:
SomeObject.defangle # ==> 0

This works like a global, but it doesn’t create a new $ variable at top level, but it is grouped within your name space.


#10

Yes SImpleobject will implement my tool protocol and the answer to your question is that I didn’t think or realize that the tool protocol could accept passed parameters. I’m still a ruby newby, so unless the API shows such an example, I still lack the lateral thinking “rubythink”. But now it makes perfect sense.

So can I now assume that I can return the new defangle from the deactivate method, so I can set the defangle at the module level so that its available for the next time I need it? I’ll try it out.


#11

Simpleobject will in fact be a Sketchup tool, which the API requires to be created as a class which then must be instantiated. But the tool instance is created on the fly each time a particular command is executed. “Sketchup.active_model.select_tool Simpleobject.new”.
So I suppose that means that by the end of a Sketchup session, there will be a instance of this tool in memory for each time the user has used the command(?)
This seems strange to me since only the latest instance is actually ever operational.
Maybe there is a way to cleanup? Can an instance of a class delete itself?


#12

Ruby uses automatic memory management. After a Tool is deactivated, the Ruby reference to it is released and it will eventually be “reaped” by the Ruby garbage collector. Objects will accumulate in memory only if your code retains references to them.


#13

Class level. Put it into SimpleObject as a class variable @@def_angle. Then any instance of the tool can access it, and change it. It becomes a shared variable for all the class’ instances, and subclass’ instances.

If you have other scopes (classes and/or submodules) that are not related to each other, but need to share variables, then it is time to learn the magic of mixin modules.
You create a submodule, call it Shared (I have before,) and define within it module @@variables.
Then mix it into any submodule or class that needs to share these variables.

module Droid

  module Shared
    @@def_angle = 45.degrees
    @@def_length = 12.inch
  end

  module Inner
    include Shared
    puts "The default angle is: #{@@def_angle.to_s}"
  end

  class Tool
    include Shared
    def activate()
      puts "The default length is: #{@@def_length.to_s}"
    end
    def deactivate(view)
      @@def_angle = 90.degrees
      @@def_length = 24.inch
    end
  end

  Sketchup::active_model.select_tool(Tool::new)

end

You can also create shared modules of constants, but I recommend keeping shared variables, shared methods and shared constants in separate mixin modules. (The reason is, they are mixed in differently, depending upon whether include or extend is used, and what the objects in the mixin module are [ie, methods, variables, or constants]. Especially true when it comes to mixing in methods.)


#14

Dan,

Before trying mixins and attributes, I tried your suggestion to pass the default angle to the tool activate method, where I set it to a class variable. That works. But then I tried to return the new default angle set by user in the VCB from the deactivate method. That does NOT work. Once a new tool interrupts this tool, no return is received back to the module level where I need to pass it the next time the same tool is used. So unless I’m missing something, the pass and return method doesn’t work.

No, this is the only tool that need this default angle, but I’ll try your shared module suggestion. The key issue is that I need to set the default angle in code myself the first time the tool is used in a Sketchup session but thereafter that default is whatever the last user VCB input was to this tool. It seems that your approach can accomplish that(?)


#15

So then do not set options in the deactivate() callback if it will be interrupted.*

Set them as soon as possible after they change, and your code determines them to be a valid value.
Probably in the onUserText() callback.


  • FYI, when a tool is interrupted, it is the suspend() callback that is called.

#16

You lost me there so let me restate the objective.
I’m trying to pass a default angle to a tool (this I can do),
but then return a possibly new default angle defined by user VCB input so it can be used the next time the tool is used. Like this:
@@defangle = 90
@@defangle = Sketchup.active_model.select_tool(PutTool.new(@@defangle))
puts @@defangle.to_s

Even if I set the new angle in the onUserText() callback, the return does not occur.
As I understand the tool object (at least before you introduced the subject of suspend and resume), all tool instances end by interruption when user clicks on a different tool. So not only does the tool instance go poof, but also the method that created the new instance goes poof. So it can’t set the return value.

Maybe I can keep the same instance of this tool alive for the entire Sketchup session by using suspend and resume?? but how to do that is beyond me.


#17

A tool is interactive, and non-blocking, because you want the user being able to move the mouse, do actions and many of the tool’s methods to be called.
model.select_tool does not return anything, and the control flow continues immediately with the following line of code. (And by the way, how should model.select_tool know what to return or which tool method’s return value it should return?)

If you don’t overwrite @@defangle with Sketchup.active_model.select_tool…, then you will see that it will somewhen (after user input) have a new value.

A tool instance, just like any object, goes “poof”, when 1) it is not anymore in use, 2) no reference points to it anymore 3) Ruby’s garbage collector runs and collects objects without reference (in regular intervals). In case of the tool, you currently do not keep a reference to it at all, but SketchUp (from outside of the ruby environment has a reference to it as long as it is the current tool). After selecting another tool, neither SketchUp nor your plugin have a reference, so it is garbage collected.


#18

So how do I do what I want to do?
I want to set a default angle of 90 at only the initial sketchup load. I know how to do that by just setting a module level variable to 90 at initial load.
When user .first uses my tool I need to pass the 90 default to a new instance of it. (No problem)
The tool activate method rotates selected objects by 90.
Tool asks user for VCB input if he wants to override the default. (usually he won’t so he’ll just select another tool and this tool will deactivate.
But if he does enter an override I use the onUserText method to rotate back to initial position, then rotate using new angle, the set new class variable for default angle.
User can continue entering new angles in VCB until he likes result.
Then he selects another tool and this one deactivates.

Later in the session he wants to use this tool again but I want the last value he entered to be the new default.

I can’t pass the new default to the new tool instance because I don’t have a reference to it.

If I rely on the tool class variable to still be alive, (no passing to the tool) then I have no way to set the intial 90 on first use of the tool.

(Maybe mixins are a way to do this but I haven’t tried that yet.)


#19

I don’t understand. Don’t think too complicated and in terms of your implementation but take a step back and think in terms of the problem (usage of a programming language). Take time and systematically learn concepts of object oriented programming.

If you do exactly what Dan has given you, the tool class in Dan’s example has access to the @@defangle class variable (because it’s in same class), Then there is neither a need to pass it as parameter, nor a problem setting the @@defangle variable directly from any method in the same class.

If instead your tool class is not the same class as where the parameter is stored, you can use the above mentioned getter/setter methods so your tool class can set a value to the class variable which is (in that case) in another class/module.

Also understand that when you pass parameters to a method in Ruby, you pass references to values.

class OuterClass # somewhere where @@defangle is
  # …
  class SomeClass
    def some_method(angle); end
  end
  # …
  SomeClass.new.some_method(@@defangle)
end

If you pass @@defangle to a method, the method sees the parameter as a local variable (the parameter name, maybe angle) and has access to the value, not to @@defangle. Since numbers are primitive types, changing 90 to angle=45 will assign a new value to angle, but neither modify the previous value, nor modify @@defangle.

Before we start iterating about all possible programming concepts, could you reveal the module/class layout of your current implementation, so that we can see where (only) the relevant variables and methods are?


#20

write to defaults and read it from there…
then it will persist, until re-set…

john