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
):
51 def parse_value(self
, value
):
52 if value
.startswith('-'):
57 def expand_to_argv(self
):
61 def parse_value(self
, value
):
63 if first
and first
in '"\'':
64 if value
[-1] == first
:
71 def expand_to_argv(self
):
74 class SelectedFiles(Value
):
75 abs_path
= None # GFile for the directory containing the files
76 to_select
= [] # Paths in the IconView's model to be selected
78 def __init__(self
, arg
, value
):
79 Value
.__init
__(self
, arg
, value
)
80 path
, leaf
= support
.split_expanded_path(value
)
82 self
.abs_path
= arg
.view
.cwd
.file.resolve_relative_path(path
)
84 self
.abs_path
= arg
.view
.cwd
.file
86 self
.to_select
= self
.get_selected_files(path
, leaf
)
91 path
, leaf
= os
.path
.split(value
)
92 case_insensitive
= (leaf
== leaf
.lower())
94 single_match_is_dir
= False
95 for i
, row
in self
.arg
.iter_matches(leaf
, case_insensitive
):
96 name
= row
[DirModel
.NAME
]
97 if prefix_match
is not None:
98 single_match_is_dir
= False # Multiple matches
99 if not name
.startswith(prefix_match
):
100 # Have to shorten the match then
102 for a
, b
in zip(prefix_match
, name
):
107 prefix_match
= ''.join(same
)
110 if row
[DirModel
.INFO
].get_file_type() == gio
.FILE_TYPE_DIRECTORY
:
111 single_match_is_dir
= True
112 if single_match_is_dir
:
114 if prefix_match
and prefix_match
!= leaf
:
115 self
.parsed
= os
.path
.join(path
, prefix_match
)
116 new
= self
.get_entry_text()
118 entry
.set_position(len(new
))
120 class Filename(SelectedFiles
):
121 selected_filename
= None
124 def parse_value(self
, value
):
127 def get_selected_files(self
, path
, leaf
):
128 iv
= self
.arg
.view
.iv
129 model
= iv
.get_model()
131 self
.selected_filename
= None
132 self
.selected_type
= None
135 return [] # Empty entry
138 self
.selected_filename
= ''
139 self
.selected_type
= gio
.FILE_TYPE_DIRECTORY
141 elif leaf
in ('.', '..'):
142 self
.selected_filename
= leaf
143 self
.selected_type
= gio
.FILE_TYPE_DIRECTORY
146 cursor_path
= (self
.arg
.view
.iv
.get_cursor() or (None, None))[0]
148 cursor_filename
= model
[model
.get_iter(cursor_path
)][0]
150 cursor_filename
= None
153 # - Select any exact match
154 # - Else, select any exact case-insensitive match
155 # - Else, select the cursor item if the prefix matches
156 # - Else, select the first prefix match
157 # - Else, select nothing
159 # If the user only entered lower-case letters do a case insensitive match
160 model
= self
.arg
.view
.iv
.get_model()
162 case_insensitive
= (leaf
== leaf
.lower())
163 exact_case_match
= None
166 for i
, row
in self
.arg
.iter_matches(leaf
, case_insensitive
):
169 exact_case_match
= model
.get_path(i
)
174 exact_match
= model
.get_path(i
)
176 prefix_match
= model
.get_path(i
)
177 if case_insensitive
and cursor_filename
:
178 cursor_filename
= cursor_filename
.lower()
180 to_select
= exact_case_match
182 to_select
= exact_match
183 elif cursor_filename
and cursor_filename
.startswith(leaf
):
184 to_select
= cursor_path
186 to_select
= prefix_match
190 if to_select
is None:
193 row
= model
[model
.get_iter(to_select
)]
194 self
.selected_filename
= row
[DirModel
.NAME
]
195 self
.selected_type
= row
[DirModel
.INFO
].get_file_type()
199 def get_entry_text(self
):
202 def get_button_label(self
):
203 return self
.selected_filename
or '(none)'
205 def expand_to_argv(self
):
207 raise Warning("Empty argument")
208 if self
.selected_filename
is None:
209 raise Warning("No filename selected")
210 abs_path
= self
.abs_path
.resolve_relative_path(self
.selected_filename
)
211 return [self
.arg
.view
.cwd
.file.get_relative_path(abs_path
) or abs_path
.get_path()]
213 def finish_edit_with_selection(self
, selected
):
215 model
= self
.arg
.view
.iv
.get_model()
217 raise Warning("No selection and no cursor item!")
218 if len(selected
) > 1:
219 raise Warning("Multiple selection!")
222 row
= model
[model
.get_iter(path
)]
223 self
.selected_filename
= row
[DirModel
.NAME
]
224 self
.selected_type
= row
[DirModel
.INFO
].get_file_type()
225 self
.value
= self
.parsed
= self
.selected_filename
227 def get_default_command(self
):
228 if self
.selected_filename
is None:
230 if self
.selected_type
== gio
.FILE_TYPE_DIRECTORY
:
235 class Newfile(SelectedFiles
):
236 def parse_value(self
, value
):
237 if value
.startswith('!'):
241 def get_selected_files(self
, path
, leaf
):
242 # No completion for new files
243 # TODO: highlight if it exists
246 def expand_to_argv(self
):
248 path
, leaf
= os
.path
.split(value
)
250 raise Warning("No name given for new file!")
252 final_dir
= self
.arg
.view
.cwd
.file.resolve_relative_path(path
)
253 unix_path
= final_dir
.get_path()
254 if not os
.path
.exists(unix_path
):
255 os
.makedirs(unix_path
)
258 def get_default_command(self
):
261 class Glob(SelectedFiles
):
262 def parse_value(self
, value
):
268 def get_selected_files(self
, path
, leaf
):
271 def match(m
, path
, iter):
273 if fnmatch
.fnmatch(name
, pattern
):
274 to_select
.append(path
)
275 self
.arg
.view
.iv
.get_model().foreach(match
)
278 def expand_to_argv(self
):
279 pattern
= self
.parsed
280 matches
= [row
[0] for i
, row
in self
.arg
.view
.iter_contents() if fnmatch
.fnmatch(row
[0], pattern
)]
282 raise Warning("Nothing matches '%s'!" % pattern
)
285 def tab(self
, entry
):
289 def __init__(self
, view
):
292 def get_entry_text(self
):
295 def get_button_label(self
):
298 def entry_changed(self
, entry
):
301 def finish_edit(self
):
307 def tab(self
, entry
):
310 def updown(self
, entry
, delta
):
313 class Argument(BaseArgument
):
314 """Represents a word entered by the user for a command."""
315 # This is a bit complicated. An argument can be any of these:
316 # - A glob pattern matching multiple filenames (*.html)
317 # - A single filename (index.html)
318 # - A set of files selected manually (a.html, b.html)
319 # - A quoted string ('*.html')
320 # - An option (--index)
321 def __init__(self
, view
):
322 BaseArgument
.__init
__(self
, view
)
323 self
.result
= self
.parse_value('')
325 def parse_value(self
, value
):
326 for t
in [Newfile
, Quote
, Option
, Glob
, Filename
]:
328 return t(self
, value
)
333 def iter_matches(self
, match
, case_insensitive
):
334 """Return all rows with a name matching match"""
335 for i
, row
in self
.view
.iter_contents():
339 if name
.startswith(match
):
342 def entry_changed(self
, entry
):
343 self
.result
= self
.parse_value(entry
.get_text())
345 if not isinstance(self
.result
, SelectedFiles
):
346 self
.view
.iv
.unselect_all()
349 cursor_path
= (self
.view
.iv
.get_cursor() or (None, None))[0]
351 # Check which directory the view should be displaying...
352 viewed
= self
.view
.view_dir
.file
354 # Switch if necessary...
355 if self
.result
.abs_path
.get_uri() != viewed
.get_uri():
356 self
.view
.set_view_dir(self
.result
.abs_path
)
360 if self
.result
.to_select
:
361 for path
in self
.result
.to_select
:
363 if cursor_path
not in self
.result
.to_select
:
364 iv
.set_cursor(self
.result
.to_select
[0])
366 def tab(self
, entry
):
367 self
.result
.tab(entry
)
369 def get_default_command(self
):
370 return self
.result
.get_default_command()
372 def updown(self
, entry
, delta
):
373 if self
.type == 'newfile':
374 leaf
= self
.value
[1:]
375 elif self
.type == 'filename':
380 leaf
= os
.path
.basename(leaf
)
382 model
= self
.view
.iv
.get_model()
383 cursor_path
= self
.view
.get_cursor_path()
386 return model
.iter_next(i
) or model
.get_iter_first()
389 n
, = model
.get_path(i
)
391 n
= model
.iter_n_children(None)
392 return model
.get_iter((n
- 1,))
394 start
= model
.get_iter(cursor_path
)
395 i
= apply_delta(start
)
397 start
= i
= model
.get_iter_root()
399 return # Empty directory
400 start
= model
[start
][DirModel
.NAME
]
402 case_insensitive
= (leaf
== leaf
.lower())
405 name
= model
[i
][DirModel
.NAME
]
410 if name
.startswith(leaf
):
411 path
= model
.get_path(i
)
412 self
.view
.iv
.unselect_all()
413 self
.view
.iv
.set_cursor(path
)
414 self
.view
.iv
.select_path(path
)
418 def finish_edit(self
):
419 self
.result
.finish_edit()
421 def get_entry_text(self
):
422 return self
.result
.get_entry_text()
424 def get_button_label(self
):
425 return self
.result
.get_button_label()
427 def expand_to_argv(self
):
428 return self
.result
.expand_to_argv()
430 class CommandArgument(BaseArgument
):
432 default_command
= None
434 def entry_changed(self
, entry
):
435 self
.command
= entry
.get_text() or None
437 def get_button_label(self
):
438 return self
.command
if self
.command
else \
439 self
.default_command
if self
.default_command
else \
442 def expand_to_argv(self
):
443 return [self
.command
or self
.default_command
]
445 def set_default_command_from_args(self
, args
):
446 self
.default_command
= None
451 self
.default_command
= args
[0].get_default_command()
454 def __init__(self
, hbox
):
459 def set_args(self
, args
):
461 self
.edit_arg
= self
.args
[-1]
465 for w
in self
.widgets
:
468 self
.active_entry
= None
470 cmd_arg
= self
.args
[0]
473 if x
is self
.edit_arg
:
475 arg
.set_text(x
.get_entry_text())
476 def entry_changed(entry
, x
= x
):
477 x
.entry_changed(entry
)
478 if x
is not cmd_arg
and not cmd_arg
.command
:
479 cmd_arg
.set_default_command_from_args(self
.args
[1:])
480 self
.widgets
[0].set_label(cmd_arg
.get_button_label())
481 arg
.connect('changed', entry_changed
)
482 self
.active_entry
= arg
484 arg
= gtk
.Button(x
.get_button_label())
485 arg
.set_relief(gtk
.RELIEF_NONE
)
486 arg
.connect('clicked', lambda b
, x
= x
: self
.activate(x
))
488 self
.hbox
.pack_start(arg
, False, True, 0)
489 self
.widgets
.append(arg
)
491 def activate(self
, x
):
492 """Start editing argument 'x'"""
493 if x
is self
.edit_arg
:
496 self
.edit_arg
.finish_edit()
498 self
.edit_arg
.view
.warning(str(ex
))
501 i
= self
.args
.index(x
)
502 self
.widgets
[i
].grab_focus()
505 if not self
.active_entry
.flags() & gtk
.HAS_FOCUS
:
506 return False # Not focussed
507 if self
.active_entry
.get_position() != len(self
.active_entry
.get_text()):
508 return False # Not at end
509 i
= self
.args
.index(self
.edit_arg
)
511 self
.edit_arg
.finish_edit()
513 self
.edit_arg
.view
.warning(str(ex
))
514 self
.edit_arg
= Argument(self
.edit_arg
.view
)
515 self
.args
.insert(i
+ 1, self
.edit_arg
)
517 self
.widgets
[i
+ 1].grab_focus()
521 if self
.active_entry
.get_position() == len(self
.active_entry
.get_text()):
522 self
.edit_arg
.tab(self
.active_entry
)
525 def updown(self
, delta
):
526 if self
.active_entry
and self
.active_entry
.flags() & gtk
.HAS_FOCUS
:
527 self
.edit_arg
.updown(self
.active_entry
, delta
)
532 def key_press_event(self
, kev
):
533 if not self
.active_entry
:
535 old_text
= self
.active_entry
.get_text()
536 self
.active_entry
.grab_focus() # Otherwise it selects the added text
537 self
.active_entry
.event(kev
)
538 return self
.active_entry
.get_text() != old_text
540 def finish_edit(self
):
541 self
.edit_arg
.finish_edit()