rubyosa 0.1.0
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.
- data/AUTHORS +9 -0
- data/COPYRIGHT +25 -0
- data/README +39 -0
- data/bin/rdoc-osa +144 -0
- data/extconf.rb +53 -0
- data/sample/Finder_show_desktop.rb +7 -0
- data/sample/QT_playall.rb +29 -0
- data/sample/iChat_uptime.rb +11 -0
- data/sample/iTunes_control.rb +64 -0
- data/sample/iTunes_fade_volume.rb +22 -0
- data/sample/iTunes_inspect.rb +14 -0
- data/sample/sdef.rb +36 -0
- data/src/lib/rbosa.rb +761 -0
- data/src/rbosa.c +551 -0
- data/src/rbosa.h +51 -0
- data/src/rbosa_conv.c +97 -0
- data/src/rbosa_sdef.c +150 -0
- metadata +69 -0
data/src/lib/rbosa.rb
ADDED
@@ -0,0 +1,761 @@
|
|
1
|
+
# Copyright (c) 2006, Apple Computer, Inc. All rights reserved.
|
2
|
+
#
|
3
|
+
# Redistribution and use in source and binary forms, with or without
|
4
|
+
# modification, are permitted provided that the following conditions
|
5
|
+
# are met:
|
6
|
+
# 1. Redistributions of source code must retain the above copyright
|
7
|
+
# notice, this list of conditions and the following disclaimer.
|
8
|
+
# 2. Redistributions in binary form must reproduce the above copyright
|
9
|
+
# notice, this list of conditions and the following disclaimer in the
|
10
|
+
# documentation and/or other materials provided with the distribution.
|
11
|
+
# 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of
|
12
|
+
# its contributors may be used to endorse or promote products derived
|
13
|
+
# from this software without specific prior written permission.
|
14
|
+
#
|
15
|
+
# THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND
|
16
|
+
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
17
|
+
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
18
|
+
# ARE DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR
|
19
|
+
# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
20
|
+
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
|
21
|
+
# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
|
22
|
+
# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
|
23
|
+
# STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
|
24
|
+
# IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
25
|
+
# POSSIBILITY OF SUCH DAMAGE.
|
26
|
+
|
27
|
+
require 'osa'
|
28
|
+
require 'date'
|
29
|
+
|
30
|
+
# Try to load RubyGems first, libxml-ruby may have been installed by it.
|
31
|
+
begin require 'rubygems' rescue LoadError end
|
32
|
+
|
33
|
+
# If libxml-ruby is not present, switch to REXML.
|
34
|
+
USE_LIBXML = begin
|
35
|
+
require 'xml/libxml'
|
36
|
+
|
37
|
+
# libxml-ruby bug workaround.
|
38
|
+
class XML::Node
|
39
|
+
alias_method :old_cmp, :==
|
40
|
+
def ==(x)
|
41
|
+
(x != nil and old_cmp(x))
|
42
|
+
end
|
43
|
+
end
|
44
|
+
true
|
45
|
+
rescue LoadError
|
46
|
+
require 'rexml/document'
|
47
|
+
|
48
|
+
# REXML -> libxml-ruby compatibility layer.
|
49
|
+
class REXML::Element
|
50
|
+
alias_method :old_find, :find
|
51
|
+
def find(path=nil, &block)
|
52
|
+
if path.nil? and block
|
53
|
+
old_find { |*x| block.call(*x) }
|
54
|
+
else
|
55
|
+
list = []
|
56
|
+
::REXML::XPath.each(self, path) { |e| list << e }
|
57
|
+
list
|
58
|
+
end
|
59
|
+
end
|
60
|
+
def [](attr)
|
61
|
+
attributes[attr]
|
62
|
+
end
|
63
|
+
def find_first(path)
|
64
|
+
::REXML::XPath.first(self, path)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
false
|
68
|
+
end
|
69
|
+
|
70
|
+
class String
|
71
|
+
def to_4cc
|
72
|
+
OSA.__four_char_code__(self)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
class OSA::Enumerator
|
77
|
+
attr_reader :code, :name, :group_code
|
78
|
+
|
79
|
+
def initialize(const, name, code, group_code)
|
80
|
+
@const, @name, @code, @group_code = const, name, code, group_code
|
81
|
+
self.class.instances[code] = self
|
82
|
+
end
|
83
|
+
|
84
|
+
def self.enum_for_code(code)
|
85
|
+
instances[code]
|
86
|
+
end
|
87
|
+
|
88
|
+
def to_s
|
89
|
+
@name
|
90
|
+
end
|
91
|
+
|
92
|
+
def inspect
|
93
|
+
"<#{@const}>"
|
94
|
+
end
|
95
|
+
|
96
|
+
#######
|
97
|
+
private
|
98
|
+
#######
|
99
|
+
|
100
|
+
def self.instances
|
101
|
+
(@@instances rescue @@instances = {})
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
class OSA::Element
|
106
|
+
REAL_NAME = CODE = nil
|
107
|
+
def to_rbobj
|
108
|
+
unless __type__ == 'null'
|
109
|
+
val = OSA.convert_to_ruby(self)
|
110
|
+
val == nil ? self : val
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
def self.from_rbobj(requested_type, value, enum_group_codes)
|
115
|
+
self.__new__(*OSA.convert_to_osa(requested_type, value, enum_group_codes))
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
class OSA::ElementList
|
120
|
+
include Enumerable
|
121
|
+
def each
|
122
|
+
self.size.times { |i| yield(self[i]) }
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
class OSA::ElementRecord
|
127
|
+
def to_hash
|
128
|
+
h = {}
|
129
|
+
self.to_a.each { |key, val| h[key] = val.to_rbobj }
|
130
|
+
return h
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
module OSA::ObjectSpecifier
|
135
|
+
def get
|
136
|
+
@app.__send_event__('core', 'getd', [['----', self]], true).to_rbobj
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
class OSA::ObjectSpecifierList
|
141
|
+
include Enumerable
|
142
|
+
|
143
|
+
def initialize(app, desired_class, container)
|
144
|
+
@app, @desired_class, @container = app, desired_class, container
|
145
|
+
end
|
146
|
+
|
147
|
+
def length
|
148
|
+
@length ||= @app.__send_event__(
|
149
|
+
'core', 'cnte',
|
150
|
+
[['----', @container], ['kocl', OSA::Element.__new__('type', @desired_class::CODE.to_4cc)]],
|
151
|
+
true).to_rbobj
|
152
|
+
end
|
153
|
+
alias_method :size, :length
|
154
|
+
|
155
|
+
def [](idx)
|
156
|
+
idx += 1 # AE starts counting at 1.
|
157
|
+
o = obj_spec_with_key(OSA::Element.__new__('long', [idx].pack('l')))
|
158
|
+
o.instance_variable_set(:@app, @app)
|
159
|
+
o.extend OSA::ObjectSpecifier
|
160
|
+
end
|
161
|
+
|
162
|
+
def each
|
163
|
+
self.length.times { |i| yield(self[i]) }
|
164
|
+
end
|
165
|
+
|
166
|
+
def get
|
167
|
+
o = obj_spec_with_key(OSA::Element.__new__('abso', 'all '.to_4cc))
|
168
|
+
o.instance_variable_set(:@app, @app)
|
169
|
+
o.extend OSA::ObjectSpecifier
|
170
|
+
o.get
|
171
|
+
end
|
172
|
+
alias_method :to_a, :get
|
173
|
+
|
174
|
+
def ==(other)
|
175
|
+
other.kind_of?(self.class) \
|
176
|
+
and other.length == self.length \
|
177
|
+
and (0..other.length).all? { |i| other[i] == self[i] }
|
178
|
+
end
|
179
|
+
|
180
|
+
#######
|
181
|
+
private
|
182
|
+
#######
|
183
|
+
|
184
|
+
def obj_spec_with_key(element)
|
185
|
+
@desired_class.__new_object_specifier__(@desired_class::CODE, @container,
|
186
|
+
'indx', element)
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
module OSA
|
191
|
+
def self.app_with_name(name)
|
192
|
+
self.__app__(*OSA.__scripting_info__(:by_name, name))
|
193
|
+
end
|
194
|
+
|
195
|
+
def self.app_with_path(path)
|
196
|
+
self.__app__(*OSA.__scripting_info__(:by_path, path))
|
197
|
+
end
|
198
|
+
|
199
|
+
def self.app_with_bundle_id(bundle_id)
|
200
|
+
self.__app__(*OSA.__scripting_info__(:by_bundle_id, bundle_id))
|
201
|
+
end
|
202
|
+
|
203
|
+
def self.app_with_signature(signature)
|
204
|
+
self.__app__(*OSA.__scripting_info__(:by_signature, signature))
|
205
|
+
end
|
206
|
+
|
207
|
+
def self.app(name)
|
208
|
+
self.app_with_name(name)
|
209
|
+
end
|
210
|
+
|
211
|
+
@conversions_to_ruby = {}
|
212
|
+
@conversions_to_osa = {}
|
213
|
+
|
214
|
+
def self.add_conversion(hash, types, block, max_arity, replace=false)
|
215
|
+
raise "Conversion block has to accept either #{(1..max_arity).to_a.join(', ')} arguments" unless (1..max_arity) === block.arity
|
216
|
+
types.each do |type|
|
217
|
+
next if !replace and hash.has_key?(type)
|
218
|
+
hash[type] = block
|
219
|
+
end
|
220
|
+
end
|
221
|
+
|
222
|
+
def self.replace_conversion_to_ruby(*types, &block)
|
223
|
+
add_conversion(@conversions_to_ruby, types, block, 3, true)
|
224
|
+
end
|
225
|
+
|
226
|
+
def self.add_conversion_to_ruby(*types, &block)
|
227
|
+
add_conversion(@conversions_to_ruby, types, block, 3)
|
228
|
+
end
|
229
|
+
|
230
|
+
def self.replace_conversion_to_osa(*types, &block)
|
231
|
+
add_conversion(@conversions_to_osa, types, block, 2, true)
|
232
|
+
end
|
233
|
+
|
234
|
+
def self.add_conversion_to_osa(*types, &block)
|
235
|
+
add_conversion(@conversions_to_osa, types, block, 2)
|
236
|
+
end
|
237
|
+
|
238
|
+
def self.convert_to_ruby(osa_object)
|
239
|
+
osa_type = osa_object.__type__.to_s
|
240
|
+
osa_data = osa_object.__data__(osa_type) if osa_type and osa_type != 'null'
|
241
|
+
if conversion = @conversions_to_ruby[osa_type]
|
242
|
+
args = [osa_data, osa_type, osa_object]
|
243
|
+
conversion.call(*args[0..(conversion.arity - 1)])
|
244
|
+
end
|
245
|
+
end
|
246
|
+
|
247
|
+
def self.convert_to_osa(requested_type, value, enum_group_codes=nil)
|
248
|
+
if conversion = @conversions_to_osa[requested_type]
|
249
|
+
args = [value, requested_type]
|
250
|
+
conversion.call(*args[0..(conversion.arity - 1)])
|
251
|
+
elsif enum_group_codes and enum_group_codes.include?(requested_type)
|
252
|
+
['enum', value.code.to_4cc]
|
253
|
+
else
|
254
|
+
STDERR.puts "unrecognized type #{requested_type}" if $VERBOSE
|
255
|
+
['null', nil]
|
256
|
+
end
|
257
|
+
end
|
258
|
+
|
259
|
+
def self.set_params(hash)
|
260
|
+
previous_values = {}
|
261
|
+
hash.each do |key, val|
|
262
|
+
ivar_key = '@' + key.to_s
|
263
|
+
previous_val = self.instance_variable_get(ivar_key)
|
264
|
+
if previous_val.nil?
|
265
|
+
raise ArgumentError, "Invalid key value (no parameter named #{key} was found)"
|
266
|
+
end
|
267
|
+
previous_values[ivar_key] = previous_val;
|
268
|
+
self.instance_variable_set(ivar_key, hash[key])
|
269
|
+
end
|
270
|
+
if block_given?
|
271
|
+
yield
|
272
|
+
previous_values.each { |key, val| self.instance_variable_set(key, val) }
|
273
|
+
end
|
274
|
+
nil
|
275
|
+
end
|
276
|
+
|
277
|
+
#######
|
278
|
+
private
|
279
|
+
#######
|
280
|
+
|
281
|
+
class DocItem
|
282
|
+
attr_reader :name, :description
|
283
|
+
def initialize(name, description)
|
284
|
+
@name = name
|
285
|
+
@description = description
|
286
|
+
end
|
287
|
+
end
|
288
|
+
|
289
|
+
class DocMethod < DocItem
|
290
|
+
attr_reader :result, :args
|
291
|
+
def initialize(name, description, result, args)
|
292
|
+
super(name, description)
|
293
|
+
@result = result
|
294
|
+
@args = args
|
295
|
+
end
|
296
|
+
def inspect
|
297
|
+
"<Method #{name} (#{description})>"
|
298
|
+
end
|
299
|
+
end
|
300
|
+
|
301
|
+
def self.__app__(name, signature, sdef)
|
302
|
+
@apps ||= {}
|
303
|
+
app = @apps[signature]
|
304
|
+
return app if app
|
305
|
+
doc = if USE_LIBXML
|
306
|
+
parser = XML::Parser.new
|
307
|
+
parser.string = sdef
|
308
|
+
parser.parse
|
309
|
+
else
|
310
|
+
REXML::Document.new(sdef)
|
311
|
+
end
|
312
|
+
|
313
|
+
# Creates a module for this app, we will define the scripting interface within it.
|
314
|
+
app_module = Module.new
|
315
|
+
self.const_set(rubyfy_constant_string(name), app_module)
|
316
|
+
|
317
|
+
# Retrieves and creates enumerations.
|
318
|
+
enum_group_codes = {}
|
319
|
+
doc.find('/dictionary/suite/enumeration').each do |element|
|
320
|
+
enum_group_code = element['code']
|
321
|
+
enum_module_name = rubyfy_constant_string(element['name'], true)
|
322
|
+
enum_module = Module.new
|
323
|
+
enum_group_codes[enum_group_code] = enum_module
|
324
|
+
|
325
|
+
documentation = []
|
326
|
+
enum_module.const_set(:DESCRIPTION, documentation)
|
327
|
+
|
328
|
+
element.find('enumerator').each do |element|
|
329
|
+
name = element['name']
|
330
|
+
enum_name = rubyfy_constant_string(name, true)
|
331
|
+
enum_code = element['code']
|
332
|
+
enum_const = app_module.name + '::' + enum_module_name + '::' + enum_name
|
333
|
+
|
334
|
+
enum = OSA::Enumerator.new(enum_const, name, enum_code, enum_group_code)
|
335
|
+
enum_module.const_set(enum_name, enum)
|
336
|
+
|
337
|
+
documentation << DocItem.new(enum_name, englishify_sentence(element['description']))
|
338
|
+
end
|
339
|
+
|
340
|
+
app_module.const_set(enum_module_name, enum_module)
|
341
|
+
end
|
342
|
+
|
343
|
+
# Retrieves and creates classes.
|
344
|
+
classes = {}
|
345
|
+
class_elements = {}
|
346
|
+
doc.find('/dictionary/suite/class').each do |element|
|
347
|
+
(class_elements[element['name']] ||= []) << element
|
348
|
+
end
|
349
|
+
class_elements.values.flatten.each do |element|
|
350
|
+
klass = add_class_from_xml_element(element, class_elements, classes, app_module)
|
351
|
+
methods_doc = []
|
352
|
+
description = englishify_sentence(element['description'])
|
353
|
+
if klass.const_defined?(:DESCRIPTION)
|
354
|
+
klass.const_set(:DESCRIPTION, description) if klass.const_get(:DESCRIPTION).nil?
|
355
|
+
else
|
356
|
+
klass.const_set(:DESCRIPTION, description)
|
357
|
+
end
|
358
|
+
if klass.const_defined?(:METHODS_DESCRIPTION)
|
359
|
+
methods_doc = klass.const_get(:METHODS_DESCRIPTION)
|
360
|
+
else
|
361
|
+
methods_doc = []
|
362
|
+
klass.const_set(:METHODS_DESCRIPTION, methods_doc)
|
363
|
+
end
|
364
|
+
|
365
|
+
# Creates properties.
|
366
|
+
element.find('property').each do |pelement|
|
367
|
+
name = pelement['name']
|
368
|
+
code = pelement['code']
|
369
|
+
type = pelement['type']
|
370
|
+
access = pelement['access']
|
371
|
+
description = pelement['description']
|
372
|
+
setter = (access == nil or access.include?('w'))
|
373
|
+
|
374
|
+
if type == 'reference'
|
375
|
+
pklass = OSA::Element
|
376
|
+
else
|
377
|
+
pklass = classes[type]
|
378
|
+
if pklass.nil?
|
379
|
+
pklass_elements = class_elements[type]
|
380
|
+
unless pklass_elements.nil?
|
381
|
+
pklass = add_class_from_xml_element(pklass_elements.first, class_elements, classes, app_module)
|
382
|
+
end
|
383
|
+
end
|
384
|
+
end
|
385
|
+
|
386
|
+
# Implicit 'get' if the property class is primitive (not defined in the sdef),
|
387
|
+
# otherwise just return an object specifier.
|
388
|
+
method_name = rubyfy_method(name, klass, type)
|
389
|
+
method_code = if pklass.nil?
|
390
|
+
<<EOC
|
391
|
+
def #{method_name}
|
392
|
+
@app.__send_event__('core', 'getd',
|
393
|
+
[['----', Element.__new_object_specifier__('prop', @app == self ? Element.__new__('null', nil) : self,
|
394
|
+
'prop', Element.__new__('type', '#{code}'.to_4cc))]],
|
395
|
+
true).to_rbobj
|
396
|
+
end
|
397
|
+
EOC
|
398
|
+
else
|
399
|
+
<<EOC
|
400
|
+
def #{method_name}
|
401
|
+
o = #{pklass.name}.__new_object_specifier__('prop', @app == self ? Element.__new__('null', nil) : self,
|
402
|
+
'prop', Element.__new__('type', '#{code}'.to_4cc))
|
403
|
+
unless OSA.lazy_events?
|
404
|
+
@app.__send_event__('core', 'getd', [['----', o]], true).to_rbobj
|
405
|
+
else
|
406
|
+
o.instance_variable_set(:@app, @app)
|
407
|
+
o.extend(OSA::ObjectSpecifier)
|
408
|
+
end
|
409
|
+
end
|
410
|
+
EOC
|
411
|
+
end
|
412
|
+
|
413
|
+
klass.class_eval(method_code)
|
414
|
+
ptypedoc = if pklass.nil?
|
415
|
+
if mod = enum_group_codes[type]
|
416
|
+
"a #{mod} enumeration"
|
417
|
+
else
|
418
|
+
type
|
419
|
+
end
|
420
|
+
else
|
421
|
+
"a #{pklass} object"
|
422
|
+
end
|
423
|
+
description[0] = description[0].chr.downcase if description
|
424
|
+
methods_doc << DocMethod.new(method_name, englishify_sentence("Gets the #{name} property -- #{description}"), DocItem.new('result', englishify_sentence("the property value, as #{ptypedoc}")), nil)
|
425
|
+
|
426
|
+
# For the setter, always send an event.
|
427
|
+
if setter
|
428
|
+
method_name = rubyfy_method(name, klass, type, true)
|
429
|
+
method_code = <<EOC
|
430
|
+
def #{method_name}(val)
|
431
|
+
@app.__send_event__('core', 'setd',
|
432
|
+
[['----', Element.__new_object_specifier__('prop', @app == self ? Element.__new__('null', nil) : self,
|
433
|
+
'prop', Element.__new__('type', '#{code}'.to_4cc))],
|
434
|
+
['data', #{new_element_code(type, 'val', enum_group_codes)}]],
|
435
|
+
true)
|
436
|
+
return nil
|
437
|
+
end
|
438
|
+
EOC
|
439
|
+
|
440
|
+
klass.class_eval(method_code)
|
441
|
+
methods_doc << DocMethod.new(method_name, englishify_sentence("Sets the #{name} property -- #{description}"), nil, [DocItem.new('val', englishify_sentence("the value to be set, as #{ptypedoc}"))])
|
442
|
+
end
|
443
|
+
end
|
444
|
+
|
445
|
+
# Creates elements.
|
446
|
+
element.find('element').each do |eelement|
|
447
|
+
type = eelement['type']
|
448
|
+
|
449
|
+
eklass = classes[type]
|
450
|
+
if eklass.nil?
|
451
|
+
eklass_elements = class_elements[type]
|
452
|
+
unless eklass_elements.nil?
|
453
|
+
eklass = add_class_from_xml_element(eklass_elements.first, class_elements, classes, app_module)
|
454
|
+
end
|
455
|
+
end
|
456
|
+
|
457
|
+
if eklass.nil?
|
458
|
+
STDERR.puts "Cannot find class '#{type}', skipping element '#{eelement}'" if $DEBUG
|
459
|
+
next
|
460
|
+
end
|
461
|
+
|
462
|
+
method_name = rubyfy_method(eklass::PLURAL, klass)
|
463
|
+
method_code = <<EOC
|
464
|
+
def #{method_name}
|
465
|
+
unless OSA.lazy_events?
|
466
|
+
@app.__send_event__('core', 'getd',
|
467
|
+
[['----', Element.__new_object_specifier__(
|
468
|
+
'#{eklass::CODE}', @app == self ? Element.__new__('null', nil) : self,
|
469
|
+
'indx', Element.__new__('abso', 'all '.to_4cc))]],
|
470
|
+
true).to_rbobj
|
471
|
+
else
|
472
|
+
ObjectSpecifierList.new(@app, #{eklass}, @app == self ? Element.__new__('null', nil) : self)
|
473
|
+
end
|
474
|
+
end
|
475
|
+
EOC
|
476
|
+
|
477
|
+
klass.class_eval(method_code)
|
478
|
+
methods_doc << DocMethod.new(method_name, englishify_sentence("Gets the #{eklass::PLURAL} associated with this object"), DocItem.new('result', englishify_sentence("an Array of #{eklass} objects")), nil)
|
479
|
+
end
|
480
|
+
end
|
481
|
+
|
482
|
+
# Having an 'application' class is required.
|
483
|
+
app_class = classes['application']
|
484
|
+
raise "No application class defined." if app_class.nil?
|
485
|
+
|
486
|
+
# Maps commands to the right classes.
|
487
|
+
all_classes_but_app = classes.values.reject { |x| x.ancestors.include?(OSA::EventDispatcher) }
|
488
|
+
doc.find('/dictionary/suite/command').each do |element|
|
489
|
+
name = element['name']
|
490
|
+
next if /NOT AVAILABLE/.match(name) # Finder's sdef (Tiger) names some commands with this 'tag'.
|
491
|
+
description = element['description']
|
492
|
+
code = element['code']
|
493
|
+
direct_parameter = element.find_first('direct-parameter')
|
494
|
+
result = element.find_first('result')
|
495
|
+
|
496
|
+
classes_to_define = []
|
497
|
+
forget_direct_parameter = true
|
498
|
+
direct_parameter_optional = false
|
499
|
+
|
500
|
+
if direct_parameter.nil?
|
501
|
+
# No direct parameter, this is for the application class.
|
502
|
+
classes_to_define << app_class
|
503
|
+
else
|
504
|
+
# We have a direct parameter:
|
505
|
+
# - map it to the right class if it's a class defined in our scripting dictionary
|
506
|
+
# - map it to all classes if it's a 'reference' and to the application class if it's optional
|
507
|
+
# - otherwise, just map it to the application class.
|
508
|
+
type = type_of_parameter(direct_parameter)
|
509
|
+
direct_parameter_optional = parameter_optional?(direct_parameter)
|
510
|
+
|
511
|
+
if type == 'reference'
|
512
|
+
classes_to_define = all_classes_but_app
|
513
|
+
classes_to_define << app_class if direct_parameter_optional
|
514
|
+
else
|
515
|
+
klass = classes[type]
|
516
|
+
if klass.nil?
|
517
|
+
forget_direct_parameter = false
|
518
|
+
classes_to_define << app_class
|
519
|
+
else
|
520
|
+
classes_to_define << klass
|
521
|
+
end
|
522
|
+
end
|
523
|
+
end
|
524
|
+
|
525
|
+
# Reject classes which are already represented by an ancestor.
|
526
|
+
if classes_to_define.length > 1
|
527
|
+
classes_to_define.uniq!
|
528
|
+
classes_to_define.reject! do |x|
|
529
|
+
classes_to_define.any? { |y| x != y and x.ancestors.include?(y) }
|
530
|
+
end
|
531
|
+
end
|
532
|
+
|
533
|
+
params = []
|
534
|
+
unless direct_parameter.nil?
|
535
|
+
params << ['direct',
|
536
|
+
'----',
|
537
|
+
direct_parameter['description'],
|
538
|
+
direct_parameter_optional,
|
539
|
+
type_of_parameter(direct_parameter)]
|
540
|
+
end
|
541
|
+
|
542
|
+
params.concat(element.find('parameter').to_a.map do |element|
|
543
|
+
[rubyfy_string(element['name'], true),
|
544
|
+
element['code'],
|
545
|
+
element['description'],
|
546
|
+
parameter_optional?(element),
|
547
|
+
type_of_parameter(element)]
|
548
|
+
end)
|
549
|
+
|
550
|
+
p_dec, p_def, p_args_doc = [], [], []
|
551
|
+
already_has_optional_args = false # Once an argument is optional, all following arguments should be optional.
|
552
|
+
params.each do |pname, pcode, pdesc, optional, ptype|
|
553
|
+
decl = pname
|
554
|
+
self_direct = (pcode == '----' and forget_direct_parameter)
|
555
|
+
defi = if self_direct
|
556
|
+
forget_direct_parameter ? "(self.is_a?(OSA::EventDispatcher) ? [] : ['----', self])" : "['----', self]"
|
557
|
+
else
|
558
|
+
"['#{pcode}', #{new_element_code(ptype, pname, enum_group_codes)}]"
|
559
|
+
end
|
560
|
+
if already_has_optional_args or (optional and !self_direct)
|
561
|
+
decl += '=nil'
|
562
|
+
defi = "(#{pname} == nil ? [] : #{defi})"
|
563
|
+
already_has_optional_args = true
|
564
|
+
end
|
565
|
+
unless self_direct
|
566
|
+
p_dec << decl
|
567
|
+
p_args_doc << DocItem.new(decl, englishify_sentence(pdesc))
|
568
|
+
end
|
569
|
+
p_def << defi
|
570
|
+
end
|
571
|
+
|
572
|
+
method_code = <<EOC
|
573
|
+
def %METHOD_NAME%(#{p_dec.join(', ')})
|
574
|
+
@app.__send_event__('#{code[0..3]}', '#{code[4..-1]}', [#{p_def.join(', ')}], #{result != nil})#{result != nil ? '.to_rbobj' : ''}
|
575
|
+
end
|
576
|
+
EOC
|
577
|
+
|
578
|
+
if result.nil?
|
579
|
+
result_type = result_doc = nil
|
580
|
+
else
|
581
|
+
result_type = type_of_parameter(result)
|
582
|
+
result_klass = classes[result_type]
|
583
|
+
result_doc = DocItem.new('result', englishify_sentence(result['description']))
|
584
|
+
end
|
585
|
+
|
586
|
+
classes_to_define.each do |klass|
|
587
|
+
method_name = rubyfy_method(name, klass, result_type)
|
588
|
+
code = method_code.sub(/%METHOD_NAME%/, method_name)
|
589
|
+
klass.class_eval(code)
|
590
|
+
methods_doc = klass.const_get(:METHODS_DESCRIPTION)
|
591
|
+
methods_doc << DocMethod.new(method_name, englishify_sentence(description), result_doc, p_args_doc)
|
592
|
+
end
|
593
|
+
end
|
594
|
+
|
595
|
+
# Returns an application instance, that's all folks!
|
596
|
+
hash = {}
|
597
|
+
classes.each_value { |klass| hash[klass::CODE] = klass }
|
598
|
+
app = app_class.__new__('sign', signature.to_4cc)
|
599
|
+
app.instance_variable_set(:@sdef, sdef)
|
600
|
+
app.instance_variable_set(:@classes, hash)
|
601
|
+
app.instance_eval 'def sdef; @sdef; end'
|
602
|
+
app.extend OSA::EventDispatcher
|
603
|
+
@apps[signature] = app
|
604
|
+
end
|
605
|
+
|
606
|
+
def self.parameter_optional?(element)
|
607
|
+
element['optional'] == 'yes'
|
608
|
+
end
|
609
|
+
|
610
|
+
def self.add_class_from_xml_element(element, class_elements, repository, app_module)
|
611
|
+
real_name = element['name']
|
612
|
+
klass = repository[real_name]
|
613
|
+
if klass.nil?
|
614
|
+
code = element['code']
|
615
|
+
inherits = element['inherits']
|
616
|
+
plural = element['plural']
|
617
|
+
|
618
|
+
if real_name == inherits
|
619
|
+
# Inheriting from itself is a common idiom when adding methods
|
620
|
+
# to a class that has already been defined, probably to avoid
|
621
|
+
# mentioning the subclass name more than once.
|
622
|
+
inherits = nil
|
623
|
+
end
|
624
|
+
|
625
|
+
if inherits.nil?
|
626
|
+
klass = Class.new(OSA::Element)
|
627
|
+
else
|
628
|
+
super_elements = class_elements[inherits]
|
629
|
+
if super_elements.nil?
|
630
|
+
STDERR.puts "sdef bug: class '#{real_name}' inherits from '#{inherits}' which is not defined - fall back inheriting from OSA::Element" if $DEBUG
|
631
|
+
klass = OSA::Element
|
632
|
+
else
|
633
|
+
super_class = add_class_from_xml_element(super_elements.first, class_elements,
|
634
|
+
repository, app_module)
|
635
|
+
klass = Class.new(super_class)
|
636
|
+
end
|
637
|
+
end
|
638
|
+
|
639
|
+
klass.class_eval 'include OSA::EventDispatcher' if real_name == 'application'
|
640
|
+
|
641
|
+
klass.const_set(:REAL_NAME, real_name) unless klass.const_defined?(:REAL_NAME)
|
642
|
+
klass.const_set(:PLURAL, plural == nil ? real_name + 's' : plural) unless klass.const_defined?(:PLURAL)
|
643
|
+
klass.const_set(:CODE, code) unless klass.const_defined?(:CODE)
|
644
|
+
|
645
|
+
app_module.const_set(rubyfy_constant_string(real_name), klass)
|
646
|
+
|
647
|
+
repository[real_name] = klass
|
648
|
+
end
|
649
|
+
|
650
|
+
return klass
|
651
|
+
end
|
652
|
+
|
653
|
+
def self.type_of_parameter(element)
|
654
|
+
type = element['type']
|
655
|
+
if type.nil?
|
656
|
+
etype = element.find_first('type')
|
657
|
+
if etype.nil? or (type = etype['type']).nil?
|
658
|
+
raise "Parameter #{element} has no type."
|
659
|
+
end
|
660
|
+
type = "list_of_#{type}" if etype['list'] == 'yes'
|
661
|
+
end
|
662
|
+
return type
|
663
|
+
end
|
664
|
+
|
665
|
+
def self.new_element_code(type, varname, enum_group_codes)
|
666
|
+
if md = /^list_of_(.+)$/.match(type)
|
667
|
+
return "#{varname}.is_a?(OSA::Element) ? #{varname} : ElementList.__new__(#{varname}.to_a.map { |x| #{new_element_code(md[1], 'x', enum_group_codes)} })"
|
668
|
+
end
|
669
|
+
"#{varname}.is_a?(OSA::Element) ? #{varname} : Element.from_rbobj('#{type}', #{varname}, #{enum_group_codes.inspect})"
|
670
|
+
end
|
671
|
+
|
672
|
+
def self.escape_string(string)
|
673
|
+
string.gsub(/[\s\-\.\/]/, '_').gsub(/&/, 'and')
|
674
|
+
end
|
675
|
+
|
676
|
+
def self.rubyfy_constant_string(string, upcase=false)
|
677
|
+
if /^\d/.match(string)
|
678
|
+
string = 'C' << string
|
679
|
+
else
|
680
|
+
string = string.dup
|
681
|
+
string[0] = string[0].chr.upcase
|
682
|
+
end
|
683
|
+
escape_string(upcase ? string.upcase : string.gsub(/\s(.)/) { |s| s[1].chr.upcase })
|
684
|
+
end
|
685
|
+
|
686
|
+
RUBY_RESERVED_KEYWORDS = ['for', 'in']
|
687
|
+
def self.rubyfy_string(string, handle_ruby_reserved_keywords=false)
|
688
|
+
# Prefix with '_' parameter names to avoid possible collisions with reserved Ruby keywords (for, etc...).
|
689
|
+
if RUBY_RESERVED_KEYWORDS.include?(string)
|
690
|
+
'_' + string
|
691
|
+
else
|
692
|
+
escape_string(string).downcase
|
693
|
+
end
|
694
|
+
end
|
695
|
+
|
696
|
+
def self.rubyfy_method(string, klass, return_type=nil, setter=false)
|
697
|
+
s = rubyfy_string(string)
|
698
|
+
if setter
|
699
|
+
# Suffix setters with '='.
|
700
|
+
s << '='
|
701
|
+
elsif return_type == 'boolean'
|
702
|
+
# Suffix predicates with '?'.
|
703
|
+
s << '?'
|
704
|
+
end
|
705
|
+
# Prefix with 'osa_' in case the class already has a method with such a name.
|
706
|
+
if klass.method_defined?(s)
|
707
|
+
s = 'osa_' + s
|
708
|
+
end
|
709
|
+
return s
|
710
|
+
end
|
711
|
+
|
712
|
+
def self.englishify_sentence(string)
|
713
|
+
return '' if string.nil?
|
714
|
+
string[0] = string[0].chr.upcase
|
715
|
+
last = string[-1].chr
|
716
|
+
string << '.' if last != '.' and last != '?' and last != '!'
|
717
|
+
return string
|
718
|
+
end
|
719
|
+
end
|
720
|
+
|
721
|
+
# String, force TEXT type to not get Unicode.
|
722
|
+
OSA.add_conversion_to_ruby('TEXT', 'utxt') { |value, type, object| object.__data__('TEXT') }
|
723
|
+
OSA.add_conversion_to_osa('string', 'text', 'Unicode text') { |value| ['TEXT', value.to_s] }
|
724
|
+
|
725
|
+
# Signed/unsigned integer.
|
726
|
+
OSA.add_conversion_to_ruby('shor', 'long', 'comp') { |value| value.unpack('l').first }
|
727
|
+
OSA.add_conversion_to_ruby('magn') { |value| value.unpack('d').first }
|
728
|
+
OSA.add_conversion_to_osa('integer', 'double integer') { |value| ['magn', [value].pack('l')] }
|
729
|
+
|
730
|
+
# Boolean.
|
731
|
+
OSA.add_conversion_to_ruby('bool') { |value| value.unpack('c').first != 0 }
|
732
|
+
OSA.add_conversion_to_osa('boolean') { |value| [(value ? 'true'.to_4cc : 'fals'.to_4cc), nil] }
|
733
|
+
OSA.add_conversion_to_ruby('true') { |value| true }
|
734
|
+
OSA.add_conversion_to_ruby('fals') { |value| false }
|
735
|
+
|
736
|
+
# Date.
|
737
|
+
OSA.add_conversion_to_ruby('ldt ') { |value|
|
738
|
+
Date.new(1904, 1, 1) + Date.time_to_day_fraction(0, 0, value.unpack('q').first)
|
739
|
+
}
|
740
|
+
|
741
|
+
# Array.
|
742
|
+
OSA.add_conversion_to_ruby('list') { |value, type, object|
|
743
|
+
object.is_a?(OSA::ElementList) ? object.to_a.map { |x| x.to_rbobj } : object
|
744
|
+
}
|
745
|
+
|
746
|
+
# File name.
|
747
|
+
# Let's use the 'furl' type here instead of 'alis', as we don't have a way to produce an alias for a file that does not exist yet.
|
748
|
+
OSA.add_conversion_to_osa('alias', 'file') { |value| ['furl', value.to_s] }
|
749
|
+
|
750
|
+
# Hash.
|
751
|
+
OSA.add_conversion_to_ruby('reco') { |value, type, object| object.is_a?(OSA::ElementRecord) ? object.to_hash : self }
|
752
|
+
|
753
|
+
# Enumerator.
|
754
|
+
OSA.add_conversion_to_ruby('enum') { |value, type, object| OSA::Enumerator.enum_for_code(object.__data__('TEXT')) or self }
|
755
|
+
|
756
|
+
# Class.
|
757
|
+
OSA.add_conversion_to_osa('type class') { |value| value.is_a?(Class) and value.ancestors.include?(OSA::Element) ? ['type', value::CODE.to_4cc] : self }
|
758
|
+
|
759
|
+
# QuickDraw Rectangle, aka "bounding rectangle".
|
760
|
+
OSA.add_conversion_to_ruby('qdrt') { |value| value.unpack('S4') }
|
761
|
+
OSA.add_conversion_to_osa('bounding rectangle') { |value| ['qdrt', value.pack('S4')] }
|