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