graffle 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/History.txt +2 -0
- data/LICENSE.txt +34 -0
- data/Manifest.txt +53 -0
- data/README.txt +19 -0
- data/Rakefile +32 -0
- data/Rakefile.hoe +23 -0
- data/bin/bin-skeleton +23 -0
- data/graffle.tmproj +335 -0
- data/lib/graffle.rb +43 -0
- data/lib/graffle/.document +4 -0
- data/lib/graffle/lib-skeleton +3 -0
- data/lib/graffle/nodoc/hacks.rb +69 -0
- data/lib/graffle/point.rb +42 -0
- data/lib/graffle/stereotypes.rb +446 -0
- data/lib/graffle/styled-text-reader.rb +52 -0
- data/lib/graffle/third-party/s4t-utils.rb +21 -0
- data/lib/graffle/third-party/s4t-utils/capturing-globals.rb +78 -0
- data/lib/graffle/third-party/s4t-utils/claims.rb +14 -0
- data/lib/graffle/third-party/s4t-utils/command-line.rb +15 -0
- data/lib/graffle/third-party/s4t-utils/error-handling.rb +20 -0
- data/lib/graffle/third-party/s4t-utils/friendly-format.rb +27 -0
- data/lib/graffle/third-party/s4t-utils/hacks.rb +32 -0
- data/lib/graffle/third-party/s4t-utils/load-path-auto-adjuster.rb +120 -0
- data/lib/graffle/third-party/s4t-utils/more-assertions.rb +29 -0
- data/lib/graffle/third-party/s4t-utils/os.rb +28 -0
- data/lib/graffle/third-party/s4t-utils/rake-task-helpers.rb +75 -0
- data/lib/graffle/third-party/s4t-utils/rakefile-common.rb +106 -0
- data/lib/graffle/third-party/s4t-utils/svn-file-movement.rb +101 -0
- data/lib/graffle/third-party/s4t-utils/test-util.rb +19 -0
- data/lib/graffle/third-party/s4t-utils/version.rb +3 -0
- data/lib/graffle/version.rb +8 -0
- data/setup.rb +1585 -0
- data/test/abstract-graphic-tests.rb +56 -0
- data/test/array-and-hash-stereotyping-tests.rb +49 -0
- data/test/document-tests.rb +117 -0
- data/test/graffle-file-types/as-a-package.graffle/data.plist +953 -0
- data/test/graffle-file-types/as-a-package.graffle/image1.png +0 -0
- data/test/graffle-file-types/as-a-package.graffle/image2.png +0 -0
- data/test/graffle-file-types/as-a-package.graffle/image3.png +0 -0
- data/test/graffle-file-types/multiple-canvases.graffle +6821 -0
- data/test/graffle-file-types/opening-tests.rb +45 -0
- data/test/graffle-file-types/two-boxes-and-a-line.graffle +347 -0
- data/test/group-tests.rb +109 -0
- data/test/hacks-tests.rb +58 -0
- data/test/line-graphic-tests.rb +155 -0
- data/test/point-tests.rb +43 -0
- data/test/set-standalone-test-paths.rb +5 -0
- data/test/shaped-graphic-tests.rb +93 -0
- data/test/sheet-tests.rb +124 -0
- data/test/styled-text-reader-tests.rb +89 -0
- data/test/test-skeleton +19 -0
- data/test/text-tests.rb +55 -0
- data/test/util.rb +15 -0
- metadata +139 -0
data/lib/graffle.rb
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
#
|
3
|
+
# Created by Brian Marick on 2007-07-06.
|
4
|
+
# Copyright (c) 2007. All rights reserved.
|
5
|
+
|
6
|
+
|
7
|
+
|
8
|
+
require 's4t-utils'
|
9
|
+
require 'ostruct'
|
10
|
+
require 'plist'
|
11
|
+
|
12
|
+
require 'graffle/version'
|
13
|
+
require 'graffle/nodoc/hacks'
|
14
|
+
require 'graffle/styled-text-reader'
|
15
|
+
require 'graffle/stereotypes'
|
16
|
+
require 'graffle/point'
|
17
|
+
|
18
|
+
# See README.txt[link:files/README_txt.html]
|
19
|
+
module Graffle
|
20
|
+
|
21
|
+
# Parse the given data and stereotype the result. Returns an object
|
22
|
+
# stereotyped as Graffle::Document.
|
23
|
+
def self.parse(filename_or_xml)
|
24
|
+
if File.directory?(filename_or_xml)
|
25
|
+
filename_or_xml = File.join(filename_or_xml, "data.plist")
|
26
|
+
end
|
27
|
+
|
28
|
+
prog1(Plist.parse_xml(filename_or_xml)) do | doc |
|
29
|
+
Document.takes_on(doc)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.stereotype(o) # :nodoc:
|
34
|
+
return true if ShapedGraphic.takes_on(o)
|
35
|
+
return true if LineGraphic.takes_on(o)
|
36
|
+
return true if Group.takes_on(o)
|
37
|
+
# Sheet has no distinguishing marks.
|
38
|
+
false
|
39
|
+
end
|
40
|
+
|
41
|
+
|
42
|
+
end
|
43
|
+
|
@@ -0,0 +1,69 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
#
|
3
|
+
# Created by Brian Marick on 2007-07-06.
|
4
|
+
# Copyright (c) 2007. All rights reserved.
|
5
|
+
|
6
|
+
require 'pp'
|
7
|
+
|
8
|
+
class Object
|
9
|
+
|
10
|
+
def behaves_like?(mod)
|
11
|
+
kind_of?(mod)
|
12
|
+
end
|
13
|
+
|
14
|
+
def behave_like(mod)
|
15
|
+
@_stereotyped_as_module__bemhack = mod
|
16
|
+
def self.stereotype; @_stereotyped_as_module__bemhack; end
|
17
|
+
|
18
|
+
_cause_printing_to_include_stereotype__bemhack
|
19
|
+
|
20
|
+
extend(self.stereotype)
|
21
|
+
self
|
22
|
+
end
|
23
|
+
alias and_behave_like behave_like
|
24
|
+
|
25
|
+
|
26
|
+
private
|
27
|
+
def _cause_printing_to_include_stereotype__bemhack
|
28
|
+
@_stereotype_print_tag__bemhack = "acts as #{stereotype.basename}: "
|
29
|
+
|
30
|
+
@_original_inspect__bemhack = self.method(:inspect)
|
31
|
+
def self.inspect
|
32
|
+
@_stereotype_print_tag__bemhack + @_original_inspect__bemhack.call
|
33
|
+
end
|
34
|
+
|
35
|
+
@_original_to_s__bemhack = self.method(:to_s)
|
36
|
+
def self.to_s
|
37
|
+
@_stereotype_print_tag__bemhack + @_original_to_s__bemhack.call
|
38
|
+
end
|
39
|
+
|
40
|
+
@_original_pretty_print__bemhack = self.method(:pretty_print)
|
41
|
+
def self.pretty_print(printer)
|
42
|
+
printer.text(@_stereotype_print_tag__bemhack)
|
43
|
+
@_original_pretty_print__bemhack.call(printer)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
|
49
|
+
|
50
|
+
class Hash
|
51
|
+
def << (hash)
|
52
|
+
self.merge!(hash)
|
53
|
+
self
|
54
|
+
end
|
55
|
+
|
56
|
+
# Default Hash to_s is lame. Note that to_s doesn't just call
|
57
|
+
# inspect because inspect might be overridden. We want what
|
58
|
+
# inspect does by default, not whatever inspect does at the moment.
|
59
|
+
alias_method :_unchanging_inspect__bemhack, :inspect
|
60
|
+
def to_s; _unchanging_inspect__bemhack; end
|
61
|
+
end
|
62
|
+
|
63
|
+
class Module
|
64
|
+
def basename
|
65
|
+
name.split("::").last
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
|
@@ -0,0 +1,42 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
#
|
3
|
+
# Created by Brian Marick on 2007-07-06.
|
4
|
+
# Copyright (c) 2007. All rights reserved.
|
5
|
+
|
6
|
+
|
7
|
+
module Graffle
|
8
|
+
|
9
|
+
# An {x,y} coordinate relative to the origin of a Sheet.
|
10
|
+
class Point
|
11
|
+
|
12
|
+
attr_reader :x, :y
|
13
|
+
|
14
|
+
# If two arguments, they are the x and y coordinates of the
|
15
|
+
# Point. Otherwise, the single argument is a string to be
|
16
|
+
# parsed to find the coordinates.
|
17
|
+
def initialize(*args)
|
18
|
+
if args.length == 2
|
19
|
+
@x = args[0]; @y = args[1]
|
20
|
+
elsif args[0] =~ /\{\{(.*),\s(.*)\}, \{.*,\s.*\}\}/
|
21
|
+
initialize(Float($1), Float($2))
|
22
|
+
elsif args[0] =~ /\{(.*),\s(.*)\}/
|
23
|
+
initialize(Float($1), Float($2))
|
24
|
+
else
|
25
|
+
# TODO: this should be user_is_bewildered(msg = "how could this point be reached?")
|
26
|
+
user_disputes("this point can be reached") { "unreachable?"}
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
include Comparable
|
31
|
+
|
32
|
+
# One Point is "less than" another if it starts higher
|
33
|
+
# than it on the page. If they start at the same height, the
|
34
|
+
# one furthest to the left is less.
|
35
|
+
def <=>(other)
|
36
|
+
return self.x <=> other.x if self.y == other.y
|
37
|
+
return self.y <=> other.y
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
|
42
|
+
end
|
@@ -0,0 +1,446 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
#
|
3
|
+
# Created by Brian Marick on 2007-07-06.
|
4
|
+
# Copyright (c) 2007. All rights reserved.
|
5
|
+
|
6
|
+
module Graffle
|
7
|
+
module Builders # :nodoc:
|
8
|
+
def self.raw(mod, basic_structure = {}, &block)
|
9
|
+
g = basic_structure.dup
|
10
|
+
g.behave_like(mod)
|
11
|
+
g.instance_eval(&block) if block
|
12
|
+
g
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.classed(mod, basic_structure = {}, &block)
|
16
|
+
g = raw(mod, basic_structure, &block)
|
17
|
+
g['Class'] = mod.basename
|
18
|
+
g
|
19
|
+
end
|
20
|
+
|
21
|
+
def abstract_graphic(&block)
|
22
|
+
Builders.raw(Graffle::AbstractGraphic, &block)
|
23
|
+
end
|
24
|
+
|
25
|
+
def shaped_graphic(&block)
|
26
|
+
Builders.classed(Graffle::ShapedGraphic, &block)
|
27
|
+
end
|
28
|
+
|
29
|
+
def line_label(&block)
|
30
|
+
prog1(shaped_graphic) do | g |
|
31
|
+
g.act_as_line_label
|
32
|
+
g.instance_eval(&block) if block
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def line_graphic(&block)
|
37
|
+
Builders.classed(Graffle::LineGraphic, &block)
|
38
|
+
end
|
39
|
+
|
40
|
+
def sheet(&block)
|
41
|
+
Builders.raw(Graffle::Sheet, {'GraphicsList' => []}, &block)
|
42
|
+
end
|
43
|
+
|
44
|
+
def group(&block)
|
45
|
+
Builders.classed(Graffle::Group, {'Graphics' => []}, &block)
|
46
|
+
end
|
47
|
+
|
48
|
+
def document(&block)
|
49
|
+
prog1(Builders.raw(Graffle::Document, &block)) { | doc |
|
50
|
+
doc['Creator'] = 'graffle.rb'
|
51
|
+
}
|
52
|
+
end
|
53
|
+
|
54
|
+
def text(string, &block)
|
55
|
+
prog1(Builders.raw(Graffle::Text, &block)) { | text |
|
56
|
+
text['Text'] = string
|
57
|
+
}
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
61
|
+
|
62
|
+
|
63
|
+
# TODO: rename AbstractGraphic Nestable
|
64
|
+
|
65
|
+
# Behavior that's common to all visible objects, be they lines,
|
66
|
+
# rectangles, text objects, etc.
|
67
|
+
module AbstractGraphic
|
68
|
+
include Comparable
|
69
|
+
|
70
|
+
# TODO: I'm not wild about the way this both checks if something's
|
71
|
+
# possible and also does it. Separate in caller?
|
72
|
+
def self.takes_on(o, mod) # :nodoc:
|
73
|
+
if graffle_class_matches?(o, mod)
|
74
|
+
o.behave_like(mod)
|
75
|
+
return true
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def self.graffle_class_matches?(o, mod) # :nodoc:
|
80
|
+
o.is_a?(Hash) && o['Class'] == mod.basename
|
81
|
+
end
|
82
|
+
|
83
|
+
# The Group or Sheet containing the object. Mostly for internal use.
|
84
|
+
attr_accessor :container
|
85
|
+
|
86
|
+
# Every visible graffle object has an integer ID.
|
87
|
+
# Mostly for internal use.
|
88
|
+
def graffle_id; self['ID']; end
|
89
|
+
def graffle_id_is(id) # :nodoc:
|
90
|
+
self['ID'] = id
|
91
|
+
end
|
92
|
+
|
93
|
+
# One AbstractGraphic is before another if its origin starts higher
|
94
|
+
# than the other's. If their origins are at the same height, the
|
95
|
+
# one furthest to the left comes before.
|
96
|
+
def before?(other)
|
97
|
+
self.origin < other.origin
|
98
|
+
end
|
99
|
+
|
100
|
+
# A Point. Must be defined in the whatever includes this module.
|
101
|
+
def origin; includer_responsibility; end
|
102
|
+
|
103
|
+
end
|
104
|
+
|
105
|
+
|
106
|
+
# An entire Graffle file, parsed. The only thing of interest is
|
107
|
+
# an array of objects stereotyped as Sheet. A Sheet is the internal
|
108
|
+
# name for what the OmniGraffle Pro GUI calls "canvases."
|
109
|
+
#
|
110
|
+
# OmniGraffle non-Pro can only produce one-canvas documents.
|
111
|
+
#
|
112
|
+
# Some one-canvas OmniGraffle documents don't actually have Sheets. So
|
113
|
+
# that you don't have to worry about that, a one-element Sheet array
|
114
|
+
# is inserted if needed.
|
115
|
+
|
116
|
+
module Document
|
117
|
+
include Builders
|
118
|
+
|
119
|
+
def sheets; self['Sheets']; end
|
120
|
+
def first_sheet; sheets[0]; end
|
121
|
+
|
122
|
+
|
123
|
+
def with(*objects) # :nodoc:
|
124
|
+
self['Sheets'] = [] unless self.has_key?('Sheets')
|
125
|
+
objects.each do | o |
|
126
|
+
self['Sheets'] << o
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
def self.takes_on(hash) # :nodoc:
|
131
|
+
doc = hash.behave_like(self)
|
132
|
+
doc.make_sure_has_sheets
|
133
|
+
doc.sheets.each do | child |
|
134
|
+
Sheet.takes_on(child)
|
135
|
+
end
|
136
|
+
true
|
137
|
+
end
|
138
|
+
|
139
|
+
|
140
|
+
# If this is a graffle document with an implicit single
|
141
|
+
# sheet, make it explicit.
|
142
|
+
# TODO: Check that OmniGraffle non-pro can read such a doc. Probably
|
143
|
+
# more needs to go along with the GraphicsList.
|
144
|
+
def make_sure_has_sheets # :nodoc:
|
145
|
+
splice_in_single_sheet unless self.has_key?('Sheets')
|
146
|
+
self
|
147
|
+
end
|
148
|
+
|
149
|
+
private
|
150
|
+
|
151
|
+
def splice_in_single_sheet
|
152
|
+
graphics = self.delete("GraphicsList")
|
153
|
+
self['Sheets'] = [ { 'GraphicsList' => graphics }]
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
# Visible graphics that aren't lines. (Those are stereotyped
|
158
|
+
# LineGraphic.)
|
159
|
+
module ShapedGraphic
|
160
|
+
include AbstractGraphic
|
161
|
+
include Builders
|
162
|
+
|
163
|
+
def self.takes_on(o) # :nodoc:
|
164
|
+
if AbstractGraphic.takes_on(o, self)
|
165
|
+
o.content.behave_like(Text) if o.has_content?
|
166
|
+
o.act_as_line_label if o.is_line_label?
|
167
|
+
true
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
# Does this object contain Text? (The kind you put in when you
|
172
|
+
# double-click on the object.)
|
173
|
+
def has_content?; self.has_key?('Text'); end
|
174
|
+
|
175
|
+
# Is this object the label for some LineGraphic?
|
176
|
+
def is_line_label?; self.has_key?('Line'); end
|
177
|
+
|
178
|
+
def act_as_line_label # :nodoc:
|
179
|
+
|
180
|
+
def self.for_line(graffle_id) # :nodoc:
|
181
|
+
self['Line'] = { 'ID' => graffle_id }
|
182
|
+
end
|
183
|
+
|
184
|
+
# The integer ID of the LineGraphic this ShapedGraphic labels.
|
185
|
+
# This method only exists if the object really is a label.
|
186
|
+
#
|
187
|
+
# BUG: A line can have more than one label.
|
188
|
+
def self.line_id; self['Line']['ID']; end
|
189
|
+
|
190
|
+
# The LineGraphic this ShapedGraphic labels.
|
191
|
+
# This method only exists if the object really is a label, so
|
192
|
+
# use is_line_label? first.
|
193
|
+
def self.line
|
194
|
+
container.find_by_id(line_id)
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
|
199
|
+
# The x coordinate of this object's bounding box.
|
200
|
+
def x; bounds.x; end
|
201
|
+
# The y coordinate of this object's bounding box.
|
202
|
+
def y; bounds.y; end
|
203
|
+
# The width of this object's bounding box.
|
204
|
+
def width; bounds.width; end
|
205
|
+
# The height of this object's bounding box.
|
206
|
+
def height; bounds.height; end
|
207
|
+
|
208
|
+
# This object's bounding box, an OpenStruct with methods x, y,
|
209
|
+
# width, and height.
|
210
|
+
def bounds
|
211
|
+
self['Bounds'] =~ /\{\{(.*),\s(.*)\}, \{(.*),\s(.*)\}\}/
|
212
|
+
OpenStruct.new(:x => Float($1), :y => Float($2),
|
213
|
+
:width => Float($3), :height => Float($4))
|
214
|
+
end
|
215
|
+
|
216
|
+
def origin # :nodoc:
|
217
|
+
Point.new(self['Bounds'])
|
218
|
+
end
|
219
|
+
|
220
|
+
def bounded_by(x, y, width, height) # :nodoc:
|
221
|
+
self << {"Bounds" => "{{#{x}, #{y}}, {#{width}, #{height}}}" }
|
222
|
+
end
|
223
|
+
|
224
|
+
|
225
|
+
|
226
|
+
# The Text within the object (or nil if there isn't any).
|
227
|
+
def content; self['Text']; end
|
228
|
+
alias_method :contents, :content
|
229
|
+
|
230
|
+
def with_text(string) # :nodoc:
|
231
|
+
self['Text'] = text(string)
|
232
|
+
end
|
233
|
+
end
|
234
|
+
|
235
|
+
# A line, be it straight, curved, or jagged. Lines can be connected to
|
236
|
+
# other objects. Even if there's no arrowhead on the line, it still has
|
237
|
+
# a notion of a _head_ and _tail_. (Alternate notation: it goes from one,
|
238
|
+
# to the other). The head or tail can be nil.
|
239
|
+
module LineGraphic
|
240
|
+
include AbstractGraphic
|
241
|
+
include Builders
|
242
|
+
|
243
|
+
def self.takes_on(o) # :nodoc:
|
244
|
+
AbstractGraphic.takes_on(o, self)
|
245
|
+
end
|
246
|
+
|
247
|
+
|
248
|
+
# The AbstractGraphic the line comes from.
|
249
|
+
def from; _follow__bemhack('Tail'); end
|
250
|
+
alias_method :tail, :from
|
251
|
+
|
252
|
+
# The AbstractGraphic the line goes to.
|
253
|
+
def to; _follow__bemhack('Head'); end
|
254
|
+
alias_method :head, :to
|
255
|
+
|
256
|
+
# The label attached to a line. In the document, the label is a
|
257
|
+
# ShapedGraphic in the same Container as its line. The label points
|
258
|
+
# to the line, not the reverse. This method, though, pretends the
|
259
|
+
# line points to the label and returns it (or nil if there is no
|
260
|
+
# label).
|
261
|
+
def label
|
262
|
+
container.graphics.find do |g|
|
263
|
+
g.respond_to?(:line_id) && g.line_id == self.graffle_id
|
264
|
+
end
|
265
|
+
end
|
266
|
+
|
267
|
+
# Almost certainly, what you care about is the Text of the label, which
|
268
|
+
# you could get like this:
|
269
|
+
# line.label.content
|
270
|
+
# This method is shorthand for that.
|
271
|
+
def label_rtf
|
272
|
+
label.content
|
273
|
+
end
|
274
|
+
|
275
|
+
# An array of Point objects that make up the line. Not editable (yet).
|
276
|
+
def points
|
277
|
+
self['Points'].collect do |p|
|
278
|
+
p =~ /\{(.*),\s(.*)\}/
|
279
|
+
Point.new(Float($1), Float($2))
|
280
|
+
end
|
281
|
+
end
|
282
|
+
|
283
|
+
# The origin of the LineGraphic is the first Point in the points array.
|
284
|
+
# That's the place the mouse pointer was when some person began to
|
285
|
+
# create the LineGraphic. It is _not_ necessarily the Point in the line
|
286
|
+
# closest to {0,0}.
|
287
|
+
def origin
|
288
|
+
Point.new(self['Points'][0])
|
289
|
+
end
|
290
|
+
|
291
|
+
private
|
292
|
+
def points_at(*points)
|
293
|
+
self['Points'] = points.collect { |p| "{#{p[0]}, #{p[1]}}" }
|
294
|
+
end
|
295
|
+
|
296
|
+
def go_from(thing); _link__bemhack('Tail', thing); end
|
297
|
+
def go_to(thing); _link__bemhack('Head', thing); end
|
298
|
+
|
299
|
+
def _follow__bemhack(which)
|
300
|
+
container.find_by_id(self[which]['ID'])
|
301
|
+
end
|
302
|
+
|
303
|
+
# TODO: note this is no good for updating, since fields other than
|
304
|
+
# ID may be destroyed.
|
305
|
+
def _link__bemhack(type, thing)
|
306
|
+
id = thing.respond_to?(:graffle_id) ? thing.graffle_id : thing
|
307
|
+
self << { type => { 'ID' => id } }
|
308
|
+
end
|
309
|
+
|
310
|
+
end
|
311
|
+
|
312
|
+
# Behavior common to Sheet and Group, both of which contain an array
|
313
|
+
# of AbstractGraphic.
|
314
|
+
# :stopdoc:
|
315
|
+
# TODO: perhaps some cleverness with Enumerator would be in order?
|
316
|
+
# :startdoc:
|
317
|
+
module Container
|
318
|
+
|
319
|
+
def with(*objects) # :nodoc:
|
320
|
+
objects.each do | o |
|
321
|
+
self.graphics << o
|
322
|
+
o.container = self # works because instance_evaled.
|
323
|
+
end
|
324
|
+
end
|
325
|
+
|
326
|
+
# Find the AbstractGraphic matching the integer id. Returns nil
|
327
|
+
# if not found.
|
328
|
+
#
|
329
|
+
# Mostly for internal use.
|
330
|
+
def find_by_id(id)
|
331
|
+
result = graphics.find { | elt | elt.graffle_id == id }
|
332
|
+
unless result
|
333
|
+
return nil unless respond_to?(:container) # at the top of the tree.
|
334
|
+
return nil unless container # A null container should only happen
|
335
|
+
# in tests. Consider a test that
|
336
|
+
# uses find_by_id on a group, but the
|
337
|
+
# group is not contained in a sheet.
|
338
|
+
result = container.find_by_id(id)
|
339
|
+
end
|
340
|
+
result
|
341
|
+
end
|
342
|
+
|
343
|
+
def self.stereotype_children_of(parent) # :nodoc:
|
344
|
+
parent.graphics.each do | child |
|
345
|
+
if Graffle.stereotype(child)
|
346
|
+
child.container = parent
|
347
|
+
else
|
348
|
+
puts "TODO: can not yet stereotype child with id #{child['ID'].inspect} and class #{child['Class'].inspect}."
|
349
|
+
pp child.keys
|
350
|
+
end
|
351
|
+
end
|
352
|
+
true
|
353
|
+
end
|
354
|
+
|
355
|
+
end
|
356
|
+
|
357
|
+
# A Sheet is what the UI calls a "canvas." It's the drawing surface.
|
358
|
+
# OmniGraffle Pro lets you have multiple canvases.
|
359
|
+
module Sheet
|
360
|
+
include Builders
|
361
|
+
include Container
|
362
|
+
|
363
|
+
# All the visible graphics within the Sheet: objects stereotyped
|
364
|
+
# as ShapedGraphic, LineGraphic, or Group.
|
365
|
+
def graphics
|
366
|
+
self['GraphicsList']
|
367
|
+
end
|
368
|
+
|
369
|
+
# Only the LineGraphic objects within the Sheet.
|
370
|
+
def all_lines
|
371
|
+
graphics.find_all do | g |
|
372
|
+
g.behaves_like?(Graffle::LineGraphic)
|
373
|
+
end
|
374
|
+
end
|
375
|
+
|
376
|
+
# The first LineGraphic within the Sheet.
|
377
|
+
# Warning: currently, that does _not_ mean the one highest on
|
378
|
+
# the page, so this is really useful only for finding the only
|
379
|
+
# line.
|
380
|
+
def first_line; all_lines[0]; end
|
381
|
+
|
382
|
+
def self.takes_on(hash) # :nodoc:
|
383
|
+
sheet = hash.behave_like(self)
|
384
|
+
Container.stereotype_children_of(sheet)
|
385
|
+
true
|
386
|
+
end
|
387
|
+
|
388
|
+
end
|
389
|
+
|
390
|
+
# What you get when you group graphics in the UI.
|
391
|
+
module Group
|
392
|
+
include Builders
|
393
|
+
include Container
|
394
|
+
include AbstractGraphic
|
395
|
+
|
396
|
+
# The visible objects that were grouped.
|
397
|
+
def graphics
|
398
|
+
self['Graphics']
|
399
|
+
end
|
400
|
+
|
401
|
+
def self.takes_on(hash) # :nodoc:
|
402
|
+
if AbstractGraphic.takes_on(hash, self)
|
403
|
+
Container.stereotype_children_of(hash)
|
404
|
+
true
|
405
|
+
end
|
406
|
+
end
|
407
|
+
|
408
|
+
# TODO: What should the origin be for groups?
|
409
|
+
|
410
|
+
# TODO: When a group gets a collective bounding-box, how
|
411
|
+
# should that incorporate lines within the group?
|
412
|
+
|
413
|
+
end
|
414
|
+
|
415
|
+
# OmniGraffle text is RTF. This gives access to an object's text.
|
416
|
+
module Text
|
417
|
+
# Return the text as the original RTF string.
|
418
|
+
def as_rtf
|
419
|
+
self['Text']
|
420
|
+
end
|
421
|
+
|
422
|
+
# Strip all the styling from the RTF string and return it as
|
423
|
+
# humble ASCII.
|
424
|
+
def as_plain_text
|
425
|
+
StyledTextReader.new(self.as_rtf).as_lines.join("\n")
|
426
|
+
end
|
427
|
+
|
428
|
+
# Return an array of arrays. Each of the inner arrays represents
|
429
|
+
# a line in the original. At the moment, the inner array is what
|
430
|
+
# you get when you split the line at double-quote boundaries. For
|
431
|
+
# example, given this string:
|
432
|
+
#
|
433
|
+
# He visits the "login" page.
|
434
|
+
# His login is "peter", his password is "paul"
|
435
|
+
#
|
436
|
+
# the method returns:
|
437
|
+
#
|
438
|
+
# [ ['He visits the', 'login', 'page.']
|
439
|
+
# ['His login is', 'peter', 'his password is', 'paul']]
|
440
|
+
#
|
441
|
+
# There's more to come here.
|
442
|
+
def as_tokens_within_lines
|
443
|
+
StyledTextReader.new(self.as_rtf).as_tokens_within_lines
|
444
|
+
end
|
445
|
+
end
|
446
|
+
end
|