1#
2# Gramps - a GTK+/GNOME based genealogy program
3#
4# Copyright (C) 2003-2006  Donald N. Allingham
5#               2009-2011  Gary Burton
6#
7# This program is free software; you can redistribute it and/or modify
8# it under the terms of the GNU General Public License as published by
9# the Free Software Foundation; either version 2 of the License, or
10# (at your option) any later version.
11#
12# This program is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15# GNU General Public License for more details.
16#
17# You should have received a copy of the GNU General Public License
18# along with this program; if not, write to the Free Software
19# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
20#
21
22#-------------------------------------------------------------------------
23#
24# GTK/Gnome modules
25#
26#-------------------------------------------------------------------------
27from gi.repository import Gtk
28from gi.repository import Pango
29
30#-------------------------------------------------------------------------
31#
32# gramps modules
33#
34#-------------------------------------------------------------------------
35from ..managedwindow import ManagedWindow
36from ..filters import SearchBar
37from ..glade import Glade
38from ..widgets.interactivesearchbox import InteractiveSearchBox
39from ..display import display_help
40from gramps.gen.const import URL_MANUAL_PAGE
41
42#-------------------------------------------------------------------------
43#
44# SelectEvent
45#
46#-------------------------------------------------------------------------
47class BaseSelector(ManagedWindow):
48    """Base class for the selectors, showing a dialog from which to select
49        one of the primary objects
50    """
51
52    NONE = -1
53    TEXT = 0
54    MARKUP = 1
55    IMAGE = 2
56
57    def __init__(self, dbstate, uistate, track=[], filter=None, skip=set(),
58                 show_search_bar = True, default=None):
59        """Set up the dialog with the dbstate and uistate, track of parent
60            windows for ManagedWindow, initial filter for the model, skip with
61            set of handles to skip in the view, and search_bar to show the
62            SearchBar at the top or not.
63        """
64        self.filter = (2, filter, False)
65
66        # Set window title, some selectors may set self.title in their __init__
67        if not hasattr(self, 'title'):
68            self.title = self.get_window_title()
69
70        ManagedWindow.__init__(self, uistate, track, self)
71
72        self.renderer = Gtk.CellRendererText()
73        self.track_ref_for_deletion("renderer")
74        self.renderer.set_property('ellipsize',Pango.EllipsizeMode.END)
75
76        self.db = dbstate.db
77        self.tree = None
78        self.model = None
79
80        self.glade = Glade()
81
82        window = self.glade.toplevel
83        self.showall = self.glade.get_object('showall')
84        title_label = self.glade.get_object('title')
85        vbox = self.glade.get_object('select_person_vbox')
86        self.tree = self.glade.get_object('plist')
87        self.tree.set_headers_visible(True)
88        self.tree.set_headers_clickable(True)
89        self.tree.connect('row-activated', self._on_row_activated)
90        self.tree.grab_focus()
91        self.define_help_button(
92            self.glade.get_object('help'), self.WIKI_HELP_PAGE,
93            self.WIKI_HELP_SEC)
94
95        # connect to signal for custom interactive-search
96        self.searchbox = InteractiveSearchBox(self.tree)
97        self.tree.connect('key-press-event', self.searchbox.treeview_keypress)
98
99        #add the search bar
100        self.search_bar = SearchBar(dbstate, uistate, self.build_tree, apply_clear=self.apply_clear)
101        filter_box = self.search_bar.build()
102        self.setup_filter()
103        vbox.pack_start(filter_box, False, False, 0)
104        vbox.reorder_child(filter_box, 1)
105
106        self.set_window(window,title_label,self.title)
107
108        #set up sorting
109        self.sort_col = 0
110        self.setupcols = True
111        self.columns = []
112        self.sortorder = Gtk.SortType.ASCENDING
113
114        self.skip_list=skip
115        self.selection = self.tree.get_selection()
116        self.track_ref_for_deletion("selection")
117
118        self._local_init()
119        self._set_size()
120
121        self.show()
122        #show or hide search bar?
123        self.set_show_search_bar(show_search_bar)
124        #Hide showall if no filter is specified
125        if self.filter[1] is not None:
126            self.showall.connect('toggled', self.show_toggle)
127            self.showall.show()
128        else:
129            self.showall.hide()
130        while Gtk.events_pending():
131            Gtk.main_iteration()
132        self.build_tree()
133        loading = self.glade.get_object('loading')
134        loading.hide()
135
136        if default:
137            self.goto_handle(default)
138
139    def goto_handle(self, handle):
140        """
141        Goto the correct row.
142        """
143        iter_ = self.model.get_iter_from_handle(handle)
144        if iter_:
145            if not (self.model.get_flags() & Gtk.TreeModelFlags.LIST_ONLY):
146                # Expand tree
147                parent_iter = self.model.iter_parent(iter_)
148                if parent_iter:
149                    parent_path = self.model.get_path(parent_iter)
150                    if parent_path:
151                        parent_path_list = parent_path.get_indices()
152                        for i in range(len(parent_path_list)):
153                            expand_path = Gtk.TreePath(
154                                    tuple([x for x in parent_path_list[:i+1]]))
155                            self.tree.expand_row(expand_path, False)
156
157            # Select active object
158            path = self.model.get_path(iter_)
159            self.selection.unselect_all()
160            self.selection.select_path(path)
161            self.tree.scroll_to_cell(path, None, 1, 0.5, 0)
162        else:
163            self.selection.unselect_all()
164
165    def add_columns(self,tree):
166        tree.set_fixed_height_mode(True)
167        titles = self.get_column_titles()
168        for ix in range(len(titles)):
169            item = titles[ix]
170            if item[2] == BaseSelector.NONE:
171                continue
172            elif item[2] == BaseSelector.TEXT:
173                column = Gtk.TreeViewColumn(item[0],self.renderer,text=item[3])
174            elif item[2] == BaseSelector.MARKUP:
175                column = Gtk.TreeViewColumn(item[0],self.renderer,markup=item[3])
176            column.set_sizing(Gtk.TreeViewColumnSizing.FIXED)
177            column.set_fixed_width(item[1])
178            column.set_resizable(True)
179            #connect click
180            column.connect('clicked', self.column_clicked, ix)
181            column.set_clickable(True)
182            ##column.set_sort_column_id(ix) # model has its own sort implemented
183            self.columns.append(column)
184            tree.append_column(column)
185
186    def build_menu_names(self, obj):
187        return (self.title, None)
188
189    def get_selected_ids(self):
190        mlist = []
191        self.selection.selected_foreach(self.select_function, mlist)
192        return mlist
193
194    def first_selected(self):
195        """ first selected entry in the Selector tree
196        """
197        mlist = []
198        self.selection.selected_foreach(self.select_function, mlist)
199        return mlist[0] if mlist else None
200
201    def select_function(self, store, path, iter_, id_list):
202        handle = store.get_handle_from_iter(iter_)
203        id_list.append(handle)
204
205    def run(self):
206        val = self.window.run()
207        result = None
208        if val == Gtk.ResponseType.OK:
209            id_list = self.get_selected_ids()
210            if id_list and id_list[0]:
211                result = self.get_from_handle_func()(id_list[0])
212            self.close()
213        elif val != Gtk.ResponseType.DELETE_EVENT:
214            self.close()
215        return result
216
217    def _on_row_activated(self, treeview, path, view_col):
218        self.window.response(Gtk.ResponseType.OK)
219
220    def _local_init(self):
221        # define selector-specific init routine
222        pass
223
224    def get_window_title(self):
225        assert False, "Must be defined in the subclass"
226
227    def get_model_class(self):
228        assert False, "Must be defined in the subclass"
229
230    def get_column_titles(self):
231        """
232        Defines the columns to show in the selector. Must be defined in the
233        subclasses.
234        :returns: a list of tuples with four entries. The four entries should
235                be 0: column header string, 1: column width,
236                2: TEXT, MARKUP or IMAGE, 3: column in the model that must be
237                used.
238        """
239        raise NotImplementedError
240
241    def get_from_handle_func(self):
242        assert False, "Must be defined in the subclass"
243
244    def set_show_search_bar(self, value):
245        """make the search bar at the top shown
246        """
247        self.show_search_bar = value
248        if not self.search_bar :
249            return
250        if self.show_search_bar :
251            self.search_bar.show()
252        else :
253            self.search_bar.hide()
254
255    def column_order(self):
256        """
257        returns a tuple indicating the column order of the model
258        """
259        return [(1, row[3], row[1], row[0]) for row in self.get_column_titles()]
260
261    def exact_search(self):
262        """
263        Returns a tuple indicating columns requiring an exact search
264        """
265        return ()
266
267    def setup_filter(self):
268        """
269        Builds the default filters and add them to the filter bar.
270        """
271        cols = [(pair[3], pair[1], pair[0] in self.exact_search())
272                    for pair in self.column_order()
273                        if pair[0]
274                ]
275        self.search_bar.setup_filter(cols)
276
277    def build_tree(self):
278        """
279        Builds the selection people see in the Selector
280        """
281        if not self.filter[1]:
282            filter_info = (False, self.search_bar.get_value(), False)
283        else:
284            filter_info = self.filter
285        if self.model:
286            sel = self.first_selected()
287        else:
288            sel = None
289
290        #set up cols the first time
291        if self.setupcols :
292            self.add_columns(self.tree)
293
294        #reset the model with correct sorting
295        self.clear_model()
296        self.model = self.get_model_class()(
297            self.db, self.uistate, self.sort_col, self.sortorder,
298            sort_map=self.column_order(), skip=self.skip_list,
299            search=filter_info)
300
301        self.tree.set_model(self.model)
302
303        #sorting arrow in column header (not on start, only on click)
304        if not self.setupcols :
305            for i in range(len(self.columns)):
306                enable_sort_flag = (i==self.sort_col)
307                self.columns[i].set_sort_indicator(enable_sort_flag)
308            self.columns[self.sort_col].set_sort_order(self.sortorder)
309
310        # set the search column to be the sorted column
311        search_col = self.column_order()[self.sort_col][1]
312        self.tree.set_search_column(search_col)
313
314        self.setupcols = False
315        if sel:
316            self.goto_handle(sel)
317
318    def column_clicked(self, obj, data):
319        if self.sort_col != data:
320            self.sortorder = Gtk.SortType.ASCENDING
321            self.sort_col = data
322        else:
323            if (self.columns[data].get_sort_order() == Gtk.SortType.DESCENDING
324                or not self.columns[data].get_sort_indicator()):
325                self.sortorder = Gtk.SortType.ASCENDING
326            else:
327                self.sortorder = Gtk.SortType.DESCENDING
328        self.build_tree()
329
330        return True
331
332    def show_toggle(self, obj):
333        filter_info = None if obj.get_active() else self.filter
334        self.clear_model()
335        self.model = self.get_model_class()(
336            self.db, self.uistate, self.sort_col, self.sortorder,
337            sort_map=self.column_order(), skip=self.skip_list,
338            search=filter_info)
339        self.tree.set_model(self.model)
340        self.tree.grab_focus()
341
342    def clear_model(self):
343        if self.model:
344            self.tree.set_model(None)
345            if hasattr(self.model, 'destroy'):
346                self.model.destroy()
347            self.model = None
348
349    def apply_clear(self):
350        self.showall.set_active(False)
351
352    def _cleanup_on_exit(self):
353        """Unset all things that can block garbage collection.
354        Finalize rest
355        """
356        self.clear_model()
357        self.db = None
358        self.tree = None
359        self.columns = None
360        self.search_bar.destroy()
361
362    def close(self, *obj):
363        ManagedWindow.close(self)
364        self._cleanup_on_exit()
365
366    def define_help_button(self, button, webpage='', section=''):
367        """ Setup to deal with help button """
368        button.connect('clicked', lambda x: display_help(webpage, section))
369