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)
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)