# -*- coding: utf-8 -*- # Copyright (C) 2009 - Jesse van den Kieboom # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin St, Fifth Floor, # Boston, MA 02110-1301, USA. import os import fnmatch import xml.sax.saxutils from gi.repository import GObject, Gio, GLib, Gdk, Gtk, Pango, Pluma from virtualdirs import VirtualDirectory class Popup(Gtk.Dialog): __gtype_name__ = "QuickOpenPopup" def __init__(self, window, paths, handler): Gtk.Dialog.__init__(self, title=_('Quick Open'), parent=window, flags=Gtk.DialogFlags.DESTROY_WITH_PARENT | Gtk.DialogFlags.MODAL, buttons=(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL)) self._open_button = self.add_button(Gtk.STOCK_OPEN, Gtk.ResponseType.ACCEPT) self._handler = handler self._build_ui() self._size = (0, 0) self._dirs = [] self._cache = {} self._theme = None self._cursor = None self._shift_start = None self._busy_cursor = Gdk.Cursor(Gdk.CursorType.WATCH) accel_group = Gtk.AccelGroup() accel_group.connect(Gdk.KEY_l, Gdk.ModifierType.CONTROL_MASK, 0, self.on_focus_entry) self.add_accel_group(accel_group) unique = [] for path in paths: if not path.get_uri() in unique: self._dirs.append(path) unique.append(path.get_uri()) def get_final_size(self): return self._size def _build_ui(self): vbox = self.get_content_area() vbox.set_spacing(3) self._entry = Gtk.Entry() self._entry.connect('changed', self.on_changed) self._entry.connect('key-press-event', self.on_key_press_event) sw = Gtk.ScrolledWindow() sw.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) sw.set_shadow_type(Gtk.ShadowType.OUT) tv = Gtk.TreeView() tv.set_headers_visible(False) self._store = Gtk.ListStore(Gio.Icon, str, GObject.Object, Gio.FileType) tv.set_model(self._store) self._treeview = tv tv.connect('row-activated', self.on_row_activated) renderer = Gtk.CellRendererPixbuf() column = Gtk.TreeViewColumn() column.pack_start(renderer, False) column.add_attribute(renderer, "gicon", 0) renderer = Gtk.CellRendererText() column.pack_start(renderer, True) column.add_attribute(renderer, "markup", 1) column.set_cell_data_func(renderer, self.on_cell_data_cb, None) tv.append_column(column) sw.add(tv) selection = tv.get_selection() selection.connect('changed', self.on_selection_changed) selection.set_mode(Gtk.SelectionMode.MULTIPLE) vbox.pack_start(self._entry, False, False, 0) vbox.pack_start(sw, True, True, 0) lbl = Gtk.Label() lbl.set_alignment(0, 0.5) lbl.set_ellipsize(Pango.EllipsizeMode.MIDDLE) self._info_label = lbl vbox.pack_start(lbl, False, False, 0) # Initial selection self.on_selection_changed(tv.get_selection()) vbox.show_all() def on_cell_data_cb(self, column, cell, model, piter, user_data): path = model.get_path(piter) if self._cursor and path == self._cursor.get_path(): style = self._treeview.get_style() bg = style.bg[Gtk.StateType.PRELIGHT] cell.set_property('cell-background-gdk', bg) cell.set_property('style', Pango.Style.ITALIC) else: cell.set_property('cell-background-set', False) cell.set_property('style-set', False) def _icon_from_stock(self, stock): theme = Gtk.icon_theme_get_default() size = Gtk.icon_size_lookup(Gtk.IconSize.MENU) pixbuf = theme.load_icon(stock, size[0], Gtk.IconLookupFlags.USE_BUILTIN) return pixbuf def _list_dir(self, gfile): entries = [] try: ret = gfile.enumerate_children("standard::*", Gio.FileQueryInfoFlags.NONE, None) except GLib.GError: pass if isinstance(ret, Gio.FileEnumerator): while True: entry = ret.next_file(None) if not entry: break entries.append((gfile.get_child(entry.get_name()), entry)) else: entries = ret children = [] for entry in entries: children.append((entry[0], entry[1].get_name(), entry[1].get_file_type(), entry[1].get_icon())) return children def _compare_entries(self, a, b, lpart): if lpart in a: if lpart in b: return cmp(a.index(lpart), b.index(lpart)) else: return -1 elif lpart in b: return 1 else: return 0 def _match_glob(self, s, glob): if glob: glob += '*' return fnmatch.fnmatch(s, glob) def do_search_dir(self, parts, d): if not parts or not d: return [] if not d in self._cache: entries = self._list_dir(d) entries.sort(lambda x, y: cmp(x[1].lower(), y[1].lower())) self._cache[d] = entries else: entries = self._cache[d] found = [] newdirs = [] lpart = parts[0].lower() for entry in entries: if not entry: continue lentry = entry[1].lower() if not lpart or lpart in lentry or self._match_glob(lentry, lpart): if entry[2] == Gio.FileType.DIRECTORY: if len(parts) > 1: newdirs.append(entry[0]) else: found.append(entry) elif entry[2] == Gio.FileType.REGULAR and \ (not lpart or len(parts) == 1): found.append(entry) found.sort(lambda a, b: self._compare_entries(a[1].lower(), b[1].lower(), lpart)) if lpart == '..': newdirs.append(d.get_parent()) for dd in newdirs: found.extend(self.do_search_dir(parts[1:], dd)) return found def _replace_insensitive(self, s, find, rep): out = '' l = s.lower() find = find.lower() last = 0 if len(find) == 0: return xml.sax.saxutils.escape(s) while True: m = l.find(find, last) if m == -1: break else: out += xml.sax.saxutils.escape(s[last:m]) + rep % (xml.sax.saxutils.escape(s[m:m + len(find)]),) last = m + len(find) return out + xml.sax.saxutils.escape(s[last:]) def make_markup(self, parts, path): out = [] for i in range(0, len(parts)): out.append(self._replace_insensitive(path[i], parts[i], "%s")) return os.sep.join(out) def _get_icon(self, f): query = f.query_info(Gio.FILE_ATTRIBUTE_STANDARD_ICON, Gio.FileQueryInfoFlags.NONE, None) if not query: return None else: return query.get_icon() def _make_parts(self, parent, child, pp): parts = [] # We went from parent, to child, using pp idx = len(pp) - 1 while idx >= 0: if pp[idx] == '..': parts.insert(0, '..') else: parts.insert(0, child.get_basename()) child = child.get_parent() idx -= 1 return parts def normalize_relative(self, parts): if not parts: return [] out = self.normalize_relative(parts[:-1]) if parts[-1] == '..': if not out or (out[-1] == '..') or len(out) == 1: out.append('..') else: del out[-1] else: out.append(parts[-1]) return out def _append_to_store(self, item): if not item in self._stored_items: self._store.append(item) self._stored_items[item] = True def _clear_store(self): self._store.clear() self._stored_items = {} def _show_virtuals(self): for d in self._dirs: if isinstance(d, VirtualDirectory): for entry in d.enumerate_children("standard::*", 0, None): self._append_to_store((entry[1].get_icon(), xml.sax.saxutils.escape(entry[1].get_name()), entry[0], entry[1].get_file_type())) def _set_busy(self, busy): if busy: self.get_window().set_cursor(self._busy_cursor) else: self.get_window().set_cursor(None) Gdk.flush() def _remove_cursor(self): if self._cursor: path = self._cursor.get_path() self._cursor = None self._store.row_changed(path, self._store.get_iter(path)) def do_search(self): self._set_busy(True) self._remove_cursor() text = self._entry.get_text().strip() self._clear_store() if text == '': self._show_virtuals() else: parts = self.normalize_relative(text.split(os.sep)) files = [] for d in self._dirs: for entry in self.do_search_dir(parts, d): pathparts = self._make_parts(d, entry[0], parts) self._append_to_store((entry[3], self.make_markup(parts, pathparts), entry[0], entry[2])) piter = self._store.get_iter_first() if piter: self._treeview.get_selection().select_path(self._store.get_path(piter)) self.get_window().set_cursor(None) self._set_busy(False) def do_show(self): Gtk.Window.do_show(self) self._entry.grab_focus() self._entry.set_text("") self.do_search() def on_changed(self, editable): self.do_search() self.on_selection_changed(self._treeview.get_selection()) def _shift_extend(self, towhere): selection = self._treeview.get_selection() if not self._shift_start: model, rows = selection.get_selected_rows() start = rows[0] self._shift_start = Gtk.TreeRowReference(self._store, start) else: start = self._shift_start.get_path() selection.unselect_all() selection.select_range(start, towhere) def _select_index(self, idx, hasctrl, hasshift): path = (idx,) if not (hasctrl or hasshift): self._treeview.get_selection().unselect_all() if hasshift: self._shift_extend(path) else: self._shift_start = None if not hasctrl: self._treeview.get_selection().select_path(path) self._treeview.scroll_to_cell(path, None, True, 0.5, 0) self._remove_cursor() if hasctrl or hasshift: self._cursor = Gtk.TreeRowReference(self._store, path) piter = self._store.get_iter(path) self._store.row_changed(path, piter) def _move_selection(self, howmany, hasctrl, hasshift): num = self._store.iter_n_children(None) if num == 0: return True # Test for cursor path = None if self._cursor: path = self._cursor.get_path() else: model, rows = self._treeview.get_selection().get_selected_rows() if len(rows) == 1: path = rows[0] if not path: if howmany > 0: self._select_index(0, hasctrl, hasshift) else: self._select_index(num - 1, hasctrl, hasshift) else: idx = path[0] if idx + howmany < 0: self._select_index(0, hasctrl, hasshift) elif idx + howmany >= num: self._select_index(num - 1, hasctrl, hasshift) else: self._select_index(idx + howmany, hasctrl, hasshift) return True def _direct_file(self): uri = self._entry.get_text() gfile = None if Pluma.utils_is_valid_uri(uri): gfile = Gio.file_new_for_uri(uri) elif os.path.isabs(uri): f = Gio.file_new_for_uri(uri) if f.query_exists(): gfile = f return gfile def _activate(self): model, rows = self._treeview.get_selection().get_selected_rows() ret = True for row in rows: s = model.get_iter(row) info = model.get(s, 2, 3) if info[1] != Gio.FileType.DIRECTORY: ret = ret and self._handler(info[0]) else: text = self._entry.get_text() for i in range(len(text) - 1, -1, -1): if text[i] == os.sep: break self._entry.set_text(os.path.join(text[:i], os.path.basename(info[0].get_uri())) + os.sep) self._entry.set_position(-1) self._entry.grab_focus() return True if rows and ret: self.destroy() if not rows: gfile = self._direct_file() if gfile and self._handler(gfile): self.destroy() else: ret = False else: ret = False return ret def toggle_cursor(self): if not self._cursor: return path = self._cursor.get_path() selection = self._treeview.get_selection() if selection.path_is_selected(path): selection.unselect_path(path) else: selection.select_path(path) def on_key_press_event(self, widget, event): move_mapping = { Gdk.KEY_Down: 1, Gdk.KEY_Up: -1, Gdk.KEY_Page_Down: 5, Gdk.KEY_Page_Up: -5 } if event.keyval == Gdk.KEY_Escape: self.destroy() return True elif event.keyval in move_mapping: return self._move_selection(move_mapping[event.keyval], event.state & Gdk.ModifierType.CONTROL_MASK, event.state & Gdk.ModifierType.SHIFT_MASK) elif event.keyval in [Gdk.KEY_Return, Gdk.KEY_KP_Enter, Gdk.KEY_Tab, Gdk.KEY_ISO_Left_Tab]: return self._activate() elif event.keyval == Gdk.KEY_space and event.state & Gdk.ModifierType.CONTROL_MASK: self.toggle_cursor() return False def on_row_activated(self, view, path, column): self._activate() def do_response(self, response): if response != Gtk.ResponseType.ACCEPT or not self._activate(): self.destroy() def do_configure_event(self, event): if self.get_realized(): alloc = self.get_allocation() self._size = (alloc.width, alloc.height) return Gtk.Dialog.do_configure_event(self, event) def on_selection_changed(self, selection): model, rows = selection.get_selected_rows() gfile = None fname = None if not rows: gfile = self._direct_file() elif len(rows) == 1: gfile = model.get(model.get_iter(rows[0]), 2)[0] else: fname = '' if gfile: if gfile.is_native(): fname = xml.sax.saxutils.escape(gfile.get_path()) else: fname = xml.sax.saxutils.escape(gfile.get_uri()) self._open_button.set_sensitive(fname != None) self._info_label.set_markup(fname or '') def on_focus_entry(self, group, accel, keyval, modifier): self._entry.grab_focus() # ex:ts=8:et: