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