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.
- data/History.txt +6 -0
- data/README.txt +49 -0
- data/bin/ec +9 -0
- data/bin/hilfer +260 -0
- data/bin/hilfer-icon.png +0 -0
- data/bin/sd +17 -0
- data/lib/hilfer/rails_locator.rb +150 -0
- data/lib/hilfer/scite_editor.rb +182 -0
- data/lib/hilfer/svn_colours.rb +58 -0
- data/lib/hilfer/tree_viewer.rb +805 -0
- metadata +75 -0
@@ -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
|