Merge branch 'geps/interactivesearch'

Use our own interactive-search box to get it
 - more efficient (binary search on sorted columns).
 - customizable (delayed launch of search to avoid text scrambling)
This commit is contained in:
Bastien Jacquet
2015-01-05 17:33:15 +01:00
4 changed files with 587 additions and 66 deletions

View File

@ -35,6 +35,7 @@ from gi.repository import Pango
from ..managedwindow import ManagedWindow from ..managedwindow import ManagedWindow
from ..filters import SearchBar from ..filters import SearchBar
from ..glade import Glade from ..glade import Glade
from ..widgets.interactivesearchbox import InteractiveSearchBox
#------------------------------------------------------------------------- #-------------------------------------------------------------------------
# #
@ -86,6 +87,10 @@ class BaseSelector(ManagedWindow):
self.tree.connect('row-activated', self._on_row_activated) self.tree.connect('row-activated', self._on_row_activated)
self.tree.grab_focus() self.tree.grab_focus()
# connect to signal for custom interactive-search
self.searchbox = InteractiveSearchBox(self.tree)
self.tree.connect('key-press-event', self.searchbox.treeview_keypress)
#add the search bar #add the search bar
self.search_bar = SearchBar(dbstate, uistate, self.build_tree) self.search_bar = SearchBar(dbstate, uistate, self.build_tree)
filter_box = self.search_bar.build() filter_box = self.search_bar.build()

View File

@ -71,6 +71,7 @@ _ = glocale.translation.sgettext
from ..ddtargets import DdTargets from ..ddtargets import DdTargets
from ..plug.quick import create_quickreport_menu, create_web_connect_menu from ..plug.quick import create_quickreport_menu, create_web_connect_menu
from ..utils import is_right_click from ..utils import is_right_click
from ..widgets.interactivesearchbox import InteractiveSearchBox
#---------------------------------------------------------------- #----------------------------------------------------------------
# #
@ -157,6 +158,7 @@ class ListView(NavigationView):
self.list.set_fixed_height_mode(True) self.list.set_fixed_height_mode(True)
self.list.connect('button-press-event', self._button_press) self.list.connect('button-press-event', self._button_press)
self.list.connect('key-press-event', self._key_press) self.list.connect('key-press-event', self._key_press)
self.searchbox = InteractiveSearchBox(self.list)
if self.drag_info(): if self.drag_info():
self.list.connect('drag_data_get', self.drag_data_get) self.list.connect('drag_data_get', self.drag_data_get)
@ -875,10 +877,10 @@ class ListView(NavigationView):
return False return False
if self.model.get_flags() & Gtk.TreeModelFlags.LIST_ONLY: if self.model.get_flags() & Gtk.TreeModelFlags.LIST_ONLY:
# Flat list # Flat list
self._key_press_flat(obj, event) return self._key_press_flat(obj, event)
else: else:
# Tree # Tree
self._key_press_tree(obj, event) return self._key_press_tree(obj, event)
def _key_press_flat(self, obj, event): def _key_press_flat(self, obj, event):
""" """
@ -888,6 +890,9 @@ class ListView(NavigationView):
if event.keyval in (Gdk.KEY_Return, Gdk.KEY_KP_Enter): if event.keyval in (Gdk.KEY_Return, Gdk.KEY_KP_Enter):
self.edit(obj) self.edit(obj)
return True return True
# Custom interactive search
if event.string:
return self.searchbox.treeview_keypress(obj, event)
return False return False
def _key_press_tree(self, obj, event): def _key_press_tree(self, obj, event):
@ -896,16 +901,15 @@ class ListView(NavigationView):
ENTER --> edit selection or open group node ENTER --> edit selection or open group node
SHIFT+ENTER --> open group node and all children nodes SHIFT+ENTER --> open group node and all children nodes
""" """
if event.get_state() & Gdk.ModifierType.SHIFT_MASK: if (event.get_state() & Gdk.ModifierType.SHIFT_MASK and
if event.keyval in (Gdk.KEY_Return, Gdk.KEY_KP_Enter): event.keyval in (Gdk.KEY_Return, Gdk.KEY_KP_Enter)):
store, paths = self.selection.get_selected_rows() store, paths = self.selection.get_selected_rows()
if paths: if paths:
iter_ = self.model.get_iter(paths[0]) iter_ = self.model.get_iter(paths[0])
handle = self.model.get_handle_from_iter(iter_) handle = self.model.get_handle_from_iter(iter_)
if len(paths) == 1 and handle is None: if len(paths) == 1 and handle is None:
return self.expand_collapse_tree_branch() return self.expand_collapse_tree_branch()
else: elif event.keyval in (Gdk.KEY_Return, Gdk.KEY_KP_Enter):
if event.keyval in (Gdk.KEY_Return, Gdk.KEY_KP_Enter):
store, paths = self.selection.get_selected_rows() store, paths = self.selection.get_selected_rows()
if paths: if paths:
iter_ = self.model.get_iter(paths[0]) iter_ = self.model.get_iter(paths[0])
@ -915,6 +919,9 @@ class ListView(NavigationView):
else: else:
self.edit(obj) self.edit(obj)
return True return True
elif event.string:
# Custom interactive search
return self.searchbox.treeview_keypress(obj, event)
return False return False
def expand_collapse_tree(self): def expand_collapse_tree(self):

View File

@ -200,7 +200,7 @@ class FlatNodeMap(object):
else: else:
self.__corr = (0, 1) self.__corr = (0, 1)
if not self._hndl2index: if not self._hndl2index:
self._hndl2index = dict([key[1], index] self._hndl2index = dict((key[1], index)
for index, key in enumerate(self._index2hndl)) for index, key in enumerate(self._index2hndl))
def real_path(self, index): def real_path(self, index):
@ -234,7 +234,7 @@ class FlatNodeMap(object):
:param handle: the key of the object for which the path in the treeview :param handle: the key of the object for which the path in the treeview
is needed is needed
:param type: an object handle :type handle: an object handle
:Returns: the path, or None if handle does not link to a path :Returns: the path, or None if handle does not link to a path
""" """
index = iter.user_data index = iter.user_data
@ -252,7 +252,7 @@ class FlatNodeMap(object):
:param handle: the key of the object for which the path in the treeview :param handle: the key of the object for which the path in the treeview
is needed is needed
:param type: an object handle :type handle: an object handle
:Returns: the path, or None if handle does not link to a path :Returns: the path, or None if handle does not link to a path
""" """
index = self._hndl2index.get(handle) index = self._hndl2index.get(handle)
@ -267,7 +267,7 @@ class FlatNodeMap(object):
:param handle: the key of the object for which the sortkey :param handle: the key of the object for which the sortkey
is needed is needed
:param type: an object handle :type handle: an object handle
:Returns: the sortkey, or None if handle is not present :Returns: the sortkey, or None if handle is not present
""" """
index = self._hndl2index.get(handle) index = self._hndl2index.get(handle)
@ -318,18 +318,14 @@ class FlatNodeMap(object):
handle = handle.decode('utf-8') handle = handle.decode('utf-8')
return handle return handle
def find_next_handle(self, iter): def iter_next(self, iter):
""" """
Finds the next handle based off the passed handle. This is accomplished Increments the iter y finding the index associated with the iter,
by finding the index associated with the iter, adding or substracting adding or substracting one.
one to find the next index, then finding the handle associated with
that.
False is returned if no next handle False is returned if no next handle
True, handle tuple otherwise
:param handle: the key of the object for which the next handle shown :param iter: Gtk.TreeModel iterator
in the treeview is needed :param type: Gtk.TreeIter
:param type: an object handle
""" """
index = iter.user_data index = iter.user_data
if index is None: if index is None:
@ -346,11 +342,10 @@ class FlatNodeMap(object):
return False return False
else: else:
index += 1 index += 1
if index >= len(self._index2hndl):
try:
return True, self._index2hndl[index][1]
except IndexError:
return False return False
iter.user_data = index
return True
def get_first_iter(self): def get_first_iter(self):
""" """
@ -381,6 +376,7 @@ class FlatNodeMap(object):
Returns the path of the inserted row Returns the path of the inserted row
:param srtkey_hndl: the (sortkey, handle) tuple that must be inserted :param srtkey_hndl: the (sortkey, handle) tuple that must be inserted
:type srtkey_hndl: sortkey key already transformed by self.sort_func, object handle
:Returns: path of the row inserted in the treeview :Returns: path of the row inserted in the treeview
:Returns type: Gtk.TreePath or None :Returns type: Gtk.TreePath or None
@ -397,13 +393,7 @@ class FlatNodeMap(object):
insert_pos = bisect.bisect_left(self._index2hndl, srtkey_hndl) insert_pos = bisect.bisect_left(self._index2hndl, srtkey_hndl)
self._index2hndl.insert(insert_pos, srtkey_hndl) self._index2hndl.insert(insert_pos, srtkey_hndl)
#make sure the index map is updated #make sure the index map is updated
if sys.version_info[0] < 3: # keep this, for speed in Python2 for srt_key,hndl in self._index2hndl[insert_pos+1:]:
for hndl, index in self._hndl2index.iteritems(): # in Python2 "if"
if index >= insert_pos:
self._hndl2index[hndl] += 1
else:
for hndl, index in self._hndl2index.items():
if index >= insert_pos:
self._hndl2index[hndl] += 1 self._hndl2index[hndl] += 1
self._hndl2index[srtkey_hndl[1]] = insert_pos self._hndl2index[srtkey_hndl[1]] = insert_pos
#update self.__corr so it remains correct #update self.__corr so it remains correct
@ -446,14 +436,8 @@ class FlatNodeMap(object):
if self._reverse: if self._reverse:
self.__corr = (len(self._index2hndl) - 1, -1) self.__corr = (len(self._index2hndl) - 1, -1)
#update the handle2path map so it remains correct #update the handle2path map so it remains correct
if sys.version_info[0] < 3: # keep this, for speed in Python2 for srt_key,hndl in self._index2hndl[index:]:
for key, val in self._hndl2index.iteritems(): # in Python2 "if" self._hndl2index[hndl] -= 1
if val > index:
self._hndl2index[key] -= 1
else:
for key, val in self._hndl2index.items():
if val > index:
self._hndl2index[key] -= 1
return Gtk.TreePath((delpath,)) return Gtk.TreePath((delpath,))
#------------------------------------------------------------------------- #-------------------------------------------------------------------------
@ -491,6 +475,7 @@ class FlatBaseModel(GObject.GObject, Gtk.TreeModel):
col = self.sort_map[scol][1] col = self.sort_map[scol][1]
else: else:
col = scol col = scol
# get the function that maps data to sort_keys
self.sort_func = lambda x: glocale.sort_key(self.smap[col](x)) self.sort_func = lambda x: glocale.sort_key(self.smap[col](x))
self.sort_col = scol self.sort_col = scol
self.skip = skip self.skip = skip
@ -592,7 +577,9 @@ class FlatBaseModel(GObject.GObject, Gtk.TreeModel):
# use cursor as a context manager # use cursor as a context manager
with self.gen_cursor() as cursor: with self.gen_cursor() as cursor:
#loop over database and store the sort field, and the handle #loop over database and store the sort field, and the handle
return sorted((self.sort_func(data), key) for key, data in cursor) srt_keys=[(self.sort_func(data), key) for key, data in cursor]
srt_keys.sort()
return srt_keys
def _rebuild_search(self, ignore=None): def _rebuild_search(self, ignore=None):
""" function called when view must be build, given a search text """ function called when view must be build, given a search text
@ -722,15 +709,15 @@ class FlatBaseModel(GObject.GObject, Gtk.TreeModel):
""" """
Get the gramps handle for an iter. Get the gramps handle for an iter.
""" """
ud = iter.user_data index = iter.user_data
if ud is None: if index is None:
##GTK3: user data may only be an integer, we store the index ##GTK3: user data may only be an integer, we store the index
##PROBLEM: pygobject 3.8 stores 0 as None, we need to correct ##PROBLEM: pygobject 3.8 stores 0 as None, we need to correct
## when using user_data for that! ## when using user_data for that!
##upstream bug: https://bugzilla.gnome.org/show_bug.cgi?id=698366 ##upstream bug: https://bugzilla.gnome.org/show_bug.cgi?id=698366
ud = 0 index = 0
index = self.node_map.real_index(ud) path = self.node_map.real_path(index)
return self.node_map.get_handle(index) return self.node_map.get_handle(path)
# The following implement the public interface of Gtk.TreeModel # The following implement the public interface of Gtk.TreeModel
@ -806,14 +793,14 @@ class FlatBaseModel(GObject.GObject, Gtk.TreeModel):
col is the model column that is needed, not the visible column! col is the model column that is needed, not the visible column!
""" """
#print ('do_get_val', iter, iter.user_data, col) #print ('do_get_val', iter, iter.user_data, col)
ud = iter.user_data index = iter.user_data
if ud is None: if index is None:
##GTK3: user data may only be an integer, we store the index ##GTK3: user data may only be an integer, we store the index
##PROBLEM: pygobject 3.8 stores 0 as None, we need to correct ##PROBLEM: pygobject 3.8 stores 0 as None, we need to correct
## when using user_data for that! ## when using user_data for that!
##upstream bug: https://bugzilla.gnome.org/show_bug.cgi?id=698366 ##upstream bug: https://bugzilla.gnome.org/show_bug.cgi?id=698366
ud = 0 index = 0
handle = self.node_map._index2hndl[ud][1] handle = self.node_map._index2hndl[index][1]
val = self._get_value(handle, col) val = self._get_value(handle, col)
#print 'val is', val, type(val) #print 'val is', val, type(val)
@ -833,13 +820,7 @@ class FlatBaseModel(GObject.GObject, Gtk.TreeModel):
Sets iter to the next node at this level of the tree Sets iter to the next node at this level of the tree
See Gtk.TreeModel See Gtk.TreeModel
""" """
#print 'do_iter_next', iter, iter.user_data return self.node_map.iter_next(iter)
handle = self.node_map.find_next_handle(iter)
if handle:
iter.user_data = self.node_map._hndl2index[handle[1]]
return True
else:
return False
def do_iter_children(self, iterparent): def do_iter_children(self, iterparent):
""" """
@ -883,7 +864,7 @@ class FlatBaseModel(GObject.GObject, Gtk.TreeModel):
#print 'do_iter_nth_child', iter, nth #print 'do_iter_nth_child', iter, nth
if iter == None: if iter == None:
return True, self.node_map.get_iter(nth) return True, self.node_map.get_iter(nth)
return False return False, None
def do_iter_parent(self, iter): def do_iter_parent(self, iter):
""" """
@ -891,4 +872,4 @@ class FlatBaseModel(GObject.GObject, Gtk.TreeModel):
See Gtk.TreeModel See Gtk.TreeModel
""" """
#print 'do_iter_parent' #print 'do_iter_parent'
return False return False, None

View File

@ -0,0 +1,528 @@
#
# Gramps - a GTK+/GNOME based genealogy program
#
# Copyright(C) 2014 Bastien Jacquet
#
# 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 Street, Fifth Floor, Boston, MA 02110-1301 USA.
#
from gi.overrides.Gtk import TreeView, Gtk
from gramps.gen.const import GRAMPS_LOCALE as glocale
"""
GtkWidget showing a box for interactive-search in Gtk.TreeView
"""
#-------------------------------------------------------------------------
#
# Python modules
#
#-------------------------------------------------------------------------
import logging
_LOG = logging.getLogger(".widgets.interactivesearch")
#-------------------------------------------------------------------------
#
# GTK modules
#
#-------------------------------------------------------------------------
from gi.repository import GObject, Gtk, Gdk
#-------------------------------------------------------------------------
#
# InteractiveSearchBox class
#
#-------------------------------------------------------------------------
class InteractiveSearchBox():
"""
Mainly adapted from gtktreeview.c
"""
_SEARCH_DIALOG_TIMEOUT = 5000
_SEARCH_DIALOG_LAUNCH_TIMEOUT = 150
def __init__(self, treeview):
self._treeview = treeview
self._search_window = None
self._search_entry = None
self._search_entry_changed_id = 0
self.__disable_popdown = False
self._entry_flush_timeout = None
self._entry_launchsearch_timeout = None
self.__selected_search_result = None
# Disable builtin interactive search by intercepting CTRL-F instead.
# self._treeview.connect('start-interactive-search',
# self.start_interactive_search)
def treeview_keypress(self, obj, event):
"""
function handling keypresses from the treeview
for the typeahead find capabilities
"""
if not event.string:
return False
if self._key_cancels_search(event.keyval):
return False
self.ensure_interactive_directory()
# Make a copy of the current text
old_text = self._search_entry.get_text()
popup_menu_id = self._search_entry.connect("popup-menu",
lambda x: True)
# Move the entry off screen
screen = self._treeview.get_screen()
self._search_window.move(screen.get_width() + 1,
screen.get_height() + 1)
self._search_window.show()
# Send the event to the window. If the preedit_changed signal is
# emitted during this event, we will set self.__imcontext_changed
new_event = Gdk.Event.copy(event)
new_event.window = self._search_window.get_window()
self._search_window.realize()
self.__imcontext_changed = False
retval = self._search_window.event(new_event)
self._search_window.hide()
self._search_entry.disconnect(popup_menu_id)
# Intercept CTRL+F keybinding because Gtk do not allow to _replace_ it.
default_accel = obj.get_modifier_mask(
Gdk.ModifierIntent.PRIMARY_ACCELERATOR)
if ((event.state & (default_accel | Gdk.ModifierType.CONTROL_MASK))
== (default_accel | Gdk.ModifierType.CONTROL_MASK)
and event.keyval in [Gdk.KEY_f, Gdk.KEY_F]):
self.__imcontext_changed = True
# self.real_start_interactive_search(event.get_device(), True)
# We check to make sure that the entry tried to handle the text,
# and that the text has changed.
new_text = self._search_entry.get_text()
text_modified = (old_text != new_text)
if (self.__imcontext_changed or # we're in a preedit
(retval and text_modified)): # ...or the text was modified
self.real_start_interactive_search(event.get_device(), False)
self._treeview.grab_focus()
return True
else:
self._search_entry.set_text("")
return False
def _preedit_changed(self, im_context, tree_view):
self.__imcontext_changed = 1
if(self._entry_flush_timeout):
GObject.source_remove(self._entry_flush_timeout)
self._entry_flush_timeout = GObject.timeout_add(
self._SEARCH_DIALOG_TIMEOUT, self.cb_entry_flush_timeout)
def ensure_interactive_directory(self):
toplevel = self._treeview.get_toplevel()
screen = self._treeview.get_screen()
if self._search_window:
if toplevel.has_group():
toplevel.get_group().add_window(self._search_window)
elif self._search_window.has_group():
self._search_window.get_group().remove_window(
self._search_window)
self._search_window.set_screen(screen)
return
self._search_window = Gtk.Window(Gtk.WindowType.POPUP)
self._search_window.set_screen(screen)
if toplevel.has_group():
toplevel.get_group().add_window(self._search_window)
self._search_window.set_type_hint(Gdk.WindowTypeHint.UTILITY)
self._search_window.set_modal(True)
self._search_window.connect("delete-event", self._delete_event)
self._search_window.connect("key-press-event", self._key_press_event)
self._search_window.connect("button-press-event",
self._button_press_event)
self._search_window.connect("scroll-event", self._scroll_event)
frame = Gtk.Frame()
frame.set_shadow_type(Gtk.ShadowType.ETCHED_IN)
frame.show()
self._search_window.add(frame)
vbox = Gtk.VBox()
vbox.show()
frame.add(vbox)
vbox.set_border_width(3)
""" add entry """
# To be change by Gtk 3.10 SearchEntry when agreed
if (Gtk.get_major_version(), Gtk.get_minor_version()) >= (3, 6):
self._search_entry = Gtk.SearchEntry()
else:
self._search_entry = Gtk.Entry()
self._search_entry.show()
self._search_entry.connect("populate-popup", self._disable_popdown)
self._search_entry.connect("activate", self._activate)
self._search_entry.connect("preedit-changed", self._preedit_changed)
vbox.add(self._search_entry)
self._search_entry.realize()
def real_start_interactive_search(self, device, keybinding):
"""
Pops up the interactive search entry. If keybinding is TRUE then
the user started this by typing the start_interactive_search
keybinding. Otherwise, it came from just typing
"""
if (self._search_window.get_visible()):
return True
self.ensure_interactive_directory()
if keybinding:
self._search_entry.set_text("")
self._position_func()
self._search_window.show()
if self._search_entry_changed_id == 0:
self._search_entry_changed_id = \
self._search_entry.connect("changed", self.delayed_changed)
# Grab focus without selecting all the text
self._search_entry.grab_focus()
self._search_entry.set_position(-1)
# send focus-in event
event = Gdk.Event(Gdk.EventType.FOCUS_CHANGE)
event.focus_change.in_ = True
event.focus_change.window = self._search_window.get_window()
self._search_entry.emit('focus-in-event', event)
# search first matching iter
self.delayed_changed(self._search_entry)
# uncomment when deleting delayed_changed
# self.search_init(self._search_entry)
return True
def cb_entry_flush_timeout(self):
event = Gdk.Event(Gdk.EventType.FOCUS_CHANGE)
event.focus_change.in_ = True
event.focus_change.window = self._treeview.get_window()
self._dialog_hide(event)
self._entry_flush_timeout = 0
return False
def delayed_changed(self, obj):
"""
This permits to start the search only a short delay after last keypress
This becomes useless with Gtk 3.10 Gtk.SearchEntry, which has a
'search-changed' signal.
"""
# renew flush timeout
self._renew_flush_timeout()
# renew search timeout
if self._entry_launchsearch_timeout:
GObject.source_remove(self._entry_launchsearch_timeout)
self._entry_launchsearch_timeout = GObject.timeout_add(
self._SEARCH_DIALOG_LAUNCH_TIMEOUT, self.search_init)
def search_init(self):
"""
This is the function performing the search
"""
self._entry_launchsearch_timeout = 0
text = self._search_entry.get_text()
if not text:
return
model = self._treeview.get_model()
selection = self._treeview.get_selection()
# disable flush timeout while searching
if self._entry_flush_timeout:
GObject.source_remove(self._entry_flush_timeout)
self._entry_flush_timeout = 0
# search
# cursor_path = self._treeview.get_cursor()[0]
# model.get_iter(cursor_path)
start_iter = model.get_iter_first()
self.search_iter(selection, start_iter, text, 0, 1)
self.__selected_search_result = 1
# renew flush timeout
self._renew_flush_timeout()
def _renew_flush_timeout(self):
if self._entry_flush_timeout:
GObject.source_remove(self._entry_flush_timeout)
self._entry_flush_timeout = GObject.timeout_add(
self._SEARCH_DIALOG_TIMEOUT, self.cb_entry_flush_timeout)
def _move(self, up=False):
text = self._search_entry.get_text()
if not text:
return
if up and self.__selected_search_result == 1:
return False
model = self._treeview.get_model()
selection = self._treeview.get_selection()
# disable flush timeout while searching
if self._entry_flush_timeout:
GObject.source_remove(self._entry_flush_timeout)
self._entry_flush_timeout = 0
# search
start_count = self.__selected_search_result + (-1 if up else 1)
start_iter = model.get_iter_first()
found_iter = self.search_iter(selection, start_iter, text, 0,
start_count)
if found_iter:
self.__selected_search_result += (-1 if up else 1)
return True
else:
# Return to old iter
self.search_iter(selection, start_iter, text, 0,
self.__selected_search_result)
return False
# renew flush timeout
self._renew_flush_timeout()
return
def _activate(self, obj):
self.cb_entry_flush_timeout()
# If we have a row selected and it's the cursor row, we activate
# the row XXX
# if self._cursor_node and \
# self._cursor_node.set_flag(Gtk.GTK_RBNODE_IS_SELECTED):
# path = _gtk_tree_path_new_from_rbtree(
# tree_view->priv->cursor_tree,
# tree_view->priv->cursor_node)
# gtk_tree_view_row_activated(tree_view, path,
# tree_view->priv->focus_column)
def _button_press_event(self, obj, event):
if not obj:
return
# keyb_device = event.device
event = Gdk.Event(Gdk.EventType.FOCUS_CHANGE)
event.focus_change.in_ = True
event.focus_change.window = self._treeview.get_window()
self._dialog_hide(event)
def _disable_popdown(self, obj, menu):
self.__disable_popdown = 1
menu.connect("hide", self._enable_popdown)
def _enable_popdown(self, obj):
self._timeout_enable_popdown = GObject.timeout_add(
self._SEARCH_DIALOG_TIMEOUT, self._real_search_enable_popdown)
def _real_search_enable_popdown(self):
self.__disable_popdown = 0
def _delete_event(self, obj, event):
if not obj:
return
self._dialog_hide(None)
def _scroll_event(self, obj, event):
retval = False
if (event.direction == Gdk.ScrollDirection.UP):
self._move(True)
retval = True
elif (event.direction == Gdk.ScrollDirection.DOWN):
self._move(False)
retval = True
if retval:
self._renew_flush_timeout()
def _key_cancels_search(self, keyval):
return keyval in [Gdk.KEY_Escape,
Gdk.KEY_Tab,
Gdk.KEY_KP_Tab,
Gdk.KEY_ISO_Left_Tab]
def _key_press_event(self, widget, event):
retval = False
# close window and cancel the search
if self._key_cancels_search(event.keyval):
self.cb_entry_flush_timeout()
return True
# Launch search
if (event.keyval in [Gdk.KEY_Return, Gdk.KEY_KP_Enter]):
if self._entry_launchsearch_timeout:
GObject.source_remove(self._entry_launchsearch_timeout)
self._entry_launchsearch_timeout = 0
self.search_init()
retval = True
default_accel = widget.get_modifier_mask(
Gdk.ModifierIntent.PRIMARY_ACCELERATOR)
# select previous matching iter
if ((event.keyval in [Gdk.KEY_Up, Gdk.KEY_KP_Up]) or
(((event.state & (default_accel | Gdk.ModifierType.SHIFT_MASK))
== (default_accel | Gdk.ModifierType.SHIFT_MASK))
and (event.keyval in [Gdk.KEY_g, Gdk.KEY_G]))):
if(not self._move(True)):
widget.error_bell()
retval = True
# select next matching iter
if ((event.keyval in [Gdk.KEY_Down, Gdk.KEY_KP_Down]) or
(((event.state & (default_accel | Gdk.ModifierType.SHIFT_MASK))
== (default_accel))
and (event.keyval in [Gdk.KEY_g, Gdk.KEY_G]))):
if(not self._move(False)):
widget.error_bell()
retval = True
# renew the flush timeout
if retval:
self._renew_flush_timeout()
return retval
def _dialog_hide(self, event):
if self.__disable_popdown:
return
if self._search_entry_changed_id:
self._search_entry.disconnect(self._search_entry_changed_id)
self._search_entry_changed_id = 0
if self._entry_flush_timeout:
GObject.source_remove(self._entry_flush_timeout)
self._entry_flush_timeout = 0
if self._entry_launchsearch_timeout:
GObject.source_remove(self._entry_launchsearch_timeout)
self._entry_launchsearch_timeout = 0
if self._search_window.get_visible():
# send focus-in event
self._search_entry.emit('focus-out-event', event)
self._search_window.hide()
self._search_entry.set_text("")
self._treeview.emit('focus-in-event', event)
self.__selected_search_result = None
def _position_func(self, userdata=None):
tree_window = self._treeview.get_window()
screen = self._treeview.get_screen()
monitor_num = screen.get_monitor_at_window(tree_window)
monitor = screen.get_monitor_workarea(monitor_num)
self._search_window.realize()
ret, tree_x, tree_y = tree_window.get_origin()
tree_width = tree_window.get_width()
tree_height = tree_window.get_height()
_, requisition = self._search_window.get_preferred_size()
if tree_x + tree_width > screen.get_width():
x = screen.get_width() - requisition.width
elif tree_x + tree_width - requisition.width < 0:
x = 0
else:
x = tree_x + tree_width - requisition.width
if tree_y + tree_height + requisition.height > screen.get_height():
y = screen.get_height() - requisition.height
elif(tree_y + tree_height < 0): # isn't really possible ...
y = 0
else:
y = tree_y + tree_height
self._search_window.move(x, y)
def search_iter_slow(self, selection, cur_iter, text, count, n):
"""
Standard row-by-row search through all rows
Should work for both List/Tree models
Both expanded and collapsed rows are searched.
"""
model = self._treeview.get_model()
search_column = self._treeview.get_search_column()
is_tree = not (model.get_flags() & Gtk.TreeModelFlags.LIST_ONLY)
while True:
if (self.search_equal_func(model, search_column,
text, cur_iter)):
count += 1
if (count == n):
found_path = model.get_path(cur_iter)
self._treeview.expand_to_path(found_path)
self._treeview.scroll_to_cell(found_path, None, 1, 0.5, 0)
selection.select_path(found_path)
self._treeview.set_cursor(found_path)
return True
if is_tree and model.iter_has_child(cur_iter):
cur_iter = model.iter_children(cur_iter)
else:
done = False
while True: # search iter of next row
next_iter = model.iter_next(cur_iter)
if next_iter:
cur_iter = next_iter
done = True
else:
cur_iter = model.iter_parent(cur_iter)
if(not cur_iter):
# we've run out of tree, done with this func
return False
if done:
break
return False
@staticmethod
def search_equal_func(model, search_column, text, cur_iter):
value = model.get_value(cur_iter, search_column)
key1 = value.lower()
key2 = text.lower()
return key1.startswith(key2)
def search_iter(self, selection, cur_iter, text, count, n):
model = self._treeview.get_model()
is_listonly = (model.get_flags() & Gtk.TreeModelFlags.LIST_ONLY)
if is_listonly and hasattr(model, "node_map"):
return self.search_iter_sorted_column_flat(selection, cur_iter,
text, count, n)
else:
return self.search_iter_slow(selection, cur_iter, text, count, n)
def search_iter_sorted_column_flat(self, selection, cur_iter, text,
count, n):
"""
Search among the currently set search-column for a cell starting with
text
It assumes that this column is currently sorted, and as
a LIST_ONLY view it therefore contains index2hndl = model.node_map._index2hndl
which is a _sorted_ list of (sortkey, handle) tuples
"""
model = self._treeview.get_model()
search_column = self._treeview.get_search_column()
is_tree = not (model.get_flags() & Gtk.TreeModelFlags.LIST_ONLY)
# If there is a sort_key index, let's use it
if not is_tree and hasattr(model, "node_map"):
import bisect
index2hndl = model.node_map._index2hndl
# create lookup key from the appropriate sort_func
# TODO: explicitely announce the data->sortkey func in models
# sort_key = model.sort_func(text)
sort_key = glocale.sort_key(text.lower())
srtkey_hndl = (sort_key, None)
lo_bound = 0 # model.get_path(cur_iter)
found_index = bisect.bisect_left(index2hndl, srtkey_hndl, lo=lo_bound)
# if insert position is at tail, no match
if found_index == len(index2hndl):
return False
srt_key, hndl = index2hndl[found_index]
# Check if insert position match for real
# (as insert position might not start with the text)
if not model[found_index][search_column].lower().startswith(text.lower()):
return False
found_path = Gtk.TreePath((model.node_map.real_path(found_index),))
self._treeview.scroll_to_cell(found_path, None, 1, 0.5, 0)
selection.select_path(found_path)
self._treeview.set_cursor(found_path)
return True
return False