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