2 @copyright: (C) 2008, Thomas Leonard
3 @see: U{http://roscidus.com}
5 import os
, sys
, fnmatch
6 from zeroinstall
.support
import tasks
# tmp
11 from gtk
import keysyms
17 class Warning(Exception):
20 RETURN_KEYS
= (keysyms
.Return
, keysyms
.KP_Enter
, keysyms
.ISO_Enter
)
22 class DirUpdated(tasks
.Blocker
):
29 def __init__(self
, view
):
32 def get_entry_text(self
):
35 def get_button_label(self
):
38 def entry_changed(self
, entry
):
41 def finish_edit(self
):
50 class Argument(BaseArgument
):
51 """Represents a word entered by the user for a command."""
52 # This is a bit complicated. An argument can be any of these:
53 # - A glob pattern matching multiple filenames (*.html)
54 # - A single filename (index.html)
55 # - A set of files selected manually (a.html, b.html)
56 # - A quoted string ('*.html')
57 # - An option (--index)
58 def __init__(self
, view
):
59 BaseArgument
.__init
__(self
, view
)
63 def type_from_value(self
, value
):
66 elif value
.startswith("!"):
68 elif value
.startswith("'") or value
.startswith('"'):
70 elif value
.startswith("-"):
77 def iter_matches(self
, match
, case_insensitive
):
78 """Return all rows with a name matching match"""
79 for i
, row
in self
.view
.iter_contents():
83 if name
.startswith(match
):
86 def entry_changed(self
, entry
):
87 self
.value
= entry
.get_text()
88 self
.type = self
.type_from_value(self
.value
)
90 if self
.type not in ('filename', 'glob'):
93 # Check which directory the view should be displaying...
94 viewed
= self
.view
.view_dir
.file
95 path
, leaf
= os
.path
.split(self
.value
)
97 abs_path
= self
.view
.cwd
.file.resolve_relative_path(path
)
99 abs_path
= self
.view
.cwd
.file
101 # Switch if necessary...
102 if abs_path
.get_uri() != viewed
.get_uri():
103 self
.view
.set_view_dir(abs_path
)
106 model
= iv
.get_model()
108 cursor_path
= (self
.view
.iv
.get_cursor() or (None, None))[0]
110 cursor_filename
= model
[model
.get_iter(cursor_path
)][0]
112 cursor_filename
= None
114 # If the user only entered lower-case letters do a case insensitive match
115 if self
.type == 'filename':
117 # - Select any exact match
118 # - Else, select any exact case-insensitive match
119 # - Else, select the cursor item if the prefix matches
120 # - Else, select the first prefix match
121 # - Else, select nothing
123 case_insensitive
= (leaf
== leaf
.lower())
124 exact_case_match
= None
127 for i
, row
in self
.iter_matches(leaf
, case_insensitive
):
130 exact_case_match
= model
.get_path(i
)
135 exact_match
= model
.get_path(i
)
137 prefix_match
= model
.get_path(i
)
138 if case_insensitive
and cursor_filename
:
139 cursor_filename
= cursor_filename
.lower()
141 to_select
= [exact_case_match
]
143 to_select
= [exact_match
]
144 elif cursor_filename
and cursor_filename
.startswith(leaf
):
145 to_select
= [cursor_path
]
147 to_select
= [prefix_match
]
150 elif self
.type == 'glob':
153 def match(m
, path
, iter):
155 if fnmatch
.fnmatch(name
, pattern
):
156 to_select
.append(path
)
161 for path
in to_select
:
163 if cursor_path
not in to_select
:
164 iv
.set_cursor(to_select
[0])
166 def tab(self
, entry
):
167 if self
.type == 'filename':
169 elif self
.type == 'newfile':
170 value
= self
.value
[1:]
174 path
, leaf
= os
.path
.split(self
.value
)
175 case_insensitive
= (leaf
== leaf
.lower())
177 single_match_is_dir
= False
178 for i
, row
in self
.iter_matches(leaf
, case_insensitive
):
179 name
= row
[directory
.DirModel
.NAME
]
180 if prefix_match
is not None:
181 single_match_is_dir
= False # Multiple matches
182 if not name
.startswith(prefix_match
):
183 # Have to shorten the match then
185 for a
, b
in zip(prefix_match
, name
):
190 prefix_match
= ''.join(same
)
193 if row
[directory
.DirModel
.INFO
].get_file_type() == gio
.FILE_TYPE_DIRECTORY
:
194 single_match_is_dir
= True
195 if single_match_is_dir
:
197 if prefix_match
and prefix_match
!= leaf
:
198 new
= os
.path
.join(path
, prefix_match
)
200 entry
.set_position(len(new
))
202 def finish_edit(self
):
204 model
= iv
.get_model()
205 cursor_path
= (iv
.get_cursor() or (None, None))[0]
206 selected
= iv
.get_selected_items()
208 if cursor_path
and selected
and cursor_path
not in selected
:
209 raise Warning("Cursor not in selection!")
210 if cursor_path
and not selected
:
211 selected
= [cursor_path
]
213 if self
.type == 'empty':
215 raise Warning("No selection and no cursor item!")
216 if len(selected
) > 1:
217 raise Warning("Multiple selection!")
220 self
.type = 'filename'
221 self
.selected_item
= model
[model
.get_iter(path
)][0]
222 if self
.type == 'filename':
223 self
.selected_filename
= None
225 path
, leaf
= os
.path
.split(self
.value
)
227 if len(selected
) != 1:
228 raise Warning("Must be one selected item!")
229 # The cursor must be on the single selected item
230 selected_name
= model
[model
.get_iter(selected
[0])][0]
231 # Selected item must match text
232 if selected_name
.lower().startswith(leaf
.lower()):
233 abs_path
= self
.view
.view_dir
.file.resolve_relative_path(selected_name
)
234 # NB: selected item may be above cwd
235 self
.selected_filename
= self
.view
.cwd
.file.get_relative_path(abs_path
) or abs_path
.get_path()
237 raise Warning("Selected item does not match entered text!")
239 def get_entry_text(self
):
242 def get_button_label(self
):
243 if self
.type == 'filename':
244 return self
.selected_filename
or '(none)'
247 def expand_to_argv(self
):
248 if self
.type == 'empty':
249 raise Warning("Empty argument")
250 if self
.type == 'filename':
251 if self
.selected_filename
is None:
252 raise Warning("No filename selected")
253 return [self
.selected_filename
]
254 elif self
.type == 'glob':
256 matches
= [row
[0] for i
, row
in self
.view
.iter_contents() if fnmatch
.fnmatch(row
[0], pattern
)]
258 raise Warning("Nothing matches '%s'!" % pattern
)
260 elif self
.type == 'newfile':
261 value
= self
.value
[1:]
262 path
, leaf
= os
.path
.split(value
)
264 raise Warning("No name given for new file!")
266 final_dir
= self
.view
.cwd
.file.resolve_relative_path(path
)
267 unix_path
= final_dir
.get_path()
268 if not os
.path
.exists(unix_path
):
269 os
.makedirs(unix_path
)
271 elif self
.type == 'quoted':
272 first
= self
.value
[0]
273 if self
.value
.endswith(first
):
274 return [self
.value
[1:-1]]
276 return [self
.value
[1:]]
277 elif self
.type == 'option':
280 assert False, "Unknown type " + self
.type
282 class CommandArgument(BaseArgument
):
285 def entry_changed(self
, entry
):
286 self
.command
= entry
.get_text() or None
288 def get_button_label(self
):
289 return self
.command
if self
.command
else "Open"
291 def expand_to_argv(self
):
292 return [self
.command
or 'rox:open']
295 def __init__(self
, hbox
):
300 def set_args(self
, args
):
302 self
.edit_arg
= self
.args
[-1]
306 for w
in self
.widgets
:
309 self
.active_entry
= None
312 if x
is self
.edit_arg
:
314 arg
.set_text(x
.get_entry_text())
315 arg
.connect('changed', x
.entry_changed
)
316 self
.active_entry
= arg
318 arg
= gtk
.Button(x
.get_button_label())
319 arg
.set_relief(gtk
.RELIEF_NONE
)
320 arg
.connect('clicked', lambda b
, x
= x
: self
.activate(x
))
322 self
.hbox
.pack_start(arg
, False, True, 0)
323 self
.widgets
.append(arg
)
325 def activate(self
, x
):
326 """Start editing argument 'x'"""
327 if x
is self
.edit_arg
:
330 self
.edit_arg
.finish_edit()
332 self
.edit_arg
.view
.warning(str(ex
))
335 i
= self
.args
.index(x
)
336 self
.widgets
[i
].grab_focus()
339 if not self
.active_entry
.flags() & gtk
.HAS_FOCUS
:
340 return False # Not focussed
341 if self
.active_entry
.get_position() != len(self
.active_entry
.get_text()):
342 return False # Not at end
343 i
= self
.args
.index(self
.edit_arg
)
345 self
.edit_arg
.finish_edit()
347 self
.edit_arg
.view
.warning(str(ex
))
348 self
.edit_arg
= Argument(self
.edit_arg
.view
)
349 self
.args
.insert(i
+ 1, self
.edit_arg
)
351 self
.widgets
[i
+ 1].grab_focus()
355 if self
.active_entry
.get_position() == len(self
.active_entry
.get_text()):
356 self
.edit_arg
.tab(self
.active_entry
)
359 def key_press_event(self
, kev
):
360 if not self
.active_entry
:
362 old_text
= self
.active_entry
.get_text()
363 self
.active_entry
.grab_focus() # Otherwise it selects the added text
364 self
.active_entry
.event(kev
)
365 return self
.active_entry
.get_text() != old_text
367 def finish_edit(self
):
368 self
.edit_arg
.finish_edit()
372 user_seen_terminal_contents
= False
373 warning_timeout
= None
377 def __init__(self
, cwd_file
):
378 builder
= gtk
.Builder()
379 builder
.add_from_file(os
.path
.join(os
.path
.dirname(__file__
), "ui.xml"))
380 self
.window
= builder
.get_object('directory')
381 self
.notebook
= builder
.get_object('notebook')
383 cd_parent
= builder
.get_object('cd-parent')
384 cd_parent
.connect('activate', lambda a
: self
.cd_parent())
386 # Must show window before adding icons, or we randomly get:
387 # The error was 'BadAlloc (insufficient resources for operation)'
390 self
.window_destroyed
= tasks
.Blocker('Window destroyed')
391 self
.window
.connect('destroy', lambda w
: self
.window_destroyed
.trigger())
393 self
.window
.connect('key-press-event', self
.key_press_event
)
395 self
.iv
= builder
.get_object('iconview')
396 self
.iv
.set_text_column(0)
397 self
.iv
.set_pixbuf_column(1)
398 self
.iv
.set_selection_mode(gtk
.SELECTION_MULTIPLE
)
400 self
.iv
.connect('item-activated', self
.item_activated
)
402 command_area
= builder
.get_object('command')
403 self
.command_argv
= ArgvView(command_area
)
405 self
.status_msg
= builder
.get_object('status_msg')
407 self
.set_cwd(cwd_file
)
410 self
.window
.show_all()
412 def iter_contents(self
):
413 m
= self
.iv
.get_model()
414 i
= m
.get_iter_root()
419 def warning(self
, msg
):
421 self
.status_msg
.set_text('')
423 if self
.warning_timeout
is not None:
424 gobject
.source_remove(self
.warning_timeout
)
425 self
.status_msg
.set_text(msg
)
426 self
.warning_timeout
= gobject
.timeout_add(2000, hide_warning
)
428 def show_terminal(self
):
429 # Actually, don't show it until we get some output...
430 if not self
.terminal
:
431 def terminal_contents_changed(vte
):
432 if self
.notebook
.get_current_page() == FILER_PAGE
:
433 self
.notebook
.set_current_page(TERMINAL_PAGE
)
434 self
.user_seen_terminal_contents
= False
436 def terminal_child_exited():
437 if self
.user_seen_terminal_contents
:
438 self
.notebook
.set_current_page(FILER_PAGE
)
440 self
.terminal
.feed('\r\nProcess complete. Press Return to return to filer view.\r\n')
441 self
.waiting_for_return
= True
444 self
.terminal
= vte
.Terminal()
445 self
.terminal
.connect('contents-changed', terminal_contents_changed
)
446 self
.terminal
.connect('child-exited', lambda vte
: gobject
.timeout_add(100, terminal_child_exited
))
449 self
.notebook
.add(self
.terminal
)
450 self
.waiting_for_return
= False
454 self
.command_argv
.set_args([CommandArgument(self
), Argument(self
)])
455 if self
.notebook
.get_current_page() == FILER_PAGE
:
457 self
.iv
.unselect_all()
459 def key_press_event(self
, window
, kev
):
460 if self
.terminal
and self
.terminal
.flags() & gtk
.HAS_FOCUS
:
461 if kev
.keyval
in RETURN_KEYS
and self
.waiting_for_return
:
462 self
.notebook
.set_current_page(FILER_PAGE
)
464 self
.user_seen_terminal_contents
= True
467 if kev
.keyval
== keysyms
.space
:
468 if self
.command_argv
.space():
471 if kev
.keyval
== keysyms
.Tab
:
472 if self
.command_argv
.tab():
475 if kev
.keyval
== keysyms
.Escape
:
478 elif kev
.keyval
in RETURN_KEYS
:
480 self
.command_argv
.finish_edit()
481 self
.run_command(self
.command_argv
.args
)
483 self
.warning(str(ex
))
488 # Are we ready for special characters?
489 if self
.command_argv
.active_entry
and self
.command_argv
.active_entry
.flags() & gtk
.HAS_FOCUS
:
490 accept_special
= True # TODO: check cursor is at end
492 accept_special
= True
495 if kev
.keyval
== keysyms
.comma
:
496 self
.command_argv
.activate(self
.command_argv
.args
[0])
498 elif kev
.keyval
== keysyms
.semicolon
and len(self
.command_argv
.args
) == 2:
499 self
.command_argv
.set_args([CommandArgument(self
)])
500 self
.command_argv
.widgets
[0].grab_focus()
503 if self
.iv
.flags() & gtk
.HAS_FOCUS
:
504 if self
.iv
.event(kev
):
505 # Handled by IconView (e.g. cursor motion)
507 elif kev
.keyval
== keysyms
.BackSpace
:
510 if not self
.command_argv
.key_press_event(kev
):
511 self
.iv
.grab_focus() # Restore focus to IconView
514 def run_in_terminal(self
, argv
):
516 self
.user_seen_terminal_contents
= True
517 self
.terminal
.fork_command(argv
[0], argv
, None, self
.cwd
.file.get_path(), False, False, False)
520 """Make the IconView show the cwd."""
521 self
.set_view_dir(self
.cwd
.file)
523 def set_view_dir(self
, dir_file
):
525 self
.view_dir
.del_ref(self
)
527 self
.view_dir
= directory
.get_dir_model(dir_file
)
528 self
.view_dir
.add_ref(self
)
530 tree_model
= gtk
.TreeModelSort(self
.view_dir
.model
)
531 tree_model
.set_sort_column_id(directory
.DirModel
.SORT
, gtk
.SORT_ASCENDING
)
532 self
.iv
.set_model(tree_model
)
533 if tree_model
.get_iter_root():
534 self
.iv
.set_cursor((0,))
536 if self
.view_dir
.error
:
537 self
.warning(str(self
.view_dir
.error
))
539 def set_cwd(self
, cwd_file
):
541 self
.cwd
.del_ref(self
)
543 self
.cwd
= directory
.get_dir_model(cwd_file
)
544 self
.cwd
.add_ref(self
)
546 self
.window
.set_title(self
.cwd
.file.get_uri())
553 blockers
= [self
.window_destroyed
]
555 tasks
.check(blockers
)
556 if self
.window_destroyed
.happened
:
559 def get_iter(self
, name
):
560 for i
, row
in self
.iter_contents():
561 if row
[directory
.DirModel
.NAME
] == name
:
563 raise Exception("File '%s' not found!" % name
)
565 def item_activated(self
, iv
, path
):
570 row
= tm
[tm
.get_iter(path
)]
571 name
= row
[directory
.DirModel
.NAME
]
572 item_info
= row
[directory
.DirModel
.INFO
]
574 child
= self
.cwd
.file.get_child(name
)
575 self
.open_item(child
)
577 def open_item(self
, item_file
):
578 item_info
= item_file
.query_info('standard::*', 0)
579 if item_info
.get_file_type() == gio
.FILE_TYPE_DIRECTORY
:
580 self
.set_cwd(item_file
)
582 self
.run_in_terminal(['gvim', item_file
.get_path()])
585 parent
= self
.cwd
.file.get_parent()
589 def run_command(self
, args
):
592 argv
+= a
.expand_to_argv()
594 builtin
= commands
.builtin_commands
.get(argv
[0], None)
596 builtin(self
, argv
[1:])
598 self
.run_in_terminal(argv
)