drx 0.0.2 → 0.3.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/{lib → examples}/drx_test.rb +3 -4
- data/{lib → examples}/drx_test2.rb +12 -7
- data/examples/drx_test3.rb +35 -0
- data/ext/{drx_ext.c → drx_core.c} +76 -45
- data/ext/extconf.rb +1 -1
- data/lib/drx/graphviz.rb +240 -0
- data/lib/drx/objinfo.rb +147 -0
- data/lib/drx/tempfiles.rb +44 -0
- data/lib/drx/tk/app.rb +363 -0
- data/lib/drx/tk/imagemap.rb +284 -0
- data/lib/drx.rb +21 -56
- metadata +16 -9
- data/lib/drxtk.rb +0 -210
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'tempfile'
|
2
|
+
|
3
|
+
# Holds a set of related temporary file names. When it goes out of scope, the files are deleted.
|
4
|
+
#
|
5
|
+
# files = Tempfiles.new('foo')
|
6
|
+
# puts files['gif']
|
7
|
+
# puts files['xml']
|
8
|
+
#
|
9
|
+
class Tempfiles
|
10
|
+
|
11
|
+
def initialize(basename=nil)
|
12
|
+
if basename.nil?
|
13
|
+
basename = $0.tr('^a-z', '_').tr('_', '') + 'xyzzy'
|
14
|
+
end
|
15
|
+
|
16
|
+
@refs = []
|
17
|
+
@paths = Hash.new do |h, suffix|
|
18
|
+
tf = Tempfile.new([basename, '.' + suffix])
|
19
|
+
tf.close
|
20
|
+
@refs << tf # We must keep a ref to this object or its finalizer will delete the temp file.
|
21
|
+
h[suffix] = tf.path
|
22
|
+
end
|
23
|
+
|
24
|
+
if block_given?
|
25
|
+
begin
|
26
|
+
yield self
|
27
|
+
ensure
|
28
|
+
unlink
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def [](suffix)
|
34
|
+
@paths[suffix]
|
35
|
+
end
|
36
|
+
|
37
|
+
# This method shouldn't be needed, as files are supposed to
|
38
|
+
# get deleted when Tempfile instances are GC'ed. But it seems
|
39
|
+
# they aren't always.
|
40
|
+
def unlink
|
41
|
+
@refs.each { |tf| tf.unlink }
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
data/lib/drx/tk/app.rb
ADDED
@@ -0,0 +1,363 @@
|
|
1
|
+
require 'tk'
|
2
|
+
#Tk.default_widget_set = :Ttk
|
3
|
+
require 'drx/tk/imagemap'
|
4
|
+
|
5
|
+
module Drx
|
6
|
+
module TkGUI
|
7
|
+
|
8
|
+
# The 'DRX_EDITOR_COMMAND' environment variable overrides this.
|
9
|
+
EDITOR_COMMAND = 'gedit +%d "%s"'
|
10
|
+
|
11
|
+
class Application
|
12
|
+
|
13
|
+
def toplevel
|
14
|
+
@toplevel ||= TkRoot.new
|
15
|
+
end
|
16
|
+
|
17
|
+
def initialize
|
18
|
+
@navigation_history = []
|
19
|
+
@eval_history = LineHistory.new
|
20
|
+
|
21
|
+
@eval_entry = TkEntry.new(toplevel) {
|
22
|
+
font 'Courier'
|
23
|
+
}
|
24
|
+
@eval_result = TkText.new(toplevel) {
|
25
|
+
font 'Courier'
|
26
|
+
height 4
|
27
|
+
foreground 'black'
|
28
|
+
background 'white'
|
29
|
+
tag_configure('error', :foreground => 'red')
|
30
|
+
tag_configure('info', :foreground => 'blue')
|
31
|
+
}
|
32
|
+
|
33
|
+
@im = TkImageMap::ImageMap.new(toplevel)
|
34
|
+
@im.select_command { |url|
|
35
|
+
if url
|
36
|
+
select_object @objs[url].the_object
|
37
|
+
else
|
38
|
+
select_object nil
|
39
|
+
end
|
40
|
+
}
|
41
|
+
@im.double_select_command { |url|
|
42
|
+
navigate_to_selected
|
43
|
+
}
|
44
|
+
@im.bind('ButtonRelease-3') {
|
45
|
+
navigate_back
|
46
|
+
}
|
47
|
+
|
48
|
+
@varsbox = TkListbox.new(toplevel) {
|
49
|
+
width 25
|
50
|
+
}
|
51
|
+
@methodsbox = TkListbox.new(toplevel) {
|
52
|
+
width 35
|
53
|
+
}
|
54
|
+
|
55
|
+
layout
|
56
|
+
|
57
|
+
@varsbox.bind('<ListboxSelect>') {
|
58
|
+
if @varsbox.has_selection?
|
59
|
+
require 'pp'
|
60
|
+
output "\n== Variable #{@varsbox.get_selection}\n\n", 'info'
|
61
|
+
output PP.pp(selected_var, '')
|
62
|
+
end
|
63
|
+
}
|
64
|
+
@varsbox.bind('ButtonRelease-3') {
|
65
|
+
if @varsbox.has_selection?
|
66
|
+
output "\n== Variable #{@varsbox.get_selection}\n\n", 'info'
|
67
|
+
output selected_var.inspect + "\n"
|
68
|
+
end
|
69
|
+
}
|
70
|
+
@varsbox.bind('Double-Button-1') {
|
71
|
+
if @varsbox.has_selection?
|
72
|
+
see selected_var
|
73
|
+
end
|
74
|
+
}
|
75
|
+
@methodsbox.bind('Double-Button-1') {
|
76
|
+
if @methodsbox.has_selection?
|
77
|
+
locate_method(current_object, @methodsbox.get_selection)
|
78
|
+
end
|
79
|
+
}
|
80
|
+
@eval_entry.bind('Key-Return') {
|
81
|
+
code = @eval_entry.value.strip
|
82
|
+
if code != ''
|
83
|
+
@eval_history.add code.dup
|
84
|
+
eval_code code
|
85
|
+
@eval_entry.value = ''
|
86
|
+
end
|
87
|
+
}
|
88
|
+
@eval_entry.bind('Key-Up') {
|
89
|
+
@eval_entry.value = @eval_history.prev!
|
90
|
+
}
|
91
|
+
@eval_entry.bind('Key-Down') {
|
92
|
+
@eval_entry.value = @eval_history.next!
|
93
|
+
}
|
94
|
+
toplevel.bind('Control-l') {
|
95
|
+
@eval_entry.focus
|
96
|
+
}
|
97
|
+
toplevel.bind('Control-r') {
|
98
|
+
# Refresh the display. Useful if you eval'ed some code that changes the
|
99
|
+
# object inspected.
|
100
|
+
navigate_to tip
|
101
|
+
# Note: it seems that #instance_eval creates a singleton for the object.
|
102
|
+
# So after eval'ing something and pressing C-r, you're going to see this
|
103
|
+
# extra class.
|
104
|
+
}
|
105
|
+
|
106
|
+
output "Please visit the homepage, http://drx.rubyforge.org/, for usage instructions.\n", 'info'
|
107
|
+
end
|
108
|
+
|
109
|
+
# Arrange the main widgets inside layout widgets.
|
110
|
+
def layout
|
111
|
+
main_frame = TkPanedwindow.new(toplevel, :orient => :vertical) {
|
112
|
+
pack :side => :top, :expand => true, :fill=> :both, :pady => 2, :padx => 2
|
113
|
+
# We push layout widgets below the main widgets in the stacking order.
|
114
|
+
# We don't want them to obscure the main ones.
|
115
|
+
lower
|
116
|
+
}
|
117
|
+
main_frame.add VBox.new toplevel, [
|
118
|
+
[Scrolled.new(toplevel, @eval_result, :vertical => true), { :expand => true, :fill => 'both' } ],
|
119
|
+
TkLabel.new(toplevel, :anchor => 'w') {
|
120
|
+
text 'Type some code to eval; \'self\' is the object at tip of diagram; prepend with "see" to examine result.'
|
121
|
+
},
|
122
|
+
@eval_entry,
|
123
|
+
]
|
124
|
+
|
125
|
+
panes = TkPanedwindow.new(main_frame, :orient => :horizontal) {
|
126
|
+
lower
|
127
|
+
}
|
128
|
+
panes.add VBox.new toplevel, [
|
129
|
+
TkLabel.new(toplevel, :text => 'Object graph (klass and super):', :anchor => 'w'),
|
130
|
+
[@im, { :expand => true, :fill => 'both' } ],
|
131
|
+
]
|
132
|
+
panes.add VBox.new toplevel, [
|
133
|
+
TkLabel.new(toplevel, :text => 'Variables (iv_tbl):', :anchor => 'w'),
|
134
|
+
[Scrolled.new(toplevel, @varsbox), { :expand => true, :fill => 'both' } ]
|
135
|
+
]
|
136
|
+
panes.add VBox.new toplevel, [
|
137
|
+
TkLabel.new(toplevel, :text => 'Methods (m_tbl):', :anchor => 'w'),
|
138
|
+
[Scrolled.new(toplevel, @methodsbox), { :expand => true, :fill => 'both' } ]
|
139
|
+
]
|
140
|
+
|
141
|
+
main_frame.add(panes)
|
142
|
+
end
|
143
|
+
|
144
|
+
# Output some text. It goes to the result textarea.
|
145
|
+
def output(s, tag=nil)
|
146
|
+
@eval_result.insert('end', s, Array(tag))
|
147
|
+
# Scroll to the bottom.
|
148
|
+
@eval_result.mark_set('insert', 'end')
|
149
|
+
@eval_result.see('end')
|
150
|
+
end
|
151
|
+
|
152
|
+
def open_up_editor(filename, lineno)
|
153
|
+
command = sprintf(ENV['DRX_EDITOR_COMMAND'] || EDITOR_COMMAND, lineno, filename)
|
154
|
+
output "Execting: #{command}...\n", 'info'
|
155
|
+
if !fork
|
156
|
+
if !Kernel.system(command)
|
157
|
+
output "Could not execute the command '#{command}'\n", 'error'
|
158
|
+
end
|
159
|
+
exit!
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
def locate_method(obj, method_name)
|
164
|
+
place = ObjInfo.new(obj).locate_method(method_name)
|
165
|
+
if !place
|
166
|
+
output "Method #{method_name} doesn't exist\n", 'info'
|
167
|
+
else
|
168
|
+
if place =~ /\A(\d+):(.*)/
|
169
|
+
open_up_editor($2, $1)
|
170
|
+
else
|
171
|
+
output "Can't locate method, because: #{place}\n", 'info'
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
def navigate_back
|
177
|
+
if @navigation_history.size > 1
|
178
|
+
@navigation_history.pop
|
179
|
+
see @navigation_history.pop
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
def selected_var
|
184
|
+
ObjInfo.new(current_object).__get_ivar(@varsbox.get_selection)
|
185
|
+
end
|
186
|
+
|
187
|
+
def eval_code(code)
|
188
|
+
see = !!code.sub!(/^see\s/, '')
|
189
|
+
begin
|
190
|
+
result = tip.instance_eval(code)
|
191
|
+
output result.inspect + "\n"
|
192
|
+
rescue StandardError, ScriptError => ex
|
193
|
+
gist = "%s: %s" % [ex.class, ex.message]
|
194
|
+
trace = ex.backtrace.reverse.drop_while { |line| line !~ /eval_code/ }.reverse
|
195
|
+
output gist + "\n" + trace.join("\n") + "\n", 'error'
|
196
|
+
end
|
197
|
+
see(result) if see
|
198
|
+
end
|
199
|
+
|
200
|
+
def current_object
|
201
|
+
# For some reason, even though ICLASS contains a copy of the iv_tbl of
|
202
|
+
# its 'klass', these variables are all nil. I think in all cases we'd
|
203
|
+
# want to see the module itself, so that's what we're going to do:
|
204
|
+
info = Drx::ObjInfo.new(@current_object)
|
205
|
+
if info.t_iclass?
|
206
|
+
# The following line is equivalent to 'Core::get_klass(@current_object)'
|
207
|
+
info.klass.the_object
|
208
|
+
else
|
209
|
+
@current_object
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
# Fills the variables listbox with a list of the object's instance variables.
|
214
|
+
def display_variables(obj)
|
215
|
+
@varsbox.delete('0', 'end')
|
216
|
+
info = ObjInfo.new(obj)
|
217
|
+
if obj and info.has_iv_tbl?
|
218
|
+
vars = info.iv_tbl.keys.map do |v| v.to_s end.sort
|
219
|
+
# Get rid of gazillions of Tk classes:
|
220
|
+
vars = vars.reject { |v| v =~ /Tk|Ttk/ }
|
221
|
+
@varsbox.insert('end', *vars)
|
222
|
+
end
|
223
|
+
end
|
224
|
+
|
225
|
+
# Fills the methods listbox with a list of the object's methods.
|
226
|
+
def display_methods(obj)
|
227
|
+
@methodsbox.delete('0', 'end')
|
228
|
+
info = ObjInfo.new(obj)
|
229
|
+
if obj and info.class_like?
|
230
|
+
methods = info.m_tbl.keys.map do |v| v.to_s end.sort
|
231
|
+
@methodsbox.insert('end', *methods)
|
232
|
+
end
|
233
|
+
end
|
234
|
+
|
235
|
+
# Loads the imagemap widget with a diagram of the object.
|
236
|
+
def display_graph(obj)
|
237
|
+
require 'drx/tempfiles'
|
238
|
+
@objs = {}
|
239
|
+
Tempfiles.new do |files|
|
240
|
+
ObjInfo.new(obj).generate_diagram(files) do |info|
|
241
|
+
@objs[info.dot_url] = info
|
242
|
+
end
|
243
|
+
@im.image = files['gif']
|
244
|
+
@im.image_map = files['map']
|
245
|
+
end
|
246
|
+
end
|
247
|
+
|
248
|
+
# Makes `obj` the primary object seen (the one which is the tip of the diagram).
|
249
|
+
def navigate_to(obj)
|
250
|
+
@current_object = obj
|
251
|
+
@navigation_history << obj
|
252
|
+
display_graph(obj)
|
253
|
+
# Trigger the update of the variables and methods tables by selecting this object
|
254
|
+
# in the imagemap.
|
255
|
+
@im.active_url = @im.urls.first
|
256
|
+
end
|
257
|
+
alias see navigate_to
|
258
|
+
|
259
|
+
# Returns the tip object in the diagram (the one passed to navigate_to())
|
260
|
+
def tip
|
261
|
+
@navigation_history.last
|
262
|
+
end
|
263
|
+
|
264
|
+
# Make `obj` the selected object. That is, the one the variable and method boxes reflect.
|
265
|
+
def select_object(obj)
|
266
|
+
@current_object = obj
|
267
|
+
display_variables(current_object)
|
268
|
+
display_methods(current_object)
|
269
|
+
end
|
270
|
+
|
271
|
+
# Navigate_to the selected object.
|
272
|
+
def navigate_to_selected
|
273
|
+
# current_object() descends T_ICLASS for us.
|
274
|
+
navigate_to(current_object)
|
275
|
+
end
|
276
|
+
|
277
|
+
def run
|
278
|
+
# @todo Skip this if Tk is already running.
|
279
|
+
Tk.mainloop
|
280
|
+
Tk.restart # So that Tk doesn't complain 'can't invoke "frame" command: application has been destroyed' next time.
|
281
|
+
end
|
282
|
+
end
|
283
|
+
|
284
|
+
# Manages history for an input line.
|
285
|
+
class LineHistory
|
286
|
+
def initialize
|
287
|
+
@entries = []
|
288
|
+
@pos = 0
|
289
|
+
end
|
290
|
+
def past_end?
|
291
|
+
@pos >= @entries.size
|
292
|
+
end
|
293
|
+
def add(s)
|
294
|
+
@entries.reject! { |ent| ent == s }
|
295
|
+
@entries << s
|
296
|
+
@pos = @entries.size
|
297
|
+
end
|
298
|
+
def prev!
|
299
|
+
@pos -= 1 if @pos > 0
|
300
|
+
current
|
301
|
+
end
|
302
|
+
def next!
|
303
|
+
@pos += 1 if not past_end?
|
304
|
+
current
|
305
|
+
end
|
306
|
+
def current
|
307
|
+
past_end? ? '' : @entries[@pos]
|
308
|
+
end
|
309
|
+
end
|
310
|
+
|
311
|
+
# Wraps scrollbars around a widget.
|
312
|
+
class Scrolled < TkFrame
|
313
|
+
def initialize(parent, the_widget, opts = { :vertical => true, :horizontal => true })
|
314
|
+
super(parent)
|
315
|
+
@the_widget = the_widget
|
316
|
+
if opts[:vertical]
|
317
|
+
TkScrollbar.new(self) { |s|
|
318
|
+
pack :side => 'right', :fill => 'y'
|
319
|
+
command { |*args| the_widget.yview *args }
|
320
|
+
the_widget.yscrollcommand { |first,last| s.set first,last }
|
321
|
+
}
|
322
|
+
end
|
323
|
+
if opts[:horizontal]
|
324
|
+
TkScrollbar.new(self) { |s|
|
325
|
+
orient 'horizontal'
|
326
|
+
pack :side => 'bottom', :fill => 'x'
|
327
|
+
command { |*args| the_widget.xview *args }
|
328
|
+
the_widget.xscrollcommand { |first,last| s.set first,last }
|
329
|
+
}
|
330
|
+
end
|
331
|
+
the_widget.raise # Since the frame is created after the widget, it obscures it by default.
|
332
|
+
the_widget.pack(:in => self, :side => 'left', :expand => 'true', :fill => 'both')
|
333
|
+
end
|
334
|
+
def raise
|
335
|
+
super
|
336
|
+
@the_widget.raise
|
337
|
+
end
|
338
|
+
end
|
339
|
+
|
340
|
+
# Arranges widgets one below the other.
|
341
|
+
class VBox < TkFrame
|
342
|
+
def initialize(parent, widgets)
|
343
|
+
super(parent)
|
344
|
+
widgets.each { |w, layout|
|
345
|
+
layout = {} if layout.nil?
|
346
|
+
layout = { :in => self, :side => 'top', :fill => 'x' }.merge layout
|
347
|
+
w.raise
|
348
|
+
w.pack(layout)
|
349
|
+
}
|
350
|
+
end
|
351
|
+
end
|
352
|
+
|
353
|
+
class ::TkListbox
|
354
|
+
def get_selection
|
355
|
+
return get(curselection[0])
|
356
|
+
end
|
357
|
+
def has_selection?
|
358
|
+
not curselection.empty?
|
359
|
+
end
|
360
|
+
end
|
361
|
+
|
362
|
+
end # module TkGUI
|
363
|
+
end # module Drx
|
@@ -0,0 +1,284 @@
|
|
1
|
+
class TkCanvas
|
2
|
+
def coords(evt)
|
3
|
+
return [canvasx(evt.x), canvasy(evt.y)]
|
4
|
+
end
|
5
|
+
end
|
6
|
+
|
7
|
+
module TkImageMap
|
8
|
+
|
9
|
+
# A widget to show an image together with an HTML imagemap.
|
10
|
+
class ImageMap < TkFrame
|
11
|
+
|
12
|
+
def select_command &block
|
13
|
+
@select_command = block
|
14
|
+
end
|
15
|
+
|
16
|
+
def double_select_command &block
|
17
|
+
@double_select_command = block
|
18
|
+
end
|
19
|
+
|
20
|
+
# Returns all the URLs.
|
21
|
+
def urls
|
22
|
+
@hotspots.map { |h| h[:url] }
|
23
|
+
end
|
24
|
+
|
25
|
+
# Delegates all bindings to the canvas. That's probably what the
|
26
|
+
# programmer expects.
|
27
|
+
def bind(*args, &block)
|
28
|
+
@canvas.bind *args, &block
|
29
|
+
end
|
30
|
+
|
31
|
+
def initialize(parent, *rest)
|
32
|
+
super
|
33
|
+
@hover_region = nil
|
34
|
+
@active_region = nil
|
35
|
+
@hotspots = []
|
36
|
+
@image_widget = nil
|
37
|
+
@select_command = proc {}
|
38
|
+
@double_select_command = proc {}
|
39
|
+
|
40
|
+
@canvas = TkCanvas.new self, :scrollregion => [0, 0, 2000, 2000]
|
41
|
+
|
42
|
+
v_scr = TkScrollbar.new(self, :orient => 'vertical').pack :side=> 'right', :fill => 'y'
|
43
|
+
h_scr = TkScrollbar.new(self, :orient => 'horizontal').pack :side=> 'bottom', :fill => 'x'
|
44
|
+
@canvas.xscrollbar(h_scr)
|
45
|
+
@canvas.yscrollbar(v_scr)
|
46
|
+
# Attach scrolling to middle button.
|
47
|
+
@canvas.bind('2', proc { |x,y| @canvas.scan_mark(x,y) }, '%x %y')
|
48
|
+
@canvas.bind('B2-Motion', proc { |x,y| @canvas.scan_dragto x, y }, '%x %y')
|
49
|
+
|
50
|
+
@canvas.pack :expand => true, :fill => 'both'
|
51
|
+
|
52
|
+
['Motion','Control-Motion'].each { |sequence|
|
53
|
+
@canvas.bind sequence do |evt|
|
54
|
+
x, y = @canvas.coords(evt)
|
55
|
+
spot = @canvas.find('overlapping', x, y, x + 1, y + 1).first
|
56
|
+
if spot and spot.cget('tags').include? 'hotspot'
|
57
|
+
new_hover_region = spot.hotspot_region
|
58
|
+
else
|
59
|
+
new_hover_region = nil
|
60
|
+
end
|
61
|
+
if new_hover_region != @hover_region
|
62
|
+
@hover_region[:hover_spot].configure(:state => 'hidden') if @hover_region
|
63
|
+
@hover_region = new_hover_region
|
64
|
+
@hover_region[:hover_spot].configure(:state => 'disabled') if @hover_region # like 'visible'
|
65
|
+
if sequence['Control']
|
66
|
+
self.active_region = @hover_region
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
}
|
71
|
+
@canvas.bind 'Button-1' do |evt|
|
72
|
+
x, y = @canvas.coords(evt)
|
73
|
+
spot = @canvas.find('overlapping', x, y, x+1, y+1).first
|
74
|
+
if spot and spot.cget('tags').include? 'hotspot'
|
75
|
+
self.active_region = spot.hotspot_region
|
76
|
+
else
|
77
|
+
self.active_region = nil
|
78
|
+
end
|
79
|
+
end
|
80
|
+
@canvas.bind 'Double-Button-1' do
|
81
|
+
@double_select_command.call(active_url) if @active_region
|
82
|
+
end
|
83
|
+
# Middle button: vertical scrolling.
|
84
|
+
@canvas.bind 'Button-4' do
|
85
|
+
@canvas.yview_scroll(-1, 'units')
|
86
|
+
end
|
87
|
+
@canvas.bind 'Button-5' do
|
88
|
+
@canvas.yview_scroll(1, 'units')
|
89
|
+
end
|
90
|
+
# Middle button: horizontal scrolling.
|
91
|
+
@canvas.bind 'Shift-Button-4' do
|
92
|
+
@canvas.xview_scroll(-1, 'units')
|
93
|
+
end
|
94
|
+
@canvas.bind 'Shift-Button-5' do
|
95
|
+
@canvas.xview_scroll(1, 'units')
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def active_region=(region)
|
100
|
+
if region != @active_region
|
101
|
+
@active_region[:active_spot].configure(:state => 'hidden') if @active_region
|
102
|
+
@active_region = region
|
103
|
+
@active_region[:active_spot].configure(:state => 'disabled') if @active_region # like 'visible'
|
104
|
+
@select_command.call(active_url)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def active_url
|
109
|
+
@active_region ? @active_region[:url] : nil
|
110
|
+
end
|
111
|
+
|
112
|
+
def active_url=(newurl)
|
113
|
+
catch :found do
|
114
|
+
@hotspots.each { |region|
|
115
|
+
if region[:url] == newurl
|
116
|
+
self.active_region = region
|
117
|
+
throw :found
|
118
|
+
end
|
119
|
+
}
|
120
|
+
# It was not found:
|
121
|
+
self.active_region = nil
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
def image=(pathname)
|
126
|
+
@image = TkPhotoImage.new :file => pathname
|
127
|
+
@canvas.configure :scrollregion => [0, 0, @image.width, @image.height]
|
128
|
+
end
|
129
|
+
|
130
|
+
# You must call this after image=()
|
131
|
+
def image_map=(pathname)
|
132
|
+
# Delete the previous spot widgets and the image widget.
|
133
|
+
@canvas.find('all').each {|w| w.destroy }
|
134
|
+
@canvas.xview_moveto(0)
|
135
|
+
@canvas.yview_moveto(0)
|
136
|
+
|
137
|
+
@hotspots = Utils.parse_imap(pathname)
|
138
|
+
|
139
|
+
#
|
140
|
+
# Create the back spots.
|
141
|
+
#
|
142
|
+
@hotspots.each do |region|
|
143
|
+
args = [@canvas, *(region[:args])]
|
144
|
+
opts = {
|
145
|
+
:fill => 'red',
|
146
|
+
:tags => ['hotspot']
|
147
|
+
}
|
148
|
+
back_spot = region[:class].new *(args + [opts])
|
149
|
+
# The following won't work, so we have to use define_method instead.
|
150
|
+
#def back_spot.hotspot_region
|
151
|
+
# region
|
152
|
+
#end
|
153
|
+
meta = class << back_spot; self; end
|
154
|
+
meta.send :define_method, :hotspot_region do
|
155
|
+
region
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
@image_widget = TkcImage.new(@canvas,0,0, :image => @image, :anchor=> 'nw')
|
160
|
+
|
161
|
+
#
|
162
|
+
# Create the hover spots.
|
163
|
+
#
|
164
|
+
@hotspots.each do |region|
|
165
|
+
args = [@canvas, *(region[:args])]
|
166
|
+
opts = {
|
167
|
+
:dash => '. ',
|
168
|
+
:width => 5,
|
169
|
+
:fill => nil,
|
170
|
+
:outline => 'black',
|
171
|
+
:state => 'hidden'
|
172
|
+
}
|
173
|
+
region[:hover_spot] = region[:class].new *(args + [opts])
|
174
|
+
opts = {
|
175
|
+
:width => 5,
|
176
|
+
:fill => nil,
|
177
|
+
:outline => 'black',
|
178
|
+
:state => 'hidden'
|
179
|
+
}
|
180
|
+
region[:active_spot] = region[:class].new *(args + [opts])
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end # class ImageMap
|
184
|
+
|
185
|
+
module Utils
|
186
|
+
|
187
|
+
# DOT outputs oval elements as polygons with many many sides. Since Tk doesn't
|
188
|
+
# draw them well, we convert such polygons to ovals.
|
189
|
+
def self.to_oval(coords)
|
190
|
+
points = coords.enum_slice(2).to_a
|
191
|
+
if points.size < 10
|
192
|
+
# If there are few sides, we assume it's a real polygon.
|
193
|
+
return nil
|
194
|
+
end
|
195
|
+
x_min, x_max = points.map {|p| p[0]}.minmax
|
196
|
+
y_min, y_max = points.map {|p| p[1]}.minmax
|
197
|
+
# @todo: try to figure out if the points trace a circle?
|
198
|
+
#center_x = x_min + (x_max - x_min) / 2.0
|
199
|
+
#center_y = y_min + (y_max - y_min) / 2.0
|
200
|
+
#points.each do |x,y|
|
201
|
+
#end
|
202
|
+
return [x_min, y_min, x_max, y_max]
|
203
|
+
end
|
204
|
+
|
205
|
+
# Parses an HTML image map.
|
206
|
+
def self.parse_imap(filepath)
|
207
|
+
hotspots = []
|
208
|
+
require 'rexml/document'
|
209
|
+
doc = REXML::Document.new(File.open(filepath))
|
210
|
+
doc.root.elements.each do |elt|
|
211
|
+
if elt.is_a? REXML::Element and elt.name == 'area'
|
212
|
+
attrs = elt.attributes
|
213
|
+
url = attrs['href']
|
214
|
+
coords = attrs['coords'].split(/,|\s+/).map{|c|c.to_i} # @todo: A circle's radius can be a percent.
|
215
|
+
case attrs['shape']
|
216
|
+
when 'poly';
|
217
|
+
if args = to_oval(coords)
|
218
|
+
hotspots << {
|
219
|
+
:args => args,
|
220
|
+
:class => TkcOval,
|
221
|
+
:url => url,
|
222
|
+
}
|
223
|
+
else
|
224
|
+
hotspots << {
|
225
|
+
:args => coords,
|
226
|
+
:class => TkcPolygon,
|
227
|
+
:url => url,
|
228
|
+
}
|
229
|
+
end
|
230
|
+
when 'rect';
|
231
|
+
hotspots << {
|
232
|
+
:args => coords,
|
233
|
+
:class => TkcRectangle,
|
234
|
+
:url => url,
|
235
|
+
}
|
236
|
+
when 'circle';
|
237
|
+
cx, cy, radius = coords
|
238
|
+
hotspots << {
|
239
|
+
:args => [cx - radius, cy - radius, cx + radius, cy + radius],
|
240
|
+
:class => TkcOval,
|
241
|
+
:url => url,
|
242
|
+
}
|
243
|
+
else raise "I don't support shape '#{attrs['shape']}'"
|
244
|
+
end
|
245
|
+
end
|
246
|
+
end
|
247
|
+
return hotspots
|
248
|
+
end
|
249
|
+
end # module Utils
|
250
|
+
|
251
|
+
end # module TkImageMap
|
252
|
+
|
253
|
+
if $0 == __FILE__ # A little demonstration.
|
254
|
+
|
255
|
+
class App # :nodoc:
|
256
|
+
def initialize
|
257
|
+
@root = TkRoot.new
|
258
|
+
im = TkImageMap::ImageMap.new(@root)
|
259
|
+
im.pack :expand => true, :fill => 'both'
|
260
|
+
im.image = 'a.gif'
|
261
|
+
im.image_map = 'a.map'
|
262
|
+
im.select_command { |url|
|
263
|
+
if url
|
264
|
+
puts 'clicked: ' + url
|
265
|
+
else
|
266
|
+
puts 'cleared'
|
267
|
+
end
|
268
|
+
}
|
269
|
+
im.double_select_command { |url|
|
270
|
+
puts 'going to ' + url
|
271
|
+
}
|
272
|
+
btn = TkButton.new @root
|
273
|
+
btn.pack
|
274
|
+
btn.command {
|
275
|
+
p 'button clicked'
|
276
|
+
im.active_url = 'http://server/obj/o_1224289036'
|
277
|
+
}
|
278
|
+
end
|
279
|
+
end
|
280
|
+
|
281
|
+
app = App.new
|
282
|
+
Tk.mainloop
|
283
|
+
|
284
|
+
end
|