1# 2# Gramps - a GTK+/GNOME based genealogy program 3# 4# Copyright (C) 2003-2007 Donald N. Allingham 5# Copyright (C) 2007-2012 Brian G. Matherly 6# Copyright (C) 2010 Jakim Friant 7# Copyright (C) 2012 Nick Hall 8# Copyright (C) 2011-2016 Paul Franklin 9# 10# This program is free software; you can redistribute it and/or modify 11# it under the terms of the GNU General Public License as published by 12# the Free Software Foundation; either version 2 of the License, or 13# (at your option) any later version. 14# 15# This program is distributed in the hope that it will be useful, 16# but WITHOUT ANY WARRANTY; without even the implied warranty of 17# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18# GNU General Public License for more details. 19# 20# You should have received a copy of the GNU General Public License 21# along with this program; if not, write to the Free Software 22# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 23# 24 25""" GUI dialog for creating and managing books """ 26 27# Written by Alex Roitman, 28# largely based on the BaseDoc classes by Don Allingham 29 30#------------------------------------------------------------------------- 31# 32# Standard Python modules 33# 34#------------------------------------------------------------------------- 35 36#------------------------------------------------------------------------ 37# 38# Set up logging 39# 40#------------------------------------------------------------------------ 41import logging 42LOG = logging.getLogger(".Book") 43 44#------------------------------------------------------------------------- 45# 46# GTK/Gnome modules 47# 48#------------------------------------------------------------------------- 49from gi.repository import Gdk 50from gi.repository import Gtk 51from gi.repository import GObject 52 53#------------------------------------------------------------------------- 54# 55# Gramps modules 56# 57#------------------------------------------------------------------------- 58from gramps.gen.const import GRAMPS_LOCALE as glocale 59_ = glocale.translation.gettext 60from ...listmodel import ListModel 61from gramps.gen.errors import FilterError, ReportError 62from gramps.gen.const import URL_MANUAL_PAGE 63from ...display import display_help 64from ...pluginmanager import GuiPluginManager 65from ...dialog import WarningDialog, ErrorDialog, QuestionDialog2 66from gramps.gen.plug.menu import PersonOption, FamilyOption 67from gramps.gen.plug.docgen import StyleSheet 68from ...managedwindow import ManagedWindow, set_titles 69from ...glade import Glade 70from ...utils import is_right_click, open_file_with_default_application 71from ...user import User 72from .. import make_gui_option 73 74# Import from specific modules in ReportBase 75from gramps.gen.plug.report import BookList, Book, BookItem, append_styles 76from gramps.gen.plug.report import CATEGORY_BOOK, book_categories 77from gramps.gen.plug.report._options import ReportOptions 78from ._reportdialog import ReportDialog 79from ._docreportdialog import DocReportDialog 80 81#------------------------------------------------------------------------ 82# 83# Private Constants 84# 85#------------------------------------------------------------------------ 86_UNSUPPORTED = _("Unsupported") 87 88_RETURN = Gdk.keyval_from_name("Return") 89_KP_ENTER = Gdk.keyval_from_name("KP_Enter") 90WIKI_HELP_PAGE = URL_MANUAL_PAGE + "_-_Reports_-_part_3" 91WIKI_HELP_SEC = _('Books') 92GENERATE_WIKI_HELP_SEC = _('Generate_Book_dialog') 93 94#------------------------------------------------------------------------ 95# 96# Private Functions 97# 98#------------------------------------------------------------------------ 99def _initialize_options(options, dbstate, uistate): 100 """ 101 Validates all options by making sure that their values are consistent with 102 the database. 103 104 menu: The Menu class 105 dbase: the database the options will be applied to 106 """ 107 if not hasattr(options, "menu"): 108 return 109 dbase = dbstate.get_database() 110 if dbase.get_total() == 0: 111 return 112 menu = options.menu 113 114 for name in menu.get_all_option_names(): 115 option = menu.get_option_by_name(name) 116 value = option.get_value() 117 118 if isinstance(option, PersonOption): 119 if not dbase.get_person_from_gramps_id(value): 120 person_handle = uistate.get_active('Person') 121 person = dbase.get_person_from_handle(person_handle) 122 option.set_value(person.get_gramps_id()) 123 124 elif isinstance(option, FamilyOption): 125 if not dbase.get_family_from_gramps_id(value): 126 person_handle = uistate.get_active('Person') 127 person = dbase.get_person_from_handle(person_handle) 128 if person is None: 129 continue 130 family_list = person.get_family_handle_list() 131 if family_list: 132 family_handle = family_list[0] 133 else: 134 try: 135 family_handle = next(dbase.iter_family_handles()) 136 except StopIteration: 137 family_handle = None 138 if family_handle: 139 family = dbase.get_family_from_handle(family_handle) 140 option.set_value(family.get_gramps_id()) 141 else: 142 print("No family specified for ", name) 143 144#------------------------------------------------------------------------ 145# 146# BookListDisplay class 147# 148#------------------------------------------------------------------------ 149class BookListDisplay: 150 """ 151 Interface into a dialog with the list of available books. 152 153 Allows the user to select and/or delete a book from the list. 154 """ 155 156 def __init__(self, booklist, nodelete=False, dosave=False, parent=None): 157 """ 158 Create a BookListDisplay object that displays the books in BookList. 159 160 booklist: books that are displayed -- a :class:`.BookList` instance 161 nodelete: if True then the Delete button is hidden 162 dosave: if True then the book list is flagged to be saved if needed 163 """ 164 165 self.booklist = booklist 166 self.dosave = dosave 167 self.xml = Glade('book.glade', toplevel='book') 168 self.top = self.xml.toplevel 169 self.unsaved_changes = False 170 171 set_titles(self.top, self.xml.get_object('title2'), 172 _('Available Books')) 173 174 if nodelete: 175 delete_button = self.xml.get_object("delete_button") 176 delete_button.hide() 177 self.xml.connect_signals({ 178 "on_booklist_cancel_clicked" : self.on_booklist_cancel_clicked, 179 "on_booklist_ok_clicked" : self.on_booklist_ok_clicked, 180 "on_booklist_delete_clicked" : self.on_booklist_delete_clicked, 181 "on_book_ok_clicked" : self.do_nothing, 182 "on_book_help_clicked" : self.do_nothing, 183 "destroy_passed_object" : self.do_nothing, 184 "on_setup_clicked" : self.do_nothing, 185 "on_down_clicked" : self.do_nothing, 186 "on_up_clicked" : self.do_nothing, 187 "on_remove_clicked" : self.do_nothing, 188 "on_add_clicked" : self.do_nothing, 189 "on_edit_clicked" : self.do_nothing, 190 "on_open_clicked" : self.do_nothing, 191 "on_save_clicked" : self.do_nothing, 192 "on_clear_clicked" : self.do_nothing 193 }) 194 self.guilistbooks = self.xml.get_object('list') 195 self.guilistbooks.connect('button-press-event', self.on_button_press) 196 self.guilistbooks.connect('key-press-event', self.on_key_pressed) 197 self.blist = ListModel(self.guilistbooks, [('Name', -1, 10)],) 198 199 self.redraw() 200 self.selection = None 201 self.top.set_transient_for(parent) 202 self.top.run() 203 204 def redraw(self): 205 """Redraws the list of currently available books""" 206 207 self.blist.model.clear() 208 names = self.booklist.get_book_names() 209 if not len(names): 210 return 211 for name in names: 212 the_iter = self.blist.add([name]) 213 if the_iter: 214 self.blist.selection.select_iter(the_iter) 215 216 def on_booklist_ok_clicked(self, obj): 217 """ 218 Return selected book. 219 Also marks the current list to be saved into the xml file, if needed. 220 """ 221 store, the_iter = self.blist.get_selected() 222 if the_iter: 223 data = self.blist.get_data(the_iter, [0]) 224 self.selection = self.booklist.get_book(str(data[0])) 225 if self.dosave and self.unsaved_changes: 226 self.booklist.set_needs_saving(True) 227 228 def on_booklist_delete_clicked(self, obj): 229 """ 230 Deletes selected book from the list. 231 232 This change is not final. OK button has to be clicked to save the list. 233 """ 234 store, the_iter = self.blist.get_selected() 235 if not the_iter: 236 return 237 data = self.blist.get_data(the_iter, [0]) 238 self.booklist.delete_book(str(data[0])) 239 self.blist.remove(the_iter) 240 self.unsaved_changes = True 241 self.top.run() 242 243 def on_booklist_cancel_clicked(self, *obj): 244 """ cancel the booklist dialog """ 245 if self.unsaved_changes: 246 qqq = QuestionDialog2( 247 _('Discard Unsaved Changes'), 248 _('You have made changes which have not been saved.'), 249 _('Proceed'), 250 _('Cancel'), 251 parent=self.top) 252 if not qqq.run(): 253 self.top.run() 254 255 def on_button_press(self, obj, event): 256 """ 257 Checks for a double click event. In the list, we want to 258 treat a double click as if it was OK button press. 259 """ 260 if (event.type == Gdk.EventType.DOUBLE_BUTTON_PRESS 261 and event.button == 1): 262 store, the_iter = self.blist.get_selected() 263 if not the_iter: 264 return False 265 self.on_booklist_ok_clicked(obj) 266 #emit OK response on dialog to close it automatically 267 self.top.response(-5) 268 return True 269 return False 270 271 def on_key_pressed(self, obj, event): 272 """ 273 Handles the return key being pressed on list. If the key is pressed, 274 the Edit button handler is called 275 """ 276 if event.type == Gdk.EventType.KEY_PRESS: 277 if event.keyval in (_RETURN, _KP_ENTER): 278 self.on_booklist_ok_clicked(obj) 279 #emit OK response on dialog to close it automatically 280 self.top.response(-5) 281 return True 282 return False 283 284 def do_nothing(self, obj): 285 """ do nothing """ 286 pass 287 288#------------------------------------------------------------------------ 289# 290# Book Options 291# 292#------------------------------------------------------------------------ 293class BookOptions(ReportOptions): 294 295 """ 296 Defines options and provides handling interface. 297 """ 298 299 def __init__(self, name, dbase): 300 ReportOptions.__init__(self, name, dbase) 301 302 # Options specific for this report 303 self.options_dict = { 304 'bookname' : '', 305 } 306 # TODO since the CLI code for the "book" generates its own "help" now, 307 # the GUI code would be faster if it didn't list all the possible books 308 self.options_help = { 309 'bookname' : ("=name", _("Name of the book. MANDATORY"), 310 BookList('books.xml', dbase).get_book_names(), 311 False), 312 } 313 314#------------------------------------------------------------------------- 315# 316# Book creation dialog 317# 318#------------------------------------------------------------------------- 319class BookSelector(ManagedWindow): 320 """ 321 Interface into a dialog setting up the book. 322 323 Allows the user to add/remove/reorder/setup items for the current book 324 and to clear/load/save/edit whole books. 325 """ 326 327 def __init__(self, dbstate, uistate): 328 self._db = dbstate.db 329 self.dbstate = dbstate 330 self.uistate = uistate 331 self.title = _('Manage Books') 332 self.file = "books.xml" 333 334 ManagedWindow.__init__(self, uistate, [], self.__class__) 335 336 self.xml = Glade('book.glade', toplevel="top") 337 window = self.xml.toplevel 338 339 title_label = self.xml.get_object('title') 340 self.set_window(window, title_label, self.title) 341 self.setup_configs('interface.bookselector', 700, 600) 342 self.show() 343 self.xml.connect_signals({ 344 "on_add_clicked" : self.on_add_clicked, 345 "on_remove_clicked" : self.on_remove_clicked, 346 "on_up_clicked" : self.on_up_clicked, 347 "on_down_clicked" : self.on_down_clicked, 348 "on_setup_clicked" : self.on_setup_clicked, 349 "on_clear_clicked" : self.on_clear_clicked, 350 "on_save_clicked" : self.on_save_clicked, 351 "on_open_clicked" : self.on_open_clicked, 352 "on_edit_clicked" : self.on_edit_clicked, 353 "on_book_help_clicked" : lambda x: display_help(WIKI_HELP_PAGE, 354 WIKI_HELP_SEC), 355 "on_book_ok_clicked" : self.on_book_ok_clicked, 356 "destroy_passed_object" : self.on_close_clicked, 357 358 # Insert dummy handlers for second top level in the glade file 359 "on_booklist_ok_clicked" : lambda _: None, 360 "on_booklist_delete_clicked" : lambda _: None, 361 "on_booklist_cancel_clicked" : lambda _: None, 362 "on_booklist_ok_clicked" : lambda _: None, 363 "on_booklist_ok_clicked" : lambda _: None, 364 }) 365 366 self.avail_tree = self.xml.get_object("avail_tree") 367 self.book_tree = self.xml.get_object("book_tree") 368 self.avail_tree.connect('button-press-event', self.avail_button_press) 369 self.book_tree.connect('button-press-event', self.book_button_press) 370 371 self.name_entry = self.xml.get_object("name_entry") 372 self.name_entry.set_text(_('New Book')) 373 374 avail_label = self.xml.get_object('avail_label') 375 avail_label.set_text("<b>%s</b>" % _("_Available items")) 376 avail_label.set_use_markup(True) 377 avail_label.set_use_underline(True) 378 book_label = self.xml.get_object('book_label') 379 book_label.set_text("<b>%s</b>" % _("Current _book")) 380 book_label.set_use_underline(True) 381 book_label.set_use_markup(True) 382 383 avail_titles = [(_('Name'), 0, 230), 384 (_('Type'), 1, 80), 385 ('', -1, 0)] 386 387 book_titles = [(_('Item name'), -1, 230), 388 (_('Type'), -1, 80), 389 ('', -1, 0), 390 (_('Subject'), -1, 50)] 391 392 self.avail_nr_cols = len(avail_titles) 393 self.book_nr_cols = len(book_titles) 394 395 self.avail_model = ListModel(self.avail_tree, avail_titles) 396 self.book_model = ListModel(self.book_tree, book_titles) 397 self.draw_avail_list() 398 399 self.book = Book() 400 self.book_list = BookList(self.file, self._db) 401 self.book_list.set_needs_saving(False) # just read in: no need to save 402 403 def build_menu_names(self, obj): 404 return (_("Book selection list"), self.title) 405 406 def draw_avail_list(self): 407 """ 408 Draw the list with the selections available for the book. 409 410 The selections are read from the book item registry. 411 """ 412 pmgr = GuiPluginManager.get_instance() 413 regbi = pmgr.get_reg_bookitems() 414 if not regbi: 415 return 416 417 available_reports = [] 418 for pdata in regbi: 419 category = _UNSUPPORTED 420 if pdata.supported and pdata.category in book_categories: 421 category = book_categories[pdata.category] 422 available_reports.append([pdata.name, category, pdata.id]) 423 for data in sorted(available_reports): 424 new_iter = self.avail_model.add(data) 425 426 self.avail_model.connect_model() 427 428 if new_iter: 429 self.avail_model.selection.select_iter(new_iter) 430 path = self.avail_model.model.get_path(new_iter) 431 col = self.avail_tree.get_column(0) 432 self.avail_tree.scroll_to_cell(path, col, 1, 1, 0.0) 433 434 def open_book(self, book): 435 """ 436 Open the book: set the current set of selections to this book's items. 437 438 book: the book object to load. 439 """ 440 if book.get_paper_name(): 441 self.book.set_paper_name(book.get_paper_name()) 442 if book.get_orientation() is not None: # 0 is legal 443 self.book.set_orientation(book.get_orientation()) 444 if book.get_paper_metric() is not None: # 0 is legal 445 self.book.set_paper_metric(book.get_paper_metric()) 446 if book.get_custom_paper_size(): 447 self.book.set_custom_paper_size(book.get_custom_paper_size()) 448 if book.get_margins(): 449 self.book.set_margins(book.get_margins()) 450 if book.get_format_name(): 451 self.book.set_format_name(book.get_format_name()) 452 if book.get_output(): 453 self.book.set_output(book.get_output()) 454 if book.get_dbname() != self._db.get_save_path(): 455 WarningDialog( 456 _('Different database'), 457 _('This book was created with the references to database ' 458 '%s.\n\n This makes references to the central person ' 459 'saved in the book invalid.\n\n' 460 'Therefore, the central person for each item is being set ' 461 'to the active person of the currently opened database.' 462 ) % book.get_dbname(), 463 parent=self.window) 464 465 self.book.clear() 466 self.book_model.clear() 467 for saved_item in book.get_item_list(): 468 name = saved_item.get_name() 469 item = BookItem(self._db, name) 470 471 # The option values were loaded magically by the book parser. 472 # But they still need to be applied to the menu options. 473 opt_dict = item.option_class.handler.options_dict 474 orig_opt_dict = saved_item.option_class.handler.options_dict 475 menu = item.option_class.menu 476 for optname in opt_dict: 477 opt_dict[optname] = orig_opt_dict[optname] 478 menu_option = menu.get_option_by_name(optname) 479 if menu_option: 480 menu_option.set_value(opt_dict[optname]) 481 482 _initialize_options(item.option_class, self.dbstate, self.uistate) 483 item.set_style_name(saved_item.get_style_name()) 484 self.book.append_item(item) 485 486 data = [item.get_translated_name(), 487 item.get_category(), item.get_name()] 488 489 data[2] = item.option_class.get_subject() 490 self.book_model.add(data) 491 492 def on_add_clicked(self, obj): 493 """ 494 Add an item to the current selections. 495 496 Use the selected available item to get the item's name in the registry. 497 """ 498 store, the_iter = self.avail_model.get_selected() 499 if not the_iter: 500 return 501 data = self.avail_model.get_data(the_iter, 502 list(range(self.avail_nr_cols))) 503 item = BookItem(self._db, data[2]) 504 _initialize_options(item.option_class, self.dbstate, self.uistate) 505 data[2] = item.option_class.get_subject() 506 self.book_model.add(data) 507 self.book.append_item(item) 508 509 def on_remove_clicked(self, obj): 510 """ 511 Remove the item from the current list of selections. 512 """ 513 store, the_iter = self.book_model.get_selected() 514 if not the_iter: 515 return 516 row = self.book_model.get_selected_row() 517 self.book.pop_item(row) 518 self.book_model.remove(the_iter) 519 520 def on_clear_clicked(self, obj): 521 """ 522 Clear the whole current book. 523 """ 524 self.book_model.clear() 525 self.book.clear() 526 527 def on_up_clicked(self, obj): 528 """ 529 Move the currently selected item one row up in the selection list. 530 """ 531 row = self.book_model.get_selected_row() 532 if not row or row == -1: 533 return 534 store, the_iter = self.book_model.get_selected() 535 data = self.book_model.get_data(the_iter, 536 list(range(self.book_nr_cols))) 537 self.book_model.remove(the_iter) 538 self.book_model.insert(row-1, data, None, 1) 539 item = self.book.pop_item(row) 540 self.book.insert_item(row-1, item) 541 542 def on_down_clicked(self, obj): 543 """ 544 Move the currently selected item one row down in the selection list. 545 """ 546 row = self.book_model.get_selected_row() 547 if row + 1 >= self.book_model.count or row == -1: 548 return 549 store, the_iter = self.book_model.get_selected() 550 data = self.book_model.get_data(the_iter, 551 list(range(self.book_nr_cols))) 552 self.book_model.remove(the_iter) 553 self.book_model.insert(row+1, data, None, 1) 554 item = self.book.pop_item(row) 555 self.book.insert_item(row+1, item) 556 557 def on_setup_clicked(self, obj): 558 """ 559 Configure currently selected item. 560 """ 561 store, the_iter = self.book_model.get_selected() 562 if not the_iter: 563 WarningDialog(_('No selected book item'), 564 _('Please select a book item to configure.'), 565 parent=self.window) 566 return 567 row = self.book_model.get_selected_row() 568 item = self.book.get_item(row) 569 option_class = item.option_class 570 option_class.handler.set_default_stylesheet_name(item.get_style_name()) 571 item.is_from_saved_book = bool(self.book.get_name()) 572 item_dialog = BookItemDialog(self.dbstate, self.uistate, 573 item, self.track) 574 575 while True: 576 response = item_dialog.window.run() 577 if response == Gtk.ResponseType.OK: 578 # dialog will be closed by connect, now continue work while 579 # rest of dialog is unresponsive, release when finished 580 style = option_class.handler.get_default_stylesheet_name() 581 item.set_style_name(style) 582 subject = option_class.get_subject() 583 self.book_model.model.set_value(the_iter, 2, subject) 584 self.book.set_item(row, item) 585 item_dialog.close() 586 break 587 elif response == Gtk.ResponseType.CANCEL: 588 item_dialog.close() 589 break 590 elif response == Gtk.ResponseType.DELETE_EVENT: 591 #just stop, in ManagedWindow, delete-event is already coupled to 592 #correct action. 593 break 594 opt_dict = option_class.handler.options_dict 595 for optname in opt_dict: 596 menu_option = option_class.menu.get_option_by_name(optname) 597 if menu_option: 598 menu_option.set_value(opt_dict[optname]) 599 600 def book_button_press(self, obj, event): 601 """ 602 Double-click on the current book selection is the same as setup. 603 Right click evokes the context menu. 604 """ 605 if (event.type == Gdk.EventType.DOUBLE_BUTTON_PRESS 606 and event.button == 1): 607 self.on_setup_clicked(obj) 608 elif is_right_click(event): 609 self.build_book_context_menu(event) 610 611 def avail_button_press(self, obj, event): 612 """ 613 Double-click on the available selection is the same as add. 614 Right click evokes the context menu. 615 """ 616 if (event.type == Gdk.EventType.DOUBLE_BUTTON_PRESS 617 and event.button == 1): 618 self.on_add_clicked(obj) 619 elif is_right_click(event): 620 self.build_avail_context_menu(event) 621 622 def build_book_context_menu(self, event): 623 """Builds the menu with item-centered and book-centered options.""" 624 625 store, the_iter = self.book_model.get_selected() 626 if the_iter: 627 sensitivity = 1 628 else: 629 sensitivity = 0 630 entries = [ 631 (_('_Up'), self.on_up_clicked, sensitivity), 632 (_('_Down'), self.on_down_clicked, sensitivity), 633 (_("Setup"), self.on_setup_clicked, sensitivity), 634 (_('_Remove'), self.on_remove_clicked, sensitivity), 635 ('', None, 0), 636 (_('Clear the book'), self.on_clear_clicked, 1), 637 (_('_Save'), self.on_save_clicked, 1), 638 (_('_Open'), self.on_open_clicked, 1), 639 (_("_Edit"), self.on_edit_clicked, 1), 640 ] 641 642 self.menu1 = Gtk.Menu() # TODO could this be just a local "menu ="? 643 self.menu1.set_reserve_toggle_size(False) 644 for title, callback, sensitivity in entries: 645 item = Gtk.MenuItem.new_with_mnemonic(title) 646 Gtk.Label.new_with_mnemonic 647 if callback: 648 item.connect("activate", callback) 649 else: 650 item = Gtk.SeparatorMenuItem() 651 item.set_sensitive(sensitivity) 652 item.show() 653 self.menu1.append(item) 654 self.menu1.popup(None, None, None, None, event.button, event.time) 655 656 def build_avail_context_menu(self, event): 657 """Builds the menu with the single Add option.""" 658 659 store, the_iter = self.avail_model.get_selected() 660 if the_iter: 661 sensitivity = 1 662 else: 663 sensitivity = 0 664 entries = [ 665 (_('_Add'), self.on_add_clicked, sensitivity), 666 ] 667 668 self.menu2 = Gtk.Menu() # TODO could this be just a local "menu ="? 669 self.menu2.set_reserve_toggle_size(False) 670 for title, callback, sensitivity in entries: 671 item = Gtk.MenuItem.new_with_mnemonic(title) 672 if callback: 673 item.connect("activate", callback) 674 item.set_sensitive(sensitivity) 675 item.show() 676 self.menu2.append(item) 677 self.menu2.popup(None, None, None, None, event.button, event.time) 678 679 def on_close_clicked(self, obj): 680 """ 681 close the BookSelector dialog, saving any changes if needed 682 """ 683 if self.book_list.get_needs_saving(): 684 self.book_list.save() 685 ManagedWindow.close(self, *obj) 686 687 def on_book_ok_clicked(self, obj): 688 """ 689 Run final BookDialog with the current book. 690 """ 691 if self.book.get_item_list(): 692 old_paper_name = self.book.get_paper_name() # from books.xml 693 old_orientation = self.book.get_orientation() 694 old_paper_metric = self.book.get_paper_metric() 695 old_custom_paper_size = self.book.get_custom_paper_size() 696 old_margins = self.book.get_margins() 697 old_format_name = self.book.get_format_name() 698 old_output = self.book.get_output() 699 BookDialog(self.dbstate, self.uistate, self.book, BookOptions, track=self.track) 700 new_paper_name = self.book.get_paper_name() 701 new_orientation = self.book.get_orientation() 702 new_paper_metric = self.book.get_paper_metric() 703 new_custom_paper_size = self.book.get_custom_paper_size() 704 new_margins = self.book.get_margins() 705 new_format_name = self.book.get_format_name() 706 new_output = self.book.get_output() 707 # only books in the booklist have a name (not "ad hoc" ones) 708 if (self.book.get_name() and 709 (old_paper_name != new_paper_name or 710 old_orientation != new_orientation or 711 old_paper_metric != new_paper_metric or 712 old_custom_paper_size != new_custom_paper_size or 713 old_margins != new_margins or 714 old_format_name != new_format_name or 715 old_output != new_output)): 716 self.book.set_dbname(self._db.get_save_path()) 717 self.book_list.set_book(self.book.get_name(), self.book) 718 self.book_list.set_needs_saving(True) 719 if self.book_list.get_needs_saving(): 720 self.book_list.save() 721 else: 722 WarningDialog(_('No items'), 723 _('This book has no items.'), 724 parent=self.window) 725 return 726 self.close() 727 728 def on_save_clicked(self, obj): 729 """ 730 Save the current book in the xml booklist file. 731 """ 732 if not self.book.get_item_list(): 733 WarningDialog(_('No items'), 734 _('This book has no items.'), 735 parent=self.window) 736 return 737 name = str(self.name_entry.get_text()) 738 if not name: 739 WarningDialog( 740 _('No book name'), 741 _('You are about to save away a book with no name.\n\n' 742 'Please give it a name before saving it away.'), 743 parent=self.window) 744 return 745 if name in self.book_list.get_book_names(): 746 qqq = QuestionDialog2( 747 _('Book name already exists'), 748 _('You are about to save away a ' 749 'book with a name which already exists.'), 750 _('Proceed'), 751 _('Cancel'), 752 parent=self.window) 753 if not qqq.run(): 754 return 755 756 # previously, the same book could be added to the booklist 757 # under multiple names, which became different books once the 758 # booklist was saved into a file so everything was fine, but 759 # this created a problem once the paper settings were added 760 # to the Book object in the BookDialog, since those settings 761 # were retrieved from the Book object in BookList.save, so mutiple 762 # books (differentiated by their names) were assigned the 763 # same paper values, so the solution is to make each Book be 764 # unique in the booklist, so if multiple copies are saved away 765 # only the last one will get the paper values assigned to it 766 # (although when the earlier books are then eventually run, 767 # they'll be assigned paper values also) 768 self.book.set_name(name) 769 self.book.set_dbname(self._db.get_save_path()) 770 self.book_list.set_book(name, self.book) 771 self.book_list.set_needs_saving(True) # user clicked on save 772 self.book = Book(self.book, exact_copy=False) # regenerate old items 773 self.book.set_name(name) 774 self.book.set_dbname(self._db.get_save_path()) 775 776 def on_open_clicked(self, obj): 777 """ 778 Run the BookListDisplay dialog to present the choice of books to open. 779 """ 780 booklistdisplay = BookListDisplay(self.book_list, nodelete=True, 781 dosave=False, parent=self.window) 782 booklistdisplay.top.destroy() 783 book = booklistdisplay.selection 784 if book: 785 self.open_book(book) 786 self.name_entry.set_text(book.get_name()) 787 self.book.set_name(book.get_name()) 788 789 def on_edit_clicked(self, obj): 790 """ 791 Run the BookListDisplay dialog to present the choice of books to delete. 792 """ 793 booklistdisplay = BookListDisplay(self.book_list, nodelete=False, 794 dosave=True, parent=self.window) 795 booklistdisplay.top.destroy() 796 book = booklistdisplay.selection 797 if book: 798 self.open_book(book) 799 self.name_entry.set_text(book.get_name()) 800 self.book.set_name(book.get_name()) 801 802#------------------------------------------------------------------------ 803# 804# Book Item Options dialog 805# 806#------------------------------------------------------------------------ 807class BookItemDialog(ReportDialog): 808 809 """ 810 This class overrides the interface methods common for different reports 811 in a way specific for this report. This is a book item dialog. 812 """ 813 814 def __init__(self, dbstate, uistate, item, track=[]): 815 option_class = item.option_class 816 name = item.get_name() 817 translated_name = item.get_translated_name() 818 self.category = CATEGORY_BOOK 819 self.database = dbstate.db 820 self.option_class = option_class 821 self.is_from_saved_book = item.is_from_saved_book 822 ReportDialog.__init__(self, dbstate, uistate, 823 option_class, name, translated_name, track) 824 825 def on_ok_clicked(self, obj): 826 """The user is satisfied with the dialog choices. Parse all options 827 and close the window.""" 828 829 # Preparation 830 self.parse_style_frame() 831 self.parse_user_options() 832 833 self.options.handler.save_options() 834 835 def setup_target_frame(self): 836 """Target frame is not used.""" 837 pass 838 839 def parse_target_frame(self): 840 """Target frame is not used.""" 841 return 1 842 843 def init_options(self, option_class): 844 try: 845 if issubclass(option_class, object): 846 self.options = option_class(self.raw_name, self.database) 847 except TypeError: 848 self.options = option_class 849 if not self.is_from_saved_book: 850 self.options.load_previous_values() 851 852 def add_user_options(self): 853 """ 854 Generic method to add user options to the gui. 855 """ 856 if not hasattr(self.options, "menu"): 857 return 858 menu = self.options.menu 859 options_dict = self.options.options_dict 860 for category in menu.get_categories(): 861 for name in menu.get_option_names(category): 862 option = menu.get_option(category, name) 863 864 # override option default with xml-saved value: 865 if name in options_dict: 866 option.set_value(options_dict[name]) 867 868 widget, label = make_gui_option(option, self.dbstate, 869 self.uistate, self.track, 870 self.is_from_saved_book) 871 if widget is not None: 872 if label: 873 self.add_frame_option(category, 874 option.get_label(), 875 widget) 876 else: 877 self.add_frame_option(category, "", widget) 878 879#------------------------------------------------------------------------- 880# 881# _BookFormatComboBox 882# 883#------------------------------------------------------------------------- 884class _BookFormatComboBox(Gtk.ComboBox): 885 """ 886 Build a menu of report types that are appropriate for a book 887 """ 888 889 def __init__(self, active): 890 891 Gtk.ComboBox.__init__(self) 892 893 pmgr = GuiPluginManager.get_instance() 894 self.__bookdoc_plugins = [] 895 for plugin in pmgr.get_docgen_plugins(): 896 if plugin.get_text_support() and plugin.get_draw_support(): 897 self.__bookdoc_plugins.append(plugin) 898 899 self.store = Gtk.ListStore(GObject.TYPE_STRING) 900 self.set_model(self.store) 901 cell = Gtk.CellRendererText() 902 self.pack_start(cell, True) 903 self.add_attribute(cell, 'text', 0) 904 905 index = 0 906 active_index = 0 907 for plugin in self.__bookdoc_plugins: 908 name = plugin.get_name() 909 self.store.append(row=[name]) 910 if plugin.get_extension() == active: 911 active_index = index 912 index += 1 913 self.set_active(active_index) 914 915 def get_active_plugin(self): 916 """ 917 Get the plugin represented by the currently active selection. 918 """ 919 return self.__bookdoc_plugins[self.get_active()] 920 921#------------------------------------------------------------------------ 922# 923# The final dialog - paper, format, target, etc. 924# 925#------------------------------------------------------------------------ 926class BookDialog(DocReportDialog): 927 """ 928 A usual Report.Dialog subclass. 929 930 Create a dialog selecting target, format, and paper/HTML options. 931 """ 932 933 def __init__(self, dbstate, uistate, book, options, track=[]): 934 self.format_menu = None 935 self.options = options 936 self.page_html_added = False 937 self.book = book 938 self.title = _('Generate Book') 939 self.database = dbstate.db 940 DocReportDialog.__init__(self, dbstate, uistate, options, 941 'book', self.title, track=track) 942 self.options.options_dict['bookname'] = self.book.get_name() 943 944 while True: 945 response = self.window.run() 946 if response != Gtk.ResponseType.HELP: 947 break 948 if response == Gtk.ResponseType.OK: 949 handler = self.options.handler 950 if self.book.get_paper_name() != handler.get_paper_name(): 951 self.book.set_paper_name(handler.get_paper_name()) 952 if self.book.get_orientation() != handler.get_orientation(): 953 self.book.set_orientation(handler.get_orientation()) 954 if self.book.get_paper_metric() != handler.get_paper_metric(): 955 self.book.set_paper_metric(handler.get_paper_metric()) 956 if (self.book.get_custom_paper_size() != 957 handler.get_custom_paper_size()): 958 self.book.set_custom_paper_size(handler.get_custom_paper_size()) 959 if self.book.get_margins() != handler.get_margins(): 960 self.book.set_margins(handler.get_margins()) 961 if self.book.get_format_name() != handler.get_format_name(): 962 self.book.set_format_name(handler.get_format_name()) 963 if self.book.get_output() != self.options.get_output(): 964 self.book.set_output(self.options.get_output()) 965 try: 966 self.make_book() 967 except (IOError, OSError) as msg: 968 ErrorDialog(str(msg), parent=self.window) 969 if response != Gtk.ResponseType.DELETE_EVENT: # already closed 970 self.close() 971 972 def setup_style_frame(self): 973 pass 974 def setup_other_frames(self): 975 pass 976 def parse_style_frame(self): 977 pass 978 979 def get_title(self): 980 """ get the title """ 981 return self.title 982 983 def get_header(self, name): 984 """ get the header """ 985 return _("Gramps Book") 986 987 def make_doc_menu(self, active=None): 988 """Build a menu of document types that are appropriate for 989 this text report. This menu will be generated based upon 990 whether the document requires table support, etc.""" 991 self.format_menu = _BookFormatComboBox(active) 992 993 def on_help_clicked(self, *obj): 994 display_help(WIKI_HELP_PAGE, GENERATE_WIKI_HELP_SEC) 995 996 def make_document(self): 997 """Create a document of the type requested by the user.""" 998 user = User(uistate=self.uistate) 999 self.rptlist = [] 1000 selected_style = StyleSheet() 1001 1002 pstyle = self.paper_frame.get_paper_style() 1003 self.doc = self.format(None, pstyle) 1004 1005 for item in self.book.get_item_list(): 1006 item.option_class.set_document(self.doc) 1007 report_class = item.get_write_item() 1008 obj = (write_book_item(self.database, report_class, 1009 item.option_class, user), 1010 item.get_translated_name()) 1011 self.rptlist.append(obj) 1012 append_styles(selected_style, item) 1013 1014 self.doc.set_style_sheet(selected_style) 1015 self.doc.open(self.target_path) 1016 1017 def make_book(self): 1018 """ 1019 The actual book. Start it out, then go through the item list 1020 and call each item's write_book_item method (which were loaded 1021 by the previous make_document method). 1022 """ 1023 1024 try: 1025 self.doc.init() 1026 newpage = 0 1027 for (rpt, name) in self.rptlist: 1028 if newpage: 1029 self.doc.page_break() 1030 newpage = 1 1031 if rpt: 1032 rpt.begin_report() 1033 rpt.write_report() 1034 self.doc.close() 1035 except ReportError as msg: 1036 (msg1, msg2) = msg.messages() 1037 msg2 += ' (%s)' % name # which report has the error? 1038 ErrorDialog(msg1, msg2, parent=self.uistate.window) 1039 return 1040 except FilterError as msg: 1041 (msg1, msg2) = msg.messages() 1042 ErrorDialog(msg1, msg2, parent=self.uistate.window) 1043 return 1044 1045 if self.open_with_app.get_active(): 1046 open_file_with_default_application(self.target_path, self.uistate) 1047 1048 def init_options(self, option_class): 1049 try: 1050 if issubclass(option_class, object): 1051 self.options = option_class(self.raw_name, self.database) 1052 except TypeError: 1053 self.options = option_class 1054 self.options.load_previous_values() 1055 handler = self.options.handler 1056 if self.book.get_paper_name(): 1057 handler.set_paper_name(self.book.get_paper_name()) 1058 if self.book.get_orientation() is not None: # 0 is legal 1059 handler.set_orientation(self.book.get_orientation()) 1060 if self.book.get_paper_metric() is not None: # 0 is legal 1061 handler.set_paper_metric(self.book.get_paper_metric()) 1062 if self.book.get_custom_paper_size(): 1063 handler.set_custom_paper_size(self.book.get_custom_paper_size()) 1064 if self.book.get_margins(): 1065 handler.set_margins(self.book.get_margins()) 1066 if self.book.get_format_name(): 1067 handler.set_format_name(self.book.get_format_name()) 1068 if self.book.get_output(): 1069 self.options.set_output(self.book.get_output()) 1070 1071#------------------------------------------------------------------------ 1072# 1073# Generic task function for book 1074# 1075#------------------------------------------------------------------------ 1076def write_book_item(database, report_class, options, user): 1077 """ 1078 Write the report using options set. 1079 All user dialog has already been handled and the output file opened. 1080 """ 1081 try: 1082 return report_class(database, options, user) 1083 except ReportError as msg: 1084 (msg1, msg2) = msg.messages() 1085 ErrorDialog(msg1, msg2, parent=user.uistate.window) 1086 except FilterError as msg: 1087 (msg1, msg2) = msg.messages() 1088 ErrorDialog(msg1, msg2, parent=user.uistate.window) 1089 except: 1090 LOG.error("Failed to write book item.", exc_info=True) 1091 return None 1092