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