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.
@@ -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