1# 2# Gramps - a GTK+/GNOME based genealogy program 3# 4# Copyright (C) 2000-2006 Donald N. Allingham 5# Copyright (C) 2010 Benny Malengier 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# Python classes 25# 26#------------------------------------------------------------------------- 27from gramps.gen.const import GRAMPS_LOCALE as glocale 28_ = glocale.translation.sgettext 29 30#------------------------------------------------------------------------- 31# 32# GTK classes 33# 34#------------------------------------------------------------------------- 35from gi.repository import Gtk 36from gi.repository import Gdk 37from gi.repository import GObject 38from gi.repository import Pango 39_TAB = Gdk.keyval_from_name("Tab") 40_ENTER = Gdk.keyval_from_name("Enter") 41 42#------------------------------------------------------------------------- 43# 44# Gramps classes 45# 46#------------------------------------------------------------------------- 47from .surnamemodel import SurnameModel 48from .embeddedlist import EmbeddedList, TEXT_EDIT_COL 49from ...ddtargets import DdTargets 50from gramps.gen.lib import Surname, NameOriginType 51from ...utils import match_primary_mask, no_match_primary_mask 52 53# table for skipping illegal control chars 54INVISIBLE = dict.fromkeys(list(range(32))) 55 56#------------------------------------------------------------------------- 57# 58# SurnameTab 59# 60#------------------------------------------------------------------------- 61class SurnameTab(EmbeddedList): 62 63 _HANDLE_COL = 5 64 _DND_TYPE = DdTargets.SURNAME 65 66 _MSG = { 67 'add' : _('Create and add a new surname'), 68 'del' : _('Remove the selected surname'), 69 'edit' : _('Edit the selected surname'), 70 'up' : _('Move the selected surname upwards'), 71 'down' : _('Move the selected surname downwards'), 72 } 73 74 #index = column in model. Value = 75 # (name, sortcol in model, width, markup/text 76 _column_names = [ 77 (_('Prefix'), 0, 150, TEXT_EDIT_COL, -1, None), 78 (_('Surname'), 1, -1, TEXT_EDIT_COL, -1, None), 79 (_('Connector'), 2, 100, TEXT_EDIT_COL, -1, None), 80 ] 81 _column_combo = (_('Origin'), -1, 150, 3) # name, sort, width, modelcol 82 _column_toggle = (_('Name|Primary'), -1, 80, 4) 83 84 def __init__(self, dbstate, uistate, track, name, on_change=None, 85 top_label='<b>%s</b>' % _("Multiple Surnames") ): 86 self.obj = name 87 self.on_change = on_change 88 self.curr_col = -1 89 self.curr_cellr = None 90 self.curr_celle = None 91 92 EmbeddedList.__init__(self, dbstate, uistate, track, _('Family Surnames'), 93 SurnameModel, move_buttons=True, 94 top_label=top_label) 95 96 def build_columns(self): 97 #first the standard text columns with normal method 98 EmbeddedList.build_columns(self) 99 100 # now we add the two special columns 101 # combobox for type 102 colno = len(self.columns) 103 name = self._column_combo[0] 104 renderer = Gtk.CellRendererCombo() 105 renderer.set_property('ellipsize', Pango.EllipsizeMode.END) 106 # set up the comboentry editable 107 no = NameOriginType() 108 self.cmborig = Gtk.ListStore(GObject.TYPE_INT, GObject.TYPE_STRING) 109 self.cmborigmap = no.get_map().copy() 110 #sort the keys based on the value 111 keys = sorted(self.cmborigmap, key=lambda x: glocale.sort_key(self.cmborigmap[x])) 112 for key in keys: 113 if key != no.get_custom(): 114 self.cmborig.append(row=[key, self.cmborigmap[key]]) 115 additional = self.dbstate.db.get_origin_types() 116 if additional: 117 for type in additional: 118 if type: 119 self.cmborig.append(row=[no.get_custom(), type]) 120 renderer.set_property("model", self.cmborig) 121 renderer.set_property("text-column", 1) 122 renderer.set_property('editable', not self.dbstate.db.readonly) 123 124 renderer.connect('editing_started', self.on_edit_start_cmb, colno) 125 renderer.connect('edited', self.on_orig_edited, self._column_combo[3]) 126 # add to treeview 127 column = Gtk.TreeViewColumn(name, renderer, text=self._column_combo[3]) 128 column.set_resizable(True) 129 column.set_sort_column_id(self._column_combo[1]) 130 column.set_min_width(self._column_combo[2]) 131 column.set_expand(False) 132 self.columns.append(column) 133 self.tree.append_column(column) 134 # toggle box for primary 135 colno += 1 136 name = self._column_toggle[0] 137 renderer = Gtk.CellRendererToggle() 138 renderer.set_property('activatable', True) 139 renderer.set_property('radio', True) 140 renderer.connect( 'toggled', self.on_prim_toggled, self._column_toggle[3]) 141 # add to treeview 142 column = Gtk.TreeViewColumn(name, renderer, active=self._column_toggle[3]) 143 column.set_resizable(False) 144 column.set_sizing(Gtk.TreeViewColumnSizing.FIXED) 145 column.set_alignment(0.5) 146 column.set_sort_column_id(self._column_toggle[1]) 147 column.set_max_width(self._column_toggle[2]) 148 self.columns.append(column) 149 self.tree.append_column(column) 150 151## def by_value(self, first, second): 152## """ 153## Method for sorting keys based on the values. 154## """ 155## fvalue = self.cmborigmap[first] 156## svalue = self.cmborigmap[second] 157## return glocale.strcoll(fvalue, svalue) 158 159 def setup_editable_col(self): 160 """ 161 inherit this and set the variables needed for editable columns 162 Variable edit_col_funcs needs to be a dictionary from model col_nr to 163 function to call for 164 Example: 165 self.edit_col_funcs ={1: {'edit_start': self.on_edit_start, 166 'edited': self.on_edited 167 }} 168 """ 169 self.edit_col_funcs = { 170 0: {'edit_start': self.on_edit_start, 171 'edited': self.on_edit_inline}, 172 1: {'edit_start': self.on_edit_start, 173 'edited': self.on_edit_inline}, 174 2: {'edit_start': self.on_edit_start, 175 'edited': self.on_edit_inline}} 176 177 def get_data(self): 178 return self.obj.get_surname_list() 179 180 def is_empty(self): 181 return len(self.model)==0 182 183 def _get_surn_from_model(self): 184 """ 185 Return new surname_list for storing in the name based on content of 186 the model 187 """ 188 new_list = [] 189 for idx in range(len(self.model)): 190 node = self.model.get_iter(idx) 191 surn = self.model.get_value(node, 5) 192 surn.set_prefix(self.model.get_value(node, 0)) 193 surn.set_surname(self.model.get_value(node, 1)) 194 surn.set_connector(self.model.get_value(node, 2)) 195 surn.get_origintype().set(self.model.get_value(node, 3)) 196 surn.set_primary(self.model.get_value(node, 4)) 197 new_list += [surn] 198 return new_list 199 200 def update(self): 201 """ 202 Store the present data in the model to the name object 203 """ 204 new_map = self._get_surn_from_model() 205 self.obj.set_surname_list(new_map) 206 # update name in previews 207 if self.on_change: 208 self.on_change() 209 210 def post_rebuild(self, prebuildpath): 211 """ 212 Called when data model has changed, in particular necessary when row 213 order is updated. 214 @param prebuildpath: path selected before rebuild, None if none 215 @type prebuildpath: tree path 216 """ 217 if self.on_change: 218 self.on_change() 219 220 def column_order(self): 221 # order of columns for EmbeddedList. Only the text columns here 222 return ((1, 0), (1, 1), (1, 2)) 223 224 def add_button_clicked(self, obj): 225 """Add button is clicked, add a surname to the person""" 226 prim = False 227 if len(self.obj.get_surname_list()) == 0: 228 prim = True 229 node = self.model.append(row=['', '', '', str(NameOriginType()), prim, 230 Surname()]) 231 self.selection.select_iter(node) 232 path = self.model.get_path(node) 233 self.tree.set_cursor_on_cell(path, 234 focus_column=self.columns[0], 235 focus_cell=None, 236 start_editing=True) 237 self.update() 238 239 def del_button_clicked(self, obj): 240 """ 241 Delete button is clicked. Remove from the model 242 """ 243 (model, node) = self.selection.get_selected() 244 if node: 245 self.model.remove(node) 246 self.update() 247 248 def on_edit_start(self, cellr, celle, path, colnr): 249 """ start of editing. Store stuff so we know when editing ends where we 250 are 251 """ 252 self.curr_col = colnr 253 self.curr_cellr = cellr 254 self.curr_celle = celle 255 256 def on_edit_start_cmb(self, cellr, celle, path, colnr): 257 """ 258 An edit starts in the origin type column 259 This means a cmb has been created as celle, and we can set up the stuff 260 we want this cmb to contain: autocompletion, stop edit when selection 261 in the cmb happens. 262 """ 263 self.on_edit_start(cellr, celle, path, colnr) 264 #set up autocomplete 265 entry = celle.get_child() 266 entry.set_width_chars(10) 267 completion = Gtk.EntryCompletion() 268 completion.set_model(self.cmborig) 269 completion.set_minimum_key_length(1) 270 completion.set_text_column(1) 271 entry.set_completion(completion) 272 # 273 celle.connect('changed', self.on_origcmb_change, path, colnr) 274 275 def on_edit_start_toggle(self, cellr, celle, path, colnr): 276 """ 277 Edit 278 """ 279 self.on_edit_start(cellr, celle, path, colnr) 280 281 def on_edit_inline(self, cell, path, new_text, colnr): 282 """ 283 Edit is happening. The model is updated and the surname objects updated. 284 colnr must be the column in the model. 285 """ 286 node = self.model.get_iter(path) 287 text = new_text.translate(INVISIBLE).strip() 288 self.model.set_value(node, colnr, text) 289 self.update() 290 291 def on_orig_edited(self, cellr, path, new_text, colnr): 292 """ 293 An edit is finished in the origin type column. For a cmb in an editor, 294 the model may only be updated when typing is finished, as editing stops 295 automatically on update of the model. 296 colnr must be the column in the model. 297 """ 298 self.on_edit_inline(cellr, path, new_text, colnr) 299 300 def on_origcmb_change(self, cmb, path, colnr): 301 """ 302 A selection occured in the cmb of the origin type column. colnr must 303 be the column in the model. 304 """ 305 act = cmb.get_active() 306 if act == -1: 307 return 308 self.on_orig_edited(None, path, 309 self.cmborig.get_value( 310 self.cmborig.get_iter((act,)),1), 311 colnr) 312 313 def on_prim_toggled(self, cell, path, colnr): 314 """ 315 Primary surname on path is toggled. colnr must be the col 316 in the model 317 """ 318 #obtain current value 319 node = self.model.get_iter(path) 320 old_val = self.model.get_value(node, colnr) 321 for nr in range(len(self.obj.get_surname_list())): 322 if nr == int(path[0]): 323 if old_val: 324 #True remains True 325 break 326 else: 327 #This value becomes True 328 self.model.set_value(self.model.get_iter((nr,)), colnr, True) 329 else: 330 self.model.set_value(self.model.get_iter((nr,)), colnr, False) 331 self.update() 332 return 333 334 def edit_button_clicked(self, obj): 335 """ Edit button clicked 336 """ 337 (model, node) = self.selection.get_selected() 338 if node: 339 path = self.model.get_path(node) 340 self.tree.set_cursor_on_cell(path, 341 focus_column=self.columns[0], 342 focus_cell=None, 343 start_editing=True) 344 345 def key_pressed(self, obj, event): 346 """ 347 Handles the key being pressed. 348 Here we make sure tab moves to next or previous value in row on TAB 349 """ 350 if not EmbeddedList.key_pressed(self, obj, event): 351 if event.type == Gdk.EventType.KEY_PRESS and event.keyval in (_TAB,): 352 if no_match_primary_mask(event.get_state(), 353 Gdk.ModifierType.SHIFT_MASK): 354 return self.next_cell() 355 elif match_primary_mask(event.get_state(), Gdk.ModifierType.SHIFT_MASK): 356 return self.prev_cell() 357 else: 358 return 359 else: 360 return 361 return True 362 363 def next_cell(self): 364 """ 365 Move to the next cell to edit it 366 """ 367 (model, node) = self.selection.get_selected() 368 if node: 369 path = self.model.get_path(node).get_indices()[0] 370 nccol = self.curr_col+1 371 if nccol < 4: 372 if self.curr_celle: 373 self.curr_celle.editing_done() 374 self.tree.set_cursor_on_cell(Gtk.TreePath((path,)), 375 focus_column=self.columns[nccol], 376 focus_cell=None, 377 start_editing=True) 378 elif nccol == 4: 379 #go to next line if there is one 380 if path < len(self.obj.get_surname_list()): 381 newpath = Gtk.TreePath((path+1,)) 382 self.curr_celle.editing_done() 383 self.selection.select_path(newpath) 384 self.tree.set_cursor_on_cell(newpath, 385 focus_column=self.columns[0], 386 focus_cell=None, 387 start_editing=True) 388 else: 389 #stop editing 390 self.curr_celle.editing_done() 391 return 392 return True 393 394 395 def prev_cell(self): 396 """ 397 Move to the next cell to edit it 398 """ 399 (model, node) = self.selection.get_selected() 400 if node: 401 path = self.model.get_path(node).get_indices()[0] 402 if self.curr_col > 0: 403 self.tree.set_cursor_on_cell(Gtk.TreePath((path,)), 404 focus_column=self.columns[self.curr_col-1], 405 focus_cell=None, 406 start_editing=True) 407 elif self.curr_col == 0: 408 #go to prev line if there is one 409 if path > 0: 410 newpath = Gtk.TreePath((path-1,)) 411 self.selection.select_path(newpath) 412 self.tree.set_cursor_on_cell(newpath, 413 focus_column=self.columns[-2], 414 focus_cell=None, 415 start_editing=True) 416 else: 417 #stop editing 418 self.curr_celle.editing_done() 419 return 420 return True 421