RUIC 0.4.1 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,5 @@
1
1
  #encoding:utf-8
2
+
2
3
  class RUIC; end
3
4
  module UIC; end
4
5
 
@@ -12,9 +13,37 @@ require_relative 'ruic/behaviors'
12
13
  require_relative 'ruic/statemachine'
13
14
  require_relative 'ruic/presentation'
14
15
 
16
+ # The `RUIC` class provides the interface for running scripts using the special DSL,
17
+ # and for running the interactive REPL.
18
+ # See the {file:README.md README} file for description of the DSL.
15
19
  class RUIC
16
20
  DEFAULTMETADATA = 'C:/Program Files (x86)/NVIDIA Corporation/UI Composer 8.0/res/DataModelMetadata/en-us/MetaData.xml'
17
21
 
22
+ # Execute a script and/or launch the interactive REPL.
23
+ #
24
+ # If you both run a `:script` and then enter the `:repl` all local variables created
25
+ # by the script will be available in the REPL.
26
+ #
27
+ # # Just run a script
28
+ # RUIC.run script:'my.ruic'
29
+ #
30
+ # # Load an application and then enter the REPL
31
+ # RUIC.run uia:'my.uia', repl:true
32
+ #
33
+ # # Run a script and then drop into the REPL
34
+ # RUIC.run script:'my.ruic', repl:true
35
+ #
36
+ # The working directory for scripts is set to the directory of the script.
37
+ #
38
+ # The working directory for the repl is set to the directory of the first
39
+ # loaded application (if any);
40
+ # failing that, the directory of the script (if any);
41
+ # failing that, the current directory.
42
+ #
43
+ # @option opts [String] :script A path to a `.ruic` script to run _(optional)_.
44
+ # @option opts [String] :uia An `.uia` file to load (before running the script and/or REPL) _(optional)_.
45
+ # @option opts [Boolean] :repl Pass `true` to enter the command-line REPL after executing the script (if any).
46
+ # @return [nil]
18
47
  def self.run(opts={})
19
48
  opts = opts
20
49
  ruic = nil
@@ -48,47 +77,75 @@ class RUIC
48
77
  end
49
78
  end
50
79
 
80
+ # Creates a new environment for executing a RUIC script.
81
+ # @param metadata [String] Path to the `MetaData.xml` file to use.
51
82
  def initialize( metadata=DEFAULTMETADATA )
52
83
  @metadata = metadata
53
84
  @apps = {}
54
85
  end
55
86
 
87
+ # Set the metadata to use; generally called from the RUIC DSL.
88
+ # @param path [String] Path to the `MetaData.xml` file, either absolute or relative to the working directory.
56
89
  def metadata(path)
57
90
  @metadata = path
58
91
  end
59
92
 
93
+ # Load an application, making it available as `app`, `app2`, etc.
94
+ # @param path [String] Path to the `*.uia` application file.
95
+ # @return [UIC::Application] The new application loaded.
60
96
  def uia(path)
61
- meta = UIC.Meta @metadata
97
+ meta = UIC.MetaData @metadata
62
98
  name = @apps.empty? ? :app : :"app#{@apps.length+1}"
63
- @apps[name] = UIC.App(meta,path)
99
+ @apps[name] = UIC.Application(meta,path)
64
100
  end
65
101
 
102
+ # @return [Binding] the shared binding used for evaluating the script and REPL
66
103
  def env
67
104
  @env ||= binding
68
105
  end
69
106
 
70
- module SelfInspecting
71
- def inspect
72
- to_s
73
- end
74
- end
107
+ # @private used as a one-off
108
+ module SelfInspecting; def inspect; to_s; end; end
75
109
 
110
+ # Used to resolve bare `app` and `app2` calls to a loaded {UIC::Application Application}.
111
+ # @return [UIC::Application] the new application loaded.
76
112
  def method_missing(name,*a)
77
113
  @apps[name] || (name=~/^app\d*/ ? "(no #{name} loaded)".extend(SelfInspecting) : super)
78
114
  end
79
115
 
116
+ # Simple assertion mechanism to be used within scripts.
117
+ #
118
+ # @example 1) simple call syntax
119
+ # # Provides a generic failure message
120
+ # assert a==b
121
+ # #=> assertion failed (my.ruic line 17)
122
+ #
123
+ # # Provides a custom failure message
124
+ # assert a==b, "a should equal b"
125
+ # #=> a should equal b : assertion failed (my.ruic line 17)
126
+ #
127
+ # @example 2) block with string syntax
128
+ # # The code in the string to eval is also the failure message
129
+ # assert{ "a==b" }
130
+ # #=> a==b : assertion failed (my.ruic line 17)
131
+ #
132
+ # @param condition [Boolean] the value to evaluate.
133
+ # @param msg [String] the nice error message to display.
134
+ # @yieldreturn [String] the code to evaluate as a condition.
80
135
  def assert(condition=:CONDITIONNOTSUPPLIED,msg=nil,&block)
81
- if block && condition==:CONDITIONNOTSUPPLIED || condition.is_a?(String)
82
- msg = condition.is_a?(String) ? condition : yield
136
+ if block && condition==:CONDITIONNOTSUPPLIED
137
+ msg = yield
83
138
  condition = msg.is_a?(String) ? eval(msg,block.binding) : msg
84
139
  end
85
- unless condition
140
+ condition || begin
86
141
  file, line, _ = caller.first.split(':')
87
- puts "#{msg && "#{msg} : "}assertion failed (#{file} line #{line})"
142
+ puts "#{"#{msg} : " unless msg.nil?}assertion failed (#{file} line #{line})"
88
143
  exit 1
89
144
  end
90
145
  end
91
146
 
147
+ # Nicer name for `puts` to be used in the DSL, printing the
148
+ # 'nice' string equivalent for all supplied arguments.
92
149
  def show(*a); puts *a.map(&:to_s); end
93
150
 
94
151
  def inspect
@@ -96,6 +153,18 @@ class RUIC
96
153
  end
97
154
  end
98
155
 
156
+ # Run a series of commands inside the RUIC DSL.
157
+ #
158
+ # @example
159
+ # require 'ruic'
160
+ # RUIC do
161
+ # uia 'test/MyProject/MyProject.uia'
162
+ # show app
163
+ # #=>UIC::Application 'MyProject.uia'>
164
+ # end
165
+ #
166
+ # If no block is supplied, this is the same as {RUIC.run RUIC.run(opts)}.
167
+ # @option opts [String] :uia Optionally load an application before running the script.
99
168
  def RUIC(opts={},&block)
100
169
  if block
101
170
  Dir.chdir(File.dirname($0)) do
@@ -1,3 +1,4 @@
1
+ # The `Application` represents the root of your UIC application, corresponding to a `.uia` file.
1
2
  class UIC::Application
2
3
  include UIC::FileBacked
3
4
 
@@ -5,17 +6,27 @@ class UIC::Application
5
6
  "<UIC::Application '#{File.basename(file)}'#{:' FILENOTFOUND' unless file_found?}>"
6
7
  end
7
8
 
9
+ # @return [UIC::MetaData] the metadata loaded for this application
8
10
  attr_reader :metadata
9
- def initialize(metadata,uia_path)
11
+
12
+ # @param metadata [UIC::MetaData] the `MetaData` to use for this application.
13
+ # @param uia_path [String] path to a `.uia` file to load.
14
+ # If omitted you will need to later set the `.file = ` for the
15
+ # instance and then call {#load_from_file}.
16
+ def initialize(metadata,uia_path=nil)
10
17
  @metadata = metadata
11
18
  self.file = uia_path
12
19
  @assets = {}
13
20
  load_from_file if file_found?
14
21
  end
15
22
 
23
+ # Loads the application from the file. If you pass the path to your `.uia`
24
+ # to {#initialize} then this method is called automatically.
25
+ #
26
+ # @return [nil]
16
27
  def load_from_file
17
- self.doc = Nokogiri.XML(File.read(file,encoding:'utf-8'))
18
- @assets = @doc.search('assets *').map do |el|
28
+ self.doc = Nokogiri.XML(File.read(file,encoding:'utf-8'))
29
+ @assets = @doc.search('assets *').map do |el|
19
30
  case el.name
20
31
  when 'behavior' then UIC::Application::Behavior
21
32
  when 'statemachine' then UIC::Application::StateMachine
@@ -23,16 +34,32 @@ class UIC::Application
23
34
  when 'renderplugin' then UIC::Application::RenderPlugin
24
35
  end.new(self,el)
25
36
  end.group_by{ |asset| asset.el.name }
37
+ nil
26
38
  end
27
39
 
40
+ # @return [Boolean] true if there are any errors with the application.
28
41
  def errors?
29
42
  !errors.empty?
30
43
  end
31
44
 
45
+ # @example
46
+ # show app.errors if app.errors?
47
+ # @return [Array<String>] an array (possibly empty) of all errors in this application (including referenced assets).
32
48
  def errors
33
49
  file_found? ? assets.flat_map(&:errors) : ["File not found: '#{file}'"]
34
50
  end
35
51
 
52
+ # Find an asset by `.uia` identifier or path to the asset.
53
+ # @example
54
+ # main1 = app['#main']
55
+ # main2 = app['MyMain.uip']
56
+ # main3 = app.main_presentation
57
+ # assert main1==main2 && main2==main3
58
+ # @param asset_id_or_path [String] an idref like `"#status"` or a relative path to the asset like `"VehicleStatus.uip"` or `"scripts/Main.lua"`.
59
+ # @return [UIC::Application::Behavior]
60
+ # @return [UIC::Application::StateMachine]
61
+ # @return [UIC::Application::Presentation]
62
+ # @return [UIC::Application::RenderPlugin]
36
63
  def [](asset_id_or_path)
37
64
  all = assets
38
65
  if asset_id_or_path.start_with?('#')
@@ -44,10 +71,12 @@ class UIC::Application
44
71
  end
45
72
  end
46
73
 
74
+ # @private do not document until working
47
75
  def unused_files
48
76
  referenced_files - directory_files
49
77
  end
50
78
 
79
+ # @private do not document until working
51
80
  def referenced_files
52
81
  # TODO: state machines can reference external scripts
53
82
  # TODO: behaviors can reference external scripts
@@ -55,10 +84,15 @@ class UIC::Application
55
84
  + presentations.flat_map{ |pres| pres.presentation.referenced_files }
56
85
  end
57
86
 
87
+ # @return [Array] all assets referenced by the application. Ordered by the order they appear in the `.uia`.
58
88
  def assets
59
89
  @assets.values.inject(:+)
60
90
  end
61
91
 
92
+ # @example
93
+ # main = app.main
94
+ # main = app.main_presentation # more-explicit alternative
95
+ # @return [UIC::Application::Presentation] the main presentation rendered by the application.
62
96
  def main_presentation
63
97
  initial_id = @doc.at('assets')['initial']
64
98
  presos = presentations
@@ -66,13 +100,18 @@ class UIC::Application
66
100
  end
67
101
  alias_method :main, :main_presentation
68
102
 
103
+ # Change which presentation is rendered for the application.
104
+ # @param presentation [UIC::Application::Presentation]
105
+ # @return [UIC::Application::Presentation]
69
106
  def main_presentation=(presentation)
70
- # TODO: set to Presentation or PresentationAsset
107
+ # TODO: set to Presentation or Application::Presentation
71
108
  # TODO: create a unique ID if none exists
72
109
  @doc.at('assets')['initial'] = presentation.id
73
110
  end
74
111
 
112
+ # @return [Hash] a mapping of image paths to arrays of elements/assets that reference that image.
75
113
  def image_usage
114
+ # TODO: this returns the same asset multiple times, with no indication of which property is using it; should switch to Properties.
76
115
  Hash[
77
116
  (presentations + statemachines)
78
117
  .map(&:image_usage)
@@ -84,31 +123,48 @@ class UIC::Application
84
123
  ].tap{ |h| h.extend(UIC::PresentableHash) }
85
124
  end
86
125
 
126
+ # @return [Array<String>] array of all image paths **used** by the application (not just in subfolders).
87
127
  def image_paths
88
128
  image_usage.keys
89
129
  end
90
130
 
131
+ # @return [Array<UIC::Application::Presentation>] all presentations referenced by the application.
91
132
  def presentations
92
133
  @assets['presentation'] ||= []
93
134
  end
94
135
 
136
+ # @return [Array<UIC::Application::Behavior>] all behaviors referenced by the application.
95
137
  def behaviors
96
138
  @assets['behavior'] ||= []
97
139
  end
98
140
 
141
+ # @return [Array<UIC::Application::StateMachine>] all state machines referenced by the application.
99
142
  def statemachines
100
143
  @assets['statemachine'] ||= []
101
144
  end
102
145
 
146
+ # @return [Array<UIC::Application::RenderPlugin>] all render plug-ins referenced by the application.
103
147
  def renderplugins
104
148
  @assets['renderplugin'] ||= []
105
149
  end
106
150
 
151
+ # Save changes to this application and every asset to disk.
107
152
  def save_all!
108
- # save!
153
+ save!
109
154
  presentations.each(&:save!)
110
- end
111
-
155
+ # TODO: enumerate other assets and save them
156
+ end
157
+
158
+ # Find an element or asset in a presentation by scripting path.
159
+ # @example
160
+ # # Four ways to find the same layer
161
+ # layer1 = app.at "main:Scene.Layer"
162
+ # layer2 = app/"main:Scene.Layer"
163
+ # layer3 = app.main.at "Scene.Layer"
164
+ # layer4 = app.main/"Scene.Layer"
165
+ #
166
+ # assert layer1==layer2 && layer2==layer3 && layer3==layer4
167
+ # @return [UIC::MetaData::Root] The found asset, or `nil` if it cannot be found.
112
168
  def at(path)
113
169
  parts = path.split(':')
114
170
  preso = parts.length==2 ? self["##{parts.first}"] : main_presentation
@@ -116,17 +172,16 @@ class UIC::Application
116
172
  preso.at(parts.last)
117
173
  end
118
174
  alias_method :/, :at
119
-
120
- def xml
121
- @doc.to_xml
122
- end
123
175
  end
124
176
 
125
177
  class << UIC
126
- def Application(metadata,uia_path)
178
+ # Create a new {UIC::Application}. Shortcut for `UIC::Application.new(...)`
179
+ # @param metadata [UIC::MetaData] the MetaData to use for this application.
180
+ # @param uia_path [String] a path to the .uia to load.
181
+ # @return [UIC::Application]
182
+ def Application(metadata,uia_path=nil)
127
183
  UIC::Application.new( metadata, uia_path )
128
184
  end
129
- alias_method :App, :Application
130
185
  end
131
186
 
132
187
  __END__
@@ -1,288 +1,290 @@
1
- #encoding: utf-8
2
- class UIC::Asset
3
- class Root
4
- @properties = {}
5
- class << self
6
- attr_reader :name
7
- def properties
8
- (ancestors[1].respond_to?(:properties) ? ancestors[1].properties : {}).merge(@properties)
9
- end
10
-
11
- def each
12
- (@by_name.values - [self]).each{ |klass| yield klass }
13
- end
14
- include Enumerable
15
-
16
- def inspect
17
- "<#{@name}>"
18
- end
19
- end
20
-
21
- def properties
22
- self.class.properties
23
- end
24
-
25
- def at(sub_path)
26
- presentation.at(sub_path,@el)
27
- end
28
- alias_method :/, :at
29
-
30
- attr_accessor :presentation, :el
31
- def initialize( presentation, element )
32
- @presentation = presentation
33
- @el = element
34
- end
35
-
36
- def type
37
- self.class.name
38
- # self.class.name.split('::').last
39
- end
40
-
41
- def parent
42
- presentation.parent_asset(@el)
43
- end
44
-
45
- def children
46
- presentation.child_assets(@el)
47
- end
48
-
49
- def find(criteria={},&block)
50
- criteria[:_under] ||= self
51
- presentation.find(criteria,&block)
52
- end
53
-
54
- # Find the owning component (even if you are a component)
55
- def component
56
- presentation.owning_component(@el)
57
- end
58
-
59
- def component?
60
- @el.name == 'Component'
61
- end
62
-
63
- def master?
64
- presentation.master?(@el)
65
- end
66
-
67
- def slide?
68
- false
69
- end
70
-
71
- def has_slide?(slide_name_or_index)
72
- presentation.has_slide?(@el,slide_name_or_index)
73
- end
74
-
75
- def slides
76
- presentation.slides_for(@el)
77
- end
78
-
79
- def on_slide(slide_name_or_index)
80
- if has_slide?(slide_name_or_index)
81
- UIC::SlideValues.new( self, slide_name_or_index )
82
- end
83
- end
84
-
85
- def path
86
- @path ||= @presentation.path_to(@el)
87
- end
88
-
89
- def name
90
- properties['name'].get( self, presentation.slide_index(@el) )
91
- end
92
-
93
- def name=( new_name )
94
- properties['name'].set( self, new_name, presentation.slide_index(@el) )
95
- end
96
-
97
- # Get the value(s) of an attribute
98
- def [](attribute_name, slide_name_or_index=nil)
99
- if property = properties[attribute_name]
100
- if slide_name_or_index
101
- property.get( self, slide_name_or_index ) if has_slide?(slide_name_or_index)
102
- else
103
- UIC::ValuesPerSlide.new(@presentation,self,property)
104
- end
105
- end
106
- end
107
-
108
- # Set the value of an attribute, either across all slides, or on a particular slide
109
- # el['foo'] = 42
110
- # el['foo',0] = 42
111
- def []=( attribute_name, slide_name_or_index=nil, new_value )
112
- if property = properties[attribute_name] then
113
- property.set(self,new_value,slide_name_or_index)
114
- end
115
- end
116
-
117
- def to_xml
118
- @el.to_xml
119
- end
120
- def inspect
121
- "<asset #{@el.name}##{@el['id']}>"
122
- end
123
-
124
- def to_s
125
- "<#{type} #{path}>"
126
- end
127
-
128
- def ==(other)
129
- (self.class==other.class) && (el==other.el)
130
- end
131
- alias_method :eql?, :==
132
- end
133
-
134
- attr_reader :by_name
135
-
136
- HIER = {}
137
- %w[Asset Slide Scene].each{ |s| HIER[s] = 'Root' }
138
- %w[Node Behavior Effect Image Layer MaterialBase PathAnchorPoint RenderPlugin].each{ |s| HIER[s]='Asset' }
139
- %w[Alias Camera Component Group Light Model Text Path].each{ |s| HIER[s]='Node' }
140
- %w[Material ReferencedMaterial].each{ |s| HIER[s]='MaterialBase' }
141
-
142
- def initialize(xml)
143
- @by_name = {'Root'=>Root}
144
-
145
- doc = Nokogiri.XML(xml)
146
- hack_in_slide_names!(doc)
147
-
148
- HIER.each do |class_name,parent_class_name|
149
- parent_class = @by_name[parent_class_name]
150
- el = doc.root.at(class_name)
151
- @by_name[class_name] = create_class(el,parent_class,el.name)
152
- UIC::Asset.const_set( el.name, @by_name[class_name] ) # give the class instance a name by pointing a constant to it :/
153
- end
154
-
155
- # Extend well-known classes with script interfaces after they are created
156
- @by_name['State'] = @by_name['Slide']
157
- @by_name['Slide'].instance_eval do
158
- attr_accessor :index, :name
159
- define_method :inspect do
160
- "<slide ##{index} of #{@el['component'] || @el.parent['component']}>"
161
- end
162
- define_method(:slide?){ true }
163
- end
164
-
165
- refmat = @by_name['ReferencedMaterial']
166
- @by_name['MaterialBase'].instance_eval do
167
- define_method :replace_with_referenced_material do
168
- type=='ReferencedMaterial' ? self : presentation.replace_asset( self, 'ReferencedMaterial', name:name )
169
- end
170
- end
171
-
172
- @by_name['Path'].instance_eval do
173
- define_method(:anchors){ find _type:'PathAnchorPoint' }
174
- end
175
-
176
- end
177
-
178
- # Creates a class from MetaData.xml with accessors for the <Property> listed.
179
- # Instances of the class are associated with a presentation and know how to
180
- # get/set values in that XML based on value types, slides, defaults.
181
- # Also used to create classes from effects, materials, and behavior preambles.
182
- def create_class(el,parent_class,name='CustomAsset')
183
- Class.new(parent_class) do
184
- @name = name
185
- @properties = Hash[ el.css("Property").map do |e|
186
- type = e['type'] || (e['list'] ? 'String' : 'Float')
187
- type = "Float" if type=="float"
188
- property = UIC::Property.const_get(type).new(e)
189
- [ property.name, UIC::Property.const_get(type).new(e) ]
190
- end ]
191
- end
192
- end
193
-
194
- def new_instance(presentation,el)
195
- klass = @by_name[el.name] || create_class(el,@by_name['Asset'],el.name)
196
- klass.new(presentation,el)
197
- end
198
-
199
- def hack_in_slide_names!(doc)
200
- doc.at('Slide') << '<Property name="name" formalName="Name" type="String" default="Slide" hidden="True" />'
201
- end
202
- end
203
-
204
- def UIC.Meta(metadata_path)
205
- UIC::Asset.new(File.read(metadata_path,encoding:'utf-8'))
206
- end
207
-
208
- class UIC::SlideCollection
209
- include Enumerable
210
- attr_reader :length
211
- def initialize(slides)
212
- @length = slides.length-1
213
- @slides = slides
214
- @lookup = {}
215
- slides.each do |s|
216
- @lookup[s.index] = s
217
- @lookup[s.name] = s
218
- end
219
- end
220
- def each
221
- @slides.each{ |s| yield(s) }
222
- end
223
- def [](index_or_name)
224
- @lookup[ index_or_name ]
225
- end
226
- def inspect
227
- "[ #{@slides.map(&:inspect).join ', '} ]"
228
- end
229
- def to_ary
230
- @slides
231
- end
232
- end
233
-
234
- class UIC::ValuesPerSlide
235
- def initialize(presentation,asset,property)
236
- raise unless presentation.is_a?(UIC::Presentation)
237
- raise unless asset.is_a?(UIC::Asset::Root)
238
- raise unless property.is_a?(UIC::Property)
239
- @preso = presentation
240
- @asset = asset
241
- @el = asset.el
242
- @property = property
243
- end
244
- def value
245
- values.first
246
- end
247
- def [](slide_name_or_index)
248
- @property.get( @asset, slide_name_or_index )
249
- end
250
- def []=(slide_name_or_index,new_value)
251
- @property.set( @asset, new_value, slide_name_or_index )
252
- end
253
- def linked?
254
- @preso.attribute_linked?(@el,@property.name)
255
- end
256
- def unlink
257
- @preso.unlink_attribute( @el, @property.name )
258
- end
259
- def link
260
- @preso.link_attribute( @el, @property.name )
261
- end
262
- def values
263
- @asset.slides.map{ |s| self[s.name] }
264
- end
265
- def inspect
266
- "<Values of '#{@asset.name}.#{@property.name}' across slides>"
267
- end
268
- alias_method :to_s, :inspect
269
- end
270
-
271
- class UIC::SlideValues
272
- def initialize( asset, slide )
273
- @asset = asset
274
- @slide = slide
275
- end
276
- def [](attribute_name)
277
- @asset[attribute_name,@slide]
278
- end
279
- def []=( attribute_name, new_value )
280
- @asset[attribute_name,@slide] = new_value
281
- end
282
- def method_missing( name, *args, &blk )
283
- asset.send(name,*args,&blk)
284
- end
285
- def inspect
286
- "<#{@asset.inspect} on slide #{@slide.inspect}>"
287
- end
1
+ #encoding: utf-8
2
+ class UIC::MetaData
3
+ class Root
4
+ @properties = {}
5
+ class << self
6
+ attr_reader :name
7
+ def properties
8
+ (ancestors[1].respond_to?(:properties) ? ancestors[1].properties : {}).merge(@properties)
9
+ end
10
+
11
+ def each
12
+ (@by_name.values - [self]).each{ |klass| yield klass }
13
+ end
14
+ include Enumerable
15
+
16
+ def inspect
17
+ "<#{@name}>"
18
+ end
19
+ end
20
+
21
+ def properties
22
+ self.class.properties
23
+ end
24
+
25
+ def at(sub_path)
26
+ presentation.at(sub_path,@el)
27
+ end
28
+ alias_method :/, :at
29
+
30
+ attr_accessor :presentation, :el
31
+ def initialize( presentation, element )
32
+ @presentation = presentation
33
+ @el = element
34
+ end
35
+
36
+ def type
37
+ self.class.name
38
+ # self.class.name.split('::').last
39
+ end
40
+
41
+ def parent
42
+ presentation.parent_asset(self)
43
+ end
44
+
45
+ def children
46
+ presentation.child_assets(self)
47
+ end
48
+
49
+ def find(criteria={},&block)
50
+ criteria[:_under] ||= self
51
+ presentation.find(criteria,&block)
52
+ end
53
+
54
+ # Find the owning component (even if you are a component)
55
+ def component
56
+ presentation.owning_component(self)
57
+ end
58
+
59
+ def component?
60
+ @el.name == 'Component'
61
+ end
62
+
63
+ def master?
64
+ presentation.master?(self)
65
+ end
66
+
67
+ def slide?
68
+ false
69
+ end
70
+
71
+ def has_slide?(slide_name_or_index)
72
+ presentation.has_slide?(self,slide_name_or_index)
73
+ end
74
+
75
+ def slides
76
+ presentation.slides_for(self)
77
+ end
78
+
79
+ def on_slide(slide_name_or_index)
80
+ if has_slide?(slide_name_or_index)
81
+ UIC::SlideValues.new( self, slide_name_or_index )
82
+ end
83
+ end
84
+
85
+ def path
86
+ @path ||= @presentation.path_to(self)
87
+ end
88
+
89
+ def name
90
+ properties['name'].get( self, presentation.slide_index(self) )
91
+ end
92
+
93
+ def name=( new_name )
94
+ properties['name'].set( self, new_name, presentation.slide_index(self) )
95
+ end
96
+
97
+ # Get the value(s) of an attribute
98
+ def [](attribute_name, slide_name_or_index=nil)
99
+ if property = properties[attribute_name]
100
+ if slide_name_or_index
101
+ property.get( self, slide_name_or_index ) if has_slide?(slide_name_or_index)
102
+ else
103
+ UIC::ValuesPerSlide.new(@presentation,self,property)
104
+ end
105
+ end
106
+ end
107
+
108
+ # Set the value of an attribute, either across all slides, or on a particular slide
109
+ # el['foo'] = 42
110
+ # el['foo',0] = 42
111
+ def []=( attribute_name, slide_name_or_index=nil, new_value )
112
+ if property = properties[attribute_name] then
113
+ property.set(self,new_value,slide_name_or_index)
114
+ end
115
+ end
116
+
117
+ def to_xml
118
+ @el.to_xml
119
+ end
120
+ def inspect
121
+ "<asset #{@el.name}##{@el['id']}>"
122
+ end
123
+
124
+ def to_s
125
+ "<#{type} #{path}>"
126
+ end
127
+
128
+ def ==(other)
129
+ (self.class==other.class) && (el==other.el)
130
+ end
131
+ alias_method :eql?, :==
132
+ end
133
+
134
+ attr_reader :by_name
135
+
136
+ HIER = {}
137
+ %w[Asset Slide Scene].each{ |s| HIER[s] = 'Root' }
138
+ %w[Node Behavior Effect Image Layer MaterialBase PathAnchorPoint RenderPlugin].each{ |s| HIER[s]='Asset' }
139
+ %w[Alias Camera Component Group Light Model Text Path].each{ |s| HIER[s]='Node' }
140
+ %w[Material ReferencedMaterial].each{ |s| HIER[s]='MaterialBase' }
141
+
142
+ def initialize(xml)
143
+ @by_name = {'Root'=>Root}
144
+
145
+ doc = Nokogiri.XML(xml)
146
+ hack_in_slide_names!(doc)
147
+
148
+ HIER.each do |class_name,parent_class_name|
149
+ parent_class = @by_name[parent_class_name]
150
+ el = doc.root.at(class_name)
151
+ @by_name[class_name] = create_class(el,parent_class,el.name)
152
+ end
153
+
154
+ # Extend well-known classes with script interfaces after they are created
155
+ @by_name['State'] = @by_name['Slide']
156
+ @by_name['Slide'].instance_eval do
157
+ attr_accessor :index, :name
158
+ define_method :inspect do
159
+ "<slide ##{index} of #{@el['component'] || @el.parent['component']}>"
160
+ end
161
+ define_method(:slide?){ true }
162
+ end
163
+
164
+ refmat = @by_name['ReferencedMaterial']
165
+ @by_name['MaterialBase'].instance_eval do
166
+ define_method :replace_with_referenced_material do
167
+ type=='ReferencedMaterial' ? self : presentation.replace_asset( self, 'ReferencedMaterial', name:name )
168
+ end
169
+ end
170
+
171
+ @by_name['Path'].instance_eval do
172
+ define_method(:anchors){ find _type:'PathAnchorPoint' }
173
+ end
174
+
175
+ end
176
+
177
+ # Creates a class from MetaData.xml with accessors for the <Property> listed.
178
+ # Instances of the class are associated with a presentation and know how to
179
+ # get/set values in that XML based on value types, slides, defaults.
180
+ # Also used to create classes from effects, materials, and behavior preambles.
181
+ def create_class(el,parent_class,name='CustomAsset')
182
+ Class.new(parent_class) do
183
+ @name = name.to_s
184
+ @properties = Hash[ el.css("Property").map do |e|
185
+ type = e['type'] || (e['list'] ? 'String' : 'Float')
186
+ type = "Float" if type=="float"
187
+ property = UIC::Property.const_get(type).new(e)
188
+ [ property.name, UIC::Property.const_get(type).new(e) ]
189
+ end ]
190
+ def self.inspect
191
+ @name
192
+ end
193
+ end
194
+ end
195
+
196
+ def new_instance(presentation,el)
197
+ klass = @by_name[el.name] || create_class(el,@by_name['Asset'],el.name)
198
+ klass.new(presentation,el)
199
+ end
200
+
201
+ def hack_in_slide_names!(doc)
202
+ doc.at('Slide') << '<Property name="name" formalName="Name" type="String" default="Slide" hidden="True" />'
203
+ end
204
+ end
205
+
206
+ def UIC.MetaData(metadata_path)
207
+ UIC::MetaData.new(File.read(metadata_path,encoding:'utf-8'))
208
+ end
209
+
210
+ class UIC::SlideCollection
211
+ include Enumerable
212
+ attr_reader :length
213
+ def initialize(slides)
214
+ @length = slides.length-1
215
+ @slides = slides
216
+ @lookup = {}
217
+ slides.each do |s|
218
+ @lookup[s.index] = s
219
+ @lookup[s.name] = s
220
+ end
221
+ end
222
+ def each
223
+ @slides.each{ |s| yield(s) }
224
+ end
225
+ def [](index_or_name)
226
+ @lookup[ index_or_name ]
227
+ end
228
+ def inspect
229
+ "[ #{@slides.map(&:inspect).join ', '} ]"
230
+ end
231
+ def to_ary
232
+ @slides
233
+ end
234
+ end
235
+
236
+ class UIC::ValuesPerSlide
237
+ def initialize(presentation,asset,property)
238
+ raise unless presentation.is_a?(UIC::Presentation)
239
+ raise unless asset.is_a?(UIC::MetaData::Root)
240
+ raise unless property.is_a?(UIC::Property)
241
+ @preso = presentation
242
+ @asset = asset
243
+ @el = asset.el
244
+ @property = property
245
+ end
246
+ def value
247
+ values.first
248
+ end
249
+ def [](slide_name_or_index)
250
+ @property.get( @asset, slide_name_or_index )
251
+ end
252
+ def []=(slide_name_or_index,new_value)
253
+ @property.set( @asset, new_value, slide_name_or_index )
254
+ end
255
+ def linked?
256
+ @preso.attribute_linked?( @asset, @property.name )
257
+ end
258
+ def unlink
259
+ @preso.unlink_attribute( @asset, @property.name )
260
+ end
261
+ def link
262
+ @preso.link_attribute( @asset, @property.name )
263
+ end
264
+ def values
265
+ @asset.slides.map{ |s| self[s.name] }
266
+ end
267
+ def inspect
268
+ "<Values of '#{@asset.name}.#{@property.name}' across slides>"
269
+ end
270
+ alias_method :to_s, :inspect
271
+ end
272
+
273
+ class UIC::SlideValues
274
+ def initialize( asset, slide )
275
+ @asset = asset
276
+ @slide = slide
277
+ end
278
+ def [](attribute_name)
279
+ @asset[attribute_name,@slide]
280
+ end
281
+ def []=( attribute_name, new_value )
282
+ @asset[attribute_name,@slide] = new_value
283
+ end
284
+ def method_missing( name, *args, &blk )
285
+ asset.send(name,*args,&blk)
286
+ end
287
+ def inspect
288
+ "<#{@asset.inspect} on slide #{@slide.inspect}>"
289
+ end
288
290
  end