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:
commit
8fd456f604
@ -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()
|
||||
|
@ -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):
|
||||
|
@ -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
|
||||
|
528
gramps/gui/widgets/interactivesearchbox.py
Normal file
528
gramps/gui/widgets/interactivesearchbox.py
Normal 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
|
Loading…
Reference in New Issue
Block a user