diff options
Diffstat (limited to 'plugins/quickopen/quickopen')
-rwxr-xr-x | plugins/quickopen/quickopen/Makefile.am | 13 | ||||
-rwxr-xr-x | plugins/quickopen/quickopen/__init__.py | 46 | ||||
-rwxr-xr-x | plugins/quickopen/quickopen/popup.py | 534 | ||||
-rwxr-xr-x | plugins/quickopen/quickopen/virtualdirs.py | 87 | ||||
-rwxr-xr-x | plugins/quickopen/quickopen/windowhelper.py | 198 |
5 files changed, 878 insertions, 0 deletions
diff --git a/plugins/quickopen/quickopen/Makefile.am b/plugins/quickopen/quickopen/Makefile.am new file mode 100755 index 00000000..88882fdf --- /dev/null +++ b/plugins/quickopen/quickopen/Makefile.am @@ -0,0 +1,13 @@ +# Quick Open Plugin + +plugindir = $(GEDIT_PLUGINS_LIBS_DIR)/quickopen +plugin_PYTHON = \ + __init__.py \ + popup.py \ + virtualdirs.py \ + windowhelper.py + +CLEANFILES = +DISTCLEANFILES = + +-include $(top_srcdir)/git.mk diff --git a/plugins/quickopen/quickopen/__init__.py b/plugins/quickopen/quickopen/__init__.py new file mode 100755 index 00000000..a41c9400 --- /dev/null +++ b/plugins/quickopen/quickopen/__init__.py @@ -0,0 +1,46 @@ +# -*- 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., 59 Temple Place, Suite 330, +# Boston, MA 02111-1307, USA. + +import gedit +from windowhelper import WindowHelper + +class QuickOpenPlugin(gedit.Plugin): + def __init__(self): + gedit.Plugin.__init__(self) + + self._popup_size = (450, 300) + self._helpers = {} + + def activate(self, window): + self._helpers[window] = WindowHelper(window, self) + + def deactivate(self, window): + self._helpers[window].deactivate() + del self._helpers[window] + + def update_ui(self, window): + self._helpers[window].update_ui() + + def get_popup_size(self): + return self._popup_size + + def set_popup_size(self, size): + self._popup_size = size + +# ex:ts=8:et: diff --git a/plugins/quickopen/quickopen/popup.py b/plugins/quickopen/quickopen/popup.py new file mode 100755 index 00000000..a80caf31 --- /dev/null +++ b/plugins/quickopen/quickopen/popup.py @@ -0,0 +1,534 @@ +# -*- 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., 59 Temple Place, Suite 330, +# Boston, MA 02111-1307, USA. + +import gtk +import gtk.gdk +import gobject +import os +import gio +import pango +import glib +import fnmatch +import gedit +import xml.sax.saxutils +from virtualdirs import VirtualDirectory + +class Popup(gtk.Dialog): + def __init__(self, window, paths, handler): + gtk.Dialog.__init__(self, + title=_('Quick Open'), + parent=window, + flags=gtk.DIALOG_DESTROY_WITH_PARENT | gtk.DIALOG_NO_SEPARATOR | gtk.DIALOG_MODAL) + + self.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL) + self._open_button = self.add_button(gtk.STOCK_OPEN, gtk.RESPONSE_ACCEPT) + + self._handler = handler + self._build_ui() + + self._dirs = [] + self._cache = {} + self._theme = None + self._cursor = None + self._shift_start = None + + accel_group = gtk.AccelGroup() + accel_group.connect_group(gtk.keysyms.l, gtk.gdk.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 _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(None, None) + sw.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) + sw.set_shadow_type(gtk.SHADOW_OUT) + + tv = gtk.TreeView() + tv.set_headers_visible(False) + + self._store = gtk.ListStore(gio.Icon, str, object, int) + 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.set_attributes(renderer, gicon=0) + + renderer = gtk.CellRendererText() + column.pack_start(renderer, True) + column.set_attributes(renderer, markup=1) + + column.set_cell_data_func(renderer, self.on_cell_data_cb) + + tv.append_column(column) + sw.add(tv) + + selection = tv.get_selection() + selection.connect('changed', self.on_selection_changed) + selection.set_mode(gtk.SELECTION_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.ELLIPSIZE_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): + path = model.get_path(piter) + + if self._cursor and path == self._cursor.get_path(): + style = self._treeview.get_style() + bg = style.bg[gtk.STATE_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.ICON_SIZE_MENU) + pixbuf = theme.load_icon(stock, size[0], gtk.ICON_LOOKUP_USE_BUILTIN) + + return pixbuf + + def _list_dir(self, gfile): + entries = [] + + try: + entries = gfile.enumerate_children("standard::*") + except glib.GError: + pass + + children = [] + + for entry in entries: + if isinstance(gfile, VirtualDirectory): + child, entry = entry + else: + child = gfile.get_child(entry.get_name()) + + children.append((child, entry.get_name(), entry.get_file_type(), entry.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.FILE_TYPE_DIRECTORY: + if len(parts) > 1: + newdirs.append(entry[0]) + else: + found.append(entry) + elif entry[2] == gio.FILE_TYPE_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], "<b>%s</b>")) + + return os.sep.join(out) + + def _get_icon(self, f): + query = f.query_info(gio.FILE_ATTRIBUTE_STANDARD_ICON) + + 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::*"): + self._append_to_store((entry[1].get_icon(), xml.sax.saxutils.escape(entry[1].get_name()), entry[0], entry[1].get_file_type())) + + 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.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.WATCH)) + 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.window.set_cursor(None) + + 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 gedit.utils.uri_is_valid(uri): + gfile = gio.File(uri) + elif os.path.isabs(uri): + f = gio.File(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.FILE_TYPE_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 = { + gtk.keysyms.Down: 1, + gtk.keysyms.Up: -1, + gtk.keysyms.Page_Down: 5, + gtk.keysyms.Page_Up: -5 + } + + if event.keyval == gtk.keysyms.Escape: + self.destroy() + return True + elif event.keyval in move_mapping: + return self._move_selection(move_mapping[event.keyval], event.state & gtk.gdk.CONTROL_MASK, event.state & gtk.gdk.SHIFT_MASK) + elif event.keyval in [gtk.keysyms.Return, gtk.keysyms.KP_Enter, gtk.keysyms.Tab, gtk.keysyms.ISO_Left_Tab]: + return self._activate() + elif event.keyval == gtk.keysyms.space and event.state & gtk.gdk.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.RESPONSE_ACCEPT or not self._activate(): + self.destroy() + + 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() + +gobject.type_register(Popup) + +# ex:ts=8:et: diff --git a/plugins/quickopen/quickopen/virtualdirs.py b/plugins/quickopen/quickopen/virtualdirs.py new file mode 100755 index 00000000..ef0b8dc4 --- /dev/null +++ b/plugins/quickopen/quickopen/virtualdirs.py @@ -0,0 +1,87 @@ +# -*- 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., 59 Temple Place, Suite 330, +# Boston, MA 02111-1307, USA. + +import gtk +import gio + +class VirtualDirectory: + def __init__(self, name): + self._name = name + self._children = [] + + def get_uri(self): + return 'virtual://' + self._name + + def get_parent(self): + return None + + def enumerate_children(self, attr): + return self._children + + def append(self, child): + if not child.is_native(): + return + + try: + info = child.query_info("standard::*") + + if info: + self._children.append((child, info)) + except: + pass + +class RecentDocumentsDirectory(VirtualDirectory): + def __init__(self, maxitems=10, screen=None): + VirtualDirectory.__init__(self, 'recent') + + self._maxitems = maxitems + self.fill(screen) + + def fill(self, screen): + if screen: + manager = gtk.recent_manager_get_for_screen(screen) + else: + manager = gtk.recent_manager_get_default() + + items = manager.get_items() + items.sort(lambda a, b: cmp(b.get_visited(), a.get_visited())) + + added = 0 + + for item in items: + if item.has_group('gedit'): + self.append(gio.File(item.get_uri())) + added += 1 + + if added >= self._maxitems: + break + +class CurrentDocumentsDirectory(VirtualDirectory): + def __init__(self, window): + VirtualDirectory.__init__(self, 'documents') + + self.fill(window) + + def fill(self, window): + for doc in window.get_documents(): + location = doc.get_location() + if location: + self.append(location) + +# ex:ts=8:et: diff --git a/plugins/quickopen/quickopen/windowhelper.py b/plugins/quickopen/quickopen/windowhelper.py new file mode 100755 index 00000000..70ea26f0 --- /dev/null +++ b/plugins/quickopen/quickopen/windowhelper.py @@ -0,0 +1,198 @@ +# -*- 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., 59 Temple Place, Suite 330, +# Boston, MA 02111-1307, USA. + +import gedit +import gtk +from popup import Popup +import os +import gedit.commands +import gio +import glib +from virtualdirs import RecentDocumentsDirectory +from virtualdirs import CurrentDocumentsDirectory + +ui_str = """<ui> + <menubar name="MenuBar"> + <menu name="FileMenu" action="File"> + <placeholder name="FileOps_2"> + <menuitem name="QuickOpen" action="QuickOpen"/> + </placeholder> + </menu> + </menubar> +</ui> +""" + +class WindowHelper: + def __init__(self, window, plugin): + self._window = window + self._plugin = plugin + + self._popup = None + self._install_menu() + + def deactivate(self): + self._uninstall_menu() + self._window = None + self._plugin = None + + def update_ui(self): + pass + + def _uninstall_menu(self): + manager = self._window.get_ui_manager() + + manager.remove_ui(self._ui_id) + manager.remove_action_group(self._action_group) + + manager.ensure_update() + + def _install_menu(self): + manager = self._window.get_ui_manager() + self._action_group = gtk.ActionGroup("GeditQuickOpenPluginActions") + self._action_group.add_actions([ + ("QuickOpen", gtk.STOCK_OPEN, _("Quick open"), + '<Ctrl><Alt>O', _("Quickly open documents"), + self.on_quick_open_activate) + ]) + + manager.insert_action_group(self._action_group, -1) + self._ui_id = manager.add_ui_from_string(ui_str) + + def _create_popup(self): + paths = [] + + # Open documents + paths.append(CurrentDocumentsDirectory(self._window)) + + doc = self._window.get_active_document() + + # Current document directory + if doc and doc.is_local(): + gfile = doc.get_location() + paths.append(gfile.get_parent()) + + # File browser root directory + if gedit.version[0] > 2 or (gedit.version[0] == 2 and (gedit.version[1] > 26 or (gedit.version[1] == 26 and gedit.version[2] >= 2))): + bus = self._window.get_message_bus() + + try: + msg = bus.send_sync('/plugins/filebrowser', 'get_root') + + if msg: + uri = msg.get_value('uri') + + if uri: + gfile = gio.File(uri) + + if gfile.is_native(): + paths.append(gfile) + + except StandardError: + pass + + # Recent documents + paths.append(RecentDocumentsDirectory(screen=self._window.get_screen())) + + # Local bookmarks + for path in self._local_bookmarks(): + paths.append(path) + + # Desktop directory + desktopdir = self._desktop_dir() + + if desktopdir: + paths.append(gio.File(desktopdir)) + + # Home directory + paths.append(gio.File(os.path.expanduser('~'))) + + self._popup = Popup(self._window, paths, self.on_activated) + + self._popup.set_default_size(*self._plugin.get_popup_size()) + self._popup.set_transient_for(self._window) + self._popup.set_position(gtk.WIN_POS_CENTER_ON_PARENT) + + self._window.get_group().add_window(self._popup) + + self._popup.connect('destroy', self.on_popup_destroy) + + def _local_bookmarks(self): + filename = os.path.expanduser('~/.gtk-bookmarks') + + if not os.path.isfile(filename): + return [] + + paths = [] + + for line in file(filename, 'r').xreadlines(): + uri = line.strip().split(" ")[0] + f = gio.File(uri) + + if f.is_native(): + try: + info = f.query_info("standard::type") + + if info and info.get_file_type() == gio.FILE_TYPE_DIRECTORY: + paths.append(f) + except glib.GError: + pass + + return paths + + def _desktop_dir(self): + config = os.getenv('XDG_CONFIG_HOME') + + if not config: + config = os.path.expanduser('~/.config') + + config = os.path.join(config, 'user-dirs.dirs') + desktopdir = None + + if os.path.isfile(config): + for line in file(config, 'r').xreadlines(): + line = line.strip() + + if line.startswith('XDG_DESKTOP_DIR'): + parts = line.split('=', 1) + desktopdir = os.path.expandvars(parts[1].strip('"').strip("'")) + break + + if not desktopdir: + desktopdir = os.path.expanduser('~/Desktop') + + return desktopdir + + # Callbacks + def on_quick_open_activate(self, action): + if not self._popup: + self._create_popup() + + self._popup.show() + + def on_popup_destroy(self, popup): + alloc = popup.get_allocation() + self._plugin.set_popup_size((alloc.width, alloc.height)) + + self._popup = None + + def on_activated(self, gfile): + gedit.commands.load_uri(self._window, gfile.get_uri(), None, -1) + return True + +# ex:ts=8:et: |