require 'rtml/assigns' # The Widget is the driving force behind RubyTML. It is interaction with Widgets of all shapes and sizes that produces # a document model and, finally, the TML document itself. A Widget is so named because it is a generic class that can # be made to encapsulate any desired functionality. The Rtml::Widget base class takes care of interfacing content # generation code with the RTML project as a whole. # # If you've done any RTML programming at all, you've been using Widgets. Does the following code look familiar? # # screen :idle, :next => :main, :timeout => 3 do # display # end # # Even core functionality such as the "screen" method is traced back to a Widget -- in this case, # +Rtml::Widgets::Screens+. # # Since interfacing with the various components of RTML is handled by the base class, writing a Widget is easy: # # class Rtml::Widgets::CardParser < Rtml::Widget # affects :screen # entry_point :card # # def card(options = {}) # raise ":parser must be provided" unless options[:parser] # parent.build :card, :parser => options[:parser], :parser_params => (options[:params] || 'read_data') # end # end # # When invoked from a Screen, this code will produce a TML card parser element: # screen . . . do # card :parser => :mag #=> # end # # If you are just interested in using the Widgets, you are encouraged to read the documentation for the various Widgets # themselves. If you find that you need to program a new Widget in order to address some missing functionality, you can # generate one easily with the command line generator: # ruby script/generate widget my_widget [entry_point1 entry_point2 . . .] # class Rtml::Widget include Rtml::Assigns # These are the accessors that RDoc uses to define what a particular RTML-specific attribute maps to. # # There are 3 kinds of accessors: # - Shared variable # * These are used to create variables that are different across parent objects, but which are shared between # multiple Widgets (whether totally separate, or instances of the same) on the same instantiated parent. # It's a little complicated, but it's critical in (for example) tracking TML variable definitions. # - Valid parent # * These are the names of TML elements (or "document" for document-level Widgets) that are allowed to instantiate # the Widget in question by calling its entry points. # - Entry point # * These are instance methods defined by the Widget which are proxied into potential parent objects. Calling an # entry point results in the configuration and eventual instantiation of the Widget which declared the entry # point. # RDOC_ACCESSORS = { :shared => 'shared variable', :affects => 'valid parent', :entry_point => "entry point", } @@widget_id_tracker = 0 class_inheritable_array :targets class_inheritable_array :shared_variables class_inheritable_array :entry_points class_inheritable_array :rtml_protected_instance_variables read_inheritable_attribute(:targets) || write_inheritable_attribute(:targets, []) read_inheritable_attribute(:shared_variables) || write_inheritable_attribute(:shared_variables, []) read_inheritable_attribute(:entry_points) || write_inheritable_attribute(:entry_points, []) read_inheritable_attribute(:rtml_protected_instance_variables) || write_inheritable_attribute(:rtml_protected_instance_variables, %w( @document @parent @source_type )) attr_reader :parent attr_reader :source_type extend Rtml::WidgetCore::ClassMethods def validate_parent(*options) options = options.flatten.collect { |o| o.kind_of?(String) ? o : o.to_s } unless options.include?(parent.name) || options.include?(parent.class.name) || ((options.include?(:document) || options.include?('document')) && parent.kind_of?(Rtml::Document)) raise ArgumentError, "Expected parent to be one of #{options.to_sentence}; found #{parent.name}" end end def document @document ||= (parent.kind_of?(Rtml::Document) || !parent.respond_to?(:document) ? parent : parent.document) end # A guaranteed unique ID for this Widget. def widget_id @widget_id ||= (@@widget_id_tracker += 1) end # Any method other than one of this widget's entry points will be delegated into #parent. # The parent's methods are not known until runtime, and it's extraordinarily slow to generate # delegation for those methods on the fly. Method_missing is the happy middle ground. def method_missing(name, *args, &block) name = name.to_s unless name.kind_of?(String) return parent.send(name, *args, &block) unless entry_points.include?(name) || !parent.respond_to?(name) # FIXME: Why does super raise an ArgumentError("no id given")? raise NoMethodError, "Method missing: #{name}" end def respond_to?(*a, &b) super || parent.respond_to?(*a, &b) end def initialize(parent) @parent = parent set_source_type! parent.widget_instances << self self.class.shared_variables.each do |sv| unless @parent.instance_variable_get("@#{sv[:name]}") if sv[:default_value] if sv[:default_value].kind_of?(Class) @parent.instance_variable_set("@#{sv[:name]}", sv[:default_value].new) else @parent.instance_variable_set("@#{sv[:name]}", sv[:default_value].dup) end end end end end # Returns true if subprocessing has been disabled. See also #disable_subprocessing! def subprocessing_disabled? @disable_subprocessing = self.class.disable_subprocessing? unless defined?(@disable_subprocessing) @disable_subprocessing end alias disable_subprocessing? subprocessing_disabled? # Use this to enable subprocessing after it has already been disabled. See #disable_subprocessing! def enable_subprocessing! @disable_subprocessing = false end # By default, if a Widget's entry point is called with a block argument, it will be passed into the #process method # of the entry point's return value. This is called subprocessing -- that is, processing the element to be returned # at a lower level. This method disables subprocessing and its related assertions. It's useful, for instance, if you # want to manually control how or when the block argument is processed, or if you want to ignore it entirely. # # There is a class method of the same name that has the same effect for the entire class; this instance method can # disable subprocessing for particular instances, and is useful if you only want subprocessing under certain # conditions. def disable_subprocessing! @disable_subprocessing = true end alias parent_type source_type private def set_source_type! case parent when Rtml::Document @source_type = :document else @source_type = parent.respond_to?(:name) ? parent.name.to_sym : parent.class.name.to_sym end end end