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
commit 8fd456f604
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 ..filters import SearchBar
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.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
self.search_bar = SearchBar(dbstate, uistate, self.build_tree)
filter_box = self.search_bar.build()

View File

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

View File

@ -200,7 +200,7 @@ class FlatNodeMap(object):
else:
self.__corr = (0, 1)
if not self._hndl2index:
self._hndl2index = dict([key[1], index]
self._hndl2index = dict((key[1], index)
for index, key in enumerate(self._index2hndl))
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
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
"""
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
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
"""
index = self._hndl2index.get(handle)
@ -267,7 +267,7 @@ class FlatNodeMap(object):
:param handle: the key of the object for which the sortkey
is needed
:param type: an object handle
:type handle: an object handle
:Returns: the sortkey, or None if handle is not present
"""
index = self._hndl2index.get(handle)
@ -317,19 +317,15 @@ class FlatNodeMap(object):
if not isinstance(handle, UNITYPE):
handle = handle.decode('utf-8')
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
by finding the index associated with the iter, adding or substracting
one to find the next index, then finding the handle associated with
that.
Increments the iter y finding the index associated with the iter,
adding or substracting one.
False is returned if no next handle
True, handle tuple otherwise
:param handle: the key of the object for which the next handle shown
in the treeview is needed
:param type: an object handle
:param iter: Gtk.TreeModel iterator
:param type: Gtk.TreeIter
"""
index = iter.user_data
if index is None:
@ -346,11 +342,10 @@ class FlatNodeMap(object):
return False
else:
index += 1
try:
return True, self._index2hndl[index][1]
except IndexError:
return False
if index >= len(self._index2hndl):
return False
iter.user_data = index
return True
def get_first_iter(self):
"""
@ -381,6 +376,7 @@ class FlatNodeMap(object):
Returns the path of the inserted row
: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 type: Gtk.TreePath or None
@ -397,14 +393,8 @@ class FlatNodeMap(object):
insert_pos = bisect.bisect_left(self._index2hndl, srtkey_hndl)
self._index2hndl.insert(insert_pos, srtkey_hndl)
#make sure the index map is updated
if sys.version_info[0] < 3: # keep this, for speed in Python2
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
for srt_key,hndl in self._index2hndl[insert_pos+1:]:
self._hndl2index[hndl] += 1
self._hndl2index[srtkey_hndl[1]] = insert_pos
#update self.__corr so it remains correct
if self._reverse:
@ -446,14 +436,8 @@ class FlatNodeMap(object):
if self._reverse:
self.__corr = (len(self._index2hndl) - 1, -1)
#update the handle2path map so it remains correct
if sys.version_info[0] < 3: # keep this, for speed in Python2
for key, val in self._hndl2index.iteritems(): # in Python2 "if"
if val > index:
self._hndl2index[key] -= 1
else:
for key, val in self._hndl2index.items():
if val > index:
self._hndl2index[key] -= 1
for srt_key,hndl in self._index2hndl[index:]:
self._hndl2index[hndl] -= 1
return Gtk.TreePath((delpath,))
#-------------------------------------------------------------------------
@ -491,6 +475,7 @@ class FlatBaseModel(GObject.GObject, Gtk.TreeModel):
col = self.sort_map[scol][1]
else:
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_col = scol
self.skip = skip
@ -592,7 +577,9 @@ class FlatBaseModel(GObject.GObject, Gtk.TreeModel):
# use cursor as a context manager
with self.gen_cursor() as cursor:
#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):
""" 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.
"""
ud = iter.user_data
if ud is None:
index = iter.user_data
if index is None:
##GTK3: user data may only be an integer, we store the index
##PROBLEM: pygobject 3.8 stores 0 as None, we need to correct
## when using user_data for that!
##upstream bug: https://bugzilla.gnome.org/show_bug.cgi?id=698366
ud = 0
index = self.node_map.real_index(ud)
return self.node_map.get_handle(index)
index = 0
path = self.node_map.real_path(index)
return self.node_map.get_handle(path)
# 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!
"""
#print ('do_get_val', iter, iter.user_data, col)
ud = iter.user_data
if ud is None:
index = iter.user_data
if index is None:
##GTK3: user data may only be an integer, we store the index
##PROBLEM: pygobject 3.8 stores 0 as None, we need to correct
## when using user_data for that!
##upstream bug: https://bugzilla.gnome.org/show_bug.cgi?id=698366
ud = 0
handle = self.node_map._index2hndl[ud][1]
index = 0
handle = self.node_map._index2hndl[index][1]
val = self._get_value(handle, col)
#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
See Gtk.TreeModel
"""
#print 'do_iter_next', iter, iter.user_data
handle = self.node_map.find_next_handle(iter)
if handle:
iter.user_data = self.node_map._hndl2index[handle[1]]
return True
else:
return False
return self.node_map.iter_next(iter)
def do_iter_children(self, iterparent):
"""
@ -883,7 +864,7 @@ class FlatBaseModel(GObject.GObject, Gtk.TreeModel):
#print 'do_iter_nth_child', iter, nth
if iter == None:
return True, self.node_map.get_iter(nth)
return False
return False, None
def do_iter_parent(self, iter):
"""
@ -891,4 +872,4 @@ class FlatBaseModel(GObject.GObject, Gtk.TreeModel):
See Gtk.TreeModel
"""
#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