graffle 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/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
|