2 @copyright: (C) 2008, Thomas Leonard
3 @see: U{http://roscidus.com}
5 import os
, sys
, fnmatch
11 from directory
import DirModel
13 class CantHandle(Exception):
17 def __init__(self
, arg
, value
):
20 self
.parsed
= self
.parse_value(value
)
22 def get_entry_text(self
):
25 def get_button_label(self
):
26 return self
.get_entry_text() or '(nothing)'
28 def finish_edit(self
):
30 model
= iv
.get_model()
31 if iv
.flags() & gtk
.HAS_FOCUS
:
32 cursor_path
= (iv
.get_cursor() or (None, None))[0]
35 selected
= iv
.get_selected_items()
37 if cursor_path
and selected
and cursor_path
not in selected
:
38 raise Warning("Cursor not in selection!")
39 if cursor_path
and not selected
:
40 selected
= [cursor_path
]
42 self
.finish_edit_with_selection(selected
)
44 def finish_edit_with_selection(self
, selected
):
47 def get_default_command(self
):
50 def updown(self
, entry
, delta
):
54 def parse_value(self
, value
):
55 if value
.startswith('-'):
60 def expand_to_argv(self
):
64 def parse_value(self
, value
):
66 if first
and first
in '"\'':
67 if value
[-1] == first
:
74 def expand_to_argv(self
):
77 class SelectedFiles(Value
):
78 abs_path
= None # GFile for the directory containing the files
79 to_select
= [] # Paths in the IconView's model to be selected
81 def __init__(self
, arg
, value
):
82 Value
.__init
__(self
, arg
, value
)
83 path
, leaf
= support
.split_expanded_path(self
.parsed
)
85 self
.abs_path
= arg
.view
.cwd
.file.resolve_relative_path(path
)
87 self
.abs_path
= arg
.view
.cwd
.file
94 path
, leaf
= os
.path
.split(value
)
95 case_insensitive
= (leaf
== leaf
.lower())
97 single_match_is_dir
= False
98 for i
, row
in self
.arg
.iter_matches(leaf
, case_insensitive
):
99 name
= row
[DirModel
.NAME
]
100 if prefix_match
is not None:
101 single_match_is_dir
= False # Multiple matches
102 if not name
.startswith(prefix_match
):
103 # Have to shorten the match then
105 for a
, b
in zip(prefix_match
, name
):
110 prefix_match
= ''.join(same
)
113 if row
[DirModel
.INFO
].get_file_type() == gio
.FILE_TYPE_DIRECTORY
:
114 single_match_is_dir
= True
115 if single_match_is_dir
:
117 if prefix_match
and prefix_match
!= leaf
:
118 self
.parsed
= os
.path
.join(path
, prefix_match
)
119 new
= self
.get_entry_text()
121 entry
.set_position(len(new
))
123 def updown(self
, entry
, delta
):
126 iv
= self
.arg
.view
.iv
127 model
= iv
.get_model()
128 cursor_path
= self
.arg
.view
.get_cursor_path()
131 return model
.iter_next(i
) or model
.get_iter_first()
134 n
, = model
.get_path(i
)
136 n
= model
.iter_n_children(None)
137 return model
.get_iter((n
- 1,))
139 start
= model
.get_iter(cursor_path
)
140 i
= apply_delta(start
)
142 start
= i
= model
.get_iter_root()
144 return # Empty directory
145 start
= model
[start
][DirModel
.NAME
]
147 case_insensitive
= (leaf
== leaf
.lower())
150 name
= model
[i
][DirModel
.NAME
]
155 if name
.startswith(leaf
):
156 path
= model
.get_path(i
)
163 class Filename(SelectedFiles
):
164 selected_filename
= None
167 def parse_value(self
, value
):
170 def get_selected_files(self
):
172 iv
= self
.arg
.view
.iv
173 model
= iv
.get_model()
175 self
.selected_filename
= None
176 self
.selected_type
= None
179 return [] # Empty entry
182 self
.selected_filename
= ''
183 self
.selected_type
= gio
.FILE_TYPE_DIRECTORY
185 elif leaf
in ('.', '..'):
186 self
.selected_filename
= leaf
187 self
.selected_type
= gio
.FILE_TYPE_DIRECTORY
190 cursor_path
= (self
.arg
.view
.iv
.get_cursor() or (None, None))[0]
192 cursor_filename
= model
[model
.get_iter(cursor_path
)][0]
194 cursor_filename
= None
197 # - Select any exact match
198 # - Else, select any exact case-insensitive match
199 # - Else, select the cursor item if the prefix matches
200 # - Else, select the first prefix match
201 # - Else, select nothing
203 # If the user only entered lower-case letters do a case insensitive match
204 model
= self
.arg
.view
.iv
.get_model()
206 case_insensitive
= (leaf
== leaf
.lower())
207 exact_case_match
= None
210 for i
, row
in self
.arg
.iter_matches(leaf
, case_insensitive
):
213 exact_case_match
= model
.get_path(i
)
218 exact_match
= model
.get_path(i
)
220 prefix_match
= model
.get_path(i
)
221 if case_insensitive
and cursor_filename
:
222 cursor_filename
= cursor_filename
.lower()
224 to_select
= exact_case_match
226 to_select
= exact_match
227 elif cursor_filename
and cursor_filename
.startswith(leaf
):
228 to_select
= cursor_path
230 to_select
= prefix_match
234 if to_select
is None:
237 row
= model
[model
.get_iter(to_select
)]
238 self
.selected_filename
= row
[DirModel
.NAME
]
239 self
.selected_type
= row
[DirModel
.INFO
].get_file_type()
243 def get_entry_text(self
):
246 def get_button_label(self
):
247 return self
.selected_filename
or '(none)'
249 def expand_to_argv(self
):
251 raise Warning("Empty argument")
252 if self
.selected_filename
is None:
253 raise Warning("No filename selected")
254 abs_path
= self
.abs_path
.resolve_relative_path(self
.selected_filename
)
255 return [self
.arg
.view
.cwd
.file.get_relative_path(abs_path
) or abs_path
.get_path()]
257 def finish_edit_with_selection(self
, selected
):
259 model
= self
.arg
.view
.iv
.get_model()
261 raise Warning("No selection and no cursor item!")
262 if len(selected
) > 1:
263 raise Warning("Multiple selection!")
266 row
= model
[model
.get_iter(path
)]
267 self
.selected_filename
= row
[DirModel
.NAME
]
268 self
.selected_type
= row
[DirModel
.INFO
].get_file_type()
269 self
.value
= self
.parsed
= self
.selected_filename
271 def get_default_command(self
):
272 if self
.selected_filename
is None:
274 if self
.selected_type
== gio
.FILE_TYPE_DIRECTORY
:
279 class Newfile(SelectedFiles
):
280 def parse_value(self
, value
):
281 if value
.startswith('!'):
285 def get_entry_text(self
):
286 return '!' + self
.parsed
288 def get_selected_files(self
):
289 # No completion for new files
290 # TODO: highlight if it exists
293 def expand_to_argv(self
):
295 path
, leaf
= os
.path
.split(value
)
297 raise Warning("No name given for new file!")
299 final_dir
= self
.arg
.view
.cwd
.file.resolve_relative_path(path
)
300 unix_path
= final_dir
.get_path()
301 if not os
.path
.exists(unix_path
):
302 os
.makedirs(unix_path
)
305 def get_default_command(self
):
308 class Glob(SelectedFiles
):
309 def updown(self
, entry
, delta
):
312 def parse_value(self
, value
):
318 def get_selected_files(self
):
321 def match(m
, path
, iter):
323 if fnmatch
.fnmatch(name
, pattern
):
324 to_select
.append(path
)
325 self
.arg
.view
.iv
.get_model().foreach(match
)
328 def expand_to_argv(self
):
329 pattern
= self
.parsed
330 matches
= [row
[0] for i
, row
in self
.arg
.view
.iter_contents() if fnmatch
.fnmatch(row
[0], pattern
)]
332 raise Warning("Nothing matches '%s'!" % pattern
)
335 def tab(self
, entry
):
339 def __init__(self
, view
):
342 def get_entry_text(self
):
345 def get_button_label(self
):
348 def entry_changed(self
, entry
):
351 def finish_edit(self
):
357 def tab(self
, entry
):
360 def updown(self
, entry
, delta
):
363 class Argument(BaseArgument
):
364 """Represents a word entered by the user for a command."""
365 # This is a bit complicated. An argument can be any of these:
366 # - A glob pattern matching multiple filenames (*.html)
367 # - A single filename (index.html)
368 # - A set of files selected manually (a.html, b.html)
369 # - A quoted string ('*.html')
370 # - An option (--index)
371 def __init__(self
, view
):
372 BaseArgument
.__init
__(self
, view
)
373 self
.result
= self
.parse_value('')
375 def parse_value(self
, value
):
376 for t
in [Newfile
, Quote
, Option
, Glob
, Filename
]:
378 return t(self
, value
)
383 def iter_matches(self
, match
, case_insensitive
):
384 """Return all rows with a name matching match"""
385 for i
, row
in self
.view
.iter_contents():
389 if name
.startswith(match
):
392 def entry_changed(self
, entry
):
393 self
.result
= self
.parse_value(entry
.get_text())
395 if not isinstance(self
.result
, SelectedFiles
):
396 self
.view
.iv
.unselect_all()
399 cursor_path
= (self
.view
.iv
.get_cursor() or (None, None))[0]
401 # Check which directory the view should be displaying...
402 viewed
= self
.view
.view_dir
.file
404 # Switch if necessary...
405 if self
.result
.abs_path
.get_uri() != viewed
.get_uri():
406 self
.view
.set_view_dir(self
.result
.abs_path
)
408 to_select
= self
.result
.get_selected_files()
413 for path
in to_select
:
415 if cursor_path
not in to_select
:
416 iv
.set_cursor(to_select
[0])
418 def tab(self
, entry
):
419 self
.result
.tab(entry
)
421 def get_default_command(self
):
422 return self
.result
.get_default_command()
424 def updown(self
, entry
, delta
):
425 self
.result
.updown(entry
, delta
)
427 def finish_edit(self
):
428 self
.result
.finish_edit()
430 def get_entry_text(self
):
431 return self
.result
.get_entry_text()
433 def get_button_label(self
):
434 return self
.result
.get_button_label()
436 def expand_to_argv(self
):
437 return self
.result
.expand_to_argv()
439 class CommandArgument(BaseArgument
):
441 default_command
= None
443 def entry_changed(self
, entry
):
444 self
.command
= entry
.get_text() or None
446 def get_button_label(self
):
447 return self
.command
if self
.command
else \
448 self
.default_command
if self
.default_command
else \
451 def expand_to_argv(self
):
452 return [self
.command
or self
.default_command
]
454 def set_default_command_from_args(self
, args
):
455 self
.default_command
= None
460 self
.default_command
= args
[0].get_default_command()
463 def __init__(self
, hbox
):
468 def set_args(self
, args
):
470 self
.edit_arg
= self
.args
[-1]
474 for w
in self
.widgets
:
477 self
.active_entry
= None
479 cmd_arg
= self
.args
[0]
482 if x
is self
.edit_arg
:
484 arg
.set_text(x
.get_entry_text())
485 def entry_changed(entry
, x
= x
):
486 x
.entry_changed(entry
)
487 if x
is not cmd_arg
and not cmd_arg
.command
:
488 cmd_arg
.set_default_command_from_args(self
.args
[1:])
489 self
.widgets
[0].set_label(cmd_arg
.get_button_label())
490 arg
.connect('changed', entry_changed
)
491 self
.active_entry
= arg
493 arg
= gtk
.Button(x
.get_button_label())
494 arg
.set_relief(gtk
.RELIEF_NONE
)
495 arg
.connect('clicked', lambda b
, x
= x
: self
.activate(x
))
497 self
.hbox
.pack_start(arg
, False, True, 0)
498 self
.widgets
.append(arg
)
500 def activate(self
, x
):
501 """Start editing argument 'x'"""
502 if x
is self
.edit_arg
:
505 self
.edit_arg
.finish_edit()
507 self
.edit_arg
.view
.warning(str(ex
))
510 i
= self
.args
.index(x
)
511 self
.widgets
[i
].grab_focus()
514 if not self
.active_entry
.flags() & gtk
.HAS_FOCUS
:
515 return False # Not focussed
516 if self
.active_entry
.get_position() != len(self
.active_entry
.get_text()):
517 return False # Not at end
518 i
= self
.args
.index(self
.edit_arg
)
520 self
.edit_arg
.finish_edit()
522 self
.edit_arg
.view
.warning(str(ex
))
523 self
.edit_arg
= Argument(self
.edit_arg
.view
)
524 self
.args
.insert(i
+ 1, self
.edit_arg
)
526 self
.widgets
[i
+ 1].grab_focus()
530 if self
.active_entry
.get_position() == len(self
.active_entry
.get_text()):
531 self
.edit_arg
.tab(self
.active_entry
)
534 def updown(self
, delta
):
535 if self
.active_entry
and self
.active_entry
.flags() & gtk
.HAS_FOCUS
:
536 self
.edit_arg
.updown(self
.active_entry
, delta
)
541 def key_press_event(self
, kev
):
542 if not self
.active_entry
:
544 old_text
= self
.active_entry
.get_text()
545 self
.active_entry
.grab_focus() # Otherwise it selects the added text
546 self
.active_entry
.event(kev
)
547 return self
.active_entry
.get_text() != old_text
549 def finish_edit(self
):
550 self
.edit_arg
.finish_edit()