1#
2# Gramps - a GTK+/GNOME based genealogy program
3#
4# Copyright (C) 2000-2007  Donald N. Allingham
5#               2009       Gary Burton
6#
7# This program is free software; you can redistribute it and/or modify
8# it under the terms of the GNU General Public License as published by
9# the Free Software Foundation; either version 2 of the License, or
10# (at your option) any later version.
11#
12# This program is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15# GNU General Public License for more details.
16#
17# You should have received a copy of the GNU General Public License
18# along with this program; if not, write to the Free Software
19# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
20#
21
22#-------------------------------------------------------------------------
23#
24# Python modules
25#
26#-------------------------------------------------------------------------
27import abc
28
29#-------------------------------------------------------------------------
30#
31# GTK modules
32#
33#-------------------------------------------------------------------------
34from gi.repository import Gtk
35from gi.repository.Gio import SimpleActionGroup
36
37#-------------------------------------------------------------------------
38#
39# Gramps modules
40#
41#-------------------------------------------------------------------------
42from gramps.gen.const import GRAMPS_LOCALE as glocale
43_ = glocale.translation.gettext
44from ..managedwindow import ManagedWindow
45from gramps.gen.datehandler import displayer, parser
46from gramps.gen.display.name import displayer as name_displayer
47from gramps.gen.config import config
48from ..utils import is_right_click
49from ..display import display_help
50from ..dialog import SaveDialog
51from gramps.gen.lib import PrimaryObject
52from ..dbguielement import DbGUIElement
53from ..uimanager import ActionGroup
54
55
56class EditPrimary(ManagedWindow, DbGUIElement, metaclass=abc.ABCMeta):
57
58    QR_CATEGORY = -1
59
60    def __init__(self, state, uistate, track, obj, get_from_handle,
61                 get_from_gramps_id, callback=None):
62        """
63        Create an edit window.
64
65        Associate a person with the window.
66
67        """
68        self.dp = parser
69        self.dd = displayer
70        self.name_displayer = name_displayer
71        self.obj = obj
72        self.dbstate = state
73        self.uistate = uistate
74        self.db = state.db
75        self.callback = callback
76        self.ok_button = None
77        self.get_from_handle = get_from_handle
78        self.get_from_gramps_id = get_from_gramps_id
79        self.contexteventbox = None
80        self.__tabs = []
81        self.action_group = None
82
83        ManagedWindow.__init__(self, uistate, track, obj)
84        DbGUIElement.__init__(self, self.db)
85
86        self.original = None
87        if self.obj.handle:
88            self.original = self.get_from_handle(self.obj.handle)
89
90        self._local_init()
91        # self.set_size() is called by self._local_init()'s self.setup_configs
92        self._create_tabbed_pages()
93        self._setup_fields()
94        self._connect_signals()
95        #if the database is changed, all info shown is invalid and the window
96        # should close
97        self.dbstate_connect_key = self.dbstate.connect('database-changed',
98                                   self._do_close)
99        self.show()
100        self._post_init()
101
102    def _local_init(self):
103        """
104        Derived class should do any pre-window initalization in this task.
105        """
106        pass
107
108    def _post_init(self):
109        """
110        Derived class should do any post-window initalization in this task.
111        """
112        pass
113
114    def _setup_fields(self):
115        pass
116
117    def _create_tabbed_pages(self):
118        pass
119
120    def _connect_signals(self):
121        pass
122
123    def build_window_key(self, obj):
124        if obj and obj.get_handle():
125            return obj.get_handle()
126        else:
127            return id(self)
128
129    def _setup_notebook_tabs(self, notebook):
130        for child in notebook.get_children():
131            label = notebook.get_tab_label(child)
132            page_no = notebook.page_num(child)
133            label.drag_dest_set(0, [], 0)
134            label.connect('drag_motion',
135                          self._switch_page_on_dnd,
136                          notebook,
137                          page_no)
138            child.set_parent_notebook(notebook)
139
140        notebook.connect('key-press-event', self.key_pressed, notebook)
141
142    def key_pressed(self, obj, event, notebook):
143        """
144        Handles the key being pressed on the notebook, pass to key press of
145        current page.
146        """
147        pag = notebook.get_current_page()
148        if not pag == -1:
149            notebook.get_nth_page(pag).key_pressed(obj, event)
150
151    def _switch_page_on_dnd(self, widget, context, x, y, time, notebook,
152                            page_no):
153        if notebook.get_current_page() != page_no:
154            notebook.set_current_page(page_no)
155
156    def _add_tab(self, notebook, page):
157        self.__tabs.append(page)
158        notebook.insert_page(page, page.get_tab_widget(), -1)
159        page.label.set_use_underline(True)
160        return page
161
162    def _cleanup_on_exit(self):
163        """Unset all things that can block garbage collection.
164        Finalize rest
165        """
166        for tab in self.__tabs:
167            if hasattr(tab, '_cleanup_on_exit'):
168                tab._cleanup_on_exit()
169        self.__tabs = None
170
171    def object_is_empty(self):
172        return self.obj.serialize()[1:] == self.empty_object().serialize()[1:]
173
174    def define_ok_button(self, button, function):
175        self.ok_button = button
176        button.connect('clicked', function)
177        button.set_sensitive(not self.db.readonly)
178
179    def define_cancel_button(self, button):
180        button.connect('clicked', self.close)
181
182    def define_help_button(self, button, webpage='', section=''):
183        button.connect('clicked', lambda x: display_help(webpage,
184                                                               section))
185
186    def _do_close(self, *obj):
187        self._cleanup_db_connects()
188        self.dbstate.disconnect(self.dbstate_connect_key)
189        self._cleanup_connects()
190        self._cleanup_on_exit()
191        if self.action_group:
192            self.uistate.uimanager.remove_action_group(self.action_group)
193        self.get_from_handle = None
194        self.get_from_gramps_id = None
195        ManagedWindow.close(self)
196        self.dbstate = None
197        self.uistate = None
198        self.db = None
199
200    def _cleanup_db_connects(self):
201        """
202        All connects that happened to signals of the db must be removed on
203        closed. This implies two things:
204        1. The connects on the main view must be disconnected
205        2. Connects done in subelements must be disconnected
206        """
207        #cleanup callbackmanager of this editor
208        self._cleanup_callbacks()
209        for tab in self.__tabs:
210            if hasattr(tab, 'callman'):
211                tab._cleanup_callbacks()
212
213    def _cleanup_connects(self):
214        """
215        Connects to interface elements to things outside the element should be
216        removed before destroying the interface
217        """
218        self._cleanup_local_connects()
219        for tab in [tab for tab in self.__tabs if hasattr(tab, '_cleanup_local_connects')]:
220            tab._cleanup_local_connects()
221
222    def _cleanup_local_connects(self):
223        """
224        Connects to interface elements to things outside the element should be
225        removed before destroying the interface. This methods cleans connects
226        of the main interface, not of the displaytabs.
227        """
228        pass
229
230    def check_for_close(self, handles):
231        """
232        Callback method for delete signals.
233        If there is a delete signal of the primary object we are editing, the
234        editor (and all child windows spawned) should be closed
235        """
236        if self.obj.get_handle() in handles:
237            self._do_close()
238
239    def close(self, *obj):
240        """If the data has changed, give the user a chance to cancel
241        the close window"""
242        if not config.get('interface.dont-ask') and self.data_has_changed():
243            SaveDialog(
244                _('Save Changes?'),
245                _('If you close without saving, the changes you '
246                  'have made will be lost'),
247                self._do_close,
248                self.save,
249                parent=self.window)
250            return True
251        else:
252            self._do_close()
253            return False
254
255    @abc.abstractmethod
256    def empty_object(self):
257        """ empty_object should be overridden in child class """
258
259    def data_has_changed(self):
260        if self.db.readonly:
261            return False
262        if self.original:
263            cmp_obj = self.original
264        else:
265            cmp_obj = self.empty_object()
266        return cmp_obj.serialize()[1:] != self.obj.serialize()[1:]
267
268    def save(self, *obj):
269        """ Save changes and close. Inheriting classes must implement this
270        """
271        self.close()
272
273    def set_contexteventbox(self, eventbox):
274        """Set the contextbox that grabs button presses if not grabbed
275            by overlying widgets.
276        """
277        self.contexteventbox = eventbox
278        self.contexteventbox.connect('button-press-event',
279                                self._contextmenu_button_press)
280
281    def _contextmenu_button_press(self, obj, event):
282        """
283        Button press event that is caught when a mousebutton has been
284        pressed while on contexteventbox
285        It opens a context menu with possible actions
286        """
287        if is_right_click(event):
288            if self.obj.get_handle() == 0 :
289                return False
290
291            #build the possible popup menu
292            menu_model = self._build_popup_ui()
293            if not menu_model:
294                return False
295            #set or unset sensitivity in popup
296            self._post_build_popup_ui()
297
298            menu = Gtk.Menu.new_from_model(menu_model)
299            menu.attach_to_widget(obj, None)
300            menu.show_all()
301            if Gtk.MINOR_VERSION < 22:
302                # ToDo The following is reported to work poorly with Wayland
303                menu.popup(None, None, None, None,
304                           event.button, event.time)
305            else:
306                menu.popup_at_pointer(event)
307            return True
308        return False
309
310    def _build_popup_ui(self):
311        """
312        Create actions and ui of context menu
313        If you don't need a popup, override this and return None
314        """
315        from ..plug.quick import create_quickreport_menu
316
317        prefix = str(id(self))
318        #get custom ui and actions
319        (ui_top, actions) = self._top_contextmenu(prefix)
320        #see which quick reports are available now:
321        ui_qr = ''
322        if self.QR_CATEGORY > -1 :
323            (ui_qr, reportactions) = create_quickreport_menu(
324                self.QR_CATEGORY, self.dbstate, self.uistate,
325                self.obj, prefix, track=self.track)
326            actions.extend(reportactions)
327
328        popupui = '''<?xml version="1.0" encoding="UTF-8"?>
329            <interface>
330            <menu id="Popup">''' + ui_top + '''
331              <section>
332            ''' + ui_qr + '''
333              </section>
334            </menu>
335            </interface>'''
336
337        builder = Gtk.Builder.new_from_string(popupui, -1)
338
339        self.action_group = ActionGroup('EditPopup' + prefix, actions,
340                                        prefix)
341        act_grp = SimpleActionGroup()
342        self.window.insert_action_group(prefix, act_grp)
343        self.window.set_application(self.uistate.uimanager.app)
344        self.uistate.uimanager.insert_action_group(self.action_group, act_grp)
345        return builder.get_object('Popup')
346
347    def _top_contextmenu(self, prefix):
348        """
349        Derived class can create a ui with menuitems and corresponding list of
350        actiongroups
351        """
352        return "", []
353
354    def _post_build_popup_ui(self):
355        """
356        Derived class should do extra actions here on the menu
357        """
358        pass
359
360    def _uses_duplicate_id(self):
361        """
362        Check whether a changed or added Gramps ID already exists in the DB.
363
364        Return True if a duplicate Gramps ID has been detected.
365
366        """
367        idval = self.obj.get_gramps_id()
368        existing = self.get_from_gramps_id(idval)
369        if existing:
370            if existing.get_handle() == self.obj.get_handle():
371                return (False, 0)
372            else:
373                return (True, idval)
374        else:
375            return (False, 0)
376