Esempio n. 1
0
class OpdsDialog(QDialog):
    def __init__(self, gui, icon, do_user_config):
        QDialog.__init__(self, gui)
        self.gui = gui
        self.do_user_config = do_user_config

        self.db = gui.current_db.new_api

        # The model for the book list
        self.model = OpdsBooksModel(None, self.dummy_books(), self.db)
        self.searchproxymodel = QSortFilterProxyModel(self)
        self.searchproxymodel.setFilterCaseSensitivity(Qt.CaseInsensitive)
        self.searchproxymodel.setFilterKeyColumn(-1)
        self.searchproxymodel.setSourceModel(self.model)

        self.layout = QGridLayout()
        self.setLayout(self.layout)

        self.setWindowTitle("OPDS Client")
        self.setWindowIcon(icon)

        labelColumnWidths = []

        self.opdsUrlLabel = QLabel("OPDS URL: ")
        self.layout.addWidget(self.opdsUrlLabel, 0, 0)
        labelColumnWidths.append(self.layout.itemAtPosition(0, 0).sizeHint().width())

        config.convertSingleStringOpdsUrlPreferenceToListOfStringsPreference()
        self.opdsUrlEditor = QComboBox(self)
        self.opdsUrlEditor.activated.connect(self.opdsUrlEditorActivated)
        self.opdsUrlEditor.addItems(prefs["opds_url"])
        self.opdsUrlEditor.setEditable(True)
        self.opdsUrlEditor.setInsertPolicy(QComboBox.InsertAtTop)
        self.layout.addWidget(self.opdsUrlEditor, 0, 1, 1, 3)
        self.opdsUrlLabel.setBuddy(self.opdsUrlEditor)

        buttonColumnNumber = 7
        buttonColumnWidths = []
        self.about_button = QPushButton("About", self)
        self.about_button.setAutoDefault(False)
        self.about_button.clicked.connect(self.about)
        self.layout.addWidget(self.about_button, 0, buttonColumnNumber)
        buttonColumnWidths.append(
            self.layout.itemAtPosition(0, buttonColumnNumber).sizeHint().width()
        )

        # Initially download the catalogs found in the root catalog of the URL
        # selected at startup.  Fail quietly on failing to open the URL
        catalogsTuple = self.model.downloadOpdsRootCatalog(
            self.gui, self.opdsUrlEditor.currentText(), False
        )
        print(catalogsTuple)
        firstCatalogTitle = catalogsTuple[0]
        self.currentOpdsCatalogs = catalogsTuple[1]  # A dictionary of title->feedURL

        self.opdsCatalogSelectorLabel = QLabel("OPDS Catalog:")
        self.layout.addWidget(self.opdsCatalogSelectorLabel, 1, 0)
        labelColumnWidths.append(self.layout.itemAtPosition(1, 0).sizeHint().width())

        self.opdsCatalogSelector = QComboBox(self)
        self.opdsCatalogSelector.setEditable(False)
        self.opdsCatalogSelectorModel = QStringListModel(self.currentOpdsCatalogs.keys())
        self.opdsCatalogSelector.setModel(self.opdsCatalogSelectorModel)
        self.opdsCatalogSelector.setCurrentText(firstCatalogTitle)
        self.layout.addWidget(self.opdsCatalogSelector, 1, 1, 1, 3)

        self.download_opds_button = QPushButton("Download OPDS", self)
        self.download_opds_button.setAutoDefault(False)
        self.download_opds_button.clicked.connect(self.download_opds)
        self.layout.addWidget(self.download_opds_button, 1, buttonColumnNumber)
        buttonColumnWidths.append(
            self.layout.itemAtPosition(1, buttonColumnNumber).sizeHint().width()
        )

        # Search GUI
        self.searchEditor = QLineEdit(self)
        self.searchEditor.returnPressed.connect(self.searchBookList)
        self.layout.addWidget(self.searchEditor, 2, buttonColumnNumber - 2, 1, 2)

        self.searchButton = QPushButton("Search", self)
        self.searchButton.setAutoDefault(False)
        self.searchButton.clicked.connect(self.searchBookList)
        self.layout.addWidget(self.searchButton, 2, buttonColumnNumber)
        buttonColumnWidths.append(
            self.layout.itemAtPosition(2, buttonColumnNumber).sizeHint().width()
        )

        # The main book list
        self.library_view = QTableView(self)
        self.library_view.setAlternatingRowColors(True)
        self.library_view.setModel(self.searchproxymodel)
        self.library_view.horizontalHeader().setSectionResizeMode(0, QHeaderView.Stretch)
        self.library_view.horizontalHeader().setSectionResizeMode(1, QHeaderView.Stretch)
        self.library_view.horizontalHeader().setSectionResizeMode(2, QHeaderView.Stretch)
        self.library_view.setSelectionBehavior(QAbstractItemView.SelectRows)
        self.resizeAllLibraryViewLinesToHeaderHeight()
        self.library_view.resizeColumnsToContents()
        self.layout.addWidget(self.library_view, 3, 0, 3, buttonColumnNumber + 1)

        self.hideNewsCheckbox = QCheckBox("Hide Newspapers", self)
        self.hideNewsCheckbox.clicked.connect(self.setHideNewspapers)
        self.hideNewsCheckbox.setChecked(prefs["hideNewspapers"])
        self.layout.addWidget(self.hideNewsCheckbox, 6, 0, 1, 3)

        self.hideBooksAlreadyInLibraryCheckbox = QCheckBox("Hide books already in library", self)
        self.hideBooksAlreadyInLibraryCheckbox.clicked.connect(self.setHideBooksAlreadyInLibrary)
        self.hideBooksAlreadyInLibraryCheckbox.setChecked(prefs["hideBooksAlreadyInLibrary"])
        self.layout.addWidget(self.hideBooksAlreadyInLibraryCheckbox, 7, 0, 1, 3)

        # Let the checkbox initial state control the filtering
        self.model.setFilterBooksThatAreNewspapers(self.hideNewsCheckbox.isChecked())
        self.model.setFilterBooksThatAreAlreadyInLibrary(
            self.hideBooksAlreadyInLibraryCheckbox.isChecked()
        )

        self.downloadButton = QPushButton("Download selected books", self)
        self.downloadButton.setAutoDefault(False)
        self.downloadButton.clicked.connect(self.downloadSelectedBooks)
        self.layout.addWidget(self.downloadButton, 6, buttonColumnNumber)
        buttonColumnWidths.append(
            self.layout.itemAtPosition(6, buttonColumnNumber).sizeHint().width()
        )

        self.fixTimestampButton = QPushButton("Fix timestamps of selection", self)
        self.fixTimestampButton.setAutoDefault(False)
        self.fixTimestampButton.clicked.connect(self.fixBookTimestamps)
        self.layout.addWidget(self.fixTimestampButton, 7, buttonColumnNumber)
        buttonColumnWidths.append(
            self.layout.itemAtPosition(7, buttonColumnNumber).sizeHint().width()
        )

        # Make all columns of the grid layout the same width as the button column
        buttonColumnWidth = max(buttonColumnWidths)
        for columnNumber in range(0, buttonColumnNumber):
            self.layout.setColumnMinimumWidth(columnNumber, buttonColumnWidth)

        # Make sure the first column isn't wider than the labels it holds
        labelColumnWidth = max(labelColumnWidths)
        self.layout.setColumnMinimumWidth(0, labelColumnWidth)

        self.resize(self.sizeHint())

    def opdsUrlEditorActivated(self, text):
        prefs["opds_url"] = config.saveOpdsUrlCombobox(self.opdsUrlEditor)
        catalogsTuple = self.model.downloadOpdsRootCatalog(
            self.gui, self.opdsUrlEditor.currentText(), True
        )
        firstCatalogTitle = catalogsTuple[0]
        self.currentOpdsCatalogs = catalogsTuple[1]  # A dictionary of title->feedURL
        self.opdsCatalogSelectorModel.setStringList(self.currentOpdsCatalogs.keys())
        self.opdsCatalogSelector.setCurrentText(firstCatalogTitle)

    def setHideNewspapers(self, checked):
        prefs["hideNewspapers"] = checked
        self.model.setFilterBooksThatAreNewspapers(checked)
        self.resizeAllLibraryViewLinesToHeaderHeight()

    def setHideBooksAlreadyInLibrary(self, checked):
        prefs["hideBooksAlreadyInLibrary"] = checked
        self.model.setFilterBooksThatAreAlreadyInLibrary(checked)
        self.resizeAllLibraryViewLinesToHeaderHeight()

    def searchBookList(self):
        searchString = self.searchEditor.text()
        print("starting book list search for: %s" % searchString)
        self.searchproxymodel.setFilterFixedString(searchString)

    def about(self):
        text = get_resources("about.txt")
        QMessageBox.about(self, "About the OPDS Client plugin", text.decode("utf-8"))

    def download_opds(self):
        opdsCatalogUrl = self.currentOpdsCatalogs.get(self.opdsCatalogSelector.currentText(), None)
        if opdsCatalogUrl is None:
            # Just give up quietly
            return
        self.model.downloadOpdsCatalog(self.gui, opdsCatalogUrl)
        if self.model.isCalibreOpdsServer():
            self.model.downloadMetadataUsingCalibreRestApi(self.opdsUrlEditor.currentText())
        self.library_view.horizontalHeader().setSectionResizeMode(0, QHeaderView.Stretch)
        self.library_view.horizontalHeader().setSectionResizeMode(1, QHeaderView.Stretch)
        self.library_view.horizontalHeader().setSectionResizeMode(2, QHeaderView.Stretch)
        self.resizeAllLibraryViewLinesToHeaderHeight()
        self.resize(self.sizeHint())

    def config(self):
        self.do_user_config(parent=self)

    def downloadSelectedBooks(self):
        selectionmodel = self.library_view.selectionModel()
        if selectionmodel.hasSelection():
            rows = selectionmodel.selectedRows()
            for row in reversed(rows):
                book = row.data(Qt.UserRole)
                self.downloadBook(book)

    def downloadBook(self, book):
        if len(book.links) > 0:
            self.gui.download_ebook(book.links[0])

    def fixBookTimestamps(self):
        selectionmodel = self.library_view.selectionModel()
        if selectionmodel.hasSelection():
            rows = selectionmodel.selectedRows()
            for row in reversed(rows):
                book = row.data(Qt.UserRole)
                self.fixBookTimestamp(book)

    def fixBookTimestamp(self, book):
        bookTimestamp = book.timestamp
        identicalBookIds = self.findIdenticalBooksForBooksWithMultipleAuthors(book)
        bookIdToValMap = {}
        for identicalBookId in identicalBookIds:
            bookIdToValMap[identicalBookId] = bookTimestamp
        if len(bookIdToValMap) < 1:
            print("Failed to set timestamp of book: %s" % book)
        self.db.set_field("timestamp", bookIdToValMap)

    def findIdenticalBooksForBooksWithMultipleAuthors(self, book):
        authorsList = book.authors
        if len(authorsList) < 2:
            return self.db.find_identical_books(book)
        # Try matching the authors one by one
        identicalBookIds = set()
        for author in authorsList:
            singleAuthorBook = Metadata(book.title, [author])
            singleAuthorIdenticalBookIds = self.db.find_identical_books(singleAuthorBook)
            identicalBookIds = identicalBookIds.union(singleAuthorIdenticalBookIds)
        return identicalBookIds

    def dummy_books(self):
        dummy_author = " " * 40
        dummy_title = " " * 60
        books_list = []
        for line in range(1, 10):
            book = DynamicBook()
            book.author = dummy_author
            book.title = dummy_title
            book.updated = datetime.datetime.now().strftime("%Y-%m-%dT%H:%M:%S+00:00")
            book.id = ""
            books_list.append(book)
        return books_list

    def resizeAllLibraryViewLinesToHeaderHeight(self):
        rowHeight = self.library_view.horizontalHeader().height()
        for rowNumber in range(0, self.library_view.model().rowCount()):
            self.library_view.setRowHeight(rowNumber, rowHeight)
class AnnotatedBooksDialog(SizePersistedDialog):
    '''
    This dialog is shown when the user fetches or imports books
    self.fetch_single_annotations controls checkmark display, behavior of fetch button
    '''
    if isosx:
        FONT = QFont('Monaco', 11)
    elif iswindows:
        FONT = QFont('Lucida Console', 9)
    elif islinux:
        FONT = QFont('Monospace', 9)
        FONT.setStyleHint(QFont.TypeWriter)

    def __init__(self, parent, book_list, get_annotations_as_HTML, source):
        self.opts = parent.opts
        self.parent = parent
        self.get_annotations_as_HTML = get_annotations_as_HTML
        self.show_confidence_colors = self.opts.prefs.get('annotated_books_dialog_show_confidence_as_bg_color', True)
        self.source = source

#         QDialog.__init__(self, parent=self.opts.gui)
        SizePersistedDialog.__init__(self, self.opts.gui, 'Annotations plugin:import annotations dialog')
        self.setWindowTitle(u'Import Annotations')
        self.setWindowIcon(self.opts.icon)
        self.l = QVBoxLayout(self)
        self.setLayout(self.l)
        self.perfect_width = 0

        from calibre_plugins.annotations.appearance import default_timestamp
        friendly_timestamp_format = plugin_prefs.get('appearance_timestamp_format', default_timestamp)

        # Are we collecting News clippings?
        collect_news_clippings = self.opts.prefs.get('cfg_news_clippings_checkbox', False)
        news_clippings_destination = self.opts.prefs.get('cfg_news_clippings_lineEdit', None)

        # Populate the table data
        self.tabledata = []
        for book_data in book_list:
            enabled = QCheckBox()
            enabled.setChecked(True)

            # last_annotation sorts by timestamp
            last_annotation = SortableTableWidgetItem(
                strftime(friendly_timestamp_format,
                         localtime(book_data['last_update'])),
                book_data['last_update'])

            # reader_app sorts case-insensitive
            reader_app = SortableTableWidgetItem(
                book_data['reader_app'],
                book_data['reader_app'].upper())

            # title, author sort by title_sort, author_sort
            if not book_data['title_sort']:
                book_data['title_sort'] = book_data['title']
            title = SortableTableWidgetItem(
                book_data['title'],
                book_data['title_sort'].upper())

            if not book_data['author_sort']:
                book_data['author_sort'] = book_data['author']
            author = SortableTableWidgetItem(
                book_data['author'],
                book_data['author_sort'].upper())

            genres = book_data['genre'].split(', ')
            if 'News' in genres and collect_news_clippings:
                cid = get_clippings_cid(self, news_clippings_destination)
                confidence = 5
            else:
                cid, confidence = parent.generate_confidence(book_data)

            # List order matches self.annotations_header
            this_book = [
                book_data['uuid'],
                book_data['book_id'],
                book_data['genre'],
                enabled,
                reader_app,
                title,
                author,
                last_annotation,
                book_data['annotations'],
                confidence]
            self.tabledata.append(this_book)

        self.tv = QTableView(self)
        self.l.addWidget(self.tv)
        self.annotations_header = ['uuid', 'book_id', 'genre', '', 'Reader App', 'Title',
                                   'Author', 'Last Annotation', 'Annotations', 'Confidence']
        self.ENABLED_COL = 3
        self.READER_APP_COL = 4
        self.TITLE_COL = 5
        self.AUTHOR_COL = 6
        self.LAST_ANNOTATION_COL = 7
        self.CONFIDENCE_COL = 9
        columns_to_center = [8]
        self.tm = MarkupTableModel(self, columns_to_center=columns_to_center)
        self.tv.setModel(self.tm)
        self.tv.setShowGrid(False)
        self.tv.setFont(self.FONT)
        self.tvSelectionModel = self.tv.selectionModel()
        self.tv.setAlternatingRowColors(not self.show_confidence_colors)
        self.tv.setShowGrid(False)
        self.tv.setWordWrap(False)
        self.tv.setSelectionBehavior(self.tv.SelectRows)

        # Connect signals
        self.tv.doubleClicked.connect(self.getTableRowDoubleClick)
        self.tv.horizontalHeader().sectionClicked.connect(self.capture_sort_column)

        # Hide the vertical self.header
        self.tv.verticalHeader().setVisible(False)

        # Hide uuid, book_id, genre, confidence
        self.tv.hideColumn(self.annotations_header.index('uuid'))
        self.tv.hideColumn(self.annotations_header.index('book_id'))
        self.tv.hideColumn(self.annotations_header.index('genre'))
        self.tv.hideColumn(self.annotations_header.index('Confidence'))

        # Set horizontal self.header props
        self.tv.horizontalHeader().setStretchLastSection(True)

        narrow_columns = ['Last Annotation', 'Reader App', 'Annotations']
        extra_width = 10
        breathing_space = 20

        # Set column width to fit contents
        self.tv.resizeColumnsToContents()
        perfect_width = 10 + (len(narrow_columns) * extra_width)
        for i in range(3, 8):
            perfect_width += self.tv.columnWidth(i) + breathing_space
        self.tv.setMinimumSize(perfect_width, 100)
        self.perfect_width = perfect_width

        # Add some width to narrow columns
        for nc in narrow_columns:
            cw = self.tv.columnWidth(self.annotations_header.index(nc))
            self.tv.setColumnWidth(self.annotations_header.index(nc), cw + extra_width)

        # Set row height
        fm = QFontMetrics(self.FONT)
        nrows = len(self.tabledata)
        for row in xrange(nrows):
            self.tv.setRowHeight(row, fm.height() + 4)

        self.tv.setSortingEnabled(True)
        sort_column = self.opts.prefs.get('annotated_books_dialog_sort_column',
                                          self.annotations_header.index('Confidence'))
        sort_order = self.opts.prefs.get('annotated_books_dialog_sort_order',
                                         Qt.DescendingOrder)
        self.tv.sortByColumn(sort_column, sort_order)

        # ~~~~~~~~ Create the ButtonBox ~~~~~~~~
        self.dialogButtonBox = QDialogButtonBox(QDialogButtonBox.Cancel | QDialogButtonBox.Help)
        self.dialogButtonBox.setOrientation(Qt.Horizontal)
        self.import_button = self.dialogButtonBox.addButton(self.dialogButtonBox.Ok)
        self.import_button.setText('Import Annotations')

        # Action buttons
        self.toggle_checkmarks_button = self.dialogButtonBox.addButton('Clear All', QDialogButtonBox.ActionRole)
        self.toggle_checkmarks_button.setObjectName('toggle_checkmarks_button')

        scb_text = 'Show match status'
        if self.show_confidence_colors:
            scb_text = "Hide match status"
        self.show_confidence_button = self.dialogButtonBox.addButton(scb_text, QDialogButtonBox.ActionRole)
        self.show_confidence_button.setObjectName('confidence_button')
        if self.show_confidence_colors:
            self.show_confidence_button.setIcon(get_icon('images/matches_hide.png'))
        else:
            self.show_confidence_button.setIcon(get_icon('images/matches_show.png'))

        self.preview_button = self.dialogButtonBox.addButton('Preview', QDialogButtonBox.ActionRole)
        self.preview_button.setObjectName('preview_button')

        self.dialogButtonBox.clicked.connect(self.show_annotated_books_dialog_clicked)
        self.l.addWidget(self.dialogButtonBox)

        # Cause our dialog size to be restored from prefs or created on first usage
        self.resize_dialog()

    def capture_sort_column(self, sort_column):
        sort_order = self.tv.horizontalHeader().sortIndicatorOrder()
        self.opts.prefs.set('annotated_books_dialog_sort_column', sort_column)
        self.opts.prefs.set('annotated_books_dialog_sort_order', sort_order)

    def fetch_selected_annotations(self):
        '''
        Invoked by 'Import annotations' button in show_annotated_books_dialog()
        Populate a list of books by Reader App:
        { 'iBooks': [{'title':, 'author':, 'uuid'}, ...],
          'Marvin': [{'title':, 'author':, 'uuid'}, ...] }
        '''
        self.selected_books = {}

        for i in range(len(self.tabledata)):
            self.tv.selectRow(i)
            enabled = bool(self.tm.arraydata[i][self.ENABLED_COL].checkState())
            if not enabled:
                continue

            reader_app = str(self.tm.arraydata[i][self.annotations_header.index('Reader App')].text())
            if not reader_app in self.selected_books:
                self.selected_books[reader_app] = []

            author = str(self.tm.arraydata[i][self.annotations_header.index('Author')].text())
            book_id = self.tm.arraydata[i][self.annotations_header.index('book_id')]
            genre = self.tm.arraydata[i][self.annotations_header.index('genre')]
            title = str(self.tm.arraydata[i][self.annotations_header.index('Title')].text())
            uuid = self.tm.arraydata[i][self.annotations_header.index('uuid')]

            book_mi = BookStruct()
            book_mi.author = author
            book_mi.book_id = book_id
            book_mi.genre = genre
            book_mi.reader_app = reader_app
            book_mi.title = title
            book_mi.uuid = uuid
            self.selected_books[reader_app].append(book_mi)

    def getTableRowDoubleClick(self, index):
        self.preview_annotations()

    def preview_annotations(self):
        """
        The listed annotations are in annotations.db.
        AnnotationsDB:annotations_to_HTML() needs title, book_id, reader_app
        """
        i = self.tvSelectionModel.currentIndex().row()
        reader_app = str(self.tm.arraydata[i][self.annotations_header.index('Reader App')].text())
        title = str(self.tm.arraydata[i][self.annotations_header.index('Title')].text())

        book_mi = BookStruct()
        book_mi.book_id = self.tm.arraydata[i][self.annotations_header.index('book_id')]
        book_mi.reader_app = reader_app
        book_mi.title = title

        # Render annotations from db
        annotations_db = ReaderApp.generate_annotations_db_name(reader_app, self.source)
        annotations = self.get_annotations_as_HTML(annotations_db, book_mi)

        PreviewDialog(book_mi, annotations, parent=self.opts.gui).exec_()

    def show_annotated_books_dialog_clicked(self, button):
        '''
        BUTTON_ROLES = ['AcceptRole', 'RejectRole', 'DestructiveRole', 'ActionRole',
                        'HelpRole', 'YesRole', 'NoRole', 'ApplyRole', 'ResetRole']
        '''
        if self.dialogButtonBox.buttonRole(button) == QDialogButtonBox.AcceptRole:
            self.fetch_selected_annotations()
            self.accept()
        elif self.dialogButtonBox.buttonRole(button) == QDialogButtonBox.ActionRole:
            if button.objectName() == 'confidence_button':
                self.toggle_confidence_colors()
            elif button.objectName() == 'preview_button':
                self.preview_annotations()
            elif button.objectName() == 'toggle_checkmarks_button':
                self.toggle_checkmarks()
        elif self.dialogButtonBox.buttonRole(button) == QDialogButtonBox.HelpRole:
            self.show_help()
        elif self.dialogButtonBox.buttonRole(button) == QDialogButtonBox.RejectRole:
            self.close()

    def show_help(self):
        '''
        Display help file
        '''
        hv = HelpView(self, self.opts.icon, self.opts.prefs,
                      html=get_resources('help/import_annotations.html'), title="Import Annotations")
        hv.show()

    def size_hint(self):
        return QtCore.QSize(self.perfect_width, self.height())

    def start_confidence_scan(self):
        self.annotated_books_scanner.start()

    def toggle_checkmarks(self):
        button_text = str(self.toggle_checkmarks_button.text())
        if button_text == 'Clear All':
            for i in range(len(self.tabledata)):
                self.tm.arraydata[i][self.ENABLED_COL].setCheckState(False)
            self.toggle_checkmarks_button.setText(' Set All ')
        else:
            for i in range(len(self.tabledata)):
                self.tm.arraydata[i][self.ENABLED_COL].setCheckState(True)
            self.toggle_checkmarks_button.setText('Clear All')
        self.tm.refresh(self.show_confidence_colors)

    def toggle_confidence_colors(self):
        self.show_confidence_colors = not self.show_confidence_colors
        self.opts.prefs.set('annotated_books_dialog_show_confidence_as_bg_color', self.show_confidence_colors)
        if self.show_confidence_colors:
            self.show_confidence_button.setText("Hide match status")
            self.show_confidence_button.setIcon(get_icon('images/matches_hide.png'))
            self.tv.sortByColumn(self.annotations_header.index('Confidence'), Qt.DescendingOrder)
            self.capture_sort_column(self.annotations_header.index('Confidence'))
        else:
            self.show_confidence_button.setText("Show match status")
            self.show_confidence_button.setIcon(get_icon('images/matches_show.png'))
        self.tv.setAlternatingRowColors(not self.show_confidence_colors)
        self.tm.refresh(self.show_confidence_colors)
class AnnotatedBooksDialog(SizePersistedDialog):
    '''
    This dialog is shown when the user fetches or imports books
    self.fetch_single_annotations controls checkmark display, behavior of fetch button
    '''
    if isosx:
        FONT = QFont('Monaco', 11)
    elif iswindows:
        FONT = QFont('Lucida Console', 9)
    elif islinux:
        FONT = QFont('Monospace', 9)
        FONT.setStyleHint(QFont.TypeWriter)

    def __init__(self, parent, book_list, get_annotations_as_HTML, source):
        self.opts = parent.opts
        self.parent = parent
        self.get_annotations_as_HTML = get_annotations_as_HTML
        self.show_confidence_colors = self.opts.prefs.get(
            'annotated_books_dialog_show_confidence_as_bg_color', True)
        self.source = source

        #         QDialog.__init__(self, parent=self.opts.gui)
        SizePersistedDialog.__init__(
            self, self.opts.gui,
            'Annotations plugin:import annotations dialog')
        self.setWindowTitle(_('Import Annotations'))
        self.setWindowIcon(self.opts.icon)
        self.l = QVBoxLayout(self)
        self.setLayout(self.l)
        self.perfect_width = 0

        from calibre_plugins.annotations.appearance import default_timestamp
        friendly_timestamp_format = plugin_prefs.get(
            'appearance_timestamp_format', default_timestamp)

        # Are we collecting News clippings?
        collect_news_clippings = self.opts.prefs.get(
            'cfg_news_clippings_checkbox', False)
        news_clippings_destination = self.opts.prefs.get(
            'cfg_news_clippings_lineEdit', None)

        # Populate the table data
        self.tabledata = []
        for book_data in book_list:
            enabled = QCheckBox()
            enabled.setChecked(True)

            # last_annotation sorts by timestamp
            last_annotation = SortableTableWidgetItem(
                strftime(friendly_timestamp_format,
                         localtime(book_data['last_update'])),
                book_data['last_update'])

            # reader_app sorts case-insensitive
            reader_app = SortableTableWidgetItem(
                book_data['reader_app'], book_data['reader_app'].upper())

            # title, author sort by title_sort, author_sort
            if not book_data['title_sort']:
                book_data['title_sort'] = book_data['title']
            title = SortableTableWidgetItem(book_data['title'],
                                            book_data['title_sort'].upper())

            if not book_data['author_sort']:
                book_data['author_sort'] = book_data['author']
            author = SortableTableWidgetItem(book_data['author'],
                                             book_data['author_sort'].upper())

            genres = book_data['genre'].split(', ')
            if 'News' in genres and collect_news_clippings:
                cid = get_clippings_cid(self, news_clippings_destination)
                confidence = 5
            else:
                cid, confidence = parent.generate_confidence(book_data)

            # List order matches self.annotations_header
            this_book = [
                book_data['uuid'], book_data['book_id'], book_data['genre'],
                enabled, reader_app, title, author, last_annotation,
                book_data['annotations'], confidence
            ]
            self.tabledata.append(this_book)

        self.tv = QTableView(self)
        self.l.addWidget(self.tv)
        self.annotations_header = [
            'uuid', 'book_id', 'genre', '',
            _('Reader App'),
            _('Title'),
            _('Author'),
            _('Last Annotation'),
            _('Annotations'),
            _('Confidence')
        ]
        self.ENABLED_COL = 3
        self.READER_APP_COL = 4
        self.TITLE_COL = 5
        self.AUTHOR_COL = 6
        self.LAST_ANNOTATION_COL = 7
        self.CONFIDENCE_COL = 9
        columns_to_center = [8]
        self.tm = MarkupTableModel(self, columns_to_center=columns_to_center)
        self.tv.setModel(self.tm)
        self.tv.setShowGrid(False)
        self.tv.setFont(self.FONT)
        self.tvSelectionModel = self.tv.selectionModel()
        self.tv.setAlternatingRowColors(not self.show_confidence_colors)
        self.tv.setShowGrid(False)
        self.tv.setWordWrap(False)
        self.tv.setSelectionBehavior(self.tv.SelectRows)

        # Connect signals
        self.tv.doubleClicked.connect(self.getTableRowDoubleClick)
        self.tv.horizontalHeader().sectionClicked.connect(
            self.capture_sort_column)

        # Hide the vertical self.header
        self.tv.verticalHeader().setVisible(False)

        # Hide uuid, book_id, genre, confidence
        self.tv.hideColumn(self.annotations_header.index('uuid'))
        self.tv.hideColumn(self.annotations_header.index('book_id'))
        self.tv.hideColumn(self.annotations_header.index('genre'))
        #         self.tv.hideColumn(self.annotations_header.index(_('Confidence')))
        self.tv.hideColumn(self.CONFIDENCE_COL)

        # Set horizontal self.header props
        self.tv.horizontalHeader().setStretchLastSection(True)

        narrow_columns = [
            _('Last Annotation'),
            _('Reader App'),
            _('Annotations')
        ]
        extra_width = 10
        breathing_space = 20

        # Set column width to fit contents
        self.tv.resizeColumnsToContents()
        perfect_width = 10 + (len(narrow_columns) * extra_width)
        for i in range(3, 8):
            perfect_width += self.tv.columnWidth(i) + breathing_space
        self.tv.setMinimumSize(perfect_width, 100)
        self.perfect_width = perfect_width

        # Add some width to narrow columns
        for nc in narrow_columns:
            cw = self.tv.columnWidth(self.annotations_header.index(nc))
            self.tv.setColumnWidth(self.annotations_header.index(nc),
                                   cw + extra_width)

        # Set row height
        fm = QFontMetrics(self.FONT)
        nrows = len(self.tabledata)
        for row in xrange(nrows):
            self.tv.setRowHeight(row, fm.height() + 4)

        self.tv.setSortingEnabled(True)
        sort_column = self.opts.prefs.get('annotated_books_dialog_sort_column',
                                          self.CONFIDENCE_COL)
        sort_order = self.opts.prefs.get('annotated_books_dialog_sort_order',
                                         Qt.DescendingOrder)
        self.tv.sortByColumn(sort_column, sort_order)

        # ~~~~~~~~ Create the ButtonBox ~~~~~~~~
        self.dialogButtonBox = QDialogButtonBox(QDialogButtonBox.Cancel
                                                | QDialogButtonBox.Help)
        self.dialogButtonBox.setOrientation(Qt.Horizontal)
        self.import_button = self.dialogButtonBox.addButton(
            self.dialogButtonBox.Ok)
        self.import_button.setText(_('Import Annotations'))

        # Action buttons
        self.toggle_checkmarks_button = self.dialogButtonBox.addButton(
            _('Clear All'), QDialogButtonBox.ActionRole)
        self.toggle_checkmarks_button.setObjectName('toggle_checkmarks_button')

        scb_text = _('Show match status')
        if self.show_confidence_colors:
            scb_text = _("Hide match status")
        self.show_confidence_button = self.dialogButtonBox.addButton(
            scb_text, QDialogButtonBox.ActionRole)
        self.show_confidence_button.setObjectName('confidence_button')
        if self.show_confidence_colors:
            self.show_confidence_button.setIcon(
                get_icon('images/matches_hide.png'))
        else:
            self.show_confidence_button.setIcon(
                get_icon('images/matches_show.png'))

        self.preview_button = self.dialogButtonBox.addButton(
            _('Preview'), QDialogButtonBox.ActionRole)
        self.preview_button.setObjectName('preview_button')

        self.dialogButtonBox.clicked.connect(
            self.show_annotated_books_dialog_clicked)
        self.l.addWidget(self.dialogButtonBox)

        # Cause our dialog size to be restored from prefs or created on first usage
        self.resize_dialog()

    def capture_sort_column(self, sort_column):
        sort_order = self.tv.horizontalHeader().sortIndicatorOrder()
        self.opts.prefs.set('annotated_books_dialog_sort_column', sort_column)
        self.opts.prefs.set('annotated_books_dialog_sort_order', sort_order)

    def fetch_selected_annotations(self):
        '''
        Invoked by 'Import annotations' button in show_annotated_books_dialog()
        Populate a list of books by Reader App:
        { 'iBooks': [{'title':, 'author':, 'uuid'}, ...],
          'Marvin': [{'title':, 'author':, 'uuid'}, ...] }
        '''
        self.selected_books = {}

        for i in range(len(self.tabledata)):
            self.tv.selectRow(i)
            enabled = bool(self.tm.arraydata[i][self.ENABLED_COL].checkState())
            if not enabled:
                continue

            reader_app = str(self.tm.arraydata[i][self.READER_APP_COL].text())
            if not reader_app in self.selected_books:
                self.selected_books[reader_app] = []

            author = str(self.tm.arraydata[i][self.AUTHOR_COL].text())
            book_id = self.tm.arraydata[i][self.annotations_header.index(
                'book_id')]
            genre = self.tm.arraydata[i][self.annotations_header.index(
                'genre')]
            title = str(self.tm.arraydata[i][self.TITLE_COL].text())
            uuid = self.tm.arraydata[i][self.annotations_header.index('uuid')]

            book_mi = BookStruct()
            book_mi.author = author
            book_mi.book_id = book_id
            book_mi.genre = genre
            book_mi.reader_app = reader_app
            book_mi.title = title
            book_mi.uuid = uuid
            self.selected_books[reader_app].append(book_mi)

    def getTableRowDoubleClick(self, index):
        self.preview_annotations()

    def preview_annotations(self):
        """
        The listed annotations are in annotations.db.
        AnnotationsDB:annotations_to_HTML() needs title, book_id, reader_app
        """
        i = self.tvSelectionModel.currentIndex().row()
        reader_app = str(self.tm.arraydata[i][self.READER_APP_COL].text())
        title = str(self.tm.arraydata[i][self.TITLE_COL].text())

        book_mi = BookStruct()
        book_mi.book_id = self.tm.arraydata[i][self.annotations_header.index(
            'book_id')]
        book_mi.reader_app = reader_app
        book_mi.title = title

        # Render annotations from db
        annotations_db = ReaderApp.generate_annotations_db_name(
            reader_app, self.source)
        annotations = self.get_annotations_as_HTML(annotations_db, book_mi)

        PreviewDialog(book_mi, annotations, parent=self.opts.gui).exec_()

    def show_annotated_books_dialog_clicked(self, button):
        '''
        BUTTON_ROLES = ['AcceptRole', 'RejectRole', 'DestructiveRole', 'ActionRole',
                        'HelpRole', 'YesRole', 'NoRole', 'ApplyRole', 'ResetRole']
        '''
        if self.dialogButtonBox.buttonRole(
                button) == QDialogButtonBox.AcceptRole:
            self.fetch_selected_annotations()
            self.accept()
        elif self.dialogButtonBox.buttonRole(
                button) == QDialogButtonBox.ActionRole:
            if button.objectName() == 'confidence_button':
                self.toggle_confidence_colors()
            elif button.objectName() == 'preview_button':
                self.preview_annotations()
            elif button.objectName() == 'toggle_checkmarks_button':
                self.toggle_checkmarks()
        elif self.dialogButtonBox.buttonRole(
                button) == QDialogButtonBox.HelpRole:
            self.show_help()
        elif self.dialogButtonBox.buttonRole(
                button) == QDialogButtonBox.RejectRole:
            self.close()

    def show_help(self):
        '''
        Display help file
        '''
        hv = HelpView(self,
                      self.opts.icon,
                      self.opts.prefs,
                      html=get_resources('help/import_annotations.html'),
                      title=_("Import Annotations"))
        hv.show()

    def size_hint(self):
        return QtCore.QSize(self.perfect_width, self.height())

    def start_confidence_scan(self):
        self.annotated_books_scanner.start()

    def toggle_checkmarks(self):
        button_text = str(self.toggle_checkmarks_button.text())
        if button_text == _('Clear All'):
            for i in range(len(self.tabledata)):
                self.tm.arraydata[i][self.ENABLED_COL].setCheckState(False)
            self.toggle_checkmarks_button.setText(_('Set All'))
        else:
            for i in range(len(self.tabledata)):
                self.tm.arraydata[i][self.ENABLED_COL].setCheckState(True)
            self.toggle_checkmarks_button.setText(_('Clear All'))
        self.tm.refresh(self.show_confidence_colors)

    def toggle_confidence_colors(self):
        self.show_confidence_colors = not self.show_confidence_colors
        self.opts.prefs.set(
            'annotated_books_dialog_show_confidence_as_bg_color',
            self.show_confidence_colors)
        if self.show_confidence_colors:
            self.show_confidence_button.setText(_("Hide match status"))
            self.show_confidence_button.setIcon(
                get_icon('images/matches_hide.png'))
            self.tv.sortByColumn(self.CONFIDENCE_COL, Qt.DescendingOrder)
            self.capture_sort_column(self.CONFIDENCE_COL)
        else:
            self.show_confidence_button.setText(_("Show match status"))
            self.show_confidence_button.setIcon(
                get_icon('images/matches_show.png'))
        self.tv.setAlternatingRowColors(not self.show_confidence_colors)
        self.tm.refresh(self.show_confidence_colors)
Esempio n. 4
0
class OpdsDialog(QDialog):

    def __init__(self, gui, icon, do_user_config):
        QDialog.__init__(self, gui)
        self.gui = gui
        self.do_user_config = do_user_config

        self.db = gui.current_db.new_api

        # The model for the book list
        self.model = OpdsBooksModel(None, self.dummy_books(), self.db)
        self.searchproxymodel = QSortFilterProxyModel(self)
        self.searchproxymodel.setFilterCaseSensitivity(Qt.CaseInsensitive)
        self.searchproxymodel.setFilterKeyColumn(-1)
        self.searchproxymodel.setSourceModel(self.model)

        self.layout = QGridLayout()
        self.setLayout(self.layout)

        self.setWindowTitle('OPDS Client')
        self.setWindowIcon(icon)

        labelColumnWidths = []

        self.opdsUrlLabel = QLabel('OPDS URL: ')
        self.layout.addWidget(self.opdsUrlLabel, 0, 0)
        labelColumnWidths.append(self.layout.itemAtPosition(0, 0).sizeHint().width())

        config.convertSingleStringOpdsUrlPreferenceToListOfStringsPreference()
        self.opdsUrlEditor = QComboBox(self)
        self.opdsUrlEditor.activated.connect(self.opdsUrlEditorActivated)
        self.opdsUrlEditor.addItems(prefs['opds_url'])
        self.opdsUrlEditor.setEditable(True)
        self.opdsUrlEditor.setInsertPolicy(QComboBox.InsertAtTop)
        self.layout.addWidget(self.opdsUrlEditor, 0, 1, 1, 3)
        self.opdsUrlLabel.setBuddy(self.opdsUrlEditor)

        buttonColumnNumber = 7
        buttonColumnWidths = []
        self.about_button = QPushButton('About', self)
        self.about_button.setAutoDefault(False)
        self.about_button.clicked.connect(self.about)
        self.layout.addWidget(self.about_button, 0, buttonColumnNumber)
        buttonColumnWidths.append(self.layout.itemAtPosition(0, buttonColumnNumber).sizeHint().width()) 

        # Initially download the catalogs found in the root catalog of the URL
        # selected at startup.  Fail quietly on failing to open the URL
        catalogsTuple = self.model.downloadOpdsRootCatalog(self.gui, self.opdsUrlEditor.currentText(), False)
        print catalogsTuple
        firstCatalogTitle = catalogsTuple[0]
        self.currentOpdsCatalogs = catalogsTuple[1] # A dictionary of title->feedURL

        self.opdsCatalogSelectorLabel = QLabel('OPDS Catalog:')
        self.layout.addWidget(self.opdsCatalogSelectorLabel, 1, 0)
        labelColumnWidths.append(self.layout.itemAtPosition(1, 0).sizeHint().width())

        self.opdsCatalogSelector = QComboBox(self)
        self.opdsCatalogSelector.setEditable(False)
        self.opdsCatalogSelectorModel = QStringListModel(self.currentOpdsCatalogs.keys())
        self.opdsCatalogSelector.setModel(self.opdsCatalogSelectorModel)
        self.opdsCatalogSelector.setCurrentText(firstCatalogTitle)
        self.layout.addWidget(self.opdsCatalogSelector, 1, 1, 1, 3)

        self.download_opds_button = QPushButton('Download OPDS', self)
        self.download_opds_button.setAutoDefault(False)
        self.download_opds_button.clicked.connect(self.download_opds)
        self.layout.addWidget(self.download_opds_button, 1, buttonColumnNumber)
        buttonColumnWidths.append(self.layout.itemAtPosition(1, buttonColumnNumber).sizeHint().width()) 

        # Search GUI
        self.searchEditor = QLineEdit(self)
        self.searchEditor.returnPressed.connect(self.searchBookList)
        self.layout.addWidget(self.searchEditor, 2, buttonColumnNumber - 2, 1, 2)

        self.searchButton = QPushButton('Search', self)
        self.searchButton.setAutoDefault(False)
        self.searchButton.clicked.connect(self.searchBookList)
        self.layout.addWidget(self.searchButton, 2, buttonColumnNumber)
        buttonColumnWidths.append(self.layout.itemAtPosition(2, buttonColumnNumber).sizeHint().width())

        # The main book list
        self.library_view = QTableView(self)
        self.library_view.setAlternatingRowColors(True)
        self.library_view.setModel(self.searchproxymodel)
        self.library_view.horizontalHeader().setSectionResizeMode(0, QHeaderView.Stretch)
        self.library_view.horizontalHeader().setSectionResizeMode(1, QHeaderView.Stretch)
        self.library_view.horizontalHeader().setSectionResizeMode(2, QHeaderView.Stretch)
        self.library_view.setSelectionBehavior(QAbstractItemView.SelectRows)
        self.resizeAllLibraryViewLinesToHeaderHeight()
        self.library_view.resizeColumnsToContents()
        self.layout.addWidget(self.library_view, 3, 0, 3, buttonColumnNumber + 1)

        self.hideNewsCheckbox = QCheckBox('Hide Newspapers', self)
        self.hideNewsCheckbox.clicked.connect(self.setHideNewspapers)
        self.hideNewsCheckbox.setChecked(prefs['hideNewspapers'])
        self.layout.addWidget(self.hideNewsCheckbox, 6, 0, 1, 3)

        self.hideBooksAlreadyInLibraryCheckbox = QCheckBox('Hide books already in library', self)
        self.hideBooksAlreadyInLibraryCheckbox.clicked.connect(self.setHideBooksAlreadyInLibrary)
        self.hideBooksAlreadyInLibraryCheckbox.setChecked(prefs['hideBooksAlreadyInLibrary'])
        self.layout.addWidget(self.hideBooksAlreadyInLibraryCheckbox, 7, 0, 1, 3)

        # Let the checkbox initial state control the filtering
        self.model.setFilterBooksThatAreNewspapers(self.hideNewsCheckbox.isChecked())
        self.model.setFilterBooksThatAreAlreadyInLibrary(self.hideBooksAlreadyInLibraryCheckbox.isChecked())

        self.downloadButton = QPushButton('Download selected books', self)
        self.downloadButton.setAutoDefault(False)
        self.downloadButton.clicked.connect(self.downloadSelectedBooks)
        self.layout.addWidget(self.downloadButton, 6, buttonColumnNumber)
        buttonColumnWidths.append(self.layout.itemAtPosition(6, buttonColumnNumber).sizeHint().width()) 

        self.fixTimestampButton = QPushButton('Fix timestamps of selection', self)
        self.fixTimestampButton.setAutoDefault(False)
        self.fixTimestampButton.clicked.connect(self.fixBookTimestamps)
        self.layout.addWidget(self.fixTimestampButton, 7, buttonColumnNumber)
        buttonColumnWidths.append(self.layout.itemAtPosition(7, buttonColumnNumber).sizeHint().width()) 

        # Make all columns of the grid layout the same width as the button column
        buttonColumnWidth = max(buttonColumnWidths)
        for columnNumber in range(0, buttonColumnNumber):
            self.layout.setColumnMinimumWidth(columnNumber, buttonColumnWidth)

        # Make sure the first column isn't wider than the labels it holds
        labelColumnWidth = max(labelColumnWidths)
        self.layout.setColumnMinimumWidth(0, labelColumnWidth)

        self.resize(self.sizeHint())

    def opdsUrlEditorActivated(self, text):
        prefs['opds_url'] = config.saveOpdsUrlCombobox(self.opdsUrlEditor)
        catalogsTuple = self.model.downloadOpdsRootCatalog(self.gui, self.opdsUrlEditor.currentText(), True)
        firstCatalogTitle = catalogsTuple[0]
        self.currentOpdsCatalogs = catalogsTuple[1] # A dictionary of title->feedURL
        self.opdsCatalogSelectorModel.setStringList(self.currentOpdsCatalogs.keys())
        self.opdsCatalogSelector.setCurrentText(firstCatalogTitle)

    def setHideNewspapers(self, checked):
        prefs['hideNewspapers'] = checked
        self.model.setFilterBooksThatAreNewspapers(checked)
        self.resizeAllLibraryViewLinesToHeaderHeight()

    def setHideBooksAlreadyInLibrary(self, checked):
        prefs['hideBooksAlreadyInLibrary'] = checked
        self.model.setFilterBooksThatAreAlreadyInLibrary(checked)
        self.resizeAllLibraryViewLinesToHeaderHeight()

    def searchBookList(self):
        searchString = self.searchEditor.text()
        print "starting book list search for: %s" % searchString
        self.searchproxymodel.setFilterFixedString(searchString)

    def about(self):
        text = get_resources('about.txt')
        QMessageBox.about(self, 'About the OPDS Client plugin', text.decode('utf-8'))

    def download_opds(self):
        opdsCatalogUrl = self.currentOpdsCatalogs.get(self.opdsCatalogSelector.currentText(), None)
        if opdsCatalogUrl is None:
            # Just give up quietly
            return
        self.model.downloadOpdsCatalog(self.gui, opdsCatalogUrl)
        if self.model.isCalibreOpdsServer():
            self.model.downloadMetadataUsingCalibreRestApi(self.opdsUrlEditor.currentText())
        self.library_view.horizontalHeader().setSectionResizeMode(0, QHeaderView.Stretch)
        self.library_view.horizontalHeader().setSectionResizeMode(1, QHeaderView.Stretch)
        self.library_view.horizontalHeader().setSectionResizeMode(2, QHeaderView.Stretch)
        self.resizeAllLibraryViewLinesToHeaderHeight()
        self.resize(self.sizeHint())

    def config(self):
        self.do_user_config(parent=self)

    def downloadSelectedBooks(self):
        selectionmodel = self.library_view.selectionModel()
        if selectionmodel.hasSelection():
            rows = selectionmodel.selectedRows()
            for row in reversed(rows):
                book = row.data(Qt.UserRole)
                self.downloadBook(book)

    def downloadBook(self, book):
        if len(book.links) > 0:
            self.gui.download_ebook(book.links[0])

    def fixBookTimestamps(self):
        selectionmodel = self.library_view.selectionModel()
        if selectionmodel.hasSelection():
            rows = selectionmodel.selectedRows()
            for row in reversed(rows):
                book = row.data(Qt.UserRole)
                self.fixBookTimestamp(book)

    def fixBookTimestamp(self, book):
        bookTimestamp = book.timestamp
        identicalBookIds = self.findIdenticalBooksForBooksWithMultipleAuthors(book)
        bookIdToValMap = {}
        for identicalBookId in identicalBookIds:
            bookIdToValMap[identicalBookId] = bookTimestamp
        if len(bookIdToValMap) < 1:
            print "Failed to set timestamp of book: %s" % book
        self.db.set_field('timestamp', bookIdToValMap)

    def findIdenticalBooksForBooksWithMultipleAuthors(self, book):
        authorsList = book.authors
        if len(authorsList) < 2:
            return self.db.find_identical_books(book)
        # Try matching the authors one by one
        identicalBookIds = set()
        for author in authorsList:
            singleAuthorBook = Metadata(book.title, [author])
            singleAuthorIdenticalBookIds = self.db.find_identical_books(singleAuthorBook)
            identicalBookIds = identicalBookIds.union(singleAuthorIdenticalBookIds)
        return identicalBookIds

    def dummy_books(self):
        dummy_author = ' ' * 40
        dummy_title = ' ' * 60
        books_list = []
        for line in range (1, 10):
            book = DynamicBook()
            book.author = dummy_author
            book.title = dummy_title
            book.updated = datetime.datetime.now().strftime('%Y-%m-%dT%H:%M:%S+00:00')
            book.id = ''
            books_list.append(book)
        return books_list

    def resizeAllLibraryViewLinesToHeaderHeight(self):
        rowHeight = self.library_view.horizontalHeader().height()
        for rowNumber in range (0, self.library_view.model().rowCount()):
            self.library_view.setRowHeight(rowNumber, rowHeight)