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
18 gtk_theme
= gtk
.icon_theme_get_default()
20 class Warning(Exception):
23 def get_themed_icon(name
, icon_size
):
25 return gtk_theme
.load_icon('text-x-generic', icon_size
, 0)
26 except gobject
.GError
:
29 icon_text_plain
= get_themed_icon('text-x-generic', icon_size
)
30 icon_dir
= get_themed_icon('folder', icon_size
)
32 RETURN_KEYS
= (keysyms
.Return
, keysyms
.KP_Enter
, keysyms
.ISO_Enter
)
34 class DirUpdated(tasks
.Blocker
):
37 class ShellController
:
38 def __init__(self
, file):
43 def build_contents(self
):
45 e
= self
.file.enumerate_children('standard::*', 0)
51 contents
[info
.get_name()] = info
52 self
.contents
= contents
55 self
.updated
.trigger()
56 self
.updated
= DirUpdated("Updates for " + self
.file.get_uri())
58 def item_activated(self
, name
):
59 item_info
= self
.contents
[name
]
60 child
= self
.file.get_child(name
)
62 if item_info
.get_file_type() == gio
.FILE_TYPE_DIRECTORY
:
66 self
.view
.run_in_terminal(['cat', child
.get_path()], self
.file.get_path())
69 parent
= self
.file.get_parent()
74 def run_command(self
, args
):
77 argv
+= a
.expand_to_argv()
79 builtin
= commands
.builtin_commands
.get(argv
[0], None)
81 builtin(self
, argv
[1:])
83 self
.view
.run_in_terminal(argv
, self
.file.get_path())
85 def gtk_tree_model(contents
):
86 tm
= gtk
.ListStore(str, gtk
.gdk
.Pixbuf
)
87 tm
.set_sort_column_id(0, gtk
.SORT_ASCENDING
)
88 for name
, info
in contents
.iteritems():
89 if not name
.startswith('.'):
92 icon
= info
.get_icon()
94 gtkicon_info
= gtk_theme
.choose_icon(icon
.get_names(), icon_size
, 0)
96 gtkicon_info
.get_filename()
97 pixbuf
= gtkicon_info
.load_icon()
99 tm
[new
] = [name
, pixbuf
]
106 def __init__(self
, view
):
109 def get_entry_text(self
):
112 def get_button_label(self
):
115 def entry_changed(self
, entry
):
118 def finish_edit(self
):
124 class Argument(BaseArgument
):
125 """Represents a word entered by the user for a command."""
126 # This is a bit complicated. An argument can be any of these:
127 # - A glob pattern matching multiple filenames (*.html)
128 # - A single filename (index.html)
129 # - A set of files selected manually (a.html, b.html)
130 # - A quoted string ('*.html')
131 # - An option (--index)
132 def __init__(self
, view
):
133 BaseArgument
.__init
__(self
, view
)
137 def type_from_value(self
, value
):
140 elif value
.startswith("'"):
142 elif value
.startswith("-"):
149 def entry_changed(self
, entry
):
150 self
.value
= entry
.get_text()
151 self
.type = self
.type_from_value(self
.value
)
153 if self
.type not in ('filename', 'glob'):
157 model
= iv
.get_model()
159 cursor_path
= (self
.view
.iv
.get_cursor() or (None, None))[0]
161 cursor_filename
= model
[model
.get_iter(cursor_path
)][0]
163 cursor_filename
= None
165 # If the user only entered lower-case letters do a case insensitive match
166 if self
.type == 'filename':
168 # - Select any exact match
169 # - Else, select any exact case-insensitive match
170 # - Else, select the cursor item if the prefix matches
171 # - Else, select the first prefix match
172 # - Else, select nothing
174 if self
.value
in self
.view
.shell
.contents
:
175 case_insensitive
= False # Exact match available
177 case_insensitive
= (self
.value
== self
.value
.lower())
180 for i
, row
in self
.view
.iter_contents():
184 if name
== self
.value
:
185 exact_match
= model
.get_path(i
)
187 if name
.startswith(self
.value
) and not prefix_match
:
188 prefix_match
= model
.get_path(i
)
190 cursor_filename
= cursor_filename
.lower()
192 to_select
= [exact_match
]
193 elif cursor_filename
.startswith(self
.value
):
194 to_select
= [cursor_path
]
196 to_select
= [prefix_match
]
199 elif self
.type == 'glob':
202 def match(m
, path
, iter):
204 if fnmatch
.fnmatch(name
, pattern
):
205 to_select
.append(path
)
210 for path
in to_select
:
212 if cursor_path
not in to_select
:
213 iv
.set_cursor(to_select
[0])
215 def finish_edit(self
):
217 model
= iv
.get_model()
218 cursor_path
= (iv
.get_cursor() or (None, None))[0]
219 selected
= iv
.get_selected_items()
221 if cursor_path
and selected
and cursor_path
not in selected
:
222 raise Warning("Cursor not in selection!")
223 if cursor_path
and not selected
:
224 selected
= [cursor_path
]
226 if self
.type == 'empty':
228 raise Warning("No selection and no cursor item!")
229 if len(selected
) > 1:
230 raise Warning("Multiple selection!")
233 self
.type = 'filename'
234 self
.selected_item
= model
[model
.get_iter(path
)][0]
235 if self
.type == 'filename':
236 self
.selected_filename
= None
238 if len(selected
) != 1:
239 raise Warning("Must be one selected item!")
240 # The cursor must be on the single selected item
241 selected_name
= model
[model
.get_iter(selected
[0])][0]
242 # Selected item must match text
243 if selected_name
.lower().startswith(self
.value
):
244 self
.selected_filename
= selected_name
246 raise Warning("Selected item does not match entered text!")
248 def get_entry_text(self
):
251 def get_button_label(self
):
252 if self
.type == 'filename':
253 return self
.selected_filename
or '(none)'
256 def expand_to_argv(self
):
257 if self
.type == 'empty':
258 raise Warning("Empty argument")
259 if self
.type == 'filename':
260 if self
.selected_filename
is None:
261 raise Warning("No filename selected")
262 return [self
.selected_filename
]
263 elif self
.type == 'glob':
265 matches
= [row
[0] for i
, row
in self
.view
.iter_contents() if fnmatch
.fnmatch(row
[0], pattern
)]
267 raise Warning("Nothing matches '%s'!" % pattern
)
273 class CommandArgument(BaseArgument
):
276 def entry_changed(self
, entry
):
277 self
.command
= entry
.get_text() or None
279 def get_button_label(self
):
280 return self
.command
if self
.command
else "Open"
282 def expand_to_argv(self
):
283 return [self
.command
or 'rox:open']
286 def __init__(self
, hbox
):
291 def set_args(self
, args
):
293 self
.edit_arg
= self
.args
[-1]
297 for w
in self
.widgets
:
300 self
.active_entry
= None
303 if x
is self
.edit_arg
:
305 arg
.set_text(x
.get_entry_text())
306 arg
.connect('changed', x
.entry_changed
)
307 self
.active_entry
= arg
309 arg
= gtk
.Button(x
.get_button_label())
310 arg
.set_relief(gtk
.RELIEF_NONE
)
311 arg
.connect('clicked', lambda b
, x
= x
: self
.activate(x
))
313 self
.hbox
.pack_start(arg
, False, True, 0)
314 self
.widgets
.append(arg
)
316 def activate(self
, x
):
317 """Start editing argument 'x'"""
318 if x
is self
.edit_arg
:
321 self
.edit_arg
.finish_edit()
323 self
.edit_arg
.view
.warning(str(ex
))
326 i
= self
.args
.index(x
)
327 self
.widgets
[i
].grab_focus()
329 def key_press_event(self
, kev
):
330 if not self
.active_entry
:
332 old_text
= self
.active_entry
.get_text()
333 self
.active_entry
.grab_focus() # Otherwise it selects the added text
334 self
.active_entry
.event(kev
)
335 return self
.active_entry
.get_text() != old_text
337 def finish_edit(self
):
338 self
.edit_arg
.finish_edit()
342 user_seen_terminal_contents
= False
343 warning_timeout
= None
345 def __init__(self
, shell
):
349 builder
= gtk
.Builder()
350 builder
.add_from_file(os
.path
.join(os
.path
.dirname(__file__
), "ui.xml"))
351 self
.window
= builder
.get_object('directory')
353 cd_parent
= builder
.get_object('cd-parent')
354 cd_parent
.connect('activate', lambda a
: self
.shell
.cd_parent())
356 # Must show window before adding icons, or we randomly get:
357 # The error was 'BadAlloc (insufficient resources for operation)'
360 self
.window_destroyed
= tasks
.Blocker('Window destroyed')
361 self
.window
.connect('destroy', lambda w
: self
.window_destroyed
.trigger())
363 self
.window
.connect('key-press-event', self
.key_press_event
)
365 self
.iv
= builder
.get_object('iconview')
366 self
.iv
.set_text_column(0)
367 self
.iv
.set_pixbuf_column(1)
368 self
.iv
.set_selection_mode(gtk
.SELECTION_MULTIPLE
)
370 self
.iv
.connect('item-activated', self
.item_activated
)
372 command_area
= builder
.get_object('command')
373 self
.command_argv
= ArgvView(command_area
)
376 self
.notebook
= builder
.get_object('notebook')
377 self
.status_msg
= builder
.get_object('status_msg')
379 self
.window
.show_all()
381 def iter_contents(self
):
382 m
= self
.iv
.get_model()
383 i
= m
.get_iter_root()
388 def warning(self
, msg
):
390 self
.status_msg
.set_text('')
392 if self
.warning_timeout
is not None:
393 gobject
.source_remove(self
.warning_timeout
)
394 self
.status_msg
.set_text(msg
)
395 self
.warning_timeout
= gobject
.timeout_add(2000, hide_warning
)
397 def show_terminal(self
):
398 if not self
.terminal
:
399 def terminal_contents_changed(vte
):
400 self
.user_seen_terminal_contents
= False
402 def terminal_child_exited():
403 if self
.user_seen_terminal_contents
:
404 self
.notebook
.set_current_page(FILER_PAGE
)
406 self
.terminal
.feed('\r\nProcess complete. Press Return to return to filer view.\r\n')
407 self
.waiting_for_return
= True
410 self
.terminal
= vte
.Terminal()
411 self
.terminal
.connect('contents-changed', terminal_contents_changed
)
412 self
.terminal
.connect('child-exited', lambda vte
: gobject
.timeout_add(100, terminal_child_exited
))
415 self
.notebook
.add(self
.terminal
)
416 self
.notebook
.set_current_page(TERMINAL_PAGE
)
417 self
.waiting_for_return
= False
420 self
.command_argv
.set_args([CommandArgument(self
), Argument(self
)])
421 if self
.terminal
is None or not (self
.terminal
.flags() & gtk
.IS_VISIBLE
):
423 self
.iv
.unselect_all()
425 def key_press_event(self
, window
, kev
):
426 if self
.terminal
and self
.terminal
.flags() & gtk
.HAS_FOCUS
:
427 if kev
.keyval
in RETURN_KEYS
and self
.waiting_for_return
:
428 self
.notebook
.set_current_page(FILER_PAGE
)
430 self
.user_seen_terminal_contents
= True
433 if kev
.keyval
== keysyms
.Escape
:
436 elif kev
.keyval
in RETURN_KEYS
:
438 self
.command_argv
.finish_edit()
439 self
.shell
.run_command(self
.command_argv
.args
)
441 self
.warning(str(ex
))
446 # Are we ready for special characters?
447 if self
.command_argv
.active_entry
and self
.command_argv
.active_entry
.flags() & gtk
.HAS_FOCUS
:
448 accept_special
= True # TODO: check cursor is at end
450 accept_special
= True
453 if kev
.keyval
== keysyms
.comma
:
454 self
.command_argv
.activate(self
.command_argv
.args
[0])
456 elif kev
.keyval
== keysyms
.semicolon
and len(self
.command_argv
.args
) == 2:
457 self
.command_argv
.set_args([CommandArgument(self
)])
458 self
.command_argv
.widgets
[0].grab_focus()
461 if self
.iv
.flags() & gtk
.HAS_FOCUS
:
462 if self
.iv
.event(kev
):
463 # Handled by IconView (e.g. cursor motion)
465 elif kev
.keyval
== keysyms
.BackSpace
:
466 self
.shell
.cd_parent()
468 if not self
.command_argv
.key_press_event(kev
):
469 self
.iv
.grab_focus() # Restore focus to IconView
472 def run_in_terminal(self
, argv
, cwd
):
474 self
.user_seen_terminal_contents
= True
475 self
.terminal
.fork_command(argv
[0], argv
, None, cwd
, False, False, False)
481 tree_model
= gtk_tree_model(self
.shell
.contents
)
482 self
.iv
.set_model(tree_model
)
483 if self
.shell
.contents
:
484 self
.iv
.set_cursor((0,))
485 self
.window
.set_title(self
.shell
.file.get_uri())
486 blockers
= [self
.shell
.updated
, self
.window_destroyed
]
488 tasks
.check(blockers
)
489 if self
.window_destroyed
.happened
:
492 def item_activated(self
, iv
, path
):
497 name
= tm
[tm
.get_iter(path
)][0]
498 self
.shell
.item_activated(name
)