require 'rugui/observable_property_proxy'
module RuGUI
# Adds support to observable properties.
module ObservablePropertySupport
# Initializes the observable properties. If you override this method, make
# sure that you call the initialize_observable_property_support
# method, so that the observable properties are initialized.
def initialize(observable_properties_values = {})
initialize_observable_property_support(observable_properties_values)
end
# Initializes observable properties, setting their initial value.
def initialize_observable_property_support(observable_properties_values = {})
self.class.observable_properties_options.each do |property, options|
value = (observable_properties_values.with_indifferent_access[property] || clone_if_possible(options[:initial_value]))
send("#{property}=", value)
end
end
# Registers an observer for this model.
#
# The observer must implement a method with this signature:
#
# property_updated(observable, property, new_value, old_value)
#
# This method is called whenever a property has changed its value. One
# option is to include the PropertyObserver module in the observer class.
#
# Optionally, if observable_name
can be given, a method with
# this signature will also be called:
#
# named_observable_property_updated(observable_name, observable, property, new_value, old_value)
#
def register_observer(observer, observable_name = nil)
initialize_observers_if_needed
@observers << observer
@named_observers[observable_name] = observer unless observable_name.nil?
end
# Called whenver the a property has changed.
def property_changed(property, new_value, old_value)
initialize_observers_if_needed
@observers.each do |observer|
observer.property_updated(self, property, new_value, old_value) if observer.respond_to?(:property_updated)
end
@named_observers.each do |observable_name, observer|
observer.named_observable_property_updated(observable_name, self, property, new_value, old_value) if observer.respond_to?(:named_observable_property_updated)
end
end
# Resets all observable properties for this observer.
#
# Since an observable property may be another observable there may exist
# some observers observing this other observable. In this scenario one
# should not attempt to set a new object into the observable property,
# because the observers would still be looking for the old observable.
#
# By calling reset!
all observable properties are reset to the
# values specified when creating it. Also if the property respond to reset
# the method will be called, unless a *reset_value* is configured, i.e., it
# is not nil
. Also, if *prevent_reset* is true, that property
# will not be reseted, even if it has a *reset_value* configured.
def reset!
self.class.observable_properties_options.each do |property, options|
unless options[:prevent_reset]
property_value = send(property)
if options[:reset_value].nil? and property_value.respond_to?(:reset!)
property_value.reset!
else
send("#{property}=", clone_if_possible(options[:reset_value]))
end
end
end
end
# Returns true
if obj
is equals to
# self
.
#
# This method checks if obj
is of the same type of
# self
and if all *core* observable_properties are equals.
def ==(obj)
if obj.is_a?(self.class)
self.class.core_observable_properties.each do |property|
return false unless obj.respond_to?(property) and respond_to?(property)
return false unless send(property) == obj.send(property)
end
return true
end
end
# Copies all observable properties from _other_observable_ to _self_
def copy_observable_properties_from(other_observable, deep = true)
self.class.observable_properties.each do |property|
if other_observable.respond_to?(property)
other_property_value = other_observable.send(property)
if other_property_value.class.include?(ObservablePropertySupport)
send(property).copy_observable_properties_from(other_property_value) if deep
else
send("#{property}=", other_property_value)
end
end
end
end
# Returns a map of all observable properties with theirs values.
def observable_properties
self.class.observable_properties.inject({}) { |properties, property| properties.merge!({ property => send(property) }) }
end
# Update observable properties values given a map of values
def update_observable_properties(values = {})
values.each { |property, value| send("#{property}=", value) if self.respond_to?("#{property}=") }
end
module ClassMethods
# Creates the necessary class inheritable attributes an initializes them.
def create_class_inheritable_attributes
self.class_inheritable_accessor :observable_properties_options
self.observable_properties_options = {}
end
# Register a observable properties for this model.
#
# Properties may be given as symbols, or strings. You can pass some
# options, in a hash, which will be used when the observable is created:
#
# - *initial_value*: The initial value for the property. This value will
# be set when the observable instance is initialized (i.e., when the
# initialize
method is called). Defaults to nil
.
# - *reset_value*: The reset value for the property. This value will be
# set when the observable instance is reset (i.e., when the
# reset!
method is called). If this is not given, the
# initial_value
will be used instead.
# - *core*: Defines whether the property should be used when comparing two
# observables. Defaults to false
.
# - *prevent_reset*: If this is true
the property will not be
# reseted. Defaults to false.
# - *boolean*: If this is true
a "question" method will be
# created for the property (i.e., for a property named foo
# a method named foo?
will be created).
#
# Examples:
#
# class MyObservable
# include RuGUI::ObservablePropertySupport
#
# observable_property :foo, :initial_value => "bar"
# observable_property :bar, :initial_value => "foo", :reset_value => "bar"
# observable_property :core_property, :core => true
# observable_property :non_resetable_property, :prevent_reset => true
#
# # And so on...
# end
def observable_property(property, options = {})
create_observable_property_options(property, options)
create_observable_property_accessors(property)
create_observable_property_boolean_readers(property, options)
end
# Returns the names of core observable properties for this class.
def core_observable_properties
core_observable_properties = []
observable_properties_options.each do |property, options|
core_observable_properties << property if options[:core] == true
end
core_observable_properties
end
# Returns the names of all observable properties for this class.
def observable_properties
observable_properties_options.keys
end
private
def create_observable_property_options(property, options = {})
self.observable_properties_options[property.to_sym] = prepare_options(options)
end
def create_observable_property_accessors(property)
self.class_eval <<-class_eval
def #{property}
@#{property}
end
def #{property}=(value)
old_value = get_old_value(@#{property})
if has_changed?(value, old_value)
@#{property} = ObservablePropertyProxy.new(value, self, '#{property}')
property_changed('#{property}', value, old_value)
end
end
class_eval
end
def create_observable_property_boolean_readers(property, options)
if options[:boolean]
self.class_eval <<-class_eval
def #{property}?
self.#{property} == true
end
class_eval
end
end
def prepare_options(options)
options = default_options.merge(options)
if options[:reset_value].nil? and not options[:initial_value].class.include?(ObservablePropertySupport)
options[:reset_value] = options[:initial_value]
end
options
end
def default_options
{ :core => false,
:initial_value => nil,
:reset_value => nil }
end
end
def self.included(base)
base.extend(ClassMethods)
base.create_class_inheritable_attributes
end
private
def initialize_observers_if_needed
@observers = [] if not defined?(@observers) or @observers.nil?
@named_observers = {} if not defined?(@named_observers) or @named_observers.nil?
end
def get_old_value(property)
begin
return property.clone
rescue TypeError
return property
end
end
def has_changed?(new_value, old_value)
!(new_value.kind_of?(old_value.class) && old_value == new_value)
end
def clone_if_possible(value)
value.clone
rescue TypeError
value
end
end
end