1#
2# Gramps - a GTK+/GNOME based genealogy program
3#
4# Copyright (C) 2007-2008  Brian G. Matherly
5# Copyright (C) 2008,2010  Gary Burton
6# Copyright (C) 2008       Craig J. Anderson
7# Copyright (C) 2009       Nick Hall
8# Copyright (C) 2010       Jakim Friant
9# Copyright (C) 2011       Adam Stein <adam@csh.rit.edu>
10# Copyright (C) 2011-2013  Paul Franklin
11#
12# This program is free software; you can redistribute it and/or modify
13# it under the terms of the GNU General Public License as published by
14# the Free Software Foundation; either version 2 of the License, or
15# (at your option) any later version.
16#
17# This program is distributed in the hope that it will be useful,
18# but WITHOUT ANY WARRANTY; without even the implied warranty of
19# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
20# GNU General Public License for more details.
21#
22# You should have received a copy of the GNU General Public License
23# along with this program; if not, write to the Free Software
24# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
25#
26
27"""
28Specific option handling for a GUI.
29"""
30
31#------------------------------------------------------------------------
32#
33# python modules
34#
35#------------------------------------------------------------------------
36import os
37
38#-------------------------------------------------------------------------
39#
40# gtk modules
41#
42#-------------------------------------------------------------------------
43from gi.repository import Gtk
44from gi.repository import Gdk
45from gi.repository import GObject
46
47#-------------------------------------------------------------------------
48#
49# gramps modules
50#
51#-------------------------------------------------------------------------
52from ..utils import ProgressMeter
53from ..pluginmanager import GuiPluginManager
54from .. import widgets
55from ..managedwindow import ManagedWindow
56from ..dialog import OptionDialog
57from ..selectors import SelectorFactory
58from gramps.gen.errors import HandleError
59from gramps.gen.display.name import displayer as _nd
60from gramps.gen.display.place import displayer as _pd
61from gramps.gen.filters import GenericFilterFactory, GenericFilter, rules
62from gramps.gen.constfunc import get_curr_dir
63from gramps.gen.const import GRAMPS_LOCALE as glocale
64_ = glocale.translation.gettext
65
66#------------------------------------------------------------------------
67#
68# Dialog window used to select a surname
69#
70#------------------------------------------------------------------------
71class LastNameDialog(ManagedWindow):
72    """
73    A dialog that allows the selection of a surname from the database.
74    """
75    def __init__(self, database, uistate, track, surnames, skip_list=set()):
76
77        ManagedWindow.__init__(self, uistate, track, self, modal=True)
78        flags = Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT
79        buttons = (_('_Cancel'), Gtk.ResponseType.REJECT,
80                   _('_OK'), Gtk.ResponseType.ACCEPT)
81        self.__dlg = Gtk.Dialog(None, uistate.window, flags, buttons)
82        self.set_window(self.__dlg, None, _('Select surname'))
83        self.setup_configs('interface.lastnamedialog', 400, 400)
84
85        # build up a container to display all of the people of interest
86        self.__model = Gtk.ListStore(GObject.TYPE_STRING, GObject.TYPE_INT)
87        self.__tree_view = Gtk.TreeView(self.__model)
88        col1 = Gtk.TreeViewColumn(_('Surname'), Gtk.CellRendererText(), text=0)
89        col2 = Gtk.TreeViewColumn(_('Count'), Gtk.CellRendererText(), text=1)
90        col1.set_resizable(True)
91        col2.set_resizable(True)
92        col1.set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE)
93        col2.set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE)
94        col1.set_sort_column_id(0)
95        col2.set_sort_column_id(1)
96        self.__tree_view.append_column(col1)
97        self.__tree_view.append_column(col2)
98        scrolled_window = Gtk.ScrolledWindow()
99        scrolled_window.add(self.__tree_view)
100        scrolled_window.set_policy(Gtk.PolicyType.AUTOMATIC,
101                                   Gtk.PolicyType.AUTOMATIC)
102        scrolled_window.set_shadow_type(Gtk.ShadowType.OUT)
103        self.__dlg.vbox.pack_start(scrolled_window, True, True, 0)
104        self.show()
105
106        if len(surnames) == 0:
107            # we could use database.get_surname_list(), but if we do that
108            # all we get is a list of names without a count...therefore
109            # we'll traverse the entire database ourself and build up a
110            # list that we can use
111#            for name in database.get_surname_list():
112#                self.__model.append([name, 0])
113
114            # build up the list of surnames, keeping track of the count for each
115            # name (this can be a lengthy process, so by passing in the
116            # dictionary we can be certain we only do this once)
117            progress = ProgressMeter(
118                _('Finding Surnames'), parent=uistate.window)
119            progress.set_pass(_('Finding surnames'),
120                              database.get_number_of_people())
121            for person in database.iter_people():
122                progress.step()
123                key = person.get_primary_name().get_surname()
124                count = 0
125                if key in surnames:
126                    count = surnames[key]
127                surnames[key] = count + 1
128            progress.close()
129
130        # insert the names and count into the model
131        for key in surnames:
132            if key.encode('iso-8859-1', 'xmlcharrefreplace') not in skip_list:
133                self.__model.append([key, surnames[key]])
134
135        # keep the list sorted starting with the most popular last name
136        # (but after sorting the whole list alphabetically first, so
137        # that surnames with the same number of people will be alphabetical)
138        self.__model.set_sort_column_id(0, Gtk.SortType.ASCENDING)
139        self.__model.set_sort_column_id(1, Gtk.SortType.DESCENDING)
140
141        # the "OK" button should be enabled/disabled based on the selection of
142        # a row
143        self.__tree_selection = self.__tree_view.get_selection()
144        self.__tree_selection.set_mode(Gtk.SelectionMode.MULTIPLE)
145        self.__tree_selection.select_path(0)
146
147    def run(self):
148        """
149        Display the dialog and return the selected surnames when done.
150        """
151        response = self.__dlg.run()
152        surname_set = set()
153        if response == Gtk.ResponseType.ACCEPT:
154            (mode, paths) = self.__tree_selection.get_selected_rows()
155            for path in paths:
156                tree_iter = self.__model.get_iter(path)
157                surname = self.__model.get_value(tree_iter, 0)
158                surname_set.add(surname)
159        if response != Gtk.ResponseType.DELETE_EVENT:
160            # ManagedWindow: set the parent dialog to be modal again
161            self.close()
162        return surname_set
163
164    def build_menu_names(self, obj):
165        return (_('Select surname'), None)
166
167#-------------------------------------------------------------------------
168#
169# GuiStringOption class
170#
171#-------------------------------------------------------------------------
172class GuiStringOption(Gtk.Entry):
173    """
174    This class displays an option that is a simple one-line string.
175    """
176    def __init__(self, option, dbstate, uistate, track, override):
177        """
178        @param option: The option to display.
179        @type option: gen.plug.menu.StringOption
180        @return: nothing
181        """
182        Gtk.Entry.__init__(self)
183        self.__option = option
184        self.set_text(self.__option.get_value())
185
186        # Set up signal handlers when the widget value is changed
187        # from user interaction or programmatically.  When handling
188        # a specific signal, we need to temporarily block the signal
189        # that would call the other signal handler.
190        self.changekey = self.connect('changed', self.__text_changed)
191        self.valuekey = self.__option.connect('value-changed',
192                                              self.__value_changed)
193
194        self.conkey = self.__option.connect('avail-changed',
195                                            self.__update_avail)
196        self.__update_avail()
197
198        self.set_tooltip_text(self.__option.get_help())
199
200    def __text_changed(self, obj): # IGNORE:W0613 - obj is unused
201        """
202        Handle the change of the value made by the user.
203        """
204        self.__option.disable_signals()
205        self.__option.set_value(self.get_text())
206        self.__option.enable_signals()
207
208    def __update_avail(self):
209        """
210        Update the availability (sensitivity) of this widget.
211        """
212        avail = self.__option.get_available()
213        self.set_sensitive(avail)
214
215    def __value_changed(self):
216        """
217        Handle the change made programmatically
218        """
219        self.handler_block(self.changekey)
220        self.set_text(self.__option.get_value())
221        self.handler_unblock(self.changekey)
222
223    def clean_up(self):
224        """
225        remove stuff that blocks garbage collection
226        """
227        self.__option.disconnect(self.valuekey)
228        self.__option.disconnect(self.conkey)
229        self.__option = None
230
231#-------------------------------------------------------------------------
232#
233# GuiColorOption class
234#
235#-------------------------------------------------------------------------
236class GuiColorOption(Gtk.ColorButton):
237    """
238    This class displays an option that allows the selection of a colour.
239    """
240    def __init__(self, option, dbstate, uistate, track, override):
241        self.__option = option
242        Gtk.ColorButton.__init__(self)
243        rgba = Gdk.RGBA()
244        rgba.parse(self.__option.get_value())
245        self.set_rgba(rgba)
246
247        # Set up signal handlers when the widget value is changed
248        # from user interaction or programmatically.  When handling
249        # a specific signal, we need to temporarily block the signal
250        # that would call the other signal handler.
251        self.changekey = self.connect('color-set', self.__color_changed)
252        self.valuekey = self.__option.connect('value-changed',
253                                              self.__value_changed)
254
255        self.conkey = self.__option.connect('avail-changed',
256                                            self.__update_avail)
257        self.__update_avail()
258
259        self.set_tooltip_text(self.__option.get_help())
260
261    def __color_changed(self, obj): # IGNORE:W0613 - obj is unused
262        """
263        Handle the change of color made by the user.
264        """
265        rgba = self.get_rgba()
266        value = '#%02x%02x%02x' % (int(rgba.red * 255),
267                                   int(rgba.green * 255),
268                                   int(rgba.blue * 255))
269
270        self.__option.disable_signals()
271        self.__option.set_value(value)
272        self.__option.enable_signals()
273
274    def __update_avail(self):
275        """
276        Update the availability (sensitivity) of this widget.
277        """
278        avail = self.__option.get_available()
279        self.set_sensitive(avail)
280
281    def __value_changed(self):
282        """
283        Handle the change made programmatically
284        """
285        self.handler_block(self.changekey)
286        rgba = Gdk.RGBA()
287        rgba.parse(self.__option.get_value())
288        self.set_rgba(rgba)
289        self.handler_unblock(self.changekey)
290
291    def clean_up(self):
292        """
293        remove stuff that blocks garbage collection
294        """
295        self.__option.disconnect(self.valuekey)
296        self.__option.disconnect(self.conkey)
297        self.__option = None
298
299#-------------------------------------------------------------------------
300#
301# GuiNumberOption class
302#
303#-------------------------------------------------------------------------
304class GuiNumberOption(Gtk.SpinButton):
305    """
306    This class displays an option that is a simple number with defined maximum
307    and minimum values.
308    """
309    def __init__(self, option, dbstate, uistate, track, override):
310        self.__option = option
311
312        decimals = 0
313        step = self.__option.get_step()
314        adj = Gtk.Adjustment(value=1,
315                             lower=self.__option.get_min(),
316                             upper=self.__option.get_max(),
317                             step_increment=step)
318
319        # Calculate the number of decimal places if necessary
320        if step < 1:
321            import math
322            decimals = int(math.log10(step) * -1)
323
324        Gtk.SpinButton.__init__(self, adjustment=adj,
325                                climb_rate=1, digits=decimals)
326        Gtk.SpinButton.set_numeric(self, True)
327
328        self.set_value(self.__option.get_value())
329
330        # Set up signal handlers when the widget value is changed
331        # from user interaction or programmatically.  When handling
332        # a specific signal, we need to temporarily block the signal
333        # that would call the other signal handler.
334        self.changekey = self.connect('value_changed',
335                                      self.__number_changed)
336        self.valuekey = self.__option.connect('value-changed',
337                                              self.__value_changed)
338
339        self.conkey = self.__option.connect('avail-changed',
340                                            self.__update_avail)
341        self.__update_avail()
342
343        self.set_tooltip_text(self.__option.get_help())
344
345    def __number_changed(self, obj): # IGNORE:W0613 - obj is unused
346        """
347        Handle the change of the value made by the user.
348        """
349        vtype = type(self.__option.get_value())
350
351        self.__option.set_value(vtype(self.get_value()))
352
353    def __update_avail(self):
354        """
355        Update the availability (sensitivity) of this widget.
356        """
357        avail = self.__option.get_available()
358        self.set_sensitive(avail)
359
360    def __value_changed(self):
361        """
362        Handle the change made programmatically
363        """
364        self.handler_block(self.changekey)
365        self.set_value(self.__option.get_value())
366        self.handler_unblock(self.changekey)
367
368    def clean_up(self):
369        """
370        remove stuff that blocks garbage collection
371        """
372        self.__option.disconnect(self.valuekey)
373        self.__option.disconnect(self.conkey)
374        self.__option = None
375
376#-------------------------------------------------------------------------
377#
378# GuiTextOption class
379#
380#-------------------------------------------------------------------------
381class GuiTextOption(Gtk.ScrolledWindow):
382    """
383    This class displays an option that is a multi-line string.
384    """
385    def __init__(self, option, dbstate, uistate, track, override):
386        self.__option = option
387        Gtk.ScrolledWindow.__init__(self)
388        self.set_shadow_type(Gtk.ShadowType.IN)
389        self.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
390        self.set_vexpand(True)
391
392        # Add a TextView
393        value = self.__option.get_value()
394        gtext = Gtk.TextView()
395        gtext.set_size_request(-1, 70)
396        gtext.get_buffer().set_text("\n".join(value))
397        gtext.set_editable(1)
398        gtext.set_wrap_mode(Gtk.WrapMode.WORD_CHAR)
399        self.add(gtext)
400        self.__buff = gtext.get_buffer()
401
402        # Set up signal handlers when the widget value is changed
403        # from user interaction or programmatically.  When handling
404        # a specific signal, we need to temporarily block the signal
405        # that would call the other signal handler.
406        self.bufcon = self.__buff.connect('changed', self.__text_changed)
407        self.valuekey = self.__option.connect('value-changed',
408                                              self.__value_changed)
409
410        # Required for tooltip
411        gtext.add_events(Gdk.EventMask.ENTER_NOTIFY_MASK)
412        gtext.add_events(Gdk.EventMask.LEAVE_NOTIFY_MASK)
413        gtext.set_tooltip_text(self.__option.get_help())
414
415    def __text_changed(self, obj): # IGNORE:W0613 - obj is unused
416        """
417        Handle the change of the value made by the user.
418        """
419        text_val = str(self.__buff.get_text(self.__buff.get_start_iter(),
420                                            self.__buff.get_end_iter(),
421                                            False))
422
423        self.__option.disable_signals()
424        self.__option.set_value(text_val.split('\n'))
425        self.__option.enable_signals()
426
427    def __value_changed(self):
428        """
429        Handle the change made programmatically
430        """
431        self.__buff.handler_block(self.bufcon)
432
433        value = self.__option.get_value()
434
435        # Can only set using a string.  If we have a string value,
436        # we'll use that.  If not, we'll assume a list and convert
437        # it into a single string by assuming each list element
438        # is separated by a newline.
439        if isinstance(value, str):
440            self.__buff.set_text(value)
441
442            # Need to manually call the other handler so that the option
443            # value is changed to be a list.  If left as a string,
444            # it would be treated as a list, meaning each character
445            # becomes a list element -- not what we want.
446            self.__text_changed(None)
447        else:
448            self.__buff.set_text("\n".join(value))
449
450        self.__buff.handler_unblock(self.bufcon)
451
452    def clean_up(self):
453        """
454        remove stuff that blocks garbage collection
455        """
456        self.__option.disconnect(self.valuekey)
457        self.__option = None
458
459        self.__buff.disconnect(self.bufcon)
460        self.__buff = None
461
462#-------------------------------------------------------------------------
463#
464# GuiBooleanOption class
465#
466#-------------------------------------------------------------------------
467class GuiBooleanOption(Gtk.CheckButton):
468    """
469    This class displays an option that is a boolean (True or False).
470    """
471    def __init__(self, option, dbstate, uistate, track, override):
472        self.__option = option
473        Gtk.CheckButton.__init__(self)
474        self.set_label(self.__option.get_label())
475        self.set_active(self.__option.get_value())
476
477        # Set up signal handlers when the widget value is changed
478        # from user interaction or programmatically.  When handling
479        # a specific signal, we need to temporarily block the signal
480        # that would call the other signal handler.
481        self.changekey = self.connect('toggled', self.__state_changed)
482        self.valuekey = self.__option.connect('value-changed',
483                                              self.__value_changed)
484
485        self.conkey = self.__option.connect('avail-changed',
486                                            self.__update_avail)
487        self.__update_avail()
488
489        self.set_tooltip_text(self.__option.get_help())
490
491    def __state_changed(self, obj): # IGNORE:W0613 - obj is unused
492        """
493        Handle the change of the value made by the user.
494        """
495        self.__option.set_value(self.get_active())
496
497    def __update_avail(self):
498        """
499        Update the availability (sensitivity) of this widget.
500        """
501        avail = self.__option.get_available()
502        self.set_sensitive(avail)
503
504    def __value_changed(self):
505        """
506        Handle the change made programmatically
507        """
508        self.handler_block(self.changekey)
509        self.set_active(self.__option.get_value())
510        self.handler_unblock(self.changekey)
511
512    def clean_up(self):
513        """
514        remove stuff that blocks garbage collection
515        """
516        self.__option.disconnect(self.valuekey)
517        self.__option.disconnect(self.conkey)
518        self.__option = None
519
520#-------------------------------------------------------------------------
521#
522# GuiEnumeratedListOption class
523#
524#-------------------------------------------------------------------------
525class GuiEnumeratedListOption(Gtk.Box):
526    """
527    This class displays an option that provides a finite number of values.
528    Each possible value is assigned a value and a description.
529    """
530    def __init__(self, option, dbstate, uistate, track, override):
531        Gtk.Box.__init__(self)
532        evt_box = Gtk.EventBox()
533        self.__option = option
534        self.__combo = Gtk.ComboBoxText()
535        if len(option.get_items()) > 18:
536            self.__combo.set_popup_fixed_width(False)
537            self.__combo.set_wrap_width(3)
538        evt_box.add(self.__combo)
539        self.pack_start(evt_box, True, True, 0)
540
541        self.__update_options()
542
543        # Set up signal handlers when the widget value is changed
544        # from user interaction or programmatically.  When handling
545        # a specific signal, we need to temporarily block the signal
546        # that would call the other signal handler.
547        self.changekey = self.__combo.connect('changed',
548                                              self.__selection_changed)
549        self.valuekey = self.__option.connect('value-changed',
550                                              self.__value_changed)
551
552        self.conkey1 = self.__option.connect('options-changed',
553                                             self.__update_options)
554        self.conkey2 = self.__option.connect('avail-changed',
555                                             self.__update_avail)
556        self.__update_avail()
557
558        self.set_tooltip_text(self.__option.get_help())
559
560    def __selection_changed(self, obj): # IGNORE:W0613 - obj is unused
561        """
562        Handle the change of the value made by the user.
563        """
564        index = self.__combo.get_active()
565        if index < 0:
566            return
567        items = self.__option.get_items()
568        value, description = items[index] # IGNORE:W0612 - description is unused
569
570        # Don't disable the __option signals as is normally done for
571        # the other widgets or bad things happen (like other needed
572        # signals don't fire)
573
574        self.__option.set_value(value)
575        self.value_changed()    # Allow overriding so that another class
576                                # can add functionality
577
578    def value_changed(self):
579        """ Allow overriding so that another class can add functionality """
580        pass
581
582    def __update_options(self):
583        """
584        Handle the change of the available options.
585        """
586        self.__combo.remove_all()
587        #self.__combo.get_model().clear()
588        cur_val = self.__option.get_value()
589        active_index = 0
590        current_index = 0
591        for (value, description) in self.__option.get_items():
592            self.__combo.append_text(description)
593            if value == cur_val:
594                active_index = current_index
595            current_index += 1
596        self.__combo.set_active(active_index)
597
598    def __update_avail(self):
599        """
600        Update the availability (sensitivity) of this widget.
601        """
602        avail = self.__option.get_available()
603        self.set_sensitive(avail)
604
605    def __value_changed(self):
606        """
607        Handle the change made programmatically
608        """
609        self.__combo.handler_block(self.changekey)
610        self.__update_options()
611        self.__combo.handler_unblock(self.changekey)
612
613    def clean_up(self):
614        """
615        remove stuff that blocks garbage collection
616        """
617        self.__option.disconnect(self.valuekey)
618        self.__option.disconnect(self.conkey1)
619        self.__option.disconnect(self.conkey2)
620        self.__option = None
621
622#-------------------------------------------------------------------------
623#
624# GuiPersonOption class
625#
626#-------------------------------------------------------------------------
627class GuiPersonOption(Gtk.Box):
628    """
629    This class displays an option that allows a person from the
630    database to be selected.
631    """
632    def __init__(self, option, dbstate, uistate, track, override):
633        """
634        @param option: The option to display.
635        @type option: gen.plug.menu.PersonOption
636        @return: nothing
637        """
638        Gtk.Box.__init__(self)
639        self.__option = option
640        self.__dbstate = dbstate
641        self.__db = dbstate.get_database()
642        self.__uistate = uistate
643        self.__track = track
644        self.__person_label = Gtk.Label()
645        self.__person_label.set_halign(Gtk.Align.START)
646
647        pevt = Gtk.EventBox()
648        pevt.add(self.__person_label)
649        person_button = widgets.SimpleButton('gtk-index',
650                                             self.__get_person_clicked)
651        person_button.set_relief(Gtk.ReliefStyle.NORMAL)
652
653        self.pack_start(pevt, False, True, 0)
654        self.pack_end(person_button, False, True, 0)
655
656        gid = self.__option.get_value()
657
658        # Pick up the active person
659        try:
660            person_handle = self.__uistate.get_active('Person')
661            person = self.__dbstate.db.get_person_from_handle(person_handle)
662
663            if override or not person:
664                # Pick up the stored option value if there is one
665                person = self.__db.get_person_from_gramps_id(gid)
666
667            if not person:
668                # If all else fails, get the default person to avoid bad values
669                person = self.__db.get_default_person()
670
671            if not person:
672                person = self.__db.find_initial_person()
673        except HandleError:
674            person = None
675
676        self.__update_person(person)
677
678        self.valuekey = self.__option.connect('value-changed',
679                                              self.__value_changed)
680
681        self.conkey = self.__option.connect('avail-changed',
682                                            self.__update_avail)
683        self.__update_avail()
684
685        pevt.set_tooltip_text(self.__option.get_help())
686        person_button.set_tooltip_text(_('Select a different person'))
687
688    def __get_person_clicked(self, obj): # IGNORE:W0613 - obj is unused
689        """
690        Handle the button to choose a different person.
691        """
692        # Create a filter for the person selector.
693        rfilter = GenericFilter()
694        rfilter.set_logical_op('or')
695        rfilter.add_rule(rules.person.IsBookmarked([]))
696        rfilter.add_rule(rules.person.HasIdOf([self.__option.get_value()]))
697
698        # Add the database home person if one exists.
699        default_person = self.__db.get_default_person()
700        if default_person:
701            gid = default_person.get_gramps_id()
702            rfilter.add_rule(rules.person.HasIdOf([gid]))
703
704        # Add the selected person if one exists.
705        person_handle = self.__uistate.get_active('Person')
706        active_person = self.__dbstate.db.get_person_from_handle(person_handle)
707        if active_person:
708            gid = active_person.get_gramps_id()
709            rfilter.add_rule(rules.person.HasIdOf([gid]))
710
711        select_class = SelectorFactory('Person')
712        sel = select_class(self.__dbstate, self.__uistate, self.__track,
713                           title=_('Select a person for the report'),
714                           filter=rfilter)
715        person = sel.run()
716        self.__update_person(person)
717
718    def __update_person(self, person):
719        """
720        Update the currently selected person.
721        """
722        if person:
723            name = _nd.display(person)
724            gid = person.get_gramps_id()
725            self.__person_label.set_text("%s (%s)" % (name, gid))
726            self.__option.set_value(gid)
727
728    def __update_avail(self):
729        """
730        Update the availability (sensitivity) of this widget.
731        """
732        avail = self.__option.get_available()
733        self.set_sensitive(avail)
734
735    def __value_changed(self):
736        """
737        Handle the change made programmatically
738        """
739        gid = self.__option.get_value()
740        name = _nd.display(self.__db.get_person_from_gramps_id(gid))
741
742        self.__person_label.set_text("%s (%s)" % (name, gid))
743
744    def clean_up(self):
745        """
746        remove stuff that blocks garbage collection
747        """
748        self.__option.disconnect(self.valuekey)
749        self.__option.disconnect(self.conkey)
750        self.__option = None
751
752#-------------------------------------------------------------------------
753#
754# GuiFamilyOption class
755#
756#-------------------------------------------------------------------------
757class GuiFamilyOption(Gtk.Box):
758    """
759    This class displays an option that allows a family from the
760    database to be selected.
761    """
762    def __init__(self, option, dbstate, uistate, track, override):
763        """
764        @param option: The option to display.
765        @type option: gen.plug.menu.FamilyOption
766        @return: nothing
767        """
768        Gtk.Box.__init__(self)
769        self.__option = option
770        self.__dbstate = dbstate
771        self.__db = dbstate.get_database()
772        self.__uistate = uistate
773        self.__track = track
774        self.__family_label = Gtk.Label()
775        self.__family_label.set_halign(Gtk.Align.START)
776
777        pevt = Gtk.EventBox()
778        pevt.add(self.__family_label)
779        family_button = widgets.SimpleButton('gtk-index',
780                                             self.__get_family_clicked)
781        family_button.set_relief(Gtk.ReliefStyle.NORMAL)
782
783        self.pack_start(pevt, False, True, 0)
784        self.pack_end(family_button, False, True, 0)
785
786        self.__initialize_family(override)
787
788        self.valuekey = self.__option.connect('value-changed',
789                                              self.__value_changed)
790
791        self.conkey = self.__option.connect('avail-changed',
792                                            self.__update_avail)
793        self.__update_avail()
794
795        pevt.set_tooltip_text(self.__option.get_help())
796        family_button.set_tooltip_text(_('Select a different family'))
797
798    def __initialize_family(self, override):
799        """
800        Find a family to initialize the option with. If there is no specified
801        family, try to find a family that the user is likely interested in.
802        """
803        family_list = []
804
805        fid = self.__option.get_value()
806        fid_family = self.__db.get_family_from_gramps_id(fid)
807        active_family = self.__uistate.get_active('Family')
808
809        if override and fid_family:
810            # Use the stored option value if there is one
811            family_list = [fid_family.get_handle()]
812
813        if active_family and not family_list:
814            # Use the active family if one is selected
815            family_list = [active_family]
816
817        if not family_list:
818            # Next try the family of the active person
819            person_handle = self.__uistate.get_active('Person')
820            person = self.__dbstate.db.get_person_from_handle(person_handle)
821            if person:
822                family_list = person.get_family_handle_list()
823
824        if fid_family and not family_list:
825            # Next try the stored option value if there is one
826            family_list = [fid_family.get_handle()]
827
828        if not family_list:
829            # Next try the family of the default person in the database.
830            person = self.__db.get_default_person()
831            if person:
832                family_list = person.get_family_handle_list()
833
834        if not family_list:
835            # Finally, take any family you can find.
836            for family in self.__db.iter_family_handles():
837                self.__update_family(family)
838                break
839        else:
840            self.__update_family(family_list[0])
841
842    def __get_family_clicked(self, obj): # IGNORE:W0613 - obj is unused
843        """
844        Handle the button to choose a different family.
845        """
846        # Create a filter for the person selector.
847        rfilter = GenericFilterFactory('Family')()
848        rfilter.set_logical_op('or')
849
850        # Add the current family
851        rfilter.add_rule(rules.family.HasIdOf([self.__option.get_value()]))
852
853        # Add all bookmarked families
854        rfilter.add_rule(rules.family.IsBookmarked([]))
855
856        # Add the families of the database home person if one exists.
857        default_person = self.__db.get_default_person()
858        if default_person:
859            family_list = default_person.get_family_handle_list()
860            for family_handle in family_list:
861                family = self.__db.get_family_from_handle(family_handle)
862                gid = family.get_gramps_id()
863                rfilter.add_rule(rules.family.HasIdOf([gid]))
864
865        # Add the families of the selected person if one exists.
866        # Same code as above one ! See bug #5032 feature request #5038
867        ### active_person = self.__uistate.get_active('Person') ###
868        #active_person = self.__db.get_default_person()
869        #if active_person:
870            #family_list = active_person.get_family_handle_list()
871            #for family_handle in family_list:
872                #family = self.__db.get_family_from_handle(family_handle)
873                #gid = family.get_gramps_id()
874                #rfilter.add_rule(rules.family.HasIdOf([gid]))
875
876        select_class = SelectorFactory('Family')
877        sel = select_class(self.__dbstate, self.__uistate, self.__track,
878                           filter=rfilter)
879        family = sel.run()
880        if family:
881            self.__update_family(family.get_handle())
882
883    def __update_family(self, handle):
884        """
885        Update the currently selected family.
886        """
887        if handle:
888            family = self.__dbstate.db.get_family_from_handle(handle)
889            family_id = family.get_gramps_id()
890            fhandle = family.get_father_handle()
891            mhandle = family.get_mother_handle()
892
893            if fhandle:
894                father = self.__db.get_person_from_handle(fhandle)
895                father_name = _nd.display(father)
896            else:
897                father_name = _("unknown father")
898
899            if mhandle:
900                mother = self.__db.get_person_from_handle(mhandle)
901                mother_name = _nd.display(mother)
902            else:
903                mother_name = _("unknown mother")
904
905            name = _("%(father_name)s and %(mother_name)s (%(family_id)s)"
906                    ) % {'father_name' : father_name,
907                         'mother_name' : mother_name,
908                         'family_id'   : family_id}
909
910            self.__family_label.set_text(name)
911            self.__option.set_value(family_id)
912
913    def __update_avail(self):
914        """
915        Update the availability (sensitivity) of this widget.
916        """
917        avail = self.__option.get_available()
918        self.set_sensitive(avail)
919
920    def __value_changed(self):
921        """
922        Handle the change made programmatically
923        """
924        fid = self.__option.get_value()
925        handle = self.__db.get_family_from_gramps_id(fid).get_handle()
926
927        # Need to disable signals as __update_family() calls set_value()
928        # which would launch the 'value-changed' signal which is what
929        # we are reacting to here in the first place (don't need the
930        # signal repeated)
931        self.__option.disable_signals()
932        self.__update_family(handle)
933        self.__option.enable_signals()
934
935    def clean_up(self):
936        """
937        remove stuff that blocks garbage collection
938        """
939        self.__option.disconnect(self.valuekey)
940        self.__option.disconnect(self.conkey)
941        self.__option = None
942
943#-------------------------------------------------------------------------
944#
945# GuiNoteOption class
946#
947#-------------------------------------------------------------------------
948class GuiNoteOption(Gtk.Box):
949    """
950    This class displays an option that allows a note from the
951    database to be selected.
952    """
953    def __init__(self, option, dbstate, uistate, track, override):
954        """
955        @param option: The option to display.
956        @type option: gen.plug.menu.NoteOption
957        @return: nothing
958        """
959        Gtk.Box.__init__(self)
960        self.__option = option
961        self.__dbstate = dbstate
962        self.__db = dbstate.get_database()
963        self.__uistate = uistate
964        self.__track = track
965        self.__note_label = Gtk.Label()
966        self.__note_label.set_halign(Gtk.Align.START)
967
968        pevt = Gtk.EventBox()
969        pevt.add(self.__note_label)
970        note_button = widgets.SimpleButton('gtk-index',
971                                           self.__get_note_clicked)
972        note_button.set_relief(Gtk.ReliefStyle.NORMAL)
973
974        self.pack_start(pevt, False, True, 0)
975        self.pack_end(note_button, False, True, 0)
976
977        # Initialize to the current value
978        nid = self.__option.get_value()
979        note = self.__db.get_note_from_gramps_id(nid)
980        self.__update_note(note)
981
982        self.valuekey = self.__option.connect('value-changed',
983                                              self.__value_changed)
984
985        self.__option.connect('avail-changed', self.__update_avail)
986        self.__update_avail()
987
988        pevt.set_tooltip_text(self.__option.get_help())
989        note_button.set_tooltip_text(_('Select an existing note'))
990
991    def __get_note_clicked(self, obj): # IGNORE:W0613 - obj is unused
992        """
993        Handle the button to choose a different note.
994        """
995        select_class = SelectorFactory('Note')
996        sel = select_class(self.__dbstate, self.__uistate, self.__track)
997        note = sel.run()
998        self.__update_note(note)
999
1000    def __update_note(self, note):
1001        """
1002        Update the currently selected note.
1003        """
1004        if note:
1005            note_id = note.get_gramps_id()
1006            txt = " ".join(note.get().split())
1007            if len(txt) > 35:
1008                txt = txt[:35] + "..."
1009            txt = "%s [%s]" % (txt, note_id)
1010
1011            self.__note_label.set_text(txt)
1012            self.__option.set_value(note_id)
1013        else:
1014            txt = "<i>%s</i>" % _('No note given, click button to select one')
1015            self.__note_label.set_text(txt)
1016            self.__note_label.set_use_markup(True)
1017            self.__option.set_value("")
1018
1019    def __update_avail(self):
1020        """
1021        Update the availability (sensitivity) of this widget.
1022        """
1023        avail = self.__option.get_available()
1024        self.set_sensitive(avail)
1025
1026    def __value_changed(self):
1027        """
1028        Handle the change made programmatically
1029        """
1030        nid = self.__option.get_value()
1031        note = self.__db.get_note_from_gramps_id(nid)
1032
1033        # Need to disable signals as __update_note() calls set_value()
1034        # which would launch the 'value-changed' signal which is what
1035        # we are reacting to here in the first place (don't need the
1036        # signal repeated)
1037        self.__option.disable_signals()
1038        self.__update_note(note)
1039        self.__option.enable_signals()
1040
1041    def clean_up(self):
1042        """
1043        remove stuff that blocks garbage collection
1044        """
1045        self.__option.disconnect(self.valuekey)
1046        self.__option = None
1047
1048#-------------------------------------------------------------------------
1049#
1050# GuiMediaOption class
1051#
1052#-------------------------------------------------------------------------
1053class GuiMediaOption(Gtk.Box):
1054    """
1055    This class displays an option that allows a media object from the
1056    database to be selected.
1057    """
1058    def __init__(self, option, dbstate, uistate, track, override):
1059        """
1060        @param option: The option to display.
1061        @type option: gen.plug.menu.MediaOption
1062        @return: nothing
1063        """
1064        Gtk.Box.__init__(self)
1065        self.__option = option
1066        self.__dbstate = dbstate
1067        self.__db = dbstate.get_database()
1068        self.__uistate = uistate
1069        self.__track = track
1070        self.__media_label = Gtk.Label()
1071        self.__media_label.set_halign(Gtk.Align.START)
1072
1073        pevt = Gtk.EventBox()
1074        pevt.add(self.__media_label)
1075        media_button = widgets.SimpleButton('gtk-index',
1076                                            self.__get_media_clicked)
1077        media_button.set_relief(Gtk.ReliefStyle.NORMAL)
1078
1079        self.pack_start(pevt, False, True, 0)
1080        self.pack_end(media_button, False, True, 0)
1081
1082        # Initialize to the current value
1083        mid = self.__option.get_value()
1084        media = self.__db.get_media_from_gramps_id(mid)
1085        self.__update_media(media)
1086
1087        self.valuekey = self.__option.connect('value-changed',
1088                                              self.__value_changed)
1089
1090        self.__option.connect('avail-changed', self.__update_avail)
1091        self.__update_avail()
1092
1093        pevt.set_tooltip_text(self.__option.get_help())
1094        media_button.set_tooltip_text(_('Select an existing media object'))
1095
1096    def __get_media_clicked(self, obj): # IGNORE:W0613 - obj is unused
1097        """
1098        Handle the button to choose a different note.
1099        """
1100        select_class = SelectorFactory('Media')
1101        sel = select_class(self.__dbstate, self.__uistate, self.__track)
1102        media = sel.run()
1103        self.__update_media(media)
1104
1105    def __update_media(self, media):
1106        """
1107        Update the currently selected media.
1108        """
1109        if media:
1110            media_id = media.get_gramps_id()
1111            txt = "%s [%s]" % (media.get_description(), media_id)
1112
1113            self.__media_label.set_text(txt)
1114            self.__option.set_value(media_id)
1115        else:
1116            txt = "<i>%s</i>" % _('No image given, click button to select one')
1117            self.__media_label.set_text(txt)
1118            self.__media_label.set_use_markup(True)
1119            self.__option.set_value("")
1120
1121    def __update_avail(self):
1122        """
1123        Update the availability (sensitivity) of this widget.
1124        """
1125        avail = self.__option.get_available()
1126        self.set_sensitive(avail)
1127
1128    def __value_changed(self):
1129        """
1130        Handle the change made programmatically
1131        """
1132        mid = self.__option.get_value()
1133        media = self.__db.get_media_from_gramps_id(mid)
1134
1135        # Need to disable signals as __update_media() calls set_value()
1136        # which would launch the 'value-changed' signal which is what
1137        # we are reacting to here in the first place (don't need the
1138        # signal repeated)
1139        self.__option.disable_signals()
1140        self.__update_media(media)
1141        self.__option.enable_signals()
1142
1143    def clean_up(self):
1144        """
1145        remove stuff that blocks garbage collection
1146        """
1147        self.__option.disconnect(self.valuekey)
1148        self.__option = None
1149
1150#-------------------------------------------------------------------------
1151#
1152# GuiPersonListOption class
1153#
1154#-------------------------------------------------------------------------
1155class GuiPersonListOption(Gtk.Box):
1156    """
1157    This class displays a widget that allows multiple people from the
1158    database to be selected.
1159    """
1160    def __init__(self, option, dbstate, uistate, track, override):
1161        """
1162        @param option: The option to display.
1163        @type option: gen.plug.menu.PersonListOption
1164        @return: nothing
1165        """
1166        Gtk.Box.__init__(self)
1167        self.__option = option
1168        self.__dbstate = dbstate
1169        self.__db = dbstate.get_database()
1170        self.__uistate = uistate
1171        self.__track = track
1172        self.set_size_request(150, 100)
1173
1174        self.__model = Gtk.ListStore(GObject.TYPE_STRING, GObject.TYPE_STRING)
1175        self.__tree_view = Gtk.TreeView(model=self.__model)
1176        col1 = Gtk.TreeViewColumn(_('Name'), Gtk.CellRendererText(), text=0)
1177        col2 = Gtk.TreeViewColumn(_('ID'), Gtk.CellRendererText(), text=1)
1178        col1.set_resizable(True)
1179        col2.set_resizable(True)
1180        col1.set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE)
1181        col2.set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE)
1182        col1.set_sort_column_id(0)
1183        col2.set_sort_column_id(1)
1184        self.__tree_view.append_column(col1)
1185        self.__tree_view.append_column(col2)
1186        self.__scrolled_window = Gtk.ScrolledWindow()
1187        self.__scrolled_window.add(self.__tree_view)
1188        self.__scrolled_window.set_policy(Gtk.PolicyType.AUTOMATIC,
1189                                          Gtk.PolicyType.AUTOMATIC)
1190        self.__scrolled_window.set_shadow_type(Gtk.ShadowType.OUT)
1191
1192        self.pack_start(self.__scrolled_window, True, True, 0)
1193
1194        self.__value_changed()
1195
1196        # now setup the '+' and '-' pushbutton for adding/removing people from
1197        # the container
1198        self.__add_person = widgets.SimpleButton('list-add',
1199                                                 self.__add_person_clicked)
1200        self.__del_person = widgets.SimpleButton('list-remove',
1201                                                 self.__del_person_clicked)
1202        self.__vbbox = Gtk.ButtonBox(orientation=Gtk.Orientation.VERTICAL)
1203        self.__vbbox.add(self.__add_person)
1204        self.__vbbox.add(self.__del_person)
1205        self.__vbbox.set_layout(Gtk.ButtonBoxStyle.SPREAD)
1206        self.pack_end(self.__vbbox, False, False, 0)
1207
1208        self.valuekey = self.__option.connect('value-changed',
1209                                              self.__value_changed)
1210
1211        self.__tree_view.set_tooltip_text(self.__option.get_help())
1212
1213    def __add_person_clicked(self, obj): # IGNORE:W0613 - obj is unused
1214        """
1215        Handle the add person button.
1216        """
1217        # people we already have must be excluded
1218        # so we don't list them multiple times
1219        skip_list = set()
1220        tree_iter = self.__model.get_iter_first()
1221        while tree_iter:
1222            gid = self.__model.get_value(tree_iter, 1) # get the GID in col. #1
1223            person = self.__db.get_person_from_gramps_id(gid)
1224            skip_list.add(person.get_handle())
1225            tree_iter = self.__model.iter_next(tree_iter)
1226
1227        select_class = SelectorFactory('Person')
1228        sel = select_class(self.__dbstate, self.__uistate,
1229                           self.__track, skip=skip_list)
1230        person = sel.run()
1231        if person:
1232            name = _nd.display(person)
1233            gid = person.get_gramps_id()
1234            self.__model.append([name, gid])
1235
1236            # if this person has a spouse, ask if we should include the spouse
1237            # in the list of "people of interest"
1238            #
1239            # NOTE: we may want to make this an optional thing, determined
1240            # by the use of a parameter at the time this class is instatiated
1241            family_list = person.get_family_handle_list()
1242            for family_handle in family_list:
1243                family = self.__db.get_family_from_handle(family_handle)
1244
1245                if person.get_handle() == family.get_father_handle():
1246                    spouse_handle = family.get_mother_handle()
1247                else:
1248                    spouse_handle = family.get_father_handle()
1249
1250                if spouse_handle and (spouse_handle not in skip_list):
1251                    spouse = self.__db.get_person_from_handle(spouse_handle)
1252                    spouse_name = _nd.display(spouse)
1253                    text = _('Also include %s?') % spouse_name
1254
1255                    prompt = OptionDialog(_('Select Person'),
1256                                          text,
1257                                          _('No'), None,
1258                                          _('Yes'), None,
1259                                          parent=self.__uistate.window)
1260                    if prompt.get_response() == Gtk.ResponseType.YES:
1261                        gid = spouse.get_gramps_id()
1262                        self.__model.append([spouse_name, gid])
1263
1264            self.__update_value()
1265
1266    def __del_person_clicked(self, obj): # IGNORE:W0613 - obj is unused
1267        """
1268        Handle the delete person button.
1269        """
1270        (path, column) = self.__tree_view.get_cursor()
1271        if path:
1272            tree_iter = self.__model.get_iter(path)
1273            self.__model.remove(tree_iter)
1274            self.__update_value()
1275
1276    def __update_value(self):
1277        """
1278        Parse the object and return.
1279        """
1280        gidlist = ''
1281        tree_iter = self.__model.get_iter_first()
1282        while tree_iter:
1283            gid = self.__model.get_value(tree_iter, 1)
1284            gidlist = gidlist + gid + ' '
1285            tree_iter = self.__model.iter_next(tree_iter)
1286
1287        # Supress signals so that the set_value() handler
1288        # (__value_changed()) doesn't get called
1289        self.__option.disable_signals()
1290        self.__option.set_value(gidlist)
1291        self.__option.enable_signals()
1292
1293    def __value_changed(self):
1294        """
1295        Handle the change made programmatically
1296        """
1297        value = self.__option.get_value()
1298
1299        if not isinstance(value, str):
1300            # Convert array into a string
1301            # (convienence so that programmers can
1302            # set value using a list)
1303            value = " ".join(value)
1304
1305            # Need to change __option value to be the string
1306
1307            self.__option.disable_signals()
1308            self.__option.set_value(value)
1309            self.__option.enable_signals()
1310
1311        # Remove all entries (the new values will REPLACE
1312        # rather than APPEND)
1313        self.__model.clear()
1314
1315        for gid in value.split():
1316            person = self.__db.get_person_from_gramps_id(gid)
1317            if person:
1318                name = _nd.display(person)
1319                self.__model.append([name, gid])
1320
1321    def clean_up(self):
1322        """
1323        remove stuff that blocks garbage collection
1324        """
1325        self.__option.disconnect(self.valuekey)
1326        self.__option = None
1327
1328#-------------------------------------------------------------------------
1329#
1330# GuiPlaceListOption class
1331#
1332#-------------------------------------------------------------------------
1333class GuiPlaceListOption(Gtk.Box):
1334    """
1335    This class displays a widget that allows multiple places from the
1336    database to be selected.
1337    """
1338    def __init__(self, option, dbstate, uistate, track, override):
1339        """
1340        @param option: The option to display.
1341        @type option: gen.plug.menu.PlaceListOption
1342        @return: nothing
1343        """
1344        Gtk.Box.__init__(self)
1345        self.__option = option
1346        self.__dbstate = dbstate
1347        self.__db = dbstate.get_database()
1348        self.__uistate = uistate
1349        self.__track = track
1350        self.set_size_request(150, 150)
1351
1352        self.__model = Gtk.ListStore(GObject.TYPE_STRING, GObject.TYPE_STRING)
1353        self.__tree_view = Gtk.TreeView(self.__model)
1354        col1 = Gtk.TreeViewColumn(_('Place'), Gtk.CellRendererText(), text=0)
1355        col2 = Gtk.TreeViewColumn(_('ID'), Gtk.CellRendererText(), text=1)
1356        col1.set_resizable(True)
1357        col2.set_resizable(True)
1358        col1.set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE)
1359        col2.set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE)
1360        col1.set_sort_column_id(0)
1361        col2.set_sort_column_id(1)
1362        self.__tree_view.append_column(col1)
1363        self.__tree_view.append_column(col2)
1364        self.__scrolled_window = Gtk.ScrolledWindow()
1365        self.__scrolled_window.add(self.__tree_view)
1366        self.__scrolled_window.set_policy(Gtk.PolicyType.AUTOMATIC,
1367                                          Gtk.PolicyType.AUTOMATIC)
1368        self.__scrolled_window.set_shadow_type(Gtk.ShadowType.OUT)
1369
1370        self.pack_start(self.__scrolled_window, True, True, 0)
1371
1372        self.__value_changed()
1373
1374        # now setup the '+' and '-' pushbutton for adding/removing places from
1375        # the container
1376        self.__add_place = widgets.SimpleButton('list-add',
1377                                                self.__add_place_clicked)
1378        self.__del_place = widgets.SimpleButton('list-remove',
1379                                                self.__del_place_clicked)
1380        self.__vbbox = Gtk.ButtonBox(orientation=Gtk.Orientation.VERTICAL)
1381        self.__vbbox.add(self.__add_place)
1382        self.__vbbox.add(self.__del_place)
1383        self.__vbbox.set_layout(Gtk.ButtonBoxStyle.SPREAD)
1384        self.pack_end(self.__vbbox, False, False, 0)
1385
1386        self.valuekey = self.__option.connect('value-changed',
1387                                              self.__value_changed)
1388
1389        self.__tree_view.set_tooltip_text(self.__option.get_help())
1390
1391    def __add_place_clicked(self, obj): # IGNORE:W0613 - obj is unused
1392        """
1393        Handle the add place button.
1394        """
1395        # places we already have must be excluded
1396        # so we don't list them multiple times
1397        skip_list = set()
1398        tree_iter = self.__model.get_iter_first()
1399        while tree_iter:
1400            gid = self.__model.get_value(tree_iter, 1) # get the GID in col. #1
1401            place = self.__db.get_place_from_gramps_id(gid)
1402            skip_list.add(place.get_handle())
1403            tree_iter = self.__model.iter_next(tree_iter)
1404
1405        select_class = SelectorFactory('Place')
1406        sel = select_class(self.__dbstate, self.__uistate,
1407                           self.__track, skip=skip_list)
1408        place = sel.run()
1409        if place:
1410            place_name = _pd.display(self.__db, place)
1411            gid = place.get_gramps_id()
1412            self.__model.append([place_name, gid])
1413            self.__update_value()
1414
1415    def __del_place_clicked(self, obj): # IGNORE:W0613 - obj is unused
1416        """
1417        Handle the delete place button.
1418        """
1419        (path, column) = self.__tree_view.get_cursor()
1420        if path:
1421            tree_iter = self.__model.get_iter(path)
1422            self.__model.remove(tree_iter)
1423            self.__update_value()
1424
1425    def __update_value(self):
1426        """
1427        Parse the object and return.
1428        """
1429        gidlist = ''
1430        tree_iter = self.__model.get_iter_first()
1431        while tree_iter:
1432            gid = self.__model.get_value(tree_iter, 1)
1433            gidlist = gidlist + gid + ' '
1434            tree_iter = self.__model.iter_next(tree_iter)
1435        self.__option.set_value(gidlist)
1436
1437    def __value_changed(self):
1438        """
1439        Handle the change made programmatically
1440        """
1441        value = self.__option.get_value()
1442
1443        if not isinstance(value, str):
1444            # Convert array into a string
1445            # (convienence so that programmers can
1446            # set value using a list)
1447            value = " ".join(value)
1448
1449            # Need to change __option value to be the string
1450
1451            self.__option.disable_signals()
1452            self.__option.set_value(value)
1453            self.__option.enable_signals()
1454
1455        # Remove all entries (the new values will REPLACE
1456        # rather than APPEND)
1457        self.__model.clear()
1458
1459        for gid in value.split():
1460            place = self.__db.get_place_from_gramps_id(gid)
1461            if place:
1462                place_name = _pd.display(self.__db, place)
1463                self.__model.append([place_name, gid])
1464
1465    def clean_up(self):
1466        """
1467        remove stuff that blocks garbage collection
1468        """
1469        self.__option.disconnect(self.valuekey)
1470        self.__option = None
1471
1472#-------------------------------------------------------------------------
1473#
1474# GuiSurnameColorOption class
1475#
1476#-------------------------------------------------------------------------
1477class GuiSurnameColorOption(Gtk.Box):
1478    """
1479    This class displays a widget that allows multiple surnames to be
1480    selected from the database, and to assign a colour (not necessarily
1481    unique) to each one.
1482    """
1483    def __init__(self, option, dbstate, uistate, track, override):
1484        """
1485        @param option: The option to display.
1486        @type option: gen.plug.menu.SurnameColorOption
1487        @return: nothing
1488        """
1489        Gtk.Box.__init__(self)
1490        self.__option = option
1491        self.__dbstate = dbstate
1492        self.__db = dbstate.get_database()
1493        self.__uistate = uistate
1494        self.__track = track
1495        item = uistate.gwm.get_item_from_track(track)
1496        self.__parent = item[0].window if isinstance(item, list) \
1497            else item.window
1498
1499        self.set_size_request(150, 150)
1500
1501        # This will get populated the first time the dialog is run,
1502        # and used each time after.
1503        self.__surnames = {}  # list of surnames and count
1504
1505        self.__model = Gtk.ListStore(GObject.TYPE_STRING, GObject.TYPE_STRING)
1506        self.__tree_view = Gtk.TreeView(model=self.__model)
1507        self.__tree_view.connect('row-activated', self.__row_clicked)
1508        col1 = Gtk.TreeViewColumn(_('Surname'), Gtk.CellRendererText(), text=0)
1509        col2 = Gtk.TreeViewColumn(_('Color'), Gtk.CellRendererText(), text=1)
1510        col1.set_resizable(True)
1511        col2.set_resizable(True)
1512        col1.set_sort_column_id(0)
1513        col1.set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE)
1514        col2.set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE)
1515        self.__tree_view.append_column(col1)
1516        self.__tree_view.append_column(col2)
1517        self.scrolled_window = Gtk.ScrolledWindow()
1518        self.scrolled_window.add(self.__tree_view)
1519        self.scrolled_window.set_policy(Gtk.PolicyType.AUTOMATIC,
1520                                        Gtk.PolicyType.AUTOMATIC)
1521        self.scrolled_window.set_shadow_type(Gtk.ShadowType.OUT)
1522        self.pack_start(self.scrolled_window, True, True, 0)
1523
1524        self.add_surname = widgets.SimpleButton('list-add',
1525                                                self.__add_clicked)
1526        self.del_surname = widgets.SimpleButton('list-remove',
1527                                                self.__del_clicked)
1528        self.vbbox = Gtk.ButtonBox(orientation=Gtk.Orientation.VERTICAL)
1529        self.vbbox.add(self.add_surname)
1530        self.vbbox.add(self.del_surname)
1531        self.vbbox.set_layout(Gtk.ButtonBoxStyle.SPREAD)
1532        self.pack_end(self.vbbox, False, False, 0)
1533
1534        self.__value_changed()
1535
1536        self.valuekey = self.__option.connect('value-changed',
1537                                              self.__value_changed)
1538
1539        self.__tree_view.set_tooltip_text(self.__option.get_help())
1540
1541    def __add_clicked(self, obj): # IGNORE:W0613 - obj is unused
1542        """
1543        Handle the add surname button.
1544        """
1545        skip_list = set()
1546        tree_iter = self.__model.get_iter_first()
1547        while tree_iter:
1548            surname = self.__model.get_value(tree_iter, 0)
1549            skip_list.add(surname.encode('iso-8859-1', 'xmlcharrefreplace'))
1550            tree_iter = self.__model.iter_next(tree_iter)
1551
1552        ln_dialog = LastNameDialog(self.__db, self.__uistate,
1553                                   self.__track, self.__surnames, skip_list)
1554        surname_set = ln_dialog.run()
1555        for surname in surname_set:
1556            self.__model.append([surname, '#ffffff'])
1557        self.__update_value()
1558
1559    def __del_clicked(self, obj): # IGNORE:W0613 - obj is unused
1560        """
1561        Handle the delete surname button.
1562        """
1563        (path, column) = self.__tree_view.get_cursor()
1564        if path:
1565            tree_iter = self.__model.get_iter(path)
1566            self.__model.remove(tree_iter)
1567            self.__update_value()
1568
1569    def __row_clicked(self, treeview, path, column):
1570        """
1571        Handle the case of a row being clicked on.
1572        """
1573        # get the surname and colour value for this family
1574        tree_iter = self.__model.get_iter(path)
1575        surname = self.__model.get_value(tree_iter, 0)
1576        rgba = Gdk.RGBA()
1577        rgba.parse(self.__model.get_value(tree_iter, 1))
1578
1579        title = _('Select color for %s') % surname
1580        colour_dialog = Gtk.ColorChooserDialog(title=title,
1581                                               transient_for=self.__parent)
1582        colour_dialog.set_rgba(rgba)
1583        response = colour_dialog.run()
1584
1585        if response == Gtk.ResponseType.OK:
1586            rgba = colour_dialog.get_rgba()
1587            colour_name = '#%02x%02x%02x' % (int(rgba.red * 255),
1588                                             int(rgba.green * 255),
1589                                             int(rgba.blue * 255))
1590            self.__model.set_value(tree_iter, 1, colour_name)
1591
1592        colour_dialog.destroy()
1593        self.__update_value()
1594
1595    def __update_value(self):
1596        """
1597        Parse the object and return.
1598        """
1599        surname_colours = ''
1600        tree_iter = self.__model.get_iter_first()
1601        while tree_iter:
1602            surname = self.__model.get_value(tree_iter, 0)
1603            #surname = surname.encode('iso-8859-1', 'xmlcharrefreplace')
1604            colour = self.__model.get_value(tree_iter, 1)
1605            # Tried to use a dictionary, and tried to save it as a tuple,
1606            # but couldn't get this to work right -- this is lame, but now
1607            # the surnames and colours are saved as a plain text string
1608            #
1609            # Hmmm...putting whitespace between the fields causes
1610            # problems when the surname has whitespace -- for example,
1611            # with surnames like "Del Monte".  So now we insert a non-
1612            # whitespace character which is unlikely to appear in
1613            # a surname.  (See bug report #2162.)
1614            surname_colours += surname + '\xb0' + colour + '\xb0'
1615            tree_iter = self.__model.iter_next(tree_iter)
1616        self.__option.set_value(surname_colours)
1617
1618    def __value_changed(self):
1619        """
1620        Handle the change made programmatically
1621        """
1622        value = self.__option.get_value()
1623
1624        if not isinstance(value, str):
1625            # Convert dictionary into a string
1626            # (convienence so that programmers can
1627            # set value using a dictionary)
1628            value_str = ""
1629
1630            for name in value:
1631                value_str += "%s\xb0%s\xb0" % (name, value[name])
1632
1633            value = value_str
1634
1635            # Need to change __option value to be the string
1636
1637            self.__option.disable_signals()
1638            self.__option.set_value(value)
1639            self.__option.enable_signals()
1640
1641        # Remove all entries (the new values will REPLACE
1642        # rather than APPEND)
1643        self.__model.clear()
1644
1645        # populate the surname/colour treeview
1646        #
1647        # For versions prior to 3.0.2, the fields were delimited with
1648        # whitespace.  However, this causes problems when the surname
1649        # also has a space within it.  When populating the control,
1650        # support both the new and old format -- look for the \xb0
1651        # delimiter, and if it isn't there, assume this is the old-
1652        # style space-delimited format.  (Bug #2162.)
1653        if value.find('\xb0') >= 0:
1654            tmp = value.split('\xb0')
1655        else:
1656            tmp = value.split(' ')
1657        while len(tmp) > 1:
1658            surname = tmp.pop(0)
1659            colour = tmp.pop(0)
1660            self.__model.append([surname, colour])
1661
1662    def clean_up(self):
1663        """
1664        remove stuff that blocks garbage collection
1665        """
1666        self.__option.disconnect(self.valuekey)
1667        self.__option = None
1668
1669#-------------------------------------------------------------------------
1670#
1671# GuiDestinationOption class
1672#
1673#-------------------------------------------------------------------------
1674class GuiDestinationOption(Gtk.Box):
1675    """
1676    This class displays an option that allows the user to select a
1677    DestinationOption.
1678    """
1679    def __init__(self, option, dbstate, uistate, track, override):
1680        """
1681        @param option: The option to display.
1682        @type option: gen.plug.menu.DestinationOption
1683        @return: nothing
1684        """
1685        Gtk.Box.__init__(self)
1686        self.__option = option
1687        self.__uistate = uistate
1688        self.__entry = Gtk.Entry()
1689        self.__entry.set_text(self.__option.get_value())
1690
1691        self.__button = Gtk.Button()
1692        img = Gtk.Image()
1693        img.set_from_icon_name('document-open', Gtk.IconSize.BUTTON)
1694        self.__button.add(img)
1695        self.__button.connect('clicked', self.__select_file)
1696
1697        self.pack_start(self.__entry, True, True, 0)
1698        self.pack_end(self.__button, False, False, 0)
1699
1700        # Set up signal handlers when the widget value is changed
1701        # from user interaction or programmatically.  When handling
1702        # a specific signal, we need to temporarily block the signal
1703        # that would call the other signal handler.
1704        self.changekey = self.__entry.connect('changed', self.__text_changed)
1705        self.valuekey = self.__option.connect('value-changed',
1706                                              self.__value_changed)
1707
1708        self.conkey1 = self.__option.connect('options-changed',
1709                                             self.__option_changed)
1710        self.conkey2 = self.__option.connect('avail-changed',
1711                                             self.__update_avail)
1712        self.__update_avail()
1713
1714        self.set_tooltip_text(self.__option.get_help())
1715
1716    def __option_changed(self):
1717        """
1718        Handle a change of the option.
1719        """
1720        extension = self.__option.get_extension()
1721        directory = self.__option.get_directory_entry()
1722        value = self.__option.get_value()
1723
1724        if not directory and not value.endswith(extension):
1725            value = value + extension
1726            self.__option.set_value(value)
1727        elif directory and value.endswith(extension):
1728            value = value[:-len(extension)]
1729            self.__option.set_value(value)
1730
1731        self.__entry.set_text(self.__option.get_value())
1732
1733    def __select_file(self, obj):
1734        """
1735        Handle the user's request to select a file (or directory).
1736        """
1737        if self.__option.get_directory_entry():
1738            my_action = Gtk.FileChooserAction.SELECT_FOLDER
1739        else:
1740            my_action = Gtk.FileChooserAction.SAVE
1741
1742        fcd = Gtk.FileChooserDialog(_("Save As"), action=my_action,
1743                                    parent=self.__uistate.window,
1744                                    buttons=(_('_Cancel'),
1745                                             Gtk.ResponseType.CANCEL,
1746                                             _('_Open'),
1747                                             Gtk.ResponseType.OK))
1748
1749        name = os.path.abspath(self.__option.get_value())
1750        if self.__option.get_directory_entry():
1751            while not os.path.isdir(name):
1752                # Keep looking up levels to find a valid drive.
1753                name, tail = os.path.split(name)
1754                if not name:
1755                    # Avoid infinite loops
1756                    name = get_curr_dir
1757            fcd.set_current_folder(name)
1758        else:
1759            fcd.set_current_name(os.path.basename(name))
1760            fcd.set_current_folder(os.path.dirname(name))
1761
1762        status = fcd.run()
1763        if status == Gtk.ResponseType.OK:
1764            path = fcd.get_filename()
1765            if path:
1766                if not self.__option.get_directory_entry() and \
1767                   not path.endswith(self.__option.get_extension()):
1768                    path = path + self.__option.get_extension()
1769                self.__entry.set_text(path)
1770                self.__option.set_value(path)
1771        fcd.destroy()
1772
1773    def __text_changed(self, obj): # IGNORE:W0613 - obj is unused
1774        """
1775        Handle the change of the value made by the user.
1776        """
1777        self.__option.disable_signals()
1778        self.__option.set_value(self.__entry.get_text())
1779        self.__option.enable_signals()
1780
1781    def __update_avail(self):
1782        """
1783        Update the availability (sensitivity) of this widget.
1784        """
1785        avail = self.__option.get_available()
1786        self.set_sensitive(avail)
1787
1788    def __value_changed(self):
1789        """
1790        Handle the change made programmatically
1791        """
1792        self.__entry.handler_block(self.changekey)
1793        self.__entry.set_text(self.__option.get_value())
1794        self.__entry.handler_unblock(self.changekey)
1795
1796    def clean_up(self):
1797        """
1798        remove stuff that blocks garbage collection
1799        """
1800        self.__option.disconnect(self.valuekey)
1801        self.__option.disconnect(self.conkey1)
1802        self.__option.disconnect(self.conkey2)
1803        self.__option = None
1804
1805#-------------------------------------------------------------------------
1806#
1807# GuiStyleOption class
1808#
1809#-------------------------------------------------------------------------
1810class GuiStyleOption(GuiEnumeratedListOption): # TODO this is likely dead code
1811    """
1812    This class displays a StyleOption.
1813    """
1814    def __init__(self, option, dbstate, uistate, track, override):
1815        """
1816        @param option: The option to display.
1817        @type option: gen.plug.menu.StyleOption
1818        @return: nothing
1819        """
1820        GuiEnumeratedListOption.__init__(self, option, dbstate,
1821                                         uistate, track)
1822        self.__option = option
1823
1824        self.__button = Gtk.Button("%s..." % _("Style Editor"))
1825        self.__button.connect('clicked', self.__on_style_edit_clicked)
1826
1827        self.pack_end(self.__button, False, False)
1828        self.uistate = uistate
1829        self.track = track
1830
1831    def __on_style_edit_clicked(self, *obj):
1832        """The user has clicked on the 'Edit Styles' button.  Create a
1833        style sheet editor object and let them play.  When they are
1834        done, update the displayed styles."""
1835        from gramps.gen.plug.docgen import StyleSheetList
1836        from .report._styleeditor import StyleListDisplay
1837        style_list = StyleSheetList(self.__option.get_style_file(),
1838                                    self.__option.get_default_style())
1839        StyleListDisplay(style_list, self.uistate, self.track)
1840
1841        new_items = []
1842        for style_name in style_list.get_style_names():
1843            new_items.append((style_name, style_name))
1844        self.__option.set_items(new_items)
1845
1846#-------------------------------------------------------------------------
1847#
1848# GuiBooleanListOption class
1849#
1850#-------------------------------------------------------------------------
1851class GuiBooleanListOption(Gtk.Box):
1852    """
1853    This class displays an option that provides a list of check boxes.
1854    Each possible value is assigned a value and a description.
1855    """
1856    def __init__(self, option, dbstate, uistate, track, override):
1857        Gtk.Box.__init__(self)
1858        self.__option = option
1859        self.__cbutton = []
1860
1861        default = option.get_value().split(',')
1862        if len(default) < 15:
1863            columns = 2 # number of checkbox columns
1864        else:
1865            columns = 3
1866        column = []
1867        for dummy in range(columns):
1868            vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
1869            self.pack_start(vbox, True, True, 0)
1870            column.append(vbox)
1871            vbox.show()
1872
1873        counter = 0
1874        this_column_counter = 0
1875        ncolumn = 0
1876        for description in option.get_descriptions():
1877            button = Gtk.CheckButton(label=description)
1878            self.__cbutton.append(button)
1879            if counter < len(default):
1880                if default[counter] == 'True':
1881                    button.set_active(True)
1882            button.connect("toggled", self.__list_changed)
1883            # show the items vertically, not alternating left and right
1884            # (if the number is uneven, the left column(s) will have one more)
1885            column[ncolumn].pack_start(button, True, True, 0)
1886            button.show()
1887            counter += 1
1888            this_column_counter += 1
1889            this_column_gets = (len(default)+(columns-(ncolumn+1))) // columns
1890            if this_column_counter + 1 > this_column_gets:
1891                ncolumn += 1
1892                this_column_counter = 0
1893
1894        self.valuekey = self.__option.connect('value-changed',
1895                                              self.__value_changed)
1896
1897        self.__option.connect('avail-changed', self.__update_avail)
1898        self.__update_avail()
1899
1900        self.set_tooltip_text(self.__option.get_help())
1901
1902    def __list_changed(self, button):
1903        """
1904        Handle the change of the value made by the user.
1905        """
1906        value = ''
1907        for button in self.__cbutton:
1908            value = value + str(button.get_active()) + ','
1909        value = value[:len(value)-1]
1910
1911        self.__option.disable_signals()
1912        self.__option.set_value(value)
1913        self.__option.enable_signals()
1914
1915    def __update_avail(self):
1916        """
1917        Update the availability (sensitivity) of this widget.
1918        """
1919        avail = self.__option.get_available()
1920        self.set_sensitive(avail)
1921
1922    def __value_changed(self):
1923        """
1924        Handle the change made programmatically
1925        """
1926        value = self.__option.get_value()
1927
1928        self.__option.disable_signals()
1929
1930        for button in self.__cbutton:
1931            for key in value:
1932                if key == button.get_label():
1933                    bool_value = (value[key] == "True" or value[key] == True)
1934                    button.set_active(bool_value)
1935
1936        # Update __option value so that it's correct
1937        self.__list_changed(None)
1938
1939        self.__option.enable_signals()
1940
1941    def clean_up(self):
1942        """
1943        remove stuff that blocks garbage collection
1944        """
1945        self.__option.disconnect(self.valuekey)
1946        self.__option = None
1947
1948#-----------------------------------------------------------------------------#
1949#                                                                             #
1950#   Table mapping menu types to gui widgets used in make_gui_option function  #
1951#                                                                             #
1952#-----------------------------------------------------------------------------#
1953
1954from gramps.gen.plug import menu as menu
1955_OPTIONS = (
1956
1957    (menu.BooleanListOption, True, GuiBooleanListOption),
1958    (menu.BooleanOption, False, GuiBooleanOption),
1959    (menu.ColorOption, True, GuiColorOption),
1960    (menu.DestinationOption, True, GuiDestinationOption),
1961    (menu.EnumeratedListOption, True, GuiEnumeratedListOption),
1962    (menu.FamilyOption, True, GuiFamilyOption),
1963    (menu.MediaOption, True, GuiMediaOption),
1964    (menu.NoteOption, True, GuiNoteOption),
1965    (menu.NumberOption, True, GuiNumberOption),
1966    (menu.PersonListOption, True, GuiPersonListOption),
1967    (menu.PersonOption, True, GuiPersonOption),
1968    (menu.PlaceListOption, True, GuiPlaceListOption),
1969    (menu.StringOption, True, GuiStringOption),
1970    (menu.StyleOption, True, GuiStyleOption),
1971    (menu.SurnameColorOption, True, GuiSurnameColorOption),
1972    (menu.TextOption, True, GuiTextOption),
1973
1974    # This entry must be last!
1975
1976    (menu.Option, None, None),
1977
1978    )
1979del menu
1980
1981def make_gui_option(option, dbstate, uistate, track, override=False):
1982    """
1983    Stand-alone function so that Options can be used in other
1984    ways, too. Takes an Option and returns a GuiOption.
1985
1986    override: if True will override the GuiOption's normal behavior
1987        (in a GuiOption-dependant fashion, for instance in a GuiPersonOption
1988        it will force the use of the options's value to set the GuiOption)
1989    """
1990
1991    label, widget = True, None
1992    pmgr = GuiPluginManager.get_instance()
1993    external_options = pmgr.get_external_opt_dict()
1994    if option.__class__ in external_options:
1995        widget = external_options[option.__class__]
1996    else:
1997        for type_, label, widget in _OPTIONS:
1998            if isinstance(option, type_):
1999                break
2000        else:
2001            raise AttributeError(
2002                "can't make GuiOption: unknown option type: '%s'" % option)
2003
2004    if widget:
2005        widget = widget(option, dbstate, uistate, track, override)
2006
2007    return widget, label
2008
2009def add_gui_options(dialog):
2010    """
2011    Stand-alone function to add user options to the GUI.
2012    """
2013    if not hasattr(dialog.options, "menu"):
2014        return
2015    o_menu = dialog.options.menu
2016    options_dict = dialog.options.options_dict
2017    for category in o_menu.get_categories():
2018        for name in o_menu.get_option_names(category):
2019            option = o_menu.get_option(category, name)
2020
2021            # override option default with xml-saved value:
2022            if name in options_dict:
2023                option.set_value(options_dict[name])
2024
2025            widget, label = make_gui_option(option, dialog.dbstate,
2026                                            dialog.uistate, dialog.track)
2027            if widget is not None:
2028                if label:
2029                    dialog.add_frame_option(category,
2030                                            option.get_label(),
2031                                            widget)
2032                else:
2033                    dialog.add_frame_option(category, "", widget)
2034