1# 2# Gramps - a GTK+/GNOME based genealogy program 3# 4# Copyright (C) 2001-2007 Donald N. Allingham 5# Copyright (C) 2009-2010 Nick Hall 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""" 23Provide the base classes for GRAMPS' DataView classes 24""" 25 26#---------------------------------------------------------------- 27# 28# python modules 29# 30#---------------------------------------------------------------- 31from abc import abstractmethod 32import html 33import logging 34 35_LOG = logging.getLogger('.navigationview') 36 37#---------------------------------------------------------------- 38# 39# gtk 40# 41#---------------------------------------------------------------- 42from gi.repository import Gdk 43from gi.repository import Gtk 44 45#---------------------------------------------------------------- 46# 47# Gramps 48# 49#---------------------------------------------------------------- 50from gramps.gen.const import GRAMPS_LOCALE as glocale 51_ = glocale.translation.sgettext 52from .pageview import PageView 53from ..uimanager import ActionGroup 54from gramps.gen.utils.db import navigation_label 55from gramps.gen.constfunc import mod_key 56from ..utils import match_primary_mask 57 58DISABLED = -1 59MRU_SIZE = 10 60 61MRU_TOP = '<section id="CommonHistory">' 62MRU_BTM = '</section>' 63 64#------------------------------------------------------------------------------ 65# 66# NavigationView 67# 68#------------------------------------------------------------------------------ 69class NavigationView(PageView): 70 """ 71 The NavigationView class is the base class for all Data Views that require 72 navigation functionalilty. Views that need bookmarks and forward/backward 73 should derive from this class. 74 """ 75 76 def __init__(self, title, pdata, state, uistate, bm_type, nav_group): 77 PageView.__init__(self, title, pdata, state, uistate) 78 self.bookmarks = bm_type(self.dbstate, self.uistate, self.change_active) 79 80 self.fwd_action = None 81 self.back_action = None 82 self.book_action = None 83 self.other_action = None 84 self.active_signal = None 85 self.mru_signal = None 86 self.nav_group = nav_group 87 self.mru_active = DISABLED 88 self.uimanager = uistate.uimanager 89 90 self.uistate.register(state, self.navigation_type(), self.nav_group) 91 92 93 def navigation_type(self): 94 """ 95 Indictates the navigation type. Navigation type can be the string 96 name of any of the primary Objects. A History object will be 97 created for it, see DisplayState.History 98 """ 99 return None 100 101 def define_actions(self): 102 """ 103 Define menu actions. 104 """ 105 PageView.define_actions(self) 106 self.bookmark_actions() 107 self.navigation_actions() 108 109 def disable_action_group(self): 110 """ 111 Normally, this would not be overridden from the base class. However, 112 in this case, we have additional action groups that need to be 113 handled correctly. 114 """ 115 PageView.disable_action_group(self) 116 117 self.uimanager.set_actions_visible(self.fwd_action, False) 118 self.uimanager.set_actions_visible(self.back_action, False) 119 120 def enable_action_group(self, obj): 121 """ 122 Normally, this would not be overridden from the base class. However, 123 in this case, we have additional action groups that need to be 124 handled correctly. 125 """ 126 PageView.enable_action_group(self, obj) 127 128 self.uimanager.set_actions_visible(self.fwd_action, True) 129 self.uimanager.set_actions_visible(self.back_action, True) 130 hobj = self.get_history() 131 self.uimanager.set_actions_sensitive(self.fwd_action, 132 not hobj.at_end()) 133 self.uimanager.set_actions_sensitive(self.back_action, 134 not hobj.at_front()) 135 136 def change_page(self): 137 """ 138 Called when the page changes. 139 """ 140 hobj = self.get_history() 141 self.uimanager.set_actions_sensitive(self.fwd_action, 142 not hobj.at_end()) 143 self.uimanager.set_actions_sensitive(self.back_action, 144 not hobj.at_front()) 145 self.uimanager.set_actions_sensitive(self.other_action, 146 not self.dbstate.db.readonly) 147 self.uistate.modify_statusbar(self.dbstate) 148 149 def set_active(self): 150 """ 151 Called when the page becomes active (displayed). 152 """ 153 PageView.set_active(self) 154 self.bookmarks.display() 155 156 hobj = self.get_history() 157 self.active_signal = hobj.connect('active-changed', self.goto_active) 158 self.mru_signal = hobj.connect('mru-changed', self.update_mru_menu) 159 self.update_mru_menu(hobj.mru, update_menu=False) 160 161 self.goto_active(None) 162 163 def set_inactive(self): 164 """ 165 Called when the page becomes inactive (not displayed). 166 """ 167 if self.active: 168 PageView.set_inactive(self) 169 self.bookmarks.undisplay() 170 hobj = self.get_history() 171 hobj.disconnect(self.active_signal) 172 hobj.disconnect(self.mru_signal) 173 self.mru_disable() 174 175 def navigation_group(self): 176 """ 177 Return the navigation group. 178 """ 179 return self.nav_group 180 181 def get_history(self): 182 """ 183 Return the history object. 184 """ 185 return self.uistate.get_history(self.navigation_type(), 186 self.navigation_group()) 187 188 def goto_active(self, active_handle): 189 """ 190 Callback (and usable function) that selects the active person 191 in the display tree. 192 """ 193 active_handle = self.uistate.get_active(self.navigation_type(), 194 self.navigation_group()) 195 if active_handle: 196 self.goto_handle(active_handle) 197 198 hobj = self.get_history() 199 self.uimanager.set_actions_sensitive(self.fwd_action, 200 not hobj.at_end()) 201 self.uimanager.set_actions_sensitive(self.back_action, 202 not hobj.at_front()) 203 204 def get_active(self): 205 """ 206 Return the handle of the active object. 207 """ 208 hobj = self.uistate.get_history(self.navigation_type(), 209 self.navigation_group()) 210 return hobj.present() 211 212 def change_active(self, handle): 213 """ 214 Changes the active object. 215 """ 216 hobj = self.get_history() 217 if handle and not hobj.lock and not (handle == hobj.present()): 218 hobj.push(handle) 219 220 @abstractmethod 221 def goto_handle(self, handle): 222 """ 223 Needs to be implemented by classes derived from this. 224 Used to move to the given handle. 225 """ 226 227 def selected_handles(self): 228 """ 229 Return the active person's handle in a list. Used for 230 compatibility with those list views that can return multiply 231 selected items. 232 """ 233 active_handle = self.uistate.get_active(self.navigation_type(), 234 self.navigation_group()) 235 return [active_handle] if active_handle else [] 236 237 #################################################################### 238 # BOOKMARKS 239 #################################################################### 240 def add_bookmark(self, *obj): 241 """ 242 Add a bookmark to the list. 243 """ 244 from gramps.gen.display.name import displayer as name_displayer 245 246 active_handle = self.uistate.get_active('Person') 247 active_person = self.dbstate.db.get_person_from_handle(active_handle) 248 if active_person: 249 self.bookmarks.add(active_handle) 250 name = name_displayer.display(active_person) 251 self.uistate.push_message(self.dbstate, 252 _("%s has been bookmarked") % name) 253 else: 254 from ..dialog import WarningDialog 255 WarningDialog( 256 _("Could Not Set a Bookmark"), 257 _("A bookmark could not be set because " 258 "no one was selected."), 259 parent=self.uistate.window) 260 261 def edit_bookmarks(self, *obj): 262 """ 263 Call the bookmark editor. 264 """ 265 self.bookmarks.edit() 266 267 def bookmark_actions(self): 268 """ 269 Define the bookmark menu actions. 270 """ 271 self.book_action = ActionGroup(name=self.title + '/Bookmark') 272 self.book_action.add_actions([ 273 ('AddBook', self.add_bookmark, '<PRIMARY>d'), 274 ('EditBook', self.edit_bookmarks, '<shift><PRIMARY>D'), 275 ]) 276 277 self._add_action_group(self.book_action) 278 279 #################################################################### 280 # NAVIGATION 281 #################################################################### 282 def navigation_actions(self): 283 """ 284 Define the navigation menu actions. 285 """ 286 # add the Forward action group to handle the Forward button 287 self.fwd_action = ActionGroup(name=self.title + '/Forward') 288 self.fwd_action.add_actions([('Forward', self.fwd_clicked, 289 "%sRight" % mod_key())]) 290 291 # add the Backward action group to handle the Forward button 292 self.back_action = ActionGroup(name=self.title + '/Backward') 293 self.back_action.add_actions([('Back', self.back_clicked, 294 "%sLeft" % mod_key())]) 295 296 self._add_action('HomePerson', self.home, "%sHome" % mod_key()) 297 298 self.other_action = ActionGroup(name=self.title + '/PersonOther') 299 self.other_action.add_actions([ 300 ('SetActive', self.set_default_person)]) 301 302 self._add_action_group(self.back_action) 303 self._add_action_group(self.fwd_action) 304 self._add_action_group(self.other_action) 305 306 def set_default_person(self, *obj): 307 """ 308 Set the default person. 309 """ 310 active = self.uistate.get_active('Person') 311 if active: 312 self.dbstate.db.set_default_person_handle(active) 313 314 def home(self, *obj): 315 """ 316 Move to the default person. 317 """ 318 defperson = self.dbstate.db.get_default_person() 319 if defperson: 320 self.change_active(defperson.get_handle()) 321 else: 322 from ..dialog import WarningDialog 323 WarningDialog(_("No Home Person"), 324 _("You need to set a 'Home Person' to go to. " 325 "Select the People View, select the person you want as " 326 "'Home Person', then confirm your choice " 327 "via the menu Edit -> Set Home Person."), 328 parent=self.uistate.window) 329 330 def jump(self, *obj): 331 """ 332 A dialog to move to a Gramps ID entered by the user. 333 """ 334 dialog = Gtk.Dialog(_('Jump to by Gramps ID'), self.uistate.window) 335 dialog.set_border_width(12) 336 label = Gtk.Label(label='<span weight="bold" size="larger">%s</span>' % 337 _('Jump to by Gramps ID')) 338 label.set_use_markup(True) 339 dialog.vbox.add(label) 340 dialog.vbox.set_spacing(10) 341 dialog.vbox.set_border_width(12) 342 hbox = Gtk.Box() 343 hbox.pack_start(Gtk.Label(label=_("%s: ") % _('ID')), True, True, 0) 344 text = Gtk.Entry() 345 text.set_activates_default(True) 346 hbox.pack_start(text, False, True, 0) 347 dialog.vbox.pack_start(hbox, False, True, 0) 348 dialog.add_buttons(_('_Cancel'), Gtk.ResponseType.CANCEL, 349 _('_Jump to'), Gtk.ResponseType.OK) 350 dialog.set_default_response(Gtk.ResponseType.OK) 351 dialog.vbox.show_all() 352 353 if dialog.run() == Gtk.ResponseType.OK: 354 gid = text.get_text() 355 handle = self.get_handle_from_gramps_id(gid) 356 if handle is not None: 357 self.change_active(handle) 358 else: 359 self.uistate.push_message( 360 self.dbstate, 361 _("Error: %s is not a valid Gramps ID") % gid) 362 dialog.destroy() 363 364 def get_handle_from_gramps_id(self, gid): 365 """ 366 Get an object handle from its Gramps ID. 367 Needs to be implemented by the inheriting class. 368 """ 369 pass 370 371 def fwd_clicked(self, *obj): 372 """ 373 Move forward one object in the history. 374 """ 375 hobj = self.get_history() 376 hobj.lock = True 377 if not hobj.at_end(): 378 hobj.forward() 379 self.uistate.modify_statusbar(self.dbstate) 380 self.uimanager.set_actions_sensitive(self.fwd_action, 381 not hobj.at_end()) 382 self.uimanager.set_actions_sensitive(self.back_action, True) 383 hobj.lock = False 384 385 def back_clicked(self, *obj): 386 """ 387 Move backward one object in the history. 388 """ 389 hobj = self.get_history() 390 hobj.lock = True 391 if not hobj.at_front(): 392 hobj.back() 393 self.uistate.modify_statusbar(self.dbstate) 394 self.uimanager.set_actions_sensitive(self.back_action, 395 not hobj.at_front()) 396 self.uimanager.set_actions_sensitive(self.fwd_action, True) 397 hobj.lock = False 398 399 #################################################################### 400 # MRU functions 401 #################################################################### 402 403 def mru_disable(self): 404 """ 405 Remove the UI and action groups for the MRU list. 406 """ 407 if self.mru_active != DISABLED: 408 self.uimanager.remove_ui(self.mru_active) 409 self.uimanager.remove_action_group(self.mru_action) 410 self.mru_active = DISABLED 411 412 def mru_enable(self, update_menu=False): 413 """ 414 Enables the UI and action groups for the MRU list. 415 """ 416 if self.mru_active == DISABLED: 417 self.uimanager.insert_action_group(self.mru_action) 418 self.mru_active = self.uimanager.add_ui_from_string(self.mru_ui) 419 if update_menu: 420 self.uimanager.update_menu() 421 422 def update_mru_menu(self, items, update_menu=True): 423 """ 424 Builds the UI and action group for the MRU list. 425 """ 426 menuitem = ''' <item> 427 <attribute name="action">win.%s%02d</attribute> 428 <attribute name="label">%s</attribute> 429 </item> 430 ''' 431 menus = '' 432 self.mru_disable() 433 nav_type = self.navigation_type() 434 hobj = self.get_history() 435 menu_len = min(len(items) - 1, MRU_SIZE) 436 437 data = [] 438 for index in range(menu_len - 1, -1, -1): 439 name, _obj = navigation_label(self.dbstate.db, nav_type, 440 items[index]) 441 menus += menuitem % (nav_type, index, html.escape(name)) 442 data.append(('%s%02d' % (nav_type, index), 443 make_callback(hobj.push, items[index]), 444 "%s%d" % (mod_key(), menu_len - 1 - index))) 445 self.mru_ui = [MRU_TOP + menus + MRU_BTM] 446 447 self.mru_action = ActionGroup(name=self.title + '/MRU') 448 self.mru_action.add_actions(data) 449 self.mru_enable(update_menu) 450 451 #################################################################### 452 # Template functions 453 #################################################################### 454 @abstractmethod 455 def build_tree(self): 456 """ 457 Rebuilds the current display. This must be overridden by the derived 458 class. 459 """ 460 461 @abstractmethod 462 def build_widget(self): 463 """ 464 Builds the container widget for the interface. Must be overridden by the 465 the base class. Returns a gtk container widget. 466 """ 467 468 def key_press_handler(self, widget, event): 469 """ 470 Handle the control+c (copy) and control+v (paste), or pass it on. 471 """ 472 if self.active: 473 if event.type == Gdk.EventType.KEY_PRESS: 474 if (event.keyval == Gdk.KEY_c and 475 match_primary_mask(event.get_state())): 476 self.call_copy() 477 return True 478 return super(NavigationView, self).key_press_handler(widget, event) 479 480 def call_copy(self): 481 """ 482 Navigation specific copy (control+c) hander. If the 483 copy can be handled, it returns true, otherwise false. 484 485 The code brings up the Clipboard (if already exists) or 486 creates it. The copy is handled through the drag and drop 487 system. 488 """ 489 nav_type = self.navigation_type() 490 handles = self.selected_handles() 491 return self.copy_to_clipboard(nav_type, handles) 492 493def make_callback(func, handle): 494 """ 495 Generates a callback function based off the passed arguments 496 """ 497 return lambda x, y: func(handle) 498