# Copyright (c) 2009 Paolo Capriotti
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# A descriptor is a rose tree with arbitrary properties at each node, used to
# define GUIs declaratively.
#
# Descriptors can be created using a DSL. For example:
#
# ex1 = Descriptor.build(:root, :name => 'parent') do
# child :name => 'foo'
# child :name => 'bar'
# merge_point
# child :name => 'hello' do
# grandchild :name => 'world'
# end
# end
#
# creates a tree which has a node with no name and three children with names
# 'foo', 'bar', and 'hello', and hello having a child of its own, called
# 'world'. Note that descriptor tags (:root, :child and
# :grandchild in the example) are completely arbitrary, but they play
# a special role when merging, together with the :name property.
#
# Merging consists of taking two descriptor trees, and matching their roots by
# tag and name. If they match, their children are recursively matched and
# merged, or simply concatenated when no match is found.
#
# For example, if ex1 above is merged with the following descriptor:
#
# ex2 = Descriptor.build(:root, :name => 'parent') do
# child :name => 'foo2'
# child :name => 'hello' do
# grandchild :name => 'world2'
# end
# end
#
# the resulting descriptor would be equivalent to the one created by:
#
# ex1_merged_with_ex2 = Descriptor.build(:root, :name => 'parent') do
# child :name => 'foo'
# child :name => 'bar'
# child :name => 'foo2'
# child :name => 'hello' do
# grandchild :name => 'world'
# grandchild :name => 'world2'
# end
# end
#
# As can be seen in the example, merge points can be used to specify exactly
# where children of merged descriptors should be inserted.
#
# Merge points can optionally have a count, which specifies the number
# of children to be inserted on that particular point. When the count is
# satisfied, additional children are added at the following merge point, or, if
# no more merge points exist, at the bottom.
#
class Descriptor
attr_reader :tag # @return [Symbol] the descriptor tag
attr_reader :opts # @return [Hash] properties for this descriptor
attr_reader :children # @return [Array] children of this descriptor
#
# Create a descriptor using the DSL.
# @param tag [Symbol] descriptor tag
# @param opts [Hash] arbitrary hash of properties
# @return [Descriptor]
#
def self.build(tag, opts = { }, &blk)
root = new(tag, opts)
builder = Builder.new(root)
builder.instance_eval(&blk) if block_given?
root
end
#
# Create a descriptor with no children.
# @param tag [Symbol] descriptor tag
# @param opts [Hash] arbitrary hash of properties
#
def initialize(tag, opts = { })
@tag = tag
@opts = opts
@children = []
end
#
# Add a child to this descriptor.
#
def add_child(desc)
@children << desc
end
#
# Add a child to this descriptor, taking merge points into account.
#
def merge_child(desc)
mp = @opts[:merge_points].first if @opts[:merge_points]
if mp
@children.insert(mp.position, desc)
@opts[:merge_points].step!
else
add_child(desc)
end
end
#
# Add a merge point to this descriptor. Newly added merge points will not
# affect existing children, even if they were added with merge_child
# @param position merge point position
# @param count maximum number of children that can be merged at this point.
# If negative, no limit on the number of mergeable children is set.
#
def add_merge_point(position, count = -1)
mp = MergePoint.new(position, count)
@opts[:merge_points] ||= MergePoint::List.new
@opts[:merge_points].add(mp)
mp
end
#
# Convert this descriptor to a human readable sexp representation. Descriptor
# properties are printed as ruby hashes.
#
def to_sexp
"(#{@tag} #{@opts.inspect}#{@children.map{|c| ' ' + c.to_sexp}.join})"
end
#
# Destructively merge this descriptor with another.
#
# Descriptors are merged if they match by tag and name, or if this descriptor
# has tag :group and the other one has a property :group
# set to the name of this descriptor.
#
# @param other the descriptor to be merged
# @return [Boolean] whether the merge was successful
#
def merge!(other)
if tag == other.tag and
opts[:name] == other.opts[:name]
# if roots match
other.children.each do |child2|
# merge each of the children of the second descriptor
merged = false
children.each do |child|
# try to match with any of the children of the first descriptor
if child.merge!(child2)
merged = true
break
end
end
# if no match is found, just add it as a child of the root
merge_child(child2.dup) unless merged
end
true
elsif tag == :group and other.opts[:group] == opts[:name]
# if the root is the group of the second descriptor, add it as a child
merge_child(other)
else
false
end
end
class MergePoint
attr_accessor :position, :count
class List
def initialize
@mps = []
end
def first
@mps.first
end
def add(mp)
@mps << mp
end
def step!
raise "Stepping invalid merge point list" if @mps.empty?
@mps.each do |mp|
mp.position += 1
end
@mps.first.count -= 1
clean!
end
private
def clean!
@mps.delete_if {|mp| not mp.valid? }
end
end
def initialize(position, count = -1)
@position = position
@count = count
raise "Creating invalid merge point" if @count == 0
end
def valid?
@count != 0
end
end
class Builder
attr_reader :__desc__
private :__desc__
def initialize(desc)
@__desc__ = desc
end
def method_missing(name, *args, &blk)
opts = if args.empty?
{ }
elsif args.size == 1
if args.first.is_a? Hash
args.first
else
{ :name => args.first }
end
else
args[-1].merge(:name => args.first)
end
child = Descriptor.new(name, opts)
self.class.new(child).instance_eval(&blk) if block_given?
__desc__.add_child(child)
end
def merge_point(count = -1)
@__desc__.add_merge_point(@__desc__.children.size, count)
end
end
end