Example #1
0
class BookSelector(ManagedWindow):
    """
    Interface into a dialog setting up the book.

    Allows the user to add/remove/reorder/setup items for the current book
    and to clear/load/save/edit whole books.
    """

    def __init__(self, dbstate, uistate):
        self._db = dbstate.db
        self.dbstate = dbstate
        self.uistate = uistate
        self.title = _('Manage Books')
        self.file = "books.xml"

        ManagedWindow.__init__(self, uistate, [], self.__class__)

        self.xml = Glade('book.glade', toplevel="top")
        window = self.xml.toplevel

        title_label = self.xml.get_object('title')
        self.set_window(window, title_label, self.title)
        window.show()
        self.xml.connect_signals({
            "on_add_clicked"        : self.on_add_clicked,
            "on_remove_clicked"     : self.on_remove_clicked,
            "on_up_clicked"         : self.on_up_clicked,
            "on_down_clicked"       : self.on_down_clicked,
            "on_setup_clicked"      : self.on_setup_clicked,
            "on_clear_clicked"      : self.on_clear_clicked,
            "on_save_clicked"       : self.on_save_clicked,
            "on_open_clicked"       : self.on_open_clicked,
            "on_edit_clicked"       : self.on_edit_clicked,
            "on_book_ok_clicked"    : self.on_book_ok_clicked,
            "destroy_passed_object" : self.on_close_clicked,

            # Insert dummy handlers for second top level in the glade file
            "on_booklist_ok_clicked"     : lambda _: None,
            "on_booklist_delete_clicked" : lambda _: None,
            "on_booklist_cancel_clicked" : lambda _: None,
            "on_booklist_ok_clicked"     : lambda _: None,
            "on_booklist_ok_clicked"     : lambda _: None,
            })

        self.avail_tree = self.xml.get_object("avail_tree")
        self.book_tree = self.xml.get_object("book_tree")
        self.avail_tree.connect('button-press-event', self.avail_button_press)
        self.book_tree.connect('button-press-event', self.book_button_press)

        self.name_entry = self.xml.get_object("name_entry")
        self.name_entry.set_text(_('New Book'))

        avail_label = self.xml.get_object('avail_label')
        avail_label.set_text("<b>%s</b>" % _("_Available items"))
        avail_label.set_use_markup(True)
        avail_label.set_use_underline(True)
        book_label = self.xml.get_object('book_label')
        book_label.set_text("<b>%s</b>" % _("Current _book"))
        book_label.set_use_underline(True)
        book_label.set_use_markup(True)

        avail_titles = [(_('Name'), 0, 230),
                        (_('Type'), 1, 80),
                        ('', -1, 0)]

        book_titles = [(_('Item name'), -1, 230),
                       (_('Type'), -1, 80),
                       ('', -1, 0),
                       (_('Subject'), -1, 50)]

        self.avail_nr_cols = len(avail_titles)
        self.book_nr_cols = len(book_titles)

        self.avail_model = ListModel(self.avail_tree, avail_titles)
        self.book_model = ListModel(self.book_tree, book_titles)
        self.draw_avail_list()

        self.book = Book()
        self.book_list = BookList(self.file, self._db)
        self.book_list.set_needs_saving(False) # just read in: no need to save

    def build_menu_names(self, obj):
        return (_("Book selection list"), self.title)

    def draw_avail_list(self):
        """
        Draw the list with the selections available for the book.

        The selections are read from the book item registry.
        """
        pmgr = GuiPluginManager.get_instance()
        regbi = pmgr.get_reg_bookitems()
        if not regbi:
            return

        available_reports = []
        for pdata in regbi:
            category = _UNSUPPORTED
            if pdata.supported and pdata.category in book_categories:
                category = book_categories[pdata.category]
            available_reports.append([pdata.name, category, pdata.id])
        for data in sorted(available_reports):
            new_iter = self.avail_model.add(data)

        self.avail_model.connect_model()

        if new_iter:
            self.avail_model.selection.select_iter(new_iter)
            path = self.avail_model.model.get_path(new_iter)
            col = self.avail_tree.get_column(0)
            self.avail_tree.scroll_to_cell(path, col, 1, 1, 0.0)

    def open_book(self, book):
        """
        Open the book: set the current set of selections to this book's items.

        book:   the book object to load.
        """
        if book.get_paper_name():
            self.book.set_paper_name(book.get_paper_name())
        if book.get_orientation() is not None: # 0 is legal
            self.book.set_orientation(book.get_orientation())
        if book.get_paper_metric() is not None: # 0 is legal
            self.book.set_paper_metric(book.get_paper_metric())
        if book.get_custom_paper_size():
            self.book.set_custom_paper_size(book.get_custom_paper_size())
        if book.get_margins():
            self.book.set_margins(book.get_margins())
        if book.get_format_name():
            self.book.set_format_name(book.get_format_name())
        if book.get_output():
            self.book.set_output(book.get_output())
        if book.get_dbname() != self._db.get_save_path():
            WarningDialog( # parent-OK
                _('Different database'),
                _('This book was created with the references to database '
                  '%s.\n\n This makes references to the central person '
                  'saved in the book invalid.\n\n'
                  'Therefore, the central person for each item is being set '
                  'to the active person of the currently opened database.'
                 ) % book.get_dbname(),
                parent=self.window)

        self.book.clear()
        self.book_model.clear()
        for saved_item in book.get_item_list():
            name = saved_item.get_name()
            item = BookItem(self._db, name)
            item.option_class = saved_item.option_class

            # The option values were loaded magically by the book parser.
            # But they still need to be applied to the menu options.
            opt_dict = item.option_class.handler.options_dict
            menu = item.option_class.menu
            for optname in opt_dict:
                menu_option = menu.get_option_by_name(optname)
                if menu_option:
                    menu_option.set_value(opt_dict[optname])

            _initialize_options(item.option_class, self.dbstate, self.uistate)
            item.set_style_name(saved_item.get_style_name())
            self.book.append_item(item)

            data = [item.get_translated_name(),
                    item.get_category(), item.get_name()]

            data[2] = item.option_class.get_subject()
            self.book_model.add(data)

    def on_add_clicked(self, obj):
        """
        Add an item to the current selections.

        Use the selected available item to get the item's name in the registry.
        """
        store, the_iter = self.avail_model.get_selected()
        if not the_iter:
            return
        data = self.avail_model.get_data(the_iter,
                                         list(range(self.avail_nr_cols)))
        item = BookItem(self._db, data[2])
        _initialize_options(item.option_class, self.dbstate, self.uistate)
        data[2] = item.option_class.get_subject()
        self.book_model.add(data)
        self.book.append_item(item)

    def on_remove_clicked(self, obj):
        """
        Remove the item from the current list of selections.
        """
        store, the_iter = self.book_model.get_selected()
        if not the_iter:
            return
        row = self.book_model.get_selected_row()
        self.book.pop_item(row)
        self.book_model.remove(the_iter)

    def on_clear_clicked(self, obj):
        """
        Clear the whole current book.
        """
        self.book_model.clear()
        self.book.clear()

    def on_up_clicked(self, obj):
        """
        Move the currently selected item one row up in the selection list.
        """
        row = self.book_model.get_selected_row()
        if not row or row == -1:
            return
        store, the_iter = self.book_model.get_selected()
        data = self.book_model.get_data(the_iter,
                                        list(range(self.book_nr_cols)))
        self.book_model.remove(the_iter)
        self.book_model.insert(row-1, data, None, 1)
        item = self.book.pop_item(row)
        self.book.insert_item(row-1, item)

    def on_down_clicked(self, obj):
        """
        Move the currently selected item one row down in the selection list.
        """
        row = self.book_model.get_selected_row()
        if row + 1 >= self.book_model.count or row == -1:
            return
        store, the_iter = self.book_model.get_selected()
        data = self.book_model.get_data(the_iter,
                                        list(range(self.book_nr_cols)))
        self.book_model.remove(the_iter)
        self.book_model.insert(row+1, data, None, 1)
        item = self.book.pop_item(row)
        self.book.insert_item(row+1, item)

    def on_setup_clicked(self, obj):
        """
        Configure currently selected item.
        """
        store, the_iter = self.book_model.get_selected()
        if not the_iter:
            WarningDialog(_('No selected book item'), # parent-OK
                          _('Please select a book item to configure.'),
                          parent=self.window)
            return
        row = self.book_model.get_selected_row()
        item = self.book.get_item(row)
        option_class = item.option_class
        option_class.handler.set_default_stylesheet_name(item.get_style_name())
        item.is_from_saved_book = bool(self.book.get_name())
        item_dialog = BookItemDialog(self.dbstate, self.uistate,
                                     item, self.track)

        while True:
            response = item_dialog.window.run()
            if response == Gtk.ResponseType.OK:
                # dialog will be closed by connect, now continue work while
                # rest of dialog is unresponsive, release when finished
                style = option_class.handler.get_default_stylesheet_name()
                item.set_style_name(style)
                subject = option_class.get_subject()
                self.book_model.model.set_value(the_iter, 2, subject)
                self.book.set_item(row, item)
                item_dialog.close()
                break
            elif response == Gtk.ResponseType.CANCEL:
                item_dialog.close()
                break
            elif response == Gtk.ResponseType.DELETE_EVENT:
                #just stop, in ManagedWindow, delete-event is already coupled to
                #correct action.
                break
        opt_dict = option_class.handler.options_dict
        for optname in opt_dict:
            menu_option = option_class.menu.get_option_by_name(optname)
            if menu_option:
                menu_option.set_value(opt_dict[optname])

    def book_button_press(self, obj, event):
        """
        Double-click on the current book selection is the same as setup.
        Right click evokes the context menu.
        """
        if event.type == Gdk.EventType._2BUTTON_PRESS and event.button == 1:
            self.on_setup_clicked(obj)
        elif is_right_click(event):
            self.build_book_context_menu(event)

    def avail_button_press(self, obj, event):
        """
        Double-click on the available selection is the same as add.
        Right click evokes the context menu.
        """
        if event.type == Gdk.EventType._2BUTTON_PRESS and event.button == 1:
            self.on_add_clicked(obj)
        elif is_right_click(event):
            self.build_avail_context_menu(event)

    def build_book_context_menu(self, event):
        """Builds the menu with item-centered and book-centered options."""

        store, the_iter = self.book_model.get_selected()
        if the_iter:
            sensitivity = 1
        else:
            sensitivity = 0
        entries = [
            (_('_Up'), 'go-up', self.on_up_clicked, sensitivity),
            (_('_Down'), 'go-down', self.on_down_clicked, sensitivity),
            (_("Setup"), None, self.on_setup_clicked, sensitivity),
            (_('_Remove'), 'list-remove', self.on_remove_clicked, sensitivity),
            ('', None, None, 0),
            (_('_Clear'), 'edit-clear', self.on_clear_clicked, 1),
            (_('_Save'), 'document-save', self.on_save_clicked, 1),
            (_('_Open'), 'document-open', self.on_open_clicked, 1),
            (_("Edit"), None, self.on_edit_clicked, 1),
        ]

        self.menu1 = Gtk.Menu() # TODO could this be just a local "menu ="?
        self.menu1.set_title(_('Book Menu'))
        for title, icon_name, callback, sensitivity in entries:
            if icon_name:
                item = Gtk.ImageMenuItem.new_with_mnemonic(title)
                img = Gtk.Image.new_from_icon_name(icon_name, Gtk.IconSize.MENU)
                item.set_image(img)
            else:
                item = Gtk.MenuItem.new_with_mnemonic(title)
            if callback:
                item.connect("activate", callback)
            item.set_sensitive(sensitivity)
            item.show()
            self.menu1.append(item)
        self.menu1.popup(None, None, None, None, event.button, event.time)

    def build_avail_context_menu(self, event):
        """Builds the menu with the single Add option."""

        store, the_iter = self.avail_model.get_selected()
        if the_iter:
            sensitivity = 1
        else:
            sensitivity = 0
        entries = [
            (_('_Add'), 'list-add', self.on_add_clicked, sensitivity),
        ]

        self.menu2 = Gtk.Menu() # TODO could this be just a local "menu ="?
        self.menu2.set_title(_('Available Items Menu'))
        for title, icon_name, callback, sensitivity in entries:
            if icon_name:
                item = Gtk.ImageMenuItem.new_with_mnemonic(title)
                img = Gtk.Image.new_from_icon_name(icon_name, Gtk.IconSize.MENU)
                item.set_image(img)
            else:
                item = Gtk.MenuItem.new_with_mnemonic(title)
            if callback:
                item.connect("activate", callback)
            item.set_sensitive(sensitivity)
            item.show()
            self.menu2.append(item)
        self.menu2.popup(None, None, None, None, event.button, event.time)

    def on_close_clicked(self, obj):
        """
        close the BookSelector dialog, saving any changes if needed
        """
        if self.book_list.get_needs_saving():
            self.book_list.save()
        ManagedWindow.close(self, *obj)

    def on_book_ok_clicked(self, obj):
        """
        Run final BookDialog with the current book.
        """
        if self.book.item_list:
            old_paper_name = self.book.get_paper_name() # from books.xml
            old_orientation = self.book.get_orientation()
            old_paper_metric = self.book.get_paper_metric()
            old_custom_paper_size = self.book.get_custom_paper_size()
            old_margins = self.book.get_margins()
            old_format_name = self.book.get_format_name()
            old_output = self.book.get_output()
            BookDialog(self.dbstate, self.uistate, self.book, BookOptions)
            new_paper_name = self.book.get_paper_name()
            new_orientation = self.book.get_orientation()
            new_paper_metric = self.book.get_paper_metric()
            new_custom_paper_size = self.book.get_custom_paper_size()
            new_margins = self.book.get_margins()
            new_format_name = self.book.get_format_name()
            new_output = self.book.get_output()
            # only books in the booklist have a name (not "ad hoc" ones)
            if (self.book.get_name() and
                    (old_paper_name != new_paper_name or
                     old_orientation != new_orientation or
                     old_paper_metric != new_paper_metric or
                     old_custom_paper_size != new_custom_paper_size or
                     old_margins != new_margins or
                     old_format_name != new_format_name or
                     old_output != new_output)):
                self.book.set_dbname(self._db.get_save_path())
                self.book_list.set_book(self.book.get_name(), self.book)
                self.book_list.set_needs_saving(True)
            if self.book_list.get_needs_saving():
                self.book_list.save()
        else:
            WarningDialog(_('No items'), # parent-OK
                          _('This book has no items.'),
                          parent=self.window)
            return
        self.close()

    def on_save_clicked(self, obj):
        """
        Save the current book in the xml booklist file.
        """
        name = str(self.name_entry.get_text())
        if not name:
            WarningDialog( # parent-OK
                _('No book name'),
                _('You are about to save away a book with no name.\n\n'
                  'Please give it a name before saving it away.'),
                parent=self.window)
            return
        if name in self.book_list.get_book_names():
            qqq = QuestionDialog2( # parent-OK
                _('Book name already exists'),
                _('You are about to save away a '
                  'book with a name which already exists.'),
                _('Proceed'),
                _('Cancel'),
                parent=self.window)
            if not qqq.run():
                return

        # previously, the same book could be added to the booklist
        # under multiple names, which became different books once the
        # booklist was saved into a file so everything was fine, but
        # this created a problem once the paper settings were added
        # to the Book object in the BookDialog, since those settings
        # were retrieved from the Book object in Book.save, so mutiple
        # books (differentiated by their names) were assigned the
        # same paper values, so the solution is to make each Book be
        # unique in the booklist, so if multiple copies are saved away
        # only the last one will get the paper values assigned to it
        # (although when the earlier books are then eventually run,
        # they'll be assigned paper values also)
        self.book.set_name(name)
        self.book.set_dbname(self._db.get_save_path())
        self.book_list.set_book(name, self.book)
        self.book_list.set_needs_saving(True) # user clicked on save
        self.book = Book(self.book, exact_copy=False) # regenerate old items
        self.book.set_name(name)
        self.book.set_dbname(self._db.get_save_path())

    def on_open_clicked(self, obj):
        """
        Run the BookListDisplay dialog to present the choice of books to open.
        """
        booklistdisplay = BookListDisplay(self.book_list,
                                          nodelete=True, dosave=False)
        booklistdisplay.top.destroy()
        book = booklistdisplay.selection
        if book:
            self.open_book(book)
            self.name_entry.set_text(book.get_name())
            self.book.set_name(book.get_name())

    def on_edit_clicked(self, obj):
        """
        Run the BookListDisplay dialog to present the choice of books to delete.
        """
        booklistdisplay = BookListDisplay(self.book_list,
                                          nodelete=False, dosave=True)
        booklistdisplay.top.destroy()
        book = booklistdisplay.selection
        if book:
            self.open_book(book)
            self.name_entry.set_text(book.get_name())
            self.book.set_name(book.get_name())
Example #2
0
class BookSelector(ManagedWindow):
    """
    Interface into a dialog setting up the book.

    Allows the user to add/remove/reorder/setup items for the current book
    and to clear/load/save/edit whole books.
    """

    def __init__(self, dbstate, uistate):
        self._db = dbstate.db
        self.dbstate = dbstate
        self.uistate = uistate
        self.title = _('Manage Books')
        self.file = "books.xml"

        ManagedWindow.__init__(self, uistate, [], self.__class__)

        self.xml = Glade('book.glade', toplevel="top")
        window = self.xml.toplevel

        title_label = self.xml.get_object('title')
        self.set_window(window, title_label, self.title)
        window.show()
        self.xml.connect_signals({
            "on_add_clicked"        : self.on_add_clicked,
            "on_remove_clicked"     : self.on_remove_clicked,
            "on_up_clicked"         : self.on_up_clicked,
            "on_down_clicked"       : self.on_down_clicked,
            "on_setup_clicked"      : self.on_setup_clicked,
            "on_clear_clicked"      : self.on_clear_clicked,
            "on_save_clicked"       : self.on_save_clicked,
            "on_open_clicked"       : self.on_open_clicked,
            "on_edit_clicked"       : self.on_edit_clicked,
            "on_book_ok_clicked"    : self.on_book_ok_clicked,
            "destroy_passed_object" : self.on_close_clicked,

            # Insert dummy handlers for second top level in the glade file
            "on_booklist_ok_clicked"     : lambda _: None,
            "on_booklist_delete_clicked" : lambda _: None,
            "on_booklist_cancel_clicked" : lambda _: None,
            "on_booklist_ok_clicked"     : lambda _: None,
            "on_booklist_ok_clicked"     : lambda _: None,
            })

        self.avail_tree = self.xml.get_object("avail_tree")
        self.book_tree = self.xml.get_object("book_tree")
        self.avail_tree.connect('button-press-event', self.avail_button_press)
        self.book_tree.connect('button-press-event', self.book_button_press)

        self.name_entry = self.xml.get_object("name_entry")
        self.name_entry.set_text(_('New Book'))

        avail_label = self.xml.get_object('avail_label')
        avail_label.set_text("<b>%s</b>" % _("_Available items"))
        avail_label.set_use_markup(True)
        avail_label.set_use_underline(True)
        book_label = self.xml.get_object('book_label')
        book_label.set_text("<b>%s</b>" % _("Current _book"))
        book_label.set_use_underline(True)
        book_label.set_use_markup(True)

        avail_titles = [(_('Name'), 0, 230),
                        (_('Type'), 1, 80),
                        ('', -1, 0)]

        book_titles = [(_('Item name'), -1, 230),
                       (_('Type'), -1, 80),
                       ('', -1, 0),
                       (_('Subject'), -1, 50)]

        self.avail_nr_cols = len(avail_titles)
        self.book_nr_cols = len(book_titles)

        self.avail_model = ListModel(self.avail_tree, avail_titles)
        self.book_model = ListModel(self.book_tree, book_titles)
        self.draw_avail_list()

        self.book = Book()
        self.book_list = BookList(self.file, self._db)
        self.book_list.set_needs_saving(False) # just read in: no need to save

    def build_menu_names(self, obj):
        return (_("Book selection list"), self.title)

    def draw_avail_list(self):
        """
        Draw the list with the selections available for the book.

        The selections are read from the book item registry.
        """
        pmgr = GuiPluginManager.get_instance()
        regbi = pmgr.get_reg_bookitems()
        if not regbi:
            return

        available_reports = []
        for pdata in regbi:
            category = _UNSUPPORTED
            if pdata.supported and pdata.category in book_categories:
                category = book_categories[pdata.category]
            available_reports.append([pdata.name, category, pdata.id])
        for data in sorted(available_reports):
            new_iter = self.avail_model.add(data)

        self.avail_model.connect_model()

        if new_iter:
            self.avail_model.selection.select_iter(new_iter)
            path = self.avail_model.model.get_path(new_iter)
            col = self.avail_tree.get_column(0)
            self.avail_tree.scroll_to_cell(path, col, 1, 1, 0.0)

    def open_book(self, book):
        """
        Open the book: set the current set of selections to this book's items.

        book:   the book object to load.
        """
        if book.get_paper_name():
            self.book.set_paper_name(book.get_paper_name())
        if book.get_orientation() is not None: # 0 is legal
            self.book.set_orientation(book.get_orientation())
        if book.get_paper_metric() is not None: # 0 is legal
            self.book.set_paper_metric(book.get_paper_metric())
        if book.get_custom_paper_size():
            self.book.set_custom_paper_size(book.get_custom_paper_size())
        if book.get_margins():
            self.book.set_margins(book.get_margins())
        if book.get_format_name():
            self.book.set_format_name(book.get_format_name())
        if book.get_output():
            self.book.set_output(book.get_output())
        if book.get_dbname() != self._db.get_save_path():
            WarningDialog(
                _('Different database'),
                _('This book was created with the references to database '
                  '%s.\n\n This makes references to the central person '
                  'saved in the book invalid.\n\n'
                  'Therefore, the central person for each item is being set '
                  'to the active person of the currently opened database.'
                 ) % book.get_dbname(),
                parent=self.window)

        self.book.clear()
        self.book_model.clear()
        for saved_item in book.get_item_list():
            name = saved_item.get_name()
            item = BookItem(self._db, name)
            item.option_class = saved_item.option_class

            # The option values were loaded magically by the book parser.
            # But they still need to be applied to the menu options.
            opt_dict = item.option_class.handler.options_dict
            menu = item.option_class.menu
            for optname in opt_dict:
                menu_option = menu.get_option_by_name(optname)
                if menu_option:
                    menu_option.set_value(opt_dict[optname])

            _initialize_options(item.option_class, self.dbstate, self.uistate)
            item.set_style_name(saved_item.get_style_name())
            self.book.append_item(item)

            data = [item.get_translated_name(),
                    item.get_category(), item.get_name()]

            data[2] = item.option_class.get_subject()
            self.book_model.add(data)

    def on_add_clicked(self, obj):
        """
        Add an item to the current selections.

        Use the selected available item to get the item's name in the registry.
        """
        store, the_iter = self.avail_model.get_selected()
        if not the_iter:
            return
        data = self.avail_model.get_data(the_iter,
                                         list(range(self.avail_nr_cols)))
        item = BookItem(self._db, data[2])
        _initialize_options(item.option_class, self.dbstate, self.uistate)
        data[2] = item.option_class.get_subject()
        self.book_model.add(data)
        self.book.append_item(item)

    def on_remove_clicked(self, obj):
        """
        Remove the item from the current list of selections.
        """
        store, the_iter = self.book_model.get_selected()
        if not the_iter:
            return
        row = self.book_model.get_selected_row()
        self.book.pop_item(row)
        self.book_model.remove(the_iter)

    def on_clear_clicked(self, obj):
        """
        Clear the whole current book.
        """
        self.book_model.clear()
        self.book.clear()

    def on_up_clicked(self, obj):
        """
        Move the currently selected item one row up in the selection list.
        """
        row = self.book_model.get_selected_row()
        if not row or row == -1:
            return
        store, the_iter = self.book_model.get_selected()
        data = self.book_model.get_data(the_iter,
                                        list(range(self.book_nr_cols)))
        self.book_model.remove(the_iter)
        self.book_model.insert(row-1, data, None, 1)
        item = self.book.pop_item(row)
        self.book.insert_item(row-1, item)

    def on_down_clicked(self, obj):
        """
        Move the currently selected item one row down in the selection list.
        """
        row = self.book_model.get_selected_row()
        if row + 1 >= self.book_model.count or row == -1:
            return
        store, the_iter = self.book_model.get_selected()
        data = self.book_model.get_data(the_iter,
                                        list(range(self.book_nr_cols)))
        self.book_model.remove(the_iter)
        self.book_model.insert(row+1, data, None, 1)
        item = self.book.pop_item(row)
        self.book.insert_item(row+1, item)

    def on_setup_clicked(self, obj):
        """
        Configure currently selected item.
        """
        store, the_iter = self.book_model.get_selected()
        if not the_iter:
            WarningDialog(_('No selected book item'),
                          _('Please select a book item to configure.'),
                          parent=self.window)
            return
        row = self.book_model.get_selected_row()
        item = self.book.get_item(row)
        option_class = item.option_class
        option_class.handler.set_default_stylesheet_name(item.get_style_name())
        item.is_from_saved_book = bool(self.book.get_name())
        item_dialog = BookItemDialog(self.dbstate, self.uistate,
                                     item, self.track)

        while True:
            response = item_dialog.window.run()
            if response == Gtk.ResponseType.OK:
                # dialog will be closed by connect, now continue work while
                # rest of dialog is unresponsive, release when finished
                style = option_class.handler.get_default_stylesheet_name()
                item.set_style_name(style)
                subject = option_class.get_subject()
                self.book_model.model.set_value(the_iter, 2, subject)
                self.book.set_item(row, item)
                item_dialog.close()
                break
            elif response == Gtk.ResponseType.CANCEL:
                item_dialog.close()
                break
            elif response == Gtk.ResponseType.DELETE_EVENT:
                #just stop, in ManagedWindow, delete-event is already coupled to
                #correct action.
                break
        opt_dict = option_class.handler.options_dict
        for optname in opt_dict:
            menu_option = option_class.menu.get_option_by_name(optname)
            if menu_option:
                menu_option.set_value(opt_dict[optname])

    def book_button_press(self, obj, event):
        """
        Double-click on the current book selection is the same as setup.
        Right click evokes the context menu.
        """
        if event.type == Gdk.EventType._2BUTTON_PRESS and event.button == 1:
            self.on_setup_clicked(obj)
        elif is_right_click(event):
            self.build_book_context_menu(event)

    def avail_button_press(self, obj, event):
        """
        Double-click on the available selection is the same as add.
        Right click evokes the context menu.
        """
        if event.type == Gdk.EventType._2BUTTON_PRESS and event.button == 1:
            self.on_add_clicked(obj)
        elif is_right_click(event):
            self.build_avail_context_menu(event)

    def build_book_context_menu(self, event):
        """Builds the menu with item-centered and book-centered options."""

        store, the_iter = self.book_model.get_selected()
        if the_iter:
            sensitivity = 1
        else:
            sensitivity = 0
        entries = [
            (_('_Up'), 'go-up', self.on_up_clicked, sensitivity),
            (_('_Down'), 'go-down', self.on_down_clicked, sensitivity),
            (_("Setup"), None, self.on_setup_clicked, sensitivity),
            (_('_Remove'), 'list-remove', self.on_remove_clicked, sensitivity),
            ('', None, None, 0),
            (_('_Clear'), 'edit-clear', self.on_clear_clicked, 1),
            (_('_Save'), 'document-save', self.on_save_clicked, 1),
            (_('_Open'), 'document-open', self.on_open_clicked, 1),
            (_("Edit"), None, self.on_edit_clicked, 1),
        ]

        self.menu1 = Gtk.Menu() # TODO could this be just a local "menu ="?
        self.menu1.set_title(_('Book Menu'))
        for title, icon_name, callback, sensitivity in entries:
            if icon_name:
                item = Gtk.ImageMenuItem.new_with_mnemonic(title)
                img = Gtk.Image.new_from_icon_name(icon_name, Gtk.IconSize.MENU)
                item.set_image(img)
            else:
                item = Gtk.MenuItem.new_with_mnemonic(title)
            if callback:
                item.connect("activate", callback)
            item.set_sensitive(sensitivity)
            item.show()
            self.menu1.append(item)
        self.menu1.popup(None, None, None, None, event.button, event.time)

    def build_avail_context_menu(self, event):
        """Builds the menu with the single Add option."""

        store, the_iter = self.avail_model.get_selected()
        if the_iter:
            sensitivity = 1
        else:
            sensitivity = 0
        entries = [
            (_('_Add'), 'list-add', self.on_add_clicked, sensitivity),
        ]

        self.menu2 = Gtk.Menu() # TODO could this be just a local "menu ="?
        self.menu2.set_title(_('Available Items Menu'))
        for title, icon_name, callback, sensitivity in entries:
            if icon_name:
                item = Gtk.ImageMenuItem.new_with_mnemonic(title)
                img = Gtk.Image.new_from_icon_name(icon_name, Gtk.IconSize.MENU)
                item.set_image(img)
            else:
                item = Gtk.MenuItem.new_with_mnemonic(title)
            if callback:
                item.connect("activate", callback)
            item.set_sensitive(sensitivity)
            item.show()
            self.menu2.append(item)
        self.menu2.popup(None, None, None, None, event.button, event.time)

    def on_close_clicked(self, obj):
        """
        close the BookSelector dialog, saving any changes if needed
        """
        if self.book_list.get_needs_saving():
            self.book_list.save()
        ManagedWindow.close(self, *obj)

    def on_book_ok_clicked(self, obj):
        """
        Run final BookDialog with the current book.
        """
        if self.book.item_list:
            old_paper_name = self.book.get_paper_name() # from books.xml
            old_orientation = self.book.get_orientation()
            old_paper_metric = self.book.get_paper_metric()
            old_custom_paper_size = self.book.get_custom_paper_size()
            old_margins = self.book.get_margins()
            old_format_name = self.book.get_format_name()
            old_output = self.book.get_output()
            BookDialog(self.dbstate, self.uistate, self.book, BookOptions)
            new_paper_name = self.book.get_paper_name()
            new_orientation = self.book.get_orientation()
            new_paper_metric = self.book.get_paper_metric()
            new_custom_paper_size = self.book.get_custom_paper_size()
            new_margins = self.book.get_margins()
            new_format_name = self.book.get_format_name()
            new_output = self.book.get_output()
            # only books in the booklist have a name (not "ad hoc" ones)
            if (self.book.get_name() and
                    (old_paper_name != new_paper_name or
                     old_orientation != new_orientation or
                     old_paper_metric != new_paper_metric or
                     old_custom_paper_size != new_custom_paper_size or
                     old_margins != new_margins or
                     old_format_name != new_format_name or
                     old_output != new_output)):
                self.book.set_dbname(self._db.get_save_path())
                self.book_list.set_book(self.book.get_name(), self.book)
                self.book_list.set_needs_saving(True)
            if self.book_list.get_needs_saving():
                self.book_list.save()
        else:
            WarningDialog(_('No items'), _('This book has no items.'),
                          parent=self.window)
            return
        self.close()

    def on_save_clicked(self, obj):
        """
        Save the current book in the xml booklist file.
        """
        name = str(self.name_entry.get_text())
        if not name:
            WarningDialog(
                _('No book name'),
                _('You are about to save away a book with no name.\n\n'
                  'Please give it a name before saving it away.'),
                parent=self.window)
            return
        if name in self.book_list.get_book_names():
            qqq = QuestionDialog2(
                _('Book name already exists'),
                _('You are about to save away a '
                  'book with a name which already exists.'),
                _('Proceed'),
                _('Cancel'),
                parent=self.window)
            if not qqq.run():
                return

        # previously, the same book could be added to the booklist
        # under multiple names, which became different books once the
        # booklist was saved into a file so everything was fine, but
        # this created a problem once the paper settings were added
        # to the Book object in the BookDialog, since those settings
        # were retrieved from the Book object in Book.save, so mutiple
        # books (differentiated by their names) were assigned the
        # same paper values, so the solution is to make each Book be
        # unique in the booklist, so if multiple copies are saved away
        # only the last one will get the paper values assigned to it
        # (although when the earlier books are then eventually run,
        # they'll be assigned paper values also)
        self.book.set_name(name)
        self.book.set_dbname(self._db.get_save_path())
        self.book_list.set_book(name, self.book)
        self.book_list.set_needs_saving(True) # user clicked on save
        self.book = Book(self.book, exact_copy=False) # regenerate old items
        self.book.set_name(name)
        self.book.set_dbname(self._db.get_save_path())

    def on_open_clicked(self, obj):
        """
        Run the BookListDisplay dialog to present the choice of books to open.
        """
        booklistdisplay = BookListDisplay(self.book_list,
                                          nodelete=True, dosave=False)
        booklistdisplay.top.destroy()
        book = booklistdisplay.selection
        if book:
            self.open_book(book)
            self.name_entry.set_text(book.get_name())
            self.book.set_name(book.get_name())

    def on_edit_clicked(self, obj):
        """
        Run the BookListDisplay dialog to present the choice of books to delete.
        """
        booklistdisplay = BookListDisplay(self.book_list,
                                          nodelete=False, dosave=True)
        booklistdisplay.top.destroy()
        book = booklistdisplay.selection
        if book:
            self.open_book(book)
            self.name_entry.set_text(book.get_name())
            self.book.set_name(book.get_name())