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