hilfer 0.9.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,805 @@
1
+ # local files
2
+ # local files
3
+ require 'hilfer/scite_editor.rb'
4
+ require 'hilfer/rails_locator.rb'
5
+ require 'hilfer/svn_colours.rb'
6
+ require 'pathname'
7
+
8
+ =begin
9
+ This is the tree viewer object. It contains a GtkTreeView
10
+ and references an editor object. It also contains a HilferItem
11
+ which is the root node of the tree, but it's never displayed
12
+ =end
13
+ class TreeViewer
14
+ include SvnColours
15
+
16
+ attr_reader :view, :editor, :root_item
17
+
18
+ # Can pass in $options[:terminal] => some_terminal
19
+ # to use different terminal types. See GnomeTerminal
20
+ # for an example
21
+ def initialize( path, editor, parent_window, location )
22
+ @terminal = $options[:terminal]
23
+ @parent_window = parent_window
24
+ @location = location
25
+ @location.activates_default = false
26
+
27
+ # these need to be window-local
28
+ @expecting_synchronize_path = false
29
+ @auto_synchronize_path = $options[:auto_sync]
30
+ @root_item = HilferItem.new( '', path )
31
+ # TODO this shuold be in set_root or something like that
32
+ @location.text = @root_item.path
33
+ @location.select_region(0,0)
34
+ @location.position = -1
35
+ @hilfer_items = {}
36
+
37
+ # locators are things that provide a set of shortcut keys
38
+ # to jump to specific directories, files, or related locations
39
+ @locators = []
40
+ @locators << RailsLocator.new( self )
41
+
42
+ # pre-populate model from filesystem, only 1 level deep
43
+ #model = Gtk::TreeStore.new( HilferItem )
44
+ model = HilferModel.new( HilferItem )
45
+ populate( model, @root_item, nil )
46
+
47
+ # Create view
48
+ @view = Gtk::TreeView.new( model )
49
+ @view.headers_visible = false
50
+ @view.selection.mode = Gtk::SELECTION_MULTIPLE
51
+
52
+ # register with the editor, and remember it
53
+ editor.register_view( self )
54
+ @editor = editor
55
+
56
+ # handle key presses
57
+ init_keys
58
+
59
+ # handle enter for location bar
60
+ @location.signal_connect( 'activate' ) do |widget|
61
+ new_path = File.expand_path( widget.text )
62
+ if File.exists? new_path
63
+ go_into( new_path )
64
+ else
65
+ dialog = Gtk::MessageDialog.new(
66
+ @window,
67
+ Gtk::Dialog::Flags::MODAL,
68
+ Gtk::MessageDialog::Type::INFO,
69
+ Gtk::MessageDialog::BUTTONS_OK,
70
+ "Directory #{widget.text} does not exist"
71
+ )
72
+ dialog.run
73
+ dialog.destroy
74
+ end
75
+ end
76
+
77
+ @location.signal_connect( 'focus-out-event' ) do |widget,event|
78
+ #make sure everything is unselected, and put the cursor at the end
79
+ widget.select_region(0,0)
80
+ widget.position = -1
81
+ end
82
+
83
+ # handle double-clicks so they don't activate the
84
+ # cell editing
85
+ @view.signal_connect( 'button-press-event' ) do
86
+ |view, event|
87
+ if event.button == 1 && event.event_type == Gdk::Event::BUTTON2_PRESS
88
+ path, column, cell_x, cell_y = view.get_path( *event.coords )
89
+ if iter = view.model.get_iter( path )
90
+ open_action( [ iter[0] ] )
91
+ true
92
+ end
93
+ end
94
+ end
95
+
96
+ # handle collapse and expand are cached in HilferItems
97
+ @view.signal_connect( 'row-collapsed' ) do
98
+ |view, iter, path|
99
+ iter[0].status = :collapsed
100
+ true
101
+ end
102
+
103
+ @view.signal_connect( 'row-expanded' ) do
104
+ |view, iter, path|
105
+ print "expanding row: #{iter[0].path}, #{iter[0].populated?}\n" if $options[:debug]
106
+ iter[0].status = :expanded
107
+ child = iter.first_child
108
+ loop do
109
+ print "expanded child: #{child[0].path}, #{child[0].populated?}\n" if $options[:debug]
110
+ populate( @view.model, child[0], child ) unless child[0].populated?
111
+ break if !child.next!
112
+ end
113
+
114
+ # remember expanded status
115
+ setstatus
116
+ true
117
+ end
118
+
119
+ # add widgets for the columns and cells
120
+ init_renderers
121
+
122
+ # all columns must be fixed for this to work
123
+ @view.fixed_height_mode = true
124
+
125
+ init_searchable
126
+
127
+ # Allow drag and drop reordering of rows
128
+ # would need to be hooked into fs move, and maybe
129
+ # the refresh so that items aren't put back in their original
130
+ # places. That's hard.
131
+ #@view.set_reorderable( true)
132
+
133
+ # set up refresh thread
134
+ #~ Thread.new( self ) do |arg|
135
+ #~ loop do
136
+ #~ sleep 2
137
+ #~ arg.refresh
138
+ #~ print "refreshed\n"
139
+ #~ end
140
+ #~ end
141
+ end
142
+
143
+ def refresh
144
+ go_into( @root_item.path )
145
+ if $options[:debug]
146
+ load 'rails_locator.rb'
147
+ load 'tree_viewer.rb'
148
+ end
149
+ end
150
+
151
+ def init_renderers
152
+ # The column to display everything in
153
+ column = Gtk::TreeViewColumn.new( "icon and name" )
154
+ column.resizable = false
155
+ column.sizing = Gtk::TreeViewColumn::FIXED
156
+
157
+ # display the icon
158
+ pixbuf_renderer = Gtk::CellRendererPixbuf.new
159
+ column.pack_start( pixbuf_renderer, false )
160
+ column.set_cell_data_func( pixbuf_renderer ) do
161
+ |column, renderer, model, iter|
162
+ item = iter[0]
163
+
164
+ # figure out which icon to use
165
+ stock =
166
+ if item.dir?
167
+ if renderer.expanded?
168
+ Gtk::Stock::OPEN
169
+ else
170
+ Gtk::Stock::OPEN
171
+ end
172
+ else
173
+ Gtk::Stock::FILE
174
+ end
175
+
176
+ # set the icon
177
+ renderer.pixbuf = @view.render_icon( stock, Gtk::IconSize::MENU, 'cell_renderer' )
178
+ end
179
+
180
+ # display the file name
181
+ @text_renderer = Gtk::CellRendererText.new
182
+ @text_renderer.ypad = 0
183
+ column.pack_start( @text_renderer, true )
184
+ column.set_cell_data_func( @text_renderer ) do
185
+ |column, renderer, model, iter|
186
+ item = iter[0]
187
+ renderer.text = item.item
188
+ renderer.foreground = item.colour
189
+ end
190
+
191
+ # Allow file renames
192
+ @text_renderer.signal_connect("editing-started") do |widget,editable,path|
193
+ editable && editable.signal_connect("editing-done") do |widget|
194
+ # Fetch the HilferItem
195
+ item = @view.model.get_iter(path)[0]
196
+
197
+ # make the new file name, in full
198
+ new_path = ( Pathname.new( item.path ).parent + editable.text )
199
+
200
+ # rename the file if it's renameable
201
+ if editable.text != item.item && !File.exists?( new_path.to_s )
202
+ # rename the file
203
+ begin
204
+ File.rename( item.path, new_path.to_s )
205
+ item.item = editable.text
206
+ item.path = new_path.to_s
207
+ rescue Exception => e
208
+ print "couldn't rename #{item.path} to #{new_path.to_s}\n"
209
+ print e, "\n"
210
+ e.backtrace.each { | line | print line, "\n" }
211
+ end
212
+ end
213
+ true
214
+ end
215
+
216
+ true
217
+ end
218
+
219
+ @view.append_column( column )
220
+ end
221
+
222
+ # Set the interactive search box
223
+ def init_searchable
224
+ # allow tree to be searchable using regular expressions
225
+ @view.search_column = 0
226
+ @view.enable_search = true
227
+ @view.set_search_equal_func do
228
+ # - model: a Gtk::TreeModel
229
+ # - column: a Column number
230
+ # - key: a String
231
+ # - iter: a Gtk::TreeIter
232
+ # - Returns: true to continue, or false to quit(the word is matched).
233
+ |model, column, key, iter|
234
+ begin
235
+ iter[0].item !~ /#{key}/
236
+ rescue
237
+ true
238
+ end
239
+ end
240
+ end
241
+
242
+ # display a dialog and delete the files if the
243
+ # response was yes. Unless the don't ask for file
244
+ # deletion flag is switched off
245
+ def confirm_delete_files( iters )
246
+ msg = "Are you sure you want to delete\n"
247
+ iters.each { |it| msg += it[0].path + "\n" }
248
+
249
+ confirm = Gtk::MessageDialog.new(
250
+ @window,
251
+ Gtk::Dialog::Flags::MODAL,
252
+ Gtk::MessageDialog::Type::QUESTION,
253
+ Gtk::MessageDialog::BUTTONS_YES_NO,
254
+ msg
255
+ )
256
+ confirm.default_response = Gtk::Dialog::RESPONSE_YES
257
+ confirm.signal_connect( 'key-press-event' ) do
258
+ |widget,event|
259
+ case event.hardware_keycode
260
+ when 57 # n pressed
261
+ confirm.response( Gtk::Dialog::RESPONSE_NO )
262
+ true
263
+ when 29 # y pressed
264
+ confirm.response( Gtk::Dialog::RESPONSE_YES )
265
+ true
266
+ end
267
+ end
268
+
269
+ # actually delete the files
270
+ confirm.run do |response|
271
+ delete_files( iters ) if response == Gtk::Dialog::RESPONSE_YES
272
+ confirm.destroy
273
+ end
274
+ end
275
+
276
+ def delete_files( iters )
277
+ iters.each do |it|
278
+ begin
279
+ File.delete it[0].path
280
+ @view.model.remove(it)
281
+ rescue
282
+ print "Can't delete #{it[0].path}"
283
+ end
284
+ end
285
+ end
286
+
287
+ # handle keypresses
288
+ def init_keys
289
+ @view.signal_connect( 'key-press-event' ) do
290
+ |widget,event|
291
+ #puts event.inspect
292
+ retval = true
293
+ case
294
+ # enter - go into directory
295
+ # or open all selected files
296
+ when event.hardware_keycode == 36 && event.state.empty?
297
+ items = []
298
+
299
+ # fetch all selected items
300
+ widget.selection.selected_each do |model, path, iter|
301
+ items << model.get_value( iter, 0 )
302
+ end
303
+
304
+ # go into if the first item is a directory
305
+ open_action( items )
306
+
307
+ # ctrl-enter opens new window with the selected root(s)
308
+ when event.hardware_keycode == 36 && event.state.control_mask?
309
+ widget.selection.selected_each do |model, path, iter|
310
+ new_path =
311
+ if !iter[0].dir?
312
+ # open the directory this file lives in
313
+ # unless we're already in that directory
314
+ iter.parent[0].path unless iter.parent.nil?
315
+ else
316
+ # open this directory
317
+ iter[0].path
318
+ end
319
+ TreeViewerWindow.new( new_path, @editor ) unless new_path.nil?
320
+ end
321
+
322
+ # backspace - go up to parent directory
323
+ when event.hardware_keycode == 22
324
+ # go up one directory
325
+ temp = Pathname.new( @root_item.path )
326
+ @root_item.path = temp.parent.realpath.to_s
327
+ go_into( @root_item.path )
328
+
329
+ # del on main keyboard or keypad deletes a file
330
+ when [91,107].include?( event.hardware_keycode )
331
+ iters = []
332
+ widget.selection.selected_each do |model, path, iter|
333
+ iters << iter
334
+ end
335
+ confirm_delete_files( iters )
336
+
337
+ # ctrl-e and F2 edits the file name
338
+ when ( event.hardware_keycode == 26 && event.state.control_mask? ) || event.hardware_keycode == 68
339
+ # make sure all the files are selected
340
+ rows = widget.selection.selected_rows
341
+ rows.each do |path|
342
+ # seems it not necessary to set this
343
+ # @text_renderer.mode = Gtk::CellRendererText::MODE_EDITABLE
344
+ @text_renderer.editable = true
345
+ # true is to start editing
346
+ view.set_cursor( path, view.get_column(0), true )
347
+ # the rest of the work is done in the renderer's handler.
348
+ @text_renderer.editable = false
349
+ end
350
+
351
+ # ctrl-d sends some debug/info commands to scite
352
+ when event.hardware_keycode == 40 && event.state.control_mask? && !event.state.shift_mask?
353
+ @editor.dump
354
+
355
+ # ctrl-b sends the current selection to the editor
356
+ when event.hardware_keycode == 56 && event.state.control_mask? && !event.state.shift_mask?
357
+ paths = []
358
+ widget.selection.selected_each do |model, path, iter|
359
+ # note use of single quotes
360
+ paths << model.get_value( iter, 0 ).path
361
+ end
362
+ @editor.insert paths
363
+
364
+ # ctrl-c copies current selections as text
365
+ when event.hardware_keycode == 54 && event.state.control_mask? && !event.state.shift_mask?
366
+ paths = ""
367
+
368
+ # fetch all selected items
369
+ # don't use a join here so we get a trailing newline
370
+ widget.selection.selected_each do |model, path, iter|
371
+ paths << model.get_value( iter, 0 ).path << "\n"
372
+ end
373
+
374
+ Gtk::Clipboard.get( Gdk::Selection::CLIPBOARD ).text = paths
375
+
376
+ # ctrl-v selects the files in the clipboard
377
+ when event.hardware_keycode == 55 && event.state.control_mask? && !event.state.shift_mask?
378
+ text = Gtk::Clipboard.get( Gdk::Selection::CLIPBOARD ).wait_for_text
379
+ paths = text.strip.split( /\s*\n\s*/ )
380
+ location_path = Pathname.new( @root_item.path )
381
+ paths = paths.map do |x|
382
+ # check for path names relative to the current location
383
+ fullpath = location_path + x
384
+ ( fullpath.exist? ? fullpath : x ).to_s
385
+ end
386
+ select_fs_paths( paths )
387
+
388
+ # ctrl-n opens new window with same root
389
+ when event.hardware_keycode == 57 && event.state.control_mask?
390
+ TreeViewerWindow.new( @root_item.path, @editor )
391
+
392
+ # ctrl-y synchronizes with current editor file
393
+ when event.hardware_keycode == 29 && event.state.control_mask?
394
+ # make sure other windows don't synchronize
395
+ @expecting_synchronize_path = true
396
+ # ask editor for current file and sync
397
+ @editor.synchronize_path
398
+
399
+ # alt-y toggles automatic synchronization
400
+ when event.hardware_keycode == 29 && event.state.mod1_mask?
401
+ @auto_synchronize_path = !@auto_synchronize_path
402
+
403
+ # ctrl-r refreshes from filesystem
404
+ when event.hardware_keycode == 27 && event.state.control_mask?
405
+ refresh
406
+
407
+ # ctrl-t opens a terminal window on the given directorties.
408
+ # Uses gnome-terminal and opens directories in tabs
409
+ when event.hardware_keycode == 28 && event.state.control_mask? && !event.state.shift_mask?
410
+ # collect all unique directories
411
+ dirs = []
412
+ widget.selection.selected_each do |model, path, iter|
413
+ dirs << if iter[0].dir?
414
+ iter[0].path
415
+ else
416
+ if iter.parent != nil
417
+ iter.parent[0].path
418
+ else
419
+ @root_item.path
420
+ end
421
+ end
422
+ end
423
+ terminal.launch( dirs )
424
+
425
+ # alt-l toggles the location bar
426
+ when event.hardware_keycode == 46 && event.state.mod1_mask?
427
+ @location.no_show_all = true
428
+ @location.visible = !@location.visible?
429
+
430
+ # ctrl-* on keypad means expand the entire tree
431
+ when event.hardware_keycode == 63 && event.state.control_mask?
432
+ # 100 levels of directories should be enough...
433
+ populate( @view.model, @root_item, nil, 100 )
434
+ @view.expand_all
435
+
436
+ # * on keypad means expand subtree
437
+ when event.hardware_keycode == 63 && event.state.empty?
438
+ widget.selection.selected_rows.each do |path|
439
+ iter = @view.model.get_iter( path )
440
+ if iter[0].dir?
441
+ expand_dir( widget, path, iter )
442
+ end
443
+ end
444
+
445
+ # shift-/ on keypad means collapse the entire tree
446
+ when event.hardware_keycode == 112 && event.state.shift_mask?
447
+ @view.collapse_all
448
+
449
+ # ctrl-left means go to parent
450
+ when event.hardware_keycode == 100 && event.state.control_mask?
451
+ widget.selection.selected_each do |model, path, iter|
452
+ if iter.parent != nil
453
+ iter.parent[0].last_child_used = path
454
+ select_iter( iter.parent )
455
+ end
456
+
457
+ # only do the first one
458
+ break
459
+ end
460
+
461
+ # ctrl-right means go to last used path, or first child
462
+ when event.hardware_keycode == 102 && event.state.control_mask?
463
+ widget.selection.selected_each do |model, path, iter|
464
+ if iter[0].dir?
465
+ widget.expand_row( iter.path, false )
466
+ if iter[0].last_child_used != nil
467
+ select_path( iter[0].last_child_used )
468
+ else
469
+ select_iter( iter.first_child )
470
+ end
471
+ end
472
+
473
+ # only do the first one
474
+ break
475
+ end
476
+
477
+ # ctrl-up means go to previous sibling with children
478
+ when event.hardware_keycode == 98 && event.state.control_mask?
479
+ widget.selection.selected_each do |model, path, iter|
480
+ if iter[0].dir?
481
+ while path.prev!
482
+ if model.get_iter( path ).has_child?
483
+ select_path( path )
484
+ break
485
+ end
486
+ end
487
+ else
488
+ select_iter iter.parent unless iter.parent.nil?
489
+ end
490
+ break
491
+ end
492
+
493
+ # ctrl-down means go to next sibling with children
494
+ when event.hardware_keycode == 104 && event.state.control_mask?
495
+ widget.selection.selected_each do |model, path, iter|
496
+ if iter[0].dir?
497
+ while iter.next!
498
+ if iter[0].dir?
499
+ select_iter( iter )
500
+ break
501
+ end
502
+ end
503
+ else
504
+ # go up to parent and then down
505
+ # to parent's following sibling
506
+ temp = iter.parent
507
+ if temp != nil
508
+ temp.next!
509
+ select_iter temp
510
+ end
511
+ end
512
+ # only do the first selection
513
+ break
514
+ end
515
+
516
+ # shift-left means select parent
517
+ when event.hardware_keycode == 100 && event.state.shift_mask?
518
+ widget.selection.selected_each do |model, path, iter|
519
+ if iter.parent != nil
520
+ select_children( widget, iter.parent )
521
+ end
522
+ end
523
+
524
+ # shift-right means select children
525
+ when event.hardware_keycode == 102 && event.state.shift_mask?
526
+ widget.selection.selected_each do |model, path, iter|
527
+ if iter[0].dir?
528
+ select_children( widget, iter )
529
+ end
530
+ end
531
+
532
+ # left means close level(s)
533
+ when event.hardware_keycode == 100 && event.state.empty?
534
+ widget.selection.selected_each do |model, path, iter|
535
+ widget.collapse_row( path ) if iter[0].dir?
536
+ end
537
+
538
+ # right means open level(s)
539
+ when event.hardware_keycode == 102
540
+ widget.selection.selected_each do
541
+ |model, path, iter|
542
+ # false means don't expand children
543
+ widget.expand_row( path, false )
544
+ end
545
+
546
+ else
547
+ # pass keypress to all locators
548
+ @locators.each {|x| retval &&= x.handle_keypress( widget, event ) }
549
+ # indicate signal not handled
550
+ if $options[:debug] && !retval
551
+ print "hardware_keycode: #{event.hardware_keycode}\n"
552
+ end
553
+ end
554
+ retval
555
+ end
556
+ end
557
+
558
+ def expand_dir( widget, path, iter )
559
+ # make sure all directories in subtree are populated
560
+ populate( @view.model, iter[0], iter, 100 )
561
+
562
+ # expand row, with children
563
+ widget.expand_row( path, true )
564
+ end
565
+
566
+ def expand_parents_of( path )
567
+ cur = path.clone
568
+ cur.up!
569
+ paths = []
570
+ while !@view.row_expanded?( cur ) && cur.depth > 0
571
+ paths << cur.clone
572
+ cur.up!
573
+ end
574
+
575
+ # now open the necessary paths from the top down
576
+ # false is don't open all children
577
+ paths.reverse.each do |x|
578
+ @view.expand_row( x, false )
579
+ @view.selection.unselect_path( x )
580
+ end
581
+ end
582
+
583
+ def unselect_all
584
+ @view.selection.unselect_all
585
+ end
586
+
587
+ def select_children( widget, iter )
588
+ widget.expand_row( iter.path, false )
589
+ (0...iter.n_children).each do |index|
590
+ select_iter( iter.nth_child( index ), false, false )
591
+ end
592
+ end
593
+
594
+ # set selection and cursor to the given GtkTreeModel path
595
+ def select_path( path, unselect = true, set_cursor = true )
596
+ unselect_all if unselect
597
+ expand_parents_of( path )
598
+ @view.selection.select_path( path )
599
+ @view.set_cursor( path, nil, false ) if set_cursor
600
+ # path, column, use_align, row_align, col_align
601
+ @view.scroll_to_cell( path, nil, true, 0.5, 0.0 )
602
+ end
603
+
604
+ # set selection and cursor to the given GtkTreeModel iter
605
+ def select_iter( iter, unselect = true, set_cursor = true )
606
+ unselect_all if unselect
607
+ expand_parents_of( iter.path )
608
+ @view.selection.select_iter( iter )
609
+ @view.set_cursor( iter.path, nil, false ) if set_cursor
610
+ # path, column, use_align, row_align, col_align
611
+ @view.scroll_to_cell( iter.path, nil, true, 0.5, 0.0 )
612
+ end
613
+
614
+ # Set selection to the given filesystem path.
615
+ # TODO could optimise this to select faster if we
616
+ # use directory names. Possibly also use select_iter if it's faster
617
+ def select_fs_path( fs_path, open = false )
618
+ @view.model.each do
619
+ |model, path, iter|
620
+ if iter[0].path == fs_path
621
+ select_path( path )
622
+ @view.expand_row( path, false ) if open
623
+ break
624
+ end
625
+ end
626
+ end
627
+
628
+ def select_fs_paths( fs_paths, open = false )
629
+ cursor_set = false
630
+ @view.model.each do
631
+ |model, path, iter|
632
+ if fs_paths.include?( iter[0].path )
633
+ @view.expand_row( path, false ) if open
634
+ # set the cursor at the beginning because
635
+ # selection seems to follow it and we want
636
+ # a multiple selection with the cursor on the first
637
+ if !cursor_set
638
+ @view.set_cursor( iter.path, nil, false )
639
+ cursor_set = true
640
+ end
641
+
642
+ select_iter( iter, false, false )
643
+ end
644
+ end
645
+
646
+ end
647
+
648
+ def select_first
649
+ # set first item as selected
650
+ @view.selection.select_path Gtk::TreePath.new( '0' )
651
+ end
652
+
653
+ # callback for the editor classes
654
+ # First checks the values of
655
+ # @expecting_synchronize_path and @auto_synchronize_path
656
+ # and then calls select_fs_path
657
+ def synchronise_editor_path( fs_path )
658
+ if ( @expecting_synchronize_path || @auto_synchronize_path )
659
+ select_fs_path fs_path
660
+ @expecting_synchronize_path = false
661
+ end
662
+ end
663
+
664
+ # called from open_action when we don't want the auto-centering
665
+ # to do its thing.
666
+ def no_synchronize
667
+ save = @auto_synchronize_path
668
+ @auto_synchronize_path = false
669
+ yield @editor
670
+ @auto_synchronize_path = save
671
+ end
672
+
673
+ # called from the keyboard handler on enter
674
+ # and the double-click handler to
675
+ # open files or go into a directory
676
+ def open_action( files )
677
+ if files.size == 1 && files.first.dir?
678
+ go_into( files.first.path )
679
+ else
680
+ nodirs = files.find_all{ |x| !x.dir? }
681
+ no_synchronize do |editor|
682
+ editor.open_action( nodirs )
683
+ end
684
+ end
685
+ end
686
+
687
+ def terminal
688
+ @terminal ||= GnomeTerminal.new
689
+ end
690
+
691
+ # Move the displayed tree to the given directory
692
+ def go_into( fs_path )
693
+ last_select = []
694
+ @view.selection.selected_each do |model, path, iter|
695
+ last_select << path
696
+ end
697
+
698
+ last_select = [ Gtk::TreePath.new( '0' ) ] if last_select.empty?
699
+
700
+ @view.model.clear
701
+ @hilfer_items.values.each { |x| x.populated = false }
702
+ # this waits forever when called by the refresh thread
703
+ @root_item.path = fs_path
704
+ @root_item.populated = false
705
+ # show location as relative to home
706
+ @location.text = @root_item.path.sub( ENV['HOME'], '~' )
707
+ populate( @view.model, @root_item, nil )
708
+ setstatus
709
+ last_select.each { |x| select_path( x ) }
710
+ end
711
+
712
+ # fetch or create the HilferItem. Attemps a lookup from the item_path
713
+ # first if not found creates it from the item_name and item_path
714
+ def fetch_hilfer_item( item_name, item_path )
715
+ if @hilfer_items.has_key?( item_path )
716
+ @hilfer_items[item_path]
717
+ else
718
+ # create new item, add to column 0
719
+ tmp = HilferItem.new( item_name, item_path )
720
+ @hilfer_items[item_path] = tmp
721
+ tmp
722
+ end
723
+ end
724
+
725
+ def do_compute_colours( item )
726
+ if respond_to? :compute_colours
727
+ compute_colours( item.path )
728
+ else
729
+ {}
730
+ end
731
+ end
732
+
733
+ # populate the model from the filesystem
734
+ # by default populate only 1 level down
735
+ def populate( model, item, parent_iter, levels_to_populate = 1, level = 0 )
736
+ iter = nil
737
+ path_colours = do_compute_colours( item )
738
+
739
+ Dir.new( item.path ).entries.sort.each do |x|
740
+ begin
741
+ # ignore directory entries
742
+ next if x =~ /^\.\.?$/
743
+ # ignore hidden files unless the flag is set
744
+ next if x =~ /^\./ && !$options[:hidden]
745
+ # ignore generated files
746
+ next if x =~ /.os$/ || x =~ /.pot$/
747
+
748
+ # the path of the new item to consider
749
+ next_item_path = item.path + '/' + x
750
+
751
+ if item.populated?
752
+ # already have iters for this item
753
+ if iter.nil?
754
+ iter = parent_iter ? parent_iter.first_child : model.iter_first
755
+ else
756
+ iter.next!
757
+ end
758
+ else
759
+ # generate new iters
760
+ iter = model.append( parent_iter )
761
+ end
762
+
763
+ # insert the HilferItem objects, from cache if
764
+ # that exists.
765
+ iter[0] = current_item = fetch_hilfer_item( x, next_item_path )
766
+ if ( colour = path_colours[next_item_path] )
767
+ current_item.colour = colour
768
+ end
769
+
770
+ # recursively populate files, so the TreeView
771
+ # knows to draw the expanders
772
+ if ( ( File.directory?( next_item_path ) && level < levels_to_populate ) || iter[0].expanded? )
773
+ print "populating level #{level} #{iter[0].path}\n" if $options[:debug]
774
+ # populate children
775
+ populate( model, iter[0], iter, levels_to_populate, level + 1 )
776
+ end
777
+
778
+ rescue Errno::EACCES
779
+ # mostly to protect against permissions errors
780
+ rescue Exception => ex
781
+ print "caught exception: #{ex} #{ex.class}\n"
782
+ ex.backtrace.each { |x| print x, "\n" }
783
+ end
784
+
785
+ end
786
+ # OK, this dir is now populated
787
+ item.populated = true
788
+
789
+ # clean deleted entries. Doesn't seem necessary?
790
+ #model.remove iter while iter != model.iter_first
791
+ end
792
+
793
+ # go through and expand / collapse based on HilferItem status
794
+ def setstatus
795
+ @view.model.each do |model, path, iter|
796
+ case iter[0].status
797
+ when :collapsed
798
+ @view.collapse_row( iter.path )
799
+ when :expanded
800
+ @view.expand_row( iter.path, false )
801
+ end
802
+ end
803
+ end
804
+
805
+ end