drx 0.0.2 → 0.3.0

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