# -*- 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, see <http://www.gnu.org/licenses/>.
import os
import platform
import functools
import fnmatch
from gi.repository import GLib, Gio, GObject, Pango, Gtk, Gdk, Gedit
import xml.sax.saxutils
from .virtualdirs import VirtualDirectory
try:
import gettext
gettext.bindtextdomain('gedit')
gettext.textdomain('gedit')
_ = gettext.gettext
except:
_ = lambda s: s
class Popup(Gtk.Dialog):
__gtype_name__ = "QuickOpenPopup"
def __init__(self, window, paths, handler):
Gtk.Dialog.__init__(self,
title=_('Quick Open'),
transient_for=window,
modal=True,
destroy_with_parent=True)
self.add_button(_("_Cancel"), Gtk.ResponseType.CANCEL)
self._open_button = self.add_button(_("_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())
self.connect('show', self.on_show)
def get_final_size(self):
return self._size
def _build_ui(self):
self.set_border_width(5)
vbox = self.get_content_area()
vbox.set_spacing(2)
action_area = self.get_action_area()
action_area.set_border_width(5)
action_area.set_spacing(6)
self._entry = Gtk.SearchEntry()
self._entry.set_placeholder_text(_('Type to search…'))
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)
column = Gtk.TreeViewColumn()
renderer = Gtk.CellRendererPixbuf()
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_halign(Gtk.Align.START)
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 _is_text(self, entry):
content_type = entry.get_content_type()
if content_type is None or Gio.content_type_is_unknown(content_type):
return True
if platform.system() != 'Windows':
if Gio.content_type_is_a(content_type, 'text/plain'):
return True
else:
if Gio.content_type_is_a(content_type, 'text'):
return True
# This covers a rare case in which on Windows the PerceivedType
# is not set to "text" but the Content Type is set to text/plain
if Gio.content_type_get_mime_type(content_type) == 'text/plain':
return True
return False
def _list_dir(self, gfile):
entries = []
try:
ret = gfile.enumerate_children("standard::*",
Gio.FileQueryInfoFlags.NONE,
None)
except GLib.Error as e:
pass
if isinstance(ret, Gio.FileEnumerator):
while True:
entry = ret.next_file(None)
if not entry:
break
if not entry.get_is_backup():
entries.append((gfile.get_child(entry.get_name()), entry))
else:
entries = ret
children = []
for entry in entries:
file_type = entry[1].get_file_type()
if file_type == Gio.FileType.REGULAR:
if not self._is_text(entry[1]):
continue
children.append((entry[0],
entry[1].get_name(),
file_type,
entry[1].get_icon()))
return children
def _compare_entries(self, a, b, lpart):
if lpart in a:
if lpart in b:
if a.index(lpart) < b.index(lpart):
return -1
elif a.index(lpart) > b.index(lpart):
return 1
else:
return 0
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 d in self._cache:
entries = self._cache[d]
else:
entries = self._list_dir(d)
entries.sort(key=lambda x: x[1].lower())
self._cache[d] = entries
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(key=functools.cmp_to_key(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,
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):
uri = item[2].get_uri()
if uri not in self._stored_items:
self._store.append(item)
self._stored_items.add(uri)
def _clear_store(self):
self._store.clear()
self._stored_items = set()
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:
path = self._store.get_path(piter)
self._treeview.get_selection().select_path(path)
self._set_busy(False)
# FIXME: override doesn't work anymore for some reason, if we override
# the widget is not realized
def on_show(self, data=None):
# 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.new(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.get_indices()[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 = Gio.file_new_for_uri(uri)
if Gedit.utils_is_valid_location(gfile) or \
(os.path.isabs(uri) and gfile.query_exists()):
return gfile
else:
return None
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:
# We destroy the popup in an idle callback to work around a crash that happens with
# GTK_IM_MODULE=xim. See https://bugzilla.gnome.org/show_bug.cgi?id=737711 .
GLib.idle_add(self.destroy)
if not rows:
gfile = self._direct_file()
if gfile and self._handler(gfile):
GLib.idle_add(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 is not None)
self._info_label.set_markup(fname or '')
def on_focus_entry(self, group, accel, keyval, modifier):
self._entry.grab_focus()
# ex:ts=4:et: