Class: Drx::TkGUI::Application

Inherits:
Object
  • Object
show all
Defined in:
lib/drx/tk/app.rb

Class Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initializeApplication

Returns a new instance of Application.



58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
# File 'lib/drx/tk/app.rb', line 58

def initialize
  @navigation_history = []
  @eval_history = LineHistory.new
  @graph_opts = { :size => '90%', :style => 'default' }

  @eval_entry = TkEntry.new(toplevel) {
    font 'Courier'
  }
  @eval_result = TkText.new(toplevel) {
    font 'Courier'
    height 4
    foreground 'black'
    background 'white'
    tag_configure('error', :foreground => 'red')
    tag_configure('info', :foreground => 'blue')
  }

  @im = TkImageMap::ImageMap.new(toplevel)
  @im.select_command { |url|
    if url
      select_object @objs[url].the_object
    else
      select_object nil
    end
  }
  @im.double_select_command { |url|
    navigate_to_selected
  }
  @im.bind('ButtonRelease-3') {
    navigate_back
  }

  @varsbox = Tk::Tile::Treeview.new(toplevel) {
    columns 'name value'
    heading_configure('name', :text => 'Name')
    heading_configure('value', :text => 'Value')
    column_configure('name', :stretch => false )
    column_configure('value', :stretch => false )
    show 'headings'
  }
  @methodsbox = Tk::Tile::Treeview.new(toplevel) {
    columns 'name arguments location'
    heading_configure('name', :text => 'Name')
    heading_configure('arguments', :text => 'Arguments')
    heading_configure('location', :text => 'Location')
    column_configure('name', :stretch => false )
    column_configure('arguments', :stretch => false )
    column_configure('location', :stretch => false )
    show 'headings'
    # We want the layout manager to allocate space for two columns only:
    displaycolumns 'name location'
  }

  @graph_size_menu = Tk::Tile::Combobox.new(toplevel) {
    set '90%'
    values ['100%', '90%', '80%', '60%']
    state :readonly
    width 6
  }
  @graph_style_menu = Tk::Tile::Combobox.new(toplevel) {
    set 'default'
    values ['default', 'crazy']
    state :readonly
    width 10
  }
  @save_btn = TkButton.new(toplevel) {
    text 'Save...'
  }
  @save_btn.command {
    save_graph tip
  }

  @show_arguments_chk = TkCheckbutton.new(toplevel) {
    text 'Show arguments'
    variable TkVariable.new(0)
  }
  @use_arguments_gem_chk = TkCheckbutton.new(toplevel) {
    text "Use the 'arguments' gem (slower)"
    variable TkVariable.new(0)
  }

  layout

  @varsbox.bind('<TreeviewSelect>') {
    if @varsbox.has_selection?
      require 'pp'
      output "\n== Variable #{@varsbox.get_selection}\n\n", 'info'
      output PP.pp(selected_var, '')
    end
  }
  @varsbox.bind('ButtonRelease-3') {
    if @varsbox.has_selection?
      output "\n== Variable #{@varsbox.get_selection}\n\n", 'info'
      output selected_var.inspect + "\n"
    end
  }
  @varsbox.bind('Double-Button-1') {
    if @varsbox.has_selection?
      see selected_var
    end
  }
  @methodsbox.bind('Double-Button-1') {
    if @methodsbox.has_selection?
      edit(current_object, @methodsbox.get_selection)
    end
  }
  @eval_entry.bind('Key-Return') {
    code = @eval_entry.value.strip
    if code != ''
      @eval_history.add code.dup
      eval_code code
      @eval_entry.value = ''
    end
  }
  @eval_entry.bind('Key-Up') {
    @eval_entry.value = @eval_history.prev!
  }
  @eval_entry.bind('Key-Down') {
    @eval_entry.value = @eval_history.next!
  }
  toplevel.bind('Control-l') {
    @eval_entry.focus
  }
  toplevel.bind('Control-r') {
    # Refresh the display. Useful if you eval'ed some code that changes the
    # object inspected.
    refresh
    # Note: it seems that #instance_eval creates a singleton for the object.
    # So after eval'ing something and pressing C-r, you're going to see this
    # extra class.
  }
  @graph_size_menu.bind('<ComboboxSelected>') {
    @graph_opts[:size] = @graph_size_menu.get
    refresh
  }
  @graph_style_menu.bind('<ComboboxSelected>') {
    @graph_opts[:style] = @graph_style_menu.get
    refresh
  }
  @show_arguments_chk.variable.trace('w') do |value,|
    if value == 1
      @use_arguments_gem_chk.raise
      @methodsbox.displaycolumns 'name arguments location'
      display_methods(current_object)
    else
      @use_arguments_gem_chk.lower
      @methodsbox.displaycolumns 'name location'
    end
  end
  @use_arguments_gem_chk.variable.trace('w') do |value,|
    ObjInfo.use_arguments_gem = (value == 1)
    display_methods(current_object)
  end
  @show_arguments_chk.variable.value = @show_arguments_chk.variable.value # Trigger the trace handler.

  output "Please visit the homepage, http://drx.rubyforge.org/, for usage instructions.\n", 'info'

  Application.load_rc
  system_customizations
  user_customizations
end

Class Attribute Details

.in_loopObject

Returns the value of attribute in_loop.



503
504
505
# File 'lib/drx/tk/app.rb', line 503

def in_loop
  @in_loop
end

Class Method Details

.first_window?Boolean

Returns:

  • (Boolean)


504
# File 'lib/drx/tk/app.rb', line 504

def first_window?; !in_loop; end

.load_rcObject

Loads ~/.drxrc.



42
43
44
45
46
47
48
# File 'lib/drx/tk/app.rb', line 42

def self.load_rc
  @rc_loaded ||= begin
    rc = File.join(ENV['HOME'] || Dir.pwd, '.drxrc')
    load rc if File.exist? rc
    1
  end
end

Instance Method Details

#current_objectObject



337
338
339
340
341
342
343
344
345
346
347
348
# File 'lib/drx/tk/app.rb', line 337

def current_object
  # For some reason, even though ICLASS contains a copy of the iv_tbl of
  # its 'klass', these variables are all nil. I think in all cases we'd
  # want to see the module itself, so that's what we're going to do:
  info = Drx::ObjInfo.new(@current_object)
  if info.t_iclass?
    # The following line is equivalent to 'Core::get_klass(@current_object)'
    info.klass.the_object
  else
    @current_object
  end
end

#display_graph(obj) ⇒ Object

Loads the imagemap widget with a diagram of the object.



436
437
438
439
440
441
442
443
444
445
446
# File 'lib/drx/tk/app.rb', line 436

def display_graph(obj)
  require 'drx/tempfiles'
  @objs = {}
  Tempfiles.new do |files|
    ObjInfo.new(obj).generate_diagram(files, @graph_opts) do |info|
      @objs[info.dot_url] = info
    end
    @im.image = files['gif']
    @im.image_map = files['map']
  end
end

#display_methods(obj) ⇒ Object

Fills the methods listbox with a list of the object’s methods.



420
421
422
423
424
425
426
427
428
429
430
431
432
433
# File 'lib/drx/tk/app.rb', line 420

def display_methods(obj)
  @methodsbox.clear
  info = ObjInfo.new(obj)
  if obj and info.class_like?
    methods = info.m_tbl.keys.map do |v| v.to_s end.sort
    methods.each do |name|
      @methodsbox.insert('', 'end', :text => name, :values => [
        name,
        show_arguments? ? pretty_arguments(info, name) : '-',
        pretty_location(info, name)
      ])
    end
  end
end

#display_variables(obj) ⇒ Object

Fills the variables listbox with a list of the object’s instance variables.



351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
# File 'lib/drx/tk/app.rb', line 351

def display_variables(obj)
  allowed_names = [/^@/, /^[A-Z]/, '__classpath__', '__tmp_classpath__', '__classid__', '__attached__']
  @varsbox.clear
  info = ObjInfo.new(obj)
  if obj and info.has_iv_tbl?
    vars = info.iv_tbl.keys.map do |v| v.to_s end.sort
    # Get rid of gazillions of Tk classes:
    vars = vars.reject { |v| v =~ /Tk|Ttk/ }
    vars.each do |name|
      begin
        value = if allowed_names.any? { |p| p === name }
                  info.get_ivar(name).inspect
                else
                  # We don't want to inspect ruby's internal variables (because
                  # they may not be Ruby values at all).
                  ''
                end
        @varsbox.insert('', 'end', :text => name, :values => [ name, value ] )
      rescue NameError
        # Referencing an autoloaded constant (in ObjInfo#get_ivar()) may
        # raise a NameError. This happens when the source file autoloaded
        # defines the moudle/class in the top-level. Example is Camping::Mab.
      end
    end
  end
end

#edit(obj, method_name) ⇒ Object



300
301
302
303
304
305
306
307
308
309
310
311
# File 'lib/drx/tk/app.rb', line 300

def edit(obj, method_name)
  location = ObjInfo.new(obj).locate_method(method_name) rescue nil
  if !location
    output "Method #{method_name} doesn't exist\n", 'info'
  else
    if location.is_a? String
      output "Can't locate method, because it's a: #{location}\n", 'info'
    else
      open_up_editor(location[0], location[1])
    end
  end
end

#eval_code(code) ⇒ Object



324
325
326
327
328
329
330
331
332
333
334
335
# File 'lib/drx/tk/app.rb', line 324

def eval_code(code)
  see = !!code.sub!(/^see\s/, '')
  begin
    result = tip.instance_eval(code)
    output result.inspect + "\n"
  rescue StandardError, ScriptError => ex
    gist = "%s: %s" % [ex.class, ex.message]
    trace = ex.backtrace.reverse.drop_while { |line| line !~ /eval_code/ }.reverse
    output gist + "\n" + trace.join("\n") + "\n", 'error'
  end
  see(result) if see
end

#hbox(*args) ⇒ Object



237
# File 'lib/drx/tk/app.rb', line 237

def hbox(*args); HBox.new(toplevel, args); end

#layoutObject

Arrange the main widgets inside layout widgets.



241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
# File 'lib/drx/tk/app.rb', line 241

def layout
  main_frame = TkPanedwindow.new(toplevel, :orient => :vertical) {
    pack :side => :top, :expand => true, :fill=> :both, :pady => 2, :padx => 2
    # We push layout widgets below the main widgets in the stacking order.
    # We don't want them to obscure the main ones.
    lower
  }
  main_frame.add vbox(
    [Scrolled.new(toplevel, @eval_result, :vertical => true), { :expand => true, :fill => 'both' } ],
    TkLabel.new(toplevel, :anchor => 'w') {
      text 'Type some code to eval; \'self\' is the object at the base of the graph; prepend with "see" to inspect result.'
    },
    @eval_entry
  )

  panes = TkPanedwindow.new(main_frame, :orient => :horizontal) {
    lower
  }
  # Note the :weight's on the followings.
  panes.add vbox(
    TkLabel.new(toplevel, :text => 'Object graph (klass and super):', :anchor => 'w'),
    [Scrolled.new(toplevel, @im), { :expand => true, :fill => 'both' } ],
    hbox(TkLabel.new(toplevel, :text => 'Size: '), @graph_size_menu,
         separator,
         TkLabel.new(toplevel, :text => 'Style: '), @graph_style_menu,
         separator,
         [@save_btn, { :pady => 5 }])
  ), :weight => 10
  panes.add vbox(
    TkLabel.new(toplevel, :text => 'Variables (iv_tbl):', :anchor => 'w'),
    [Scrolled.new(toplevel, @varsbox), { :expand => true, :fill => 'both' } ]
  ), :weight => 50
  panes.add vbox(
    TkLabel.new(toplevel, :text => 'Methods (m_tbl):', :anchor => 'w'),
    [Scrolled.new(toplevel, @methodsbox), { :expand => true, :fill => 'both' } ],
    hbox(@show_arguments_chk, separator, @use_arguments_gem_chk)
  ), :weight => 10

  main_frame.add(panes)
end


313
314
315
316
317
318
# File 'lib/drx/tk/app.rb', line 313

def navigate_back
  if @navigation_history.size > 1
    @navigation_history.pop
    see @navigation_history.pop
  end
end

Makes ‘obj` the primary object seen (the one which is the tip of the diagram).



468
469
470
471
472
473
474
475
476
# File 'lib/drx/tk/app.rb', line 468

def navigate_to(obj)
  @current_object = obj
  @navigation_history << obj
  display_graph(obj)
  update_title(obj)
  # Trigger the update of the variables and methods tables by selecting this object
  # in the imagemap.
  @im.active_url = @im.urls.first
end

Navigate_to the selected object.



497
498
499
500
# File 'lib/drx/tk/app.rb', line 497

def navigate_to_selected
  # current_object() descends T_ICLASS for us.
  navigate_to(current_object)
end

#open_up_editor(filename, lineno) ⇒ Object



290
291
292
293
294
295
296
297
298
# File 'lib/drx/tk/app.rb', line 290

def open_up_editor(filename, lineno)
  command = sprintf(ENV['DRX_EDITOR_COMMAND'] || EDITOR_COMMAND, lineno, filename)
  output "Executing: #{command}...\n", 'info'
  Thread.new do
    if !Kernel.system(command)
      output "Could not execute the command '#{command}'\n", 'error'
    end
  end
end

#output(s, tag = nil) ⇒ Object

Output some text. It goes to the result textarea.



283
284
285
286
287
288
# File 'lib/drx/tk/app.rb', line 283

def output(s, tag=nil)
  @eval_result.insert('end', s, Array(tag))
  # Scroll to the bottom.
  @eval_result.mark_set('insert', 'end')
  @eval_result.see('end')
end

#pretty_arguments(info, name) ⇒ Object

Returns a string describing a method’s arguments, for use in GUIs.



399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
# File 'lib/drx/tk/app.rb', line 399

def pretty_arguments(info, name)
  args = info.method_arguments(name)
  return args.map do |arg|
    case arg[0]
    when :req;   (arg[1] || 'arg').to_s
    when :opt;   (arg[1] || 'arg').to_s + '=' + (arg[2] || '?')
    when :rest;  '*' + (arg[1] || 'args').to_s
    when :block; '&' + (arg[1] || 'arg').to_s
    end
  end.join(', ')
rescue NameError
  return '---'
rescue SyntaxError => e
  'SYNTAX ERROR: ' + e.to_s
end

#pretty_location(info, method) ⇒ Object

Returns a terse method location, for use in GUIs.



379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
# File 'lib/drx/tk/app.rb', line 379

def pretty_location(info, method)
  location = begin
               info.locate_method(method)
             rescue NameError
               if method != '<Allocator>'
                 # Since we're using the GUI, the method has to exist. The
                 # only possibility here is that it's an undef'ed method entry.
                 '<undef>'
               end
             end
  if location.nil?
    ''
  elsif location.is_a? String
    location
  else
    File.basename location[0]
  end
end

#refreshObject

Refreshes the display.



485
486
487
# File 'lib/drx/tk/app.rb', line 485

def refresh
  navigate_to tip
end

#runObject



507
508
509
510
511
512
513
514
# File 'lib/drx/tk/app.rb', line 507

def run
  return if Application.in_loop
  # @todo Any other way to detect that Tk's mainloop is already running?
  Application.in_loop = true
  Tk.mainloop
  Application.in_loop = false
  Tk.restart # So that Tk doesn't complain 'can't invoke "frame" command: application has been destroyed' next time.
end

#save_graph(obj) ⇒ Object

Saves the graph to a file.



449
450
451
452
453
454
455
456
457
458
# File 'lib/drx/tk/app.rb', line 449

def save_graph(obj)
  require 'drx/tempfiles'
  Tempfiles.new do |files|
    ObjInfo.new(obj).generate_diagram(files, @graph_opts)
    if (output = Tk.getSaveFile(:parent => toplevel, :defaultextension => '.gif')) != ''
      require 'fileutils'
      FileUtils.cp(files['gif'], output)
    end
  end
end

#select_object(obj) ⇒ Object

Make ‘obj` the selected object. That is, the one the variable and method boxes reflect.



490
491
492
493
494
# File 'lib/drx/tk/app.rb', line 490

def select_object(obj)
   @current_object = obj
   display_variables(current_object)
   display_methods(current_object)
end

#selected_varObject



320
321
322
# File 'lib/drx/tk/app.rb', line 320

def selected_var
  ObjInfo.new(current_object).get_ivar(@varsbox.get_selection)
end

#separatorObject



238
# File 'lib/drx/tk/app.rb', line 238

def separator; TkLabel.new(toplevel, :text => '  '); end

#show_arguments?Boolean

Returns:

  • (Boolean)


415
416
417
# File 'lib/drx/tk/app.rb', line 415

def show_arguments?
  @show_arguments_chk.variable == 1
end

#system_customizationsObject

The following are default customizations. They are subjective in nature and users may knock them out in their ~/.drxrc.



227
228
229
230
231
232
233
234
# File 'lib/drx/tk/app.rb', line 227

def system_customizations
  if Application.first_window?
    # Try to make the Unixy GUI less ugly.
    if Tk::Tile.respond_to? :themes and Tk.windowingsystem == 'x11' and Tk::Tile.themes.include? 'clam'
      Tk::Tile.set_theme 'clam'
    end
  end
end

#tipObject

Returns the tip object in the diagram (the one passed to navigate_to())



480
481
482
# File 'lib/drx/tk/app.rb', line 480

def tip
  @navigation_history.last
end

#toplevelObject

Returns the top-level frame in which to show ourselves.



51
52
53
54
55
56
# File 'lib/drx/tk/app.rb', line 51

def toplevel
  @toplevel ||= begin
    # We're showing ourselves inside a TkRoot, unless one already exists.
    Application.first_window? ? TkRoot.new : TkToplevel.new
  end
end

#update_title(obj) ⇒ Object

Updates the window title (usually shown in the taskbar).



461
462
463
464
465
# File 'lib/drx/tk/app.rb', line 461

def update_title(obj)
  toplevel.title = 'Drx: ' + begin
    obj.is_a?(Module) ? obj.name : obj.class.name
  end.to_s # In case of singletons, #name returns nil, so to_s enforces a string.
end

#user_customizationsObject

Users may redefine this method in their ~/.drxrc to fine-tune the app.



222
223
# File 'lib/drx/tk/app.rb', line 222

def user_customizations
end

#vbox(*args) ⇒ Object



236
# File 'lib/drx/tk/app.rb', line 236

def vbox(*args); VBox.new(toplevel, args); end