RUIC 0.4.1 → 0.4.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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