2 @copyright: (C) 2008, Thomas Leonard
3 @see: U{http://roscidus.com}
5 import os
, sys
, fnmatch
11 from directory
import DirModel
14 def __init__(self
, view
):
17 def get_entry_text(self
):
20 def get_button_label(self
):
23 def entry_changed(self
, entry
):
26 def finish_edit(self
):
35 def updown(self
, entry
, delta
):
38 class Argument(BaseArgument
):
39 """Represents a word entered by the user for a command."""
40 # This is a bit complicated. An argument can be any of these:
41 # - A glob pattern matching multiple filenames (*.html)
42 # - A single filename (index.html)
43 # - A set of files selected manually (a.html, b.html)
44 # - A quoted string ('*.html')
45 # - An option (--index)
46 def __init__(self
, view
):
47 BaseArgument
.__init
__(self
, view
)
51 def type_from_value(self
, value
):
54 elif value
.startswith("!"):
56 elif value
.startswith("'") or value
.startswith('"'):
58 elif value
.startswith("-"):
65 def iter_matches(self
, match
, case_insensitive
):
66 """Return all rows with a name matching match"""
67 for i
, row
in self
.view
.iter_contents():
71 if name
.startswith(match
):
74 def entry_changed(self
, entry
):
75 self
.value
= entry
.get_text()
76 self
.type = self
.type_from_value(self
.value
)
78 if self
.type not in ('filename', 'glob', 'newfile'):
81 if self
.type == 'newfile':
82 value
= self
.value
[1:]
86 # Check which directory the view should be displaying...
87 viewed
= self
.view
.view_dir
.file
89 path
, leaf
= support
.split_expanded_path(value
)
91 abs_path
= self
.view
.cwd
.file.resolve_relative_path(path
)
93 abs_path
= self
.view
.cwd
.file
95 # Switch if necessary...
96 if abs_path
.get_uri() != viewed
.get_uri():
97 self
.view
.set_view_dir(abs_path
)
99 if self
.type == 'newfile':
100 return # Just switch views, no auto-complete
103 model
= iv
.get_model()
105 cursor_path
= (self
.view
.iv
.get_cursor() or (None, None))[0]
107 cursor_filename
= model
[model
.get_iter(cursor_path
)][0]
109 cursor_filename
= None
111 # If the user only entered lower-case letters do a case insensitive match
112 if self
.type == 'filename':
114 # - Select any exact match
115 # - Else, select any exact case-insensitive match
116 # - Else, select the cursor item if the prefix matches
117 # - Else, select the first prefix match
118 # - Else, select nothing
120 case_insensitive
= (leaf
== leaf
.lower())
121 exact_case_match
= None
124 for i
, row
in self
.iter_matches(leaf
, case_insensitive
):
127 exact_case_match
= model
.get_path(i
)
132 exact_match
= model
.get_path(i
)
134 prefix_match
= model
.get_path(i
)
135 if case_insensitive
and cursor_filename
:
136 cursor_filename
= cursor_filename
.lower()
138 to_select
= [exact_case_match
]
140 to_select
= [exact_match
]
141 elif cursor_filename
and cursor_filename
.startswith(leaf
):
142 to_select
= [cursor_path
]
144 to_select
= [prefix_match
]
147 elif self
.type == 'glob':
150 def match(m
, path
, iter):
152 if fnmatch
.fnmatch(name
, pattern
):
153 to_select
.append(path
)
158 for path
in to_select
:
160 if cursor_path
not in to_select
:
161 iv
.set_cursor(to_select
[0])
163 def tab(self
, entry
):
164 if self
.type == 'filename':
166 elif self
.type == 'newfile':
167 value
= self
.value
[1:]
171 path
, leaf
= os
.path
.split(value
)
172 case_insensitive
= (leaf
== leaf
.lower())
174 single_match_is_dir
= False
175 for i
, row
in self
.iter_matches(leaf
, case_insensitive
):
176 name
= row
[DirModel
.NAME
]
177 if prefix_match
is not None:
178 single_match_is_dir
= False # Multiple matches
179 if not name
.startswith(prefix_match
):
180 # Have to shorten the match then
182 for a
, b
in zip(prefix_match
, name
):
187 prefix_match
= ''.join(same
)
190 if row
[DirModel
.INFO
].get_file_type() == gio
.FILE_TYPE_DIRECTORY
:
191 single_match_is_dir
= True
192 if single_match_is_dir
:
194 if prefix_match
and prefix_match
!= leaf
:
195 new
= os
.path
.join(path
, prefix_match
)
196 if self
.type == 'newfile':
199 entry
.set_position(len(new
))
201 def updown(self
, entry
, delta
):
202 if self
.type == 'newfile':
203 leaf
= self
.value
[1:]
204 elif self
.type == 'filename':
209 leaf
= os
.path
.basename(leaf
)
211 model
= self
.view
.iv
.get_model()
212 cursor_path
= self
.view
.get_cursor_path()
215 return model
.iter_next(i
) or model
.get_iter_first()
218 n
, = model
.get_path(i
)
220 n
= model
.iter_n_children(None)
221 return model
.get_iter((n
- 1,))
223 start
= model
.get_iter(cursor_path
)
224 i
= apply_delta(start
)
226 start
= i
= model
.get_iter_root()
228 return # Empty directory
229 start
= model
[start
][DirModel
.NAME
]
231 case_insensitive
= (leaf
== leaf
.lower())
234 name
= model
[i
][DirModel
.NAME
]
239 if name
.startswith(leaf
):
240 path
= model
.get_path(i
)
241 self
.view
.iv
.unselect_all()
242 self
.view
.iv
.set_cursor(path
)
243 self
.view
.iv
.select_path(path
)
247 def finish_edit(self
):
249 model
= iv
.get_model()
250 cursor_path
= (iv
.get_cursor() or (None, None))[0]
251 selected
= iv
.get_selected_items()
253 if cursor_path
and selected
and cursor_path
not in selected
:
254 raise Warning("Cursor not in selection!")
255 if cursor_path
and not selected
:
256 selected
= [cursor_path
]
258 if self
.type == 'empty':
260 raise Warning("No selection and no cursor item!")
261 if len(selected
) > 1:
262 raise Warning("Multiple selection!")
265 self
.type = 'filename'
266 self
.selected_item
= model
[model
.get_iter(path
)][0]
267 if self
.type == 'filename':
268 self
.selected_filename
= None
270 path
, leaf
= support
.split_expanded_path(self
.value
)
272 if len(selected
) != 1:
273 raise Warning("Must be one selected item!")
274 # The cursor must be on the single selected item
275 selected_name
= model
[model
.get_iter(selected
[0])][0]
276 # Selected item must match text
277 if selected_name
.lower().startswith(leaf
.lower()):
278 abs_path
= self
.view
.view_dir
.file.resolve_relative_path(selected_name
)
279 # NB: selected item may be above cwd
280 self
.selected_filename
= self
.view
.cwd
.file.get_relative_path(abs_path
) or abs_path
.get_path()
282 raise Warning("Selected item does not match entered text!")
284 def get_entry_text(self
):
287 def get_button_label(self
):
288 if self
.type == 'filename':
289 return self
.selected_filename
or '(none)'
292 def expand_to_argv(self
):
293 if self
.type == 'empty':
294 raise Warning("Empty argument")
295 if self
.type == 'filename':
296 if self
.selected_filename
is None:
297 raise Warning("No filename selected")
298 return [self
.selected_filename
]
299 elif self
.type == 'glob':
301 matches
= [row
[0] for i
, row
in self
.view
.iter_contents() if fnmatch
.fnmatch(row
[0], pattern
)]
303 raise Warning("Nothing matches '%s'!" % pattern
)
305 elif self
.type == 'newfile':
306 value
= self
.value
[1:]
307 path
, leaf
= os
.path
.split(value
)
309 raise Warning("No name given for new file!")
311 final_dir
= self
.view
.cwd
.file.resolve_relative_path(path
)
312 unix_path
= final_dir
.get_path()
313 if not os
.path
.exists(unix_path
):
314 os
.makedirs(unix_path
)
316 elif self
.type == 'quoted':
317 first
= self
.value
[0]
318 if self
.value
.endswith(first
):
319 return [self
.value
[1:-1]]
321 return [self
.value
[1:]]
322 elif self
.type == 'option':
325 assert False, "Unknown type " + self
.type
327 class CommandArgument(BaseArgument
):
330 def entry_changed(self
, entry
):
331 self
.command
= entry
.get_text() or None
333 def get_button_label(self
):
334 return self
.command
if self
.command
else "Open"
336 def expand_to_argv(self
):
337 return [self
.command
]
340 def __init__(self
, hbox
):
345 def set_args(self
, args
):
347 self
.edit_arg
= self
.args
[-1]
351 for w
in self
.widgets
:
354 self
.active_entry
= None
357 if x
is self
.edit_arg
:
359 arg
.set_text(x
.get_entry_text())
360 arg
.connect('changed', x
.entry_changed
)
361 self
.active_entry
= arg
363 arg
= gtk
.Button(x
.get_button_label())
364 arg
.set_relief(gtk
.RELIEF_NONE
)
365 arg
.connect('clicked', lambda b
, x
= x
: self
.activate(x
))
367 self
.hbox
.pack_start(arg
, False, True, 0)
368 self
.widgets
.append(arg
)
370 def activate(self
, x
):
371 """Start editing argument 'x'"""
372 if x
is self
.edit_arg
:
375 self
.edit_arg
.finish_edit()
377 self
.edit_arg
.view
.warning(str(ex
))
380 i
= self
.args
.index(x
)
381 self
.widgets
[i
].grab_focus()
384 if not self
.active_entry
.flags() & gtk
.HAS_FOCUS
:
385 return False # Not focussed
386 if self
.active_entry
.get_position() != len(self
.active_entry
.get_text()):
387 return False # Not at end
388 i
= self
.args
.index(self
.edit_arg
)
390 self
.edit_arg
.finish_edit()
392 self
.edit_arg
.view
.warning(str(ex
))
393 self
.edit_arg
= Argument(self
.edit_arg
.view
)
394 self
.args
.insert(i
+ 1, self
.edit_arg
)
396 self
.widgets
[i
+ 1].grab_focus()
400 if self
.active_entry
.get_position() == len(self
.active_entry
.get_text()):
401 self
.edit_arg
.tab(self
.active_entry
)
404 def updown(self
, delta
):
405 if self
.active_entry
and self
.active_entry
.flags() & gtk
.HAS_FOCUS
:
406 self
.edit_arg
.updown(self
.active_entry
, delta
)
411 def key_press_event(self
, kev
):
412 if not self
.active_entry
:
414 old_text
= self
.active_entry
.get_text()
415 self
.active_entry
.grab_focus() # Otherwise it selects the added text
416 self
.active_entry
.event(kev
)
417 return self
.active_entry
.get_text() != old_text
419 def finish_edit(self
):
420 self
.edit_arg
.finish_edit()