class BookInfo(QDialog): closed = pyqtSignal(object) def __init__(self, parent, view, row, link_delegate): QDialog.__init__(self, parent) self.normal_brush = QBrush(Qt.white) self.marked_brush = QBrush(Qt.lightGray) self.marked = None self.gui = parent self.splitter = QSplitter(self) self._l = l = QVBoxLayout(self) self.setLayout(l) l.addWidget(self.splitter) self.cover = CoverView(self, show_size=gprefs['bd_overlay_cover_size']) self.cover.resizeEvent = self.cover_view_resized self.cover.cover_changed.connect(self.cover_changed) self.cover_pixmap = None self.cover.sizeHint = self.details_size_hint self.splitter.addWidget(self.cover) self.details = Details(parent.book_details.book_info, self) self.details.anchor_clicked.connect(self.on_link_clicked) self.link_delegate = link_delegate self.details.setAttribute(Qt.WA_OpaquePaintEvent, False) palette = self.details.palette() self.details.setAcceptDrops(False) palette.setBrush(QPalette.Base, Qt.transparent) self.details.setPalette(palette) self.c = QWidget(self) self.c.l = l2 = QGridLayout(self.c) l2.setContentsMargins(0, 0, 0, 0) self.c.setLayout(l2) l2.addWidget(self.details, 0, 0, 1, -1) self.splitter.addWidget(self.c) self.fit_cover = QCheckBox(_('Fit &cover within view'), self) self.fit_cover.setChecked( gprefs.get('book_info_dialog_fit_cover', True)) self.hl = hl = QHBoxLayout() hl.setContentsMargins(0, 0, 0, 0) l2.addLayout(hl, l2.rowCount(), 0, 1, -1) hl.addWidget(self.fit_cover), hl.addStretch() self.clabel = QLabel( '<div style="text-align: right"><a href="calibre:conf" title="{}" style="text-decoration: none">{}</a>' .format(_('Configure this view'), _('Configure'))) self.clabel.linkActivated.connect(self.configure) hl.addWidget(self.clabel) self.previous_button = QPushButton(QIcon(I('previous.png')), _('&Previous'), self) self.previous_button.clicked.connect(self.previous) l2.addWidget(self.previous_button, l2.rowCount(), 0) self.next_button = QPushButton(QIcon(I('next.png')), _('&Next'), self) self.next_button.clicked.connect(self.next) l2.addWidget(self.next_button, l2.rowCount() - 1, 1) self.view = view self.current_row = None self.refresh(row) self.view.model().new_bookdisplay_data.connect(self.slave) self.fit_cover.stateChanged.connect(self.toggle_cover_fit) self.ns = QShortcut(QKeySequence('Alt+Right'), self) self.ns.activated.connect(self.next) self.ps = QShortcut(QKeySequence('Alt+Left'), self) self.ps.activated.connect(self.previous) self.next_button.setToolTip( _('Next [%s]') % unicode_type(self.ns.key().toString(QKeySequence.NativeText))) self.previous_button.setToolTip( _('Previous [%s]') % unicode_type(self.ps.key().toString(QKeySequence.NativeText))) geom = QCoreApplication.instance().desktop().availableGeometry(self) screen_height = geom.height() - 100 screen_width = geom.width() - 100 self.resize(max(int(screen_width / 2), 700), screen_height) saved_layout = gprefs.get('book_info_dialog_layout', None) if saved_layout is not None: try: self.restoreGeometry(saved_layout[0]) self.splitter.restoreState(saved_layout[1]) except Exception: pass def configure(self): d = Configure(get_gui().current_db, self) if d.exec_() == d.Accepted: if self.current_row is not None: mi = self.view.model().get_book_display_info(self.current_row) if mi is not None: self.refresh(self.current_row, mi=mi) def on_link_clicked(self, qurl): link = unicode_type(qurl.toString(NO_URL_FORMATTING)) self.link_delegate(link) def done(self, r): saved_layout = (bytearray(self.saveGeometry()), bytearray(self.splitter.saveState())) gprefs.set('book_info_dialog_layout', saved_layout) ret = QDialog.done(self, r) self.view.model().new_bookdisplay_data.disconnect(self.slave) self.view = self.link_delegate = self.gui = None self.closed.emit(self) return ret def cover_changed(self, data): if self.current_row is not None: id_ = self.view.model().id(self.current_row) self.view.model().db.set_cover(id_, data) self.gui.refresh_cover_browser() ci = self.view.currentIndex() if ci.isValid(): self.view.model().current_changed(ci, ci) def details_size_hint(self): return QSize(350, 550) def toggle_cover_fit(self, state): gprefs.set('book_info_dialog_fit_cover', self.fit_cover.isChecked()) self.resize_cover() def cover_view_resized(self, event): QTimer.singleShot(1, self.resize_cover) def slave(self, mi): self.refresh(mi.row_number, mi) def move(self, delta=1): idx = self.view.currentIndex() if idx.isValid(): m = self.view.model() ni = m.index(idx.row() + delta, idx.column()) if ni.isValid(): if self.view.isVisible(): self.view.scrollTo(ni) self.view.setCurrentIndex(ni) def next(self): self.move() def previous(self): self.move(-1) def resize_cover(self): if self.cover_pixmap is None: return pixmap = self.cover_pixmap if self.fit_cover.isChecked(): scaled, new_width, new_height = fit_image( pixmap.width(), pixmap.height(), self.cover.size().width() - 10, self.cover.size().height() - 10) if scaled: try: dpr = self.devicePixelRatioF() except AttributeError: dpr = self.devicePixelRatio() pixmap = pixmap.scaled(int(dpr * new_width), int(dpr * new_height), Qt.KeepAspectRatio, Qt.SmoothTransformation) pixmap.setDevicePixelRatio(dpr) self.cover.set_pixmap(pixmap) self.update_cover_tooltip() def update_cover_tooltip(self): tt = '' if self.marked: tt = _('This book is marked') if self.marked in { True, 'true' } else _('This book is marked as: %s') % self.marked tt += '\n\n' if self.cover_pixmap is not None: sz = self.cover_pixmap.size() tt += _('Cover size: %(width)d x %(height)d pixels') % dict( width=sz.width(), height=sz.height()) self.cover.setToolTip(tt) self.cover.pixmap_size = sz.width(), sz.height() def refresh(self, row, mi=None): if isinstance(row, QModelIndex): row = row.row() if row == self.current_row and mi is None: return mi = self.view.model().get_book_display_info(row) if mi is None else mi if mi is None: # Indicates books was deleted from library, or row numbers have # changed return self.previous_button.setEnabled(False if row == 0 else True) self.next_button.setEnabled(False if row == self.view.model().rowCount(QModelIndex()) - 1 else True) self.current_row = row self.setWindowTitle(mi.title) self.cover_pixmap = QPixmap.fromImage(mi.cover_data[1]) try: dpr = self.devicePixelRatioF() except AttributeError: dpr = self.devicePixelRatio() self.cover_pixmap.setDevicePixelRatio(dpr) self.resize_cover() html = render_html(mi, True, self, pref_name='popup_book_display_fields') set_html(mi, html, self.details) self.marked = mi.marked self.cover.setBackgroundBrush( self.marked_brush if mi.marked else self.normal_brush) self.update_cover_tooltip()
class MetadataSingleDialogBase(ResizableDialog): view_format = pyqtSignal(object, object) cc_two_column = tweaks['metadata_single_use_2_cols_for_custom_fields'] one_line_comments_toolbar = False use_toolbutton_for_config_metadata = True def __init__(self, db, parent=None, editing_multiple=False): self.db = db self.changed = set() self.books_to_refresh = set() self.rows_to_refresh = set() self.metadata_before_fetch = None self.editing_multiple = editing_multiple self.comments_edit_state_at_apply = {} ResizableDialog.__init__(self, parent) def setupUi(self, *args): # {{{ self.resize(990, 670) self.download_shortcut = QShortcut(self) self.download_shortcut.setKey(QKeySequence('Ctrl+D', QKeySequence.PortableText)) p = self.parent() if hasattr(p, 'keyboard'): kname = u'Interface Action: Edit Metadata (Edit Metadata) : menu action : download' sc = p.keyboard.keys_map.get(kname, None) if sc: self.download_shortcut.setKey(sc[0]) self.swap_title_author_shortcut = s = QShortcut(self) s.setKey(QKeySequence('Alt+Down', QKeySequence.PortableText)) self.button_box = bb = QDialogButtonBox(self) self.button_box.accepted.connect(self.accept) self.button_box.rejected.connect(self.reject) self.next_button = QPushButton(QIcon(I('forward.png')), _('Next'), self) self.next_button.setShortcut(QKeySequence('Alt+Right')) self.next_button.clicked.connect(self.next_clicked) self.prev_button = QPushButton(QIcon(I('back.png')), _('Previous'), self) self.prev_button.setShortcut(QKeySequence('Alt+Left')) self.button_box.addButton(self.prev_button, bb.ActionRole) self.button_box.addButton(self.next_button, bb.ActionRole) self.prev_button.clicked.connect(self.prev_clicked) bb.setStandardButtons(bb.Ok|bb.Cancel) bb.button(bb.Ok).setDefault(True) self.scroll_area = QScrollArea(self) self.scroll_area.setFrameShape(QScrollArea.NoFrame) self.scroll_area.setWidgetResizable(True) self.central_widget = QTabWidget(self) self.scroll_area.setWidget(self.central_widget) self.l = QVBoxLayout(self) self.setLayout(self.l) self.l.addWidget(self.scroll_area) ll = self.button_box_layout = QHBoxLayout() self.l.addLayout(ll) ll.addSpacing(10) ll.addWidget(self.button_box) self.setWindowIcon(QIcon(I('edit_input.png'))) self.setWindowTitle(BASE_TITLE) self.create_basic_metadata_widgets() if len(self.db.custom_column_label_map): self.create_custom_metadata_widgets() self.comments_edit_state_at_apply = {self.comments:None} self.do_layout() geom = gprefs.get('metasingle_window_geometry3', None) if geom is not None: self.restoreGeometry(bytes(geom)) # }}} def create_basic_metadata_widgets(self): # {{{ self.basic_metadata_widgets = [] self.languages = LanguagesEdit(self) self.basic_metadata_widgets.append(self.languages) self.title = TitleEdit(self) self.title.textChanged.connect(self.update_window_title) self.deduce_title_sort_button = QToolButton(self) self.deduce_title_sort_button.setToolTip( _('Automatically create the title sort entry based on the current ' 'title entry.\nUsing this button to create title sort will ' 'change title sort from red to green.')) self.deduce_title_sort_button.setWhatsThis( self.deduce_title_sort_button.toolTip()) self.title_sort = TitleSortEdit(self, self.title, self.deduce_title_sort_button, self.languages) self.basic_metadata_widgets.extend([self.title, self.title_sort]) self.deduce_author_sort_button = b = RightClickButton(self) b.setToolTip('<p>' + _('Automatically create the author sort entry based on the current ' 'author entry. Using this button to create author sort will ' 'change author sort from red to green. There is a menu of ' 'functions available under this button. Click and hold ' 'on the button to see it.') + '</p>') if isosx: # Workaround for https://bugreports.qt-project.org/browse/QTBUG-41017 class Menu(QMenu): def mouseReleaseEvent(self, ev): ac = self.actionAt(ev.pos()) if ac is not None: ac.trigger() return QMenu.mouseReleaseEvent(self, ev) b.m = m = Menu() else: b.m = m = QMenu() ac = m.addAction(QIcon(I('forward.png')), _('Set author sort from author')) ac2 = m.addAction(QIcon(I('back.png')), _('Set author from author sort')) ac3 = m.addAction(QIcon(I('user_profile.png')), _('Manage authors')) ac4 = m.addAction(QIcon(I('next.png')), _('Copy author to author sort')) ac5 = m.addAction(QIcon(I('previous.png')), _('Copy author sort to author')) b.setMenu(m) self.authors = AuthorsEdit(self, ac3) self.author_sort = AuthorSortEdit(self, self.authors, b, self.db, ac, ac2, ac4, ac5) self.basic_metadata_widgets.extend([self.authors, self.author_sort]) self.swap_title_author_button = QToolButton(self) self.swap_title_author_button.setIcon(QIcon(I('swap.png'))) self.swap_title_author_button.setToolTip(_( 'Swap the author and title') + ' [%s]' % self.swap_title_author_shortcut.key().toString(QKeySequence.NativeText)) self.swap_title_author_button.clicked.connect(self.swap_title_author) self.swap_title_author_shortcut.activated.connect(self.swap_title_author_button.click) self.manage_authors_button = QToolButton(self) self.manage_authors_button.setIcon(QIcon(I('user_profile.png'))) self.manage_authors_button.setToolTip('<p>' + _( 'Manage authors. Use to rename authors and correct ' 'individual author\'s sort values') + '</p>') self.manage_authors_button.clicked.connect(self.authors.manage_authors) self.series = SeriesEdit(self) self.clear_series_button = QToolButton(self) self.clear_series_button.setToolTip( _('Clear series')) self.clear_series_button.clicked.connect(self.series.clear) self.series_index = SeriesIndexEdit(self, self.series) self.basic_metadata_widgets.extend([self.series, self.series_index]) self.formats_manager = FormatsManager(self, self.copy_fmt) # We want formats changes to be committed before title/author, as # otherwise we could have data loss if the title/author changed and the # user was trying to add an extra file from the old books directory. self.basic_metadata_widgets.insert(0, self.formats_manager) self.formats_manager.metadata_from_format_button.clicked.connect( self.metadata_from_format) self.formats_manager.cover_from_format_button.clicked.connect( self.cover_from_format) self.cover = Cover(self) self.cover.download_cover.connect(self.download_cover) self.basic_metadata_widgets.append(self.cover) self.comments = CommentsEdit(self, self.one_line_comments_toolbar) self.basic_metadata_widgets.append(self.comments) self.rating = RatingEdit(self) self.clear_ratings_button = QToolButton(self) self.clear_ratings_button.setToolTip(_('Clear rating')) self.clear_ratings_button.setIcon(QIcon(I('trash.png'))) self.clear_ratings_button.clicked.connect(self.rating.zero) self.basic_metadata_widgets.append(self.rating) self.tags = TagsEdit(self) self.tags_editor_button = QToolButton(self) self.tags_editor_button.setToolTip(_('Open Tag Editor')) self.tags_editor_button.setIcon(QIcon(I('chapters.png'))) self.tags_editor_button.clicked.connect(self.tags_editor) self.clear_tags_button = QToolButton(self) self.clear_tags_button.setToolTip(_('Clear all tags')) self.clear_tags_button.setIcon(QIcon(I('trash.png'))) self.clear_tags_button.clicked.connect(self.tags.clear) self.basic_metadata_widgets.append(self.tags) self.identifiers = IdentifiersEdit(self) self.basic_metadata_widgets.append(self.identifiers) self.clear_identifiers_button = QToolButton(self) self.clear_identifiers_button.setIcon(QIcon(I('trash.png'))) self.clear_identifiers_button.setToolTip(_('Clear Ids')) self.clear_identifiers_button.clicked.connect(self.identifiers.clear) self.paste_isbn_button = QToolButton(self) self.paste_isbn_button.setToolTip('<p>' + _('Paste the contents of the clipboard into the ' 'identifiers box prefixed with isbn:') + '</p>') self.paste_isbn_button.setIcon(QIcon(I('edit-paste.png'))) self.paste_isbn_button.clicked.connect(self.identifiers.paste_isbn) self.publisher = PublisherEdit(self) self.basic_metadata_widgets.append(self.publisher) self.timestamp = DateEdit(self) self.pubdate = PubdateEdit(self) self.basic_metadata_widgets.extend([self.timestamp, self.pubdate]) self.fetch_metadata_button = b = RightClickButton(self) # The following rigmarole is needed so that Qt gives the button the # same height as the other buttons in the dialog. There is no way to # center the text in a QToolButton with an icon, so we cant just set an # icon b.setIcon(QIcon(I('download-metadata.png'))) b.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) b.setMinimumHeight(b.sizeHint().height()) b.setIcon(QIcon()) b.setText(_('&Download metadata')), b.setPopupMode(b.DelayedPopup) b.setToolTip(_('Download metadata for this book [%s]') % self.download_shortcut.key().toString(QKeySequence.NativeText)) b.setSizePolicy(QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)) self.fetch_metadata_button.clicked.connect(self.fetch_metadata) self.fetch_metadata_menu = m = QMenu(self.fetch_metadata_button) m.addAction(QIcon(I('edit-undo.png')), _('Undo last metadata download'), self.undo_fetch_metadata) self.fetch_metadata_button.setMenu(m) self.download_shortcut.activated.connect(self.fetch_metadata_button.click) font = self.fmb_font = QFont() font.setBold(True) self.fetch_metadata_button.setFont(font) if self.use_toolbutton_for_config_metadata: self.config_metadata_button = QToolButton(self) self.config_metadata_button.setIcon(QIcon(I('config.png'))) else: self.config_metadata_button = QPushButton(self) self.config_metadata_button.setText(_('Configure download metadata')) self.config_metadata_button.setIcon(QIcon(I('config.png'))) self.config_metadata_button.clicked.connect(self.configure_metadata) self.config_metadata_button.setToolTip( _('Change how calibre downloads metadata')) # }}} def create_custom_metadata_widgets(self): # {{{ self.custom_metadata_widgets_parent = w = QWidget(self) layout = QGridLayout() w.setLayout(layout) self.custom_metadata_widgets, self.__cc_spacers = \ populate_metadata_page(layout, self.db, None, parent=w, bulk=False, two_column=self.cc_two_column) self.__custom_col_layouts = [layout] for widget in self.custom_metadata_widgets: if isinstance(widget, Comments): self.comments_edit_state_at_apply[widget] = None # }}} def set_custom_metadata_tab_order(self, before=None, after=None): # {{{ sto = QWidget.setTabOrder if getattr(self, 'custom_metadata_widgets', []): ans = self.custom_metadata_widgets for i in range(len(ans)-1): if before is not None and i == 0: pass if len(ans[i+1].widgets) == 2: sto(ans[i].widgets[-1], ans[i+1].widgets[1]) else: sto(ans[i].widgets[-1], ans[i+1].widgets[0]) for c in range(2, len(ans[i].widgets), 2): sto(ans[i].widgets[c-1], ans[i].widgets[c+1]) if after is not None: pass # }}} def do_view_format(self, path, fmt): if path: self.view_format.emit(None, path) else: self.view_format.emit(self.book_id, fmt) def copy_fmt(self, fmt, f): self.db.copy_format_to(self.book_id, fmt, f, index_is_id=True) def do_layout(self): raise NotImplementedError() def __call__(self, id_): self.book_id = id_ self.books_to_refresh = set([]) self.metadata_before_fetch = None for widget in self.basic_metadata_widgets: widget.initialize(self.db, id_) for widget in getattr(self, 'custom_metadata_widgets', []): widget.initialize(id_) if callable(self.set_current_callback): self.set_current_callback(id_) # Commented out as it doesn't play nice with Next, Prev buttons # self.fetch_metadata_button.setFocus(Qt.OtherFocusReason) # Miscellaneous interaction methods {{{ def update_window_title(self, *args): title = self.title.current_val if len(title) > 50: title = title[:50] + u'\u2026' self.setWindowTitle(BASE_TITLE + ' - ' + title + ' - ' + _(' [%(num)d of %(tot)d]')%dict(num=self.current_row+1, tot=len(self.row_list))) def swap_title_author(self, *args): title = self.title.current_val self.title.current_val = authors_to_string(self.authors.current_val) self.authors.current_val = string_to_authors(title) self.title_sort.auto_generate() self.author_sort.auto_generate() def tags_editor(self, *args): self.tags.edit(self.db, self.book_id) def metadata_from_format(self, *args): mi, ext = self.formats_manager.get_selected_format_metadata(self.db, self.book_id) if mi is not None: self.update_from_mi(mi) def get_pdf_cover(self): pdfpath = self.formats_manager.get_format_path(self.db, self.book_id, 'pdf') from calibre.gui2.metadata.pdf_covers import PDFCovers d = PDFCovers(pdfpath, parent=self) if d.exec_() == d.Accepted: cpath = d.cover_path if cpath: with open(cpath, 'rb') as f: self.update_cover(f.read(), 'PDF') d.cleanup() def cover_from_format(self, *args): ext = self.formats_manager.get_selected_format() if ext is None: return if ext == 'pdf': return self.get_pdf_cover() try: mi, ext = self.formats_manager.get_selected_format_metadata(self.db, self.book_id) except (IOError, OSError) as err: if getattr(err, 'errno', None) == errno.EACCES: # Permission denied import traceback fname = err.filename if err.filename else 'file' error_dialog(self, _('Permission denied'), _('Could not open %s. Is it being used by another' ' program?')%fname, det_msg=traceback.format_exc(), show=True) return raise if mi is None: return cdata = None if mi.cover and os.access(mi.cover, os.R_OK): cdata = open(mi.cover).read() elif mi.cover_data[1] is not None: cdata = mi.cover_data[1] if cdata is None: error_dialog(self, _('Could not read cover'), _('Could not read cover from %s format')%ext).exec_() return self.update_cover(cdata, ext) def update_cover(self, cdata, fmt): orig = self.cover.current_val self.cover.current_val = cdata if self.cover.current_val is None: self.cover.current_val = orig return error_dialog(self, _('Could not read cover'), _('The cover in the %s format is invalid')%fmt, show=True) return def update_from_mi(self, mi, update_sorts=True, merge_tags=True, merge_comments=False): fw = self.focusWidget() if not mi.is_null('title'): self.title.set_value(mi.title) if update_sorts: self.title_sort.auto_generate() if not mi.is_null('authors'): self.authors.set_value(mi.authors) if not mi.is_null('author_sort'): self.author_sort.set_value(mi.author_sort) elif update_sorts and not mi.is_null('authors'): self.author_sort.auto_generate() if not mi.is_null('rating'): try: self.rating.set_value(mi.rating) except: pass if not mi.is_null('publisher'): self.publisher.set_value(mi.publisher) if not mi.is_null('tags'): old_tags = self.tags.current_val tags = mi.tags if mi.tags else [] if old_tags and merge_tags: ltags, lotags = {t.lower() for t in tags}, {t.lower() for t in old_tags} tags = [t for t in tags if t.lower() in ltags-lotags] + old_tags self.tags.set_value(tags) if not mi.is_null('identifiers'): current = self.identifiers.current_val current.update(mi.identifiers) self.identifiers.set_value(current) if not mi.is_null('pubdate'): self.pubdate.set_value(mi.pubdate) if not mi.is_null('series') and mi.series.strip(): self.series.set_value(mi.series) if mi.series_index is not None: self.series_index.reset_original() self.series_index.set_value(float(mi.series_index)) if not mi.is_null('languages'): langs = [canonicalize_lang(x) for x in mi.languages] langs = [x for x in langs if x is not None] if langs: self.languages.set_value(langs) if mi.comments and mi.comments.strip(): val = mi.comments if val and merge_comments: cval = self.comments.current_val if cval: val = merge_two_comments(cval, val) self.comments.set_value(val) if fw is not None: fw.setFocus(Qt.OtherFocusReason) def fetch_metadata(self, *args): d = FullFetch(self.cover.pixmap(), self) ret = d.start(title=self.title.current_val, authors=self.authors.current_val, identifiers=self.identifiers.current_val) if ret == d.Accepted: self.metadata_before_fetch = {f:getattr(self, f).current_val for f in fetched_fields} from calibre.ebooks.metadata.sources.prefs import msprefs mi = d.book dummy = Metadata(_('Unknown')) for f in msprefs['ignore_fields']: if ':' not in f: setattr(mi, f, getattr(dummy, f)) if mi is not None: pd = mi.pubdate if pd is not None: # Put the downloaded published date into the local timezone # as we discard time info and the date is timezone # invariant. This prevents the as_local_timezone() call in # update_from_mi from changing the pubdate mi.pubdate = datetime(pd.year, pd.month, pd.day, tzinfo=local_tz) self.update_from_mi(mi, merge_comments=msprefs['append_comments']) if d.cover_pixmap is not None: self.metadata_before_fetch['cover'] = self.cover.current_val self.cover.current_val = pixmap_to_data(d.cover_pixmap) def undo_fetch_metadata(self): if self.metadata_before_fetch is None: return error_dialog(self, _('No downloaded metadata'), _( 'There is no downloaded metadata to undo'), show=True) for field, val in self.metadata_before_fetch.iteritems(): getattr(self, field).current_val = val self.metadata_before_fetch = None def configure_metadata(self): from calibre.gui2.preferences import show_config_widget gui = self.parent() show_config_widget('Sharing', 'Metadata download', parent=self, gui=gui, never_shutdown=True) def download_cover(self, *args): from calibre.gui2.metadata.single_download import CoverFetch d = CoverFetch(self.cover.pixmap(), self) ret = d.start(self.title.current_val, self.authors.current_val, self.identifiers.current_val) if ret == d.Accepted: if d.cover_pixmap is not None: self.cover.current_val = pixmap_to_data(d.cover_pixmap) # }}} def to_book_metadata(self): mi = Metadata(_('Unknown')) if self.db is None: return mi mi.set_all_user_metadata(self.db.field_metadata.custom_field_metadata()) for widget in self.basic_metadata_widgets: widget.apply_to_metadata(mi) for widget in getattr(self, 'custom_metadata_widgets', []): widget.apply_to_metadata(mi) return mi def apply_changes(self): self.changed.add(self.book_id) if self.db is None: # break_cycles has already been called, don't know why this should # happen but a user reported it return True self.comments_edit_state_at_apply = {w:w.tab for w in self.comments_edit_state_at_apply} for widget in self.basic_metadata_widgets: try: if hasattr(widget, 'validate_for_commit'): title, msg, det_msg = widget.validate_for_commit() if title is not None: error_dialog(self, title, msg, det_msg=det_msg, show=True) return False widget.commit(self.db, self.book_id) self.books_to_refresh |= getattr(widget, 'books_to_refresh', set()) except (IOError, OSError) as err: if getattr(err, 'errno', None) == errno.EACCES: # Permission denied import traceback fname = getattr(err, 'filename', None) p = 'Locked file: %s\n\n'%fname if fname else '' error_dialog(self, _('Permission denied'), _('Could not change the on disk location of this' ' book. Is it open in another program?'), det_msg=p+traceback.format_exc(), show=True) return False raise for widget in getattr(self, 'custom_metadata_widgets', []): self.books_to_refresh |= widget.commit(self.book_id) self.db.commit() rows = self.db.refresh_ids(list(self.books_to_refresh)) if rows: self.rows_to_refresh |= set(rows) return True def accept(self): self.save_state() if not self.apply_changes(): return if self.editing_multiple and self.current_row != len(self.row_list) - 1: num = len(self.row_list) - 1 - self.current_row from calibre.gui2 import question_dialog if not question_dialog( self, _('Are you sure?'), _('There are still %d more books to edit in this set.' ' Are you sure you want to stop? Use the Next button' ' instead of the OK button to move through books in the set.') % num, yes_text=_('&Stop editing'), no_text=_('&Continue editing'), yes_icon='dot_red.png', no_icon='dot_green.png', default_yes=False, skip_dialog_name='edit-metadata-single-confirm-ok-on-multiple'): return self.do_one(delta=1, apply_changes=False) ResizableDialog.accept(self) def reject(self): self.save_state() ResizableDialog.reject(self) def save_state(self): try: gprefs['metasingle_window_geometry3'] = bytearray(self.saveGeometry()) except: # Weird failure, see https://bugs.launchpad.net/bugs/995271 import traceback traceback.print_exc() # Dialog use methods {{{ def start(self, row_list, current_row, view_slot=None, set_current_callback=None): self.row_list = row_list self.current_row = current_row if view_slot is not None: self.view_format.connect(view_slot) self.set_current_callback = set_current_callback self.do_one(apply_changes=False) ret = self.exec_() self.break_cycles() return ret def next_clicked(self): if not self.apply_changes(): return self.do_one(delta=1, apply_changes=False) def prev_clicked(self): if not self.apply_changes(): return self.do_one(delta=-1, apply_changes=False) def do_one(self, delta=0, apply_changes=True): if apply_changes: self.apply_changes() self.current_row += delta self.update_window_title() prev = next_ = None if self.current_row > 0: prev = self.db.title(self.row_list[self.current_row-1]) if self.current_row < len(self.row_list) - 1: next_ = self.db.title(self.row_list[self.current_row+1]) if next_ is not None: tip = (_('Save changes and edit the metadata of %s')+ ' [Alt+Right]')%next_ self.next_button.setToolTip(tip) self.next_button.setEnabled(next_ is not None) if prev is not None: tip = (_('Save changes and edit the metadata of %s')+ ' [Alt+Left]')%prev self.prev_button.setToolTip(tip) self.prev_button.setEnabled(prev is not None) self.button_box.button(self.button_box.Ok).setDefault(True) self.button_box.button(self.button_box.Ok).setFocus(Qt.OtherFocusReason) self(self.db.id(self.row_list[self.current_row])) for w, state in self.comments_edit_state_at_apply.iteritems(): if state == 'code': w.tab = 'code' def break_cycles(self): # Break any reference cycles that could prevent python # from garbage collecting this dialog self.set_current_callback = self.db = None self.metadata_before_fetch = None def disconnect(signal): try: signal.disconnect() except: pass # Fails if view format was never connected disconnect(self.view_format) for b in ('next_button', 'prev_button'): x = getattr(self, b, None) if x is not None: disconnect(x.clicked) for widget in self.basic_metadata_widgets: bc = getattr(widget, 'break_cycles', None) if bc is not None and callable(bc): bc() for widget in getattr(self, 'custom_metadata_widgets', []): widget.break_cycles()
class MetadataSingleDialogBase(ResizableDialog): view_format = pyqtSignal(object, object) cc_two_column = tweaks['metadata_single_use_2_cols_for_custom_fields'] one_line_comments_toolbar = False use_toolbutton_for_config_metadata = True def __init__(self, db, parent=None, editing_multiple=False): self.db = db self.changed = set() self.books_to_refresh = set() self.rows_to_refresh = set() self.metadata_before_fetch = None self.editing_multiple = editing_multiple ResizableDialog.__init__(self, parent) def setupUi(self, *args): # {{{ self.resize(990, 670) self.download_shortcut = QShortcut(self) self.download_shortcut.setKey(QKeySequence('Ctrl+D', QKeySequence.PortableText)) p = self.parent() if hasattr(p, 'keyboard'): kname = u'Interface Action: Edit Metadata (Edit Metadata) : menu action : download' sc = p.keyboard.keys_map.get(kname, None) if sc: self.download_shortcut.setKey(sc[0]) self.swap_title_author_shortcut = s = QShortcut(self) s.setKey(QKeySequence('Alt+Down', QKeySequence.PortableText)) self.button_box = bb = QDialogButtonBox(self) self.button_box.accepted.connect(self.accept) self.button_box.rejected.connect(self.reject) self.next_button = QPushButton(QIcon(I('forward.png')), _('Next'), self) self.next_button.setShortcut(QKeySequence('Alt+Right')) self.next_button.clicked.connect(self.next_clicked) self.prev_button = QPushButton(QIcon(I('back.png')), _('Previous'), self) self.prev_button.setShortcut(QKeySequence('Alt+Left')) self.button_box.addButton(self.prev_button, bb.ActionRole) self.button_box.addButton(self.next_button, bb.ActionRole) self.prev_button.clicked.connect(self.prev_clicked) bb.setStandardButtons(bb.Ok|bb.Cancel) bb.button(bb.Ok).setDefault(True) self.scroll_area = QScrollArea(self) self.scroll_area.setFrameShape(QScrollArea.NoFrame) self.scroll_area.setWidgetResizable(True) self.central_widget = QTabWidget(self) self.scroll_area.setWidget(self.central_widget) self.l = QVBoxLayout(self) self.setLayout(self.l) self.l.addWidget(self.scroll_area) ll = self.button_box_layout = QHBoxLayout() self.l.addLayout(ll) ll.addSpacing(10) ll.addWidget(self.button_box) self.setWindowIcon(QIcon(I('edit_input.png'))) self.setWindowTitle(BASE_TITLE) self.create_basic_metadata_widgets() if len(self.db.custom_column_label_map): self.create_custom_metadata_widgets() self.do_layout() geom = gprefs.get('metasingle_window_geometry3', None) if geom is not None: self.restoreGeometry(bytes(geom)) # }}} def create_basic_metadata_widgets(self): # {{{ self.basic_metadata_widgets = [] self.languages = LanguagesEdit(self) self.basic_metadata_widgets.append(self.languages) self.title = TitleEdit(self) self.title.textChanged.connect(self.update_window_title) self.deduce_title_sort_button = QToolButton(self) self.deduce_title_sort_button.setToolTip( _('Automatically create the title sort entry based on the current ' 'title entry.\nUsing this button to create title sort will ' 'change title sort from red to green.')) self.deduce_title_sort_button.setWhatsThis( self.deduce_title_sort_button.toolTip()) self.title_sort = TitleSortEdit(self, self.title, self.deduce_title_sort_button, self.languages) self.basic_metadata_widgets.extend([self.title, self.title_sort]) self.deduce_author_sort_button = b = RightClickButton(self) b.setToolTip('<p>' + _('Automatically create the author sort entry based on the current ' 'author entry. Using this button to create author sort will ' 'change author sort from red to green. There is a menu of ' 'functions available under this button. Click and hold ' 'on the button to see it.') + '</p>') if isosx: # Workaround for https://bugreports.qt-project.org/browse/QTBUG-41017 class Menu(QMenu): def mouseReleaseEvent(self, ev): ac = self.actionAt(ev.pos()) if ac is not None: ac.trigger() return QMenu.mouseReleaseEvent(self, ev) b.m = m = Menu() else: b.m = m = QMenu() ac = m.addAction(QIcon(I('forward.png')), _('Set author sort from author')) ac2 = m.addAction(QIcon(I('back.png')), _('Set author from author sort')) ac3 = m.addAction(QIcon(I('user_profile.png')), _('Manage authors')) ac4 = m.addAction(QIcon(I('next.png')), _('Copy author to author sort')) ac5 = m.addAction(QIcon(I('previous.png')), _('Copy author sort to author')) b.setMenu(m) self.authors = AuthorsEdit(self, ac3) self.author_sort = AuthorSortEdit(self, self.authors, b, self.db, ac, ac2, ac4, ac5) self.basic_metadata_widgets.extend([self.authors, self.author_sort]) self.swap_title_author_button = QToolButton(self) self.swap_title_author_button.setIcon(QIcon(I('swap.png'))) self.swap_title_author_button.setToolTip(_( 'Swap the author and title') + ' [%s]' % self.swap_title_author_shortcut.key().toString(QKeySequence.NativeText)) self.swap_title_author_button.clicked.connect(self.swap_title_author) self.swap_title_author_shortcut.activated.connect(self.swap_title_author_button.click) self.manage_authors_button = QToolButton(self) self.manage_authors_button.setIcon(QIcon(I('user_profile.png'))) self.manage_authors_button.setToolTip('<p>' + _( 'Manage authors. Use to rename authors and correct ' 'individual author\'s sort values') + '</p>') self.manage_authors_button.clicked.connect(self.authors.manage_authors) self.series = SeriesEdit(self) self.clear_series_button = QToolButton(self) self.clear_series_button.setToolTip( _('Clear series')) self.clear_series_button.clicked.connect(self.series.clear) self.series_index = SeriesIndexEdit(self, self.series) self.basic_metadata_widgets.extend([self.series, self.series_index]) self.formats_manager = FormatsManager(self, self.copy_fmt) # We want formats changes to be committed before title/author, as # otherwise we could have data loss if the title/author changed and the # user was trying to add an extra file from the old books directory. self.basic_metadata_widgets.insert(0, self.formats_manager) self.formats_manager.metadata_from_format_button.clicked.connect( self.metadata_from_format) self.formats_manager.cover_from_format_button.clicked.connect( self.cover_from_format) self.cover = Cover(self) self.cover.download_cover.connect(self.download_cover) self.basic_metadata_widgets.append(self.cover) self.comments = CommentsEdit(self, self.one_line_comments_toolbar) self.basic_metadata_widgets.append(self.comments) self.rating = RatingEdit(self) self.clear_ratings_button = QToolButton(self) self.clear_ratings_button.setToolTip(_('Clear rating')) self.clear_ratings_button.setIcon(QIcon(I('trash.png'))) self.clear_ratings_button.clicked.connect(self.rating.zero) self.basic_metadata_widgets.append(self.rating) self.tags = TagsEdit(self) self.tags_editor_button = QToolButton(self) self.tags_editor_button.setToolTip(_('Open Tag Editor')) self.tags_editor_button.setIcon(QIcon(I('chapters.png'))) self.tags_editor_button.clicked.connect(self.tags_editor) self.clear_tags_button = QToolButton(self) self.clear_tags_button.setToolTip(_('Clear all tags')) self.clear_tags_button.setIcon(QIcon(I('trash.png'))) self.clear_tags_button.clicked.connect(self.tags.clear) self.basic_metadata_widgets.append(self.tags) self.identifiers = IdentifiersEdit(self) self.basic_metadata_widgets.append(self.identifiers) self.clear_identifiers_button = QToolButton(self) self.clear_identifiers_button.setIcon(QIcon(I('trash.png'))) self.clear_identifiers_button.setToolTip(_('Clear Ids')) self.clear_identifiers_button.clicked.connect(self.identifiers.clear) self.paste_isbn_button = QToolButton(self) self.paste_isbn_button.setToolTip('<p>' + _('Paste the contents of the clipboard into the ' 'identifiers box prefixed with isbn:') + '</p>') self.paste_isbn_button.setIcon(QIcon(I('edit-paste.png'))) self.paste_isbn_button.clicked.connect(self.identifiers.paste_isbn) self.publisher = PublisherEdit(self) self.basic_metadata_widgets.append(self.publisher) self.timestamp = DateEdit(self) self.pubdate = PubdateEdit(self) self.basic_metadata_widgets.extend([self.timestamp, self.pubdate]) self.fetch_metadata_button = b = RightClickButton(self) # The following rigmarole is needed so that Qt gives the button the # same height as the other buttons in the dialog. There is no way to # center the text in a QToolButton with an icon, so we cant just set an # icon b.setIcon(QIcon(I('download-metadata.png'))) b.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) b.setMinimumHeight(b.sizeHint().height()) b.setIcon(QIcon()) b.setText(_('&Download metadata')), b.setPopupMode(b.DelayedPopup) b.setToolTip(_('Download metadata for this book [%s]') % self.download_shortcut.key().toString(QKeySequence.NativeText)) b.setSizePolicy(QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)) self.fetch_metadata_button.clicked.connect(self.fetch_metadata) self.fetch_metadata_menu = m = QMenu(self.fetch_metadata_button) m.addAction(QIcon(I('edit-undo.png')), _('Undo last metadata download'), self.undo_fetch_metadata) self.fetch_metadata_button.setMenu(m) self.download_shortcut.activated.connect(self.fetch_metadata_button.click) font = self.fmb_font = QFont() font.setBold(True) self.fetch_metadata_button.setFont(font) if self.use_toolbutton_for_config_metadata: self.config_metadata_button = QToolButton(self) self.config_metadata_button.setIcon(QIcon(I('config.png'))) else: self.config_metadata_button = QPushButton(self) self.config_metadata_button.setText(_('Configure download metadata')) self.config_metadata_button.setIcon(QIcon(I('config.png'))) self.config_metadata_button.clicked.connect(self.configure_metadata) self.config_metadata_button.setToolTip( _('Change how calibre downloads metadata')) # }}} def create_custom_metadata_widgets(self): # {{{ self.custom_metadata_widgets_parent = w = QWidget(self) layout = QGridLayout() w.setLayout(layout) self.custom_metadata_widgets, self.__cc_spacers = \ populate_metadata_page(layout, self.db, None, parent=w, bulk=False, two_column=self.cc_two_column) self.__custom_col_layouts = [layout] # }}} def set_custom_metadata_tab_order(self, before=None, after=None): # {{{ sto = QWidget.setTabOrder if getattr(self, 'custom_metadata_widgets', []): ans = self.custom_metadata_widgets for i in range(len(ans)-1): if before is not None and i == 0: pass if len(ans[i+1].widgets) == 2: sto(ans[i].widgets[-1], ans[i+1].widgets[1]) else: sto(ans[i].widgets[-1], ans[i+1].widgets[0]) for c in range(2, len(ans[i].widgets), 2): sto(ans[i].widgets[c-1], ans[i].widgets[c+1]) if after is not None: pass # }}} def do_view_format(self, path, fmt): if path: self.view_format.emit(None, path) else: self.view_format.emit(self.book_id, fmt) def copy_fmt(self, fmt, f): self.db.copy_format_to(self.book_id, fmt, f, index_is_id=True) def do_layout(self): raise NotImplementedError() def __call__(self, id_): self.book_id = id_ self.books_to_refresh = set([]) self.metadata_before_fetch = None for widget in self.basic_metadata_widgets: widget.initialize(self.db, id_) for widget in getattr(self, 'custom_metadata_widgets', []): widget.initialize(id_) if callable(self.set_current_callback): self.set_current_callback(id_) # Commented out as it doesn't play nice with Next, Prev buttons # self.fetch_metadata_button.setFocus(Qt.OtherFocusReason) # Miscellaneous interaction methods {{{ def update_window_title(self, *args): title = self.title.current_val if len(title) > 50: title = title[:50] + u'\u2026' self.setWindowTitle(BASE_TITLE + ' - ' + title + ' - ' + _(' [%(num)d of %(tot)d]')%dict(num=self.current_row+1, tot=len(self.row_list))) def swap_title_author(self, *args): title = self.title.current_val self.title.current_val = authors_to_string(self.authors.current_val) self.authors.current_val = string_to_authors(title) self.title_sort.auto_generate() self.author_sort.auto_generate() def tags_editor(self, *args): self.tags.edit(self.db, self.book_id) def metadata_from_format(self, *args): mi, ext = self.formats_manager.get_selected_format_metadata(self.db, self.book_id) if mi is not None: self.update_from_mi(mi) def get_pdf_cover(self): pdfpath = self.formats_manager.get_format_path(self.db, self.book_id, 'pdf') from calibre.gui2.metadata.pdf_covers import PDFCovers d = PDFCovers(pdfpath, parent=self) if d.exec_() == d.Accepted: cpath = d.cover_path if cpath: with open(cpath, 'rb') as f: self.update_cover(f.read(), 'PDF') d.cleanup() def cover_from_format(self, *args): ext = self.formats_manager.get_selected_format() if ext is None: return if ext == 'pdf': return self.get_pdf_cover() try: mi, ext = self.formats_manager.get_selected_format_metadata(self.db, self.book_id) except (IOError, OSError) as err: if getattr(err, 'errno', None) == errno.EACCES: # Permission denied import traceback fname = err.filename if err.filename else 'file' error_dialog(self, _('Permission denied'), _('Could not open %s. Is it being used by another' ' program?')%fname, det_msg=traceback.format_exc(), show=True) return raise if mi is None: return cdata = None if mi.cover and os.access(mi.cover, os.R_OK): cdata = open(mi.cover).read() elif mi.cover_data[1] is not None: cdata = mi.cover_data[1] if cdata is None: error_dialog(self, _('Could not read cover'), _('Could not read cover from %s format')%ext).exec_() return self.update_cover(cdata, ext) def update_cover(self, cdata, fmt): orig = self.cover.current_val self.cover.current_val = cdata if self.cover.current_val is None: self.cover.current_val = orig return error_dialog(self, _('Could not read cover'), _('The cover in the %s format is invalid')%fmt, show=True) return def update_from_mi(self, mi, update_sorts=True, merge_tags=True, merge_comments=False): fw = self.focusWidget() if not mi.is_null('title'): self.title.set_value(mi.title) if update_sorts: self.title_sort.auto_generate() if not mi.is_null('authors'): self.authors.set_value(mi.authors) if not mi.is_null('author_sort'): self.author_sort.set_value(mi.author_sort) elif update_sorts: self.author_sort.auto_generate() if not mi.is_null('rating'): try: self.rating.set_value(mi.rating) except: pass if not mi.is_null('publisher'): self.publisher.set_value(mi.publisher) if not mi.is_null('tags'): old_tags = self.tags.current_val tags = mi.tags if mi.tags else [] if old_tags and merge_tags: ltags, lotags = {t.lower() for t in tags}, {t.lower() for t in old_tags} tags = [t for t in tags if t.lower() in ltags-lotags] + old_tags self.tags.set_value(tags) if not mi.is_null('identifiers'): current = self.identifiers.current_val current.update(mi.identifiers) self.identifiers.set_value(current) if not mi.is_null('pubdate'): self.pubdate.set_value(mi.pubdate) if not mi.is_null('series') and mi.series.strip(): self.series.set_value(mi.series) if mi.series_index is not None: self.series_index.reset_original() self.series_index.set_value(float(mi.series_index)) if not mi.is_null('languages'): langs = [canonicalize_lang(x) for x in mi.languages] langs = [x for x in langs if x is not None] if langs: self.languages.set_value(langs) if mi.comments and mi.comments.strip(): val = mi.comments if val and merge_comments: cval = self.comments.current_val if cval: val = merge_two_comments(cval, val) self.comments.set_value(val) if fw is not None: fw.setFocus(Qt.OtherFocusReason) def fetch_metadata(self, *args): d = FullFetch(self.cover.pixmap(), self) ret = d.start(title=self.title.current_val, authors=self.authors.current_val, identifiers=self.identifiers.current_val) if ret == d.Accepted: self.metadata_before_fetch = {f:getattr(self, f).current_val for f in fetched_fields} from calibre.ebooks.metadata.sources.prefs import msprefs mi = d.book dummy = Metadata(_('Unknown')) for f in msprefs['ignore_fields']: if ':' not in f: setattr(mi, f, getattr(dummy, f)) if mi is not None: pd = mi.pubdate if pd is not None: # Put the downloaded published date into the local timezone # as we discard time info and the date is timezone # invariant. This prevents the as_local_timezone() call in # update_from_mi from changing the pubdate mi.pubdate = datetime(pd.year, pd.month, pd.day, tzinfo=local_tz) self.update_from_mi(mi, merge_comments=msprefs['append_comments']) if d.cover_pixmap is not None: self.metadata_before_fetch['cover'] = self.cover.current_val self.cover.current_val = pixmap_to_data(d.cover_pixmap) def undo_fetch_metadata(self): if self.metadata_before_fetch is None: return error_dialog(self, _('No downloaded metadata'), _( 'There is no downloaded metadata to undo'), show=True) for field, val in self.metadata_before_fetch.iteritems(): getattr(self, field).current_val = val self.metadata_before_fetch = None def configure_metadata(self): from calibre.gui2.preferences import show_config_widget gui = self.parent() show_config_widget('Sharing', 'Metadata download', parent=self, gui=gui, never_shutdown=True) def download_cover(self, *args): from calibre.gui2.metadata.single_download import CoverFetch d = CoverFetch(self.cover.pixmap(), self) ret = d.start(self.title.current_val, self.authors.current_val, self.identifiers.current_val) if ret == d.Accepted: if d.cover_pixmap is not None: self.cover.current_val = pixmap_to_data(d.cover_pixmap) # }}} def to_book_metadata(self): mi = Metadata(_('Unknown')) if self.db is None: return mi mi.set_all_user_metadata(self.db.field_metadata.custom_field_metadata()) for widget in self.basic_metadata_widgets: widget.apply_to_metadata(mi) for widget in getattr(self, 'custom_metadata_widgets', []): widget.apply_to_metadata(mi) return mi def apply_changes(self): self.changed.add(self.book_id) if self.db is None: # break_cycles has already been called, don't know why this should # happen but a user reported it return True for widget in self.basic_metadata_widgets: try: if hasattr(widget, 'validate_for_commit'): title, msg, det_msg = widget.validate_for_commit() if title is not None: error_dialog(self, title, msg, det_msg=det_msg, show=True) return False widget.commit(self.db, self.book_id) self.books_to_refresh |= getattr(widget, 'books_to_refresh', set()) except (IOError, OSError) as err: if getattr(err, 'errno', None) == errno.EACCES: # Permission denied import traceback fname = getattr(err, 'filename', None) p = 'Locked file: %s\n\n'%fname if fname else '' error_dialog(self, _('Permission denied'), _('Could not change the on disk location of this' ' book. Is it open in another program?'), det_msg=p+traceback.format_exc(), show=True) return False raise for widget in getattr(self, 'custom_metadata_widgets', []): self.books_to_refresh |= widget.commit(self.book_id) self.db.commit() rows = self.db.refresh_ids(list(self.books_to_refresh)) if rows: self.rows_to_refresh |= set(rows) return True def accept(self): self.save_state() if not self.apply_changes(): return if self.editing_multiple and self.current_row != len(self.row_list) - 1: num = len(self.row_list) - 1 - self.current_row from calibre.gui2 import question_dialog if not question_dialog( self, _('Are you sure?'), _('There are still %d more books to edit in this set.' ' Are you sure you want to stop? Use the Next button' ' instead of the OK button to move through books in the set.') % num, yes_text=_('&Stop editing'), no_text=_('&Continue editing'), yes_icon='dot_red.png', no_icon='dot_green.png', default_yes=False, skip_dialog_name='edit-metadata-single-confirm-ok-on-multiple'): return self.do_one(delta=1, apply_changes=False) ResizableDialog.accept(self) def reject(self): self.save_state() ResizableDialog.reject(self) def save_state(self): try: gprefs['metasingle_window_geometry3'] = bytearray(self.saveGeometry()) except: # Weird failure, see https://bugs.launchpad.net/bugs/995271 import traceback traceback.print_exc() # Dialog use methods {{{ def start(self, row_list, current_row, view_slot=None, set_current_callback=None): self.row_list = row_list self.current_row = current_row if view_slot is not None: self.view_format.connect(view_slot) self.set_current_callback = set_current_callback self.do_one(apply_changes=False) ret = self.exec_() self.break_cycles() return ret def next_clicked(self): if not self.apply_changes(): return self.do_one(delta=1, apply_changes=False) def prev_clicked(self): if not self.apply_changes(): return self.do_one(delta=-1, apply_changes=False) def do_one(self, delta=0, apply_changes=True): if apply_changes: self.apply_changes() self.current_row += delta self.update_window_title() prev = next_ = None if self.current_row > 0: prev = self.db.title(self.row_list[self.current_row-1]) if self.current_row < len(self.row_list) - 1: next_ = self.db.title(self.row_list[self.current_row+1]) if next_ is not None: tip = (_('Save changes and edit the metadata of %s')+ ' [Alt+Right]')%next_ self.next_button.setToolTip(tip) self.next_button.setEnabled(next_ is not None) if prev is not None: tip = (_('Save changes and edit the metadata of %s')+ ' [Alt+Left]')%prev self.prev_button.setToolTip(tip) self.prev_button.setEnabled(prev is not None) self.button_box.button(self.button_box.Ok).setDefault(True) self.button_box.button(self.button_box.Ok).setFocus(Qt.OtherFocusReason) self(self.db.id(self.row_list[self.current_row])) def break_cycles(self): # Break any reference cycles that could prevent python # from garbage collecting this dialog self.set_current_callback = self.db = None self.metadata_before_fetch = None def disconnect(signal): try: signal.disconnect() except: pass # Fails if view format was never connected disconnect(self.view_format) for b in ('next_button', 'prev_button'): x = getattr(self, b, None) if x is not None: disconnect(x.clicked) for widget in self.basic_metadata_widgets: bc = getattr(widget, 'break_cycles', None) if bc is not None and callable(bc): bc() for widget in getattr(self, 'custom_metadata_widgets', []): widget.break_cycles()
class BookInfo(QDialog): closed = pyqtSignal(object) def __init__(self, parent, view, row, link_delegate): QDialog.__init__(self, parent) self.normal_brush = QBrush(Qt.white) self.marked_brush = QBrush(Qt.lightGray) self.marked = None self.gui = parent self.splitter = QSplitter(self) self._l = l = QVBoxLayout(self) self.setLayout(l) l.addWidget(self.splitter) self.cover = CoverView(self) self.cover.resizeEvent = self.cover_view_resized self.cover.cover_changed.connect(self.cover_changed) self.cover_pixmap = None self.cover.sizeHint = self.details_size_hint self.splitter.addWidget(self.cover) self.details = Details(parent.book_details.book_info, self) self.details.page().setLinkDelegationPolicy(self.details.page().DelegateAllLinks) self.details.linkClicked.connect(self.link_clicked) s = self.details.page().settings() s.setAttribute(s.JavascriptEnabled, False) self.css = css() self.link_delegate = link_delegate self.details.setAttribute(Qt.WA_OpaquePaintEvent, False) palette = self.details.palette() self.details.setAcceptDrops(False) palette.setBrush(QPalette.Base, Qt.transparent) self.details.page().setPalette(palette) self.c = QWidget(self) self.c.l = l2 = QGridLayout(self.c) self.c.setLayout(l2) l2.addWidget(self.details, 0, 0, 1, -1) self.splitter.addWidget(self.c) self.fit_cover = QCheckBox(_('Fit &cover within view'), self) self.fit_cover.setChecked(gprefs.get('book_info_dialog_fit_cover', True)) l2.addWidget(self.fit_cover, l2.rowCount(), 0, 1, -1) self.previous_button = QPushButton(QIcon(I('previous.png')), _('&Previous'), self) self.previous_button.clicked.connect(self.previous) l2.addWidget(self.previous_button, l2.rowCount(), 0) self.next_button = QPushButton(QIcon(I('next.png')), _('&Next'), self) self.next_button.clicked.connect(self.next) l2.addWidget(self.next_button, l2.rowCount() - 1, 1) self.view = view self.current_row = None self.refresh(row) self.view.model().new_bookdisplay_data.connect(self.slave) self.fit_cover.stateChanged.connect(self.toggle_cover_fit) self.ns = QShortcut(QKeySequence('Alt+Right'), self) self.ns.activated.connect(self.next) self.ps = QShortcut(QKeySequence('Alt+Left'), self) self.ps.activated.connect(self.previous) self.next_button.setToolTip(_('Next [%s]')% unicode(self.ns.key().toString(QKeySequence.NativeText))) self.previous_button.setToolTip(_('Previous [%s]')% unicode(self.ps.key().toString(QKeySequence.NativeText))) geom = QCoreApplication.instance().desktop().availableGeometry(self) screen_height = geom.height() - 100 screen_width = geom.width() - 100 self.resize(max(int(screen_width/2), 700), screen_height) saved_layout = gprefs.get('book_info_dialog_layout', None) if saved_layout is not None: try: self.restoreGeometry(saved_layout[0]) self.splitter.restoreState(saved_layout[1]) except Exception: pass def link_clicked(self, qurl): link = unicode(qurl.toString(NO_URL_FORMATTING)) self.link_delegate(link) def done(self, r): saved_layout = (bytearray(self.saveGeometry()), bytearray(self.splitter.saveState())) gprefs.set('book_info_dialog_layout', saved_layout) ret = QDialog.done(self, r) self.view.model().new_bookdisplay_data.disconnect(self.slave) self.view = self.link_delegate = self.gui = None self.closed.emit(self) return ret def cover_changed(self, data): if self.current_row is not None: id_ = self.view.model().id(self.current_row) self.view.model().db.set_cover(id_, data) self.gui.refresh_cover_browser() ci = self.view.currentIndex() if ci.isValid(): self.view.model().current_changed(ci, ci) def details_size_hint(self): return QSize(350, 550) def toggle_cover_fit(self, state): gprefs.set('book_info_dialog_fit_cover', self.fit_cover.isChecked()) self.resize_cover() def cover_view_resized(self, event): QTimer.singleShot(1, self.resize_cover) def slave(self, mi): self.refresh(mi.row_number, mi) def move(self, delta=1): idx = self.view.currentIndex() if idx.isValid(): m = self.view.model() ni = m.index(idx.row() + delta, idx.column()) if ni.isValid(): if self.view.isVisible(): self.view.scrollTo(ni) self.view.setCurrentIndex(ni) def next(self): self.move() def previous(self): self.move(-1) def resize_cover(self): if self.cover_pixmap is None: return pixmap = self.cover_pixmap if self.fit_cover.isChecked(): scaled, new_width, new_height = fit_image(pixmap.width(), pixmap.height(), self.cover.size().width()-10, self.cover.size().height()-10) if scaled: try: dpr = self.devicePixelRatioF() except AttributeError: dpr = self.devicePixelRatio() pixmap = pixmap.scaled(int(dpr * new_width), int(dpr * new_height), Qt.KeepAspectRatio, Qt.SmoothTransformation) pixmap.setDevicePixelRatio(dpr) self.cover.set_pixmap(pixmap) self.update_cover_tooltip() def update_cover_tooltip(self): tt = '' if self.marked: tt = _('This book is marked') if self.marked in {True, 'true'} else _( 'This book is marked as: %s') % self.marked tt += '\n\n' if self.cover_pixmap is not None: sz = self.cover_pixmap.size() tt += _('Cover size: %(width)d x %(height)d pixels')%dict(width=sz.width(), height=sz.height()) self.cover.setToolTip(tt) def refresh(self, row, mi=None): if isinstance(row, QModelIndex): row = row.row() if row == self.current_row and mi is None: return mi = self.view.model().get_book_display_info(row) if mi is None else mi if mi is None: # Indicates books was deleted from library, or row numbers have # changed return self.previous_button.setEnabled(False if row == 0 else True) self.next_button.setEnabled(False if row == self.view.model().rowCount(QModelIndex())-1 else True) self.current_row = row self.setWindowTitle(mi.title) self.cover_pixmap = QPixmap.fromImage(mi.cover_data[1]) try: dpr = self.devicePixelRatioF() except AttributeError: dpr = self.devicePixelRatio() self.cover_pixmap.setDevicePixelRatio(dpr) self.resize_cover() html = render_html(mi, self.css, True, self, all_fields=True) self.details.setHtml(html) self.marked = mi.marked self.cover.setBackgroundBrush(self.marked_brush if mi.marked else self.normal_brush) self.update_cover_tooltip()
class BookInfo(QDialog): closed = pyqtSignal(object) def __init__(self, parent, view, row, link_delegate): QDialog.__init__(self, parent) self.normal_brush = QBrush(Qt.white) self.marked_brush = QBrush(Qt.lightGray) self.marked = None self.gui = parent self.splitter = QSplitter(self) self._l = l = QVBoxLayout(self) self.setLayout(l) l.addWidget(self.splitter) self.cover = CoverView(self) self.cover.resizeEvent = self.cover_view_resized self.cover.cover_changed.connect(self.cover_changed) self.cover_pixmap = None self.cover.sizeHint = self.details_size_hint self.splitter.addWidget(self.cover) self.details = QWebView(self) self.details.sizeHint = self.details_size_hint self.details.page().setLinkDelegationPolicy( self.details.page().DelegateAllLinks) self.details.linkClicked.connect(self.link_clicked) s = self.details.page().settings() s.setAttribute(s.JavascriptEnabled, False) self.css = css() self.link_delegate = link_delegate self.details.setAttribute(Qt.WA_OpaquePaintEvent, False) palette = self.details.palette() self.details.setAcceptDrops(False) palette.setBrush(QPalette.Base, Qt.transparent) self.details.page().setPalette(palette) self.c = QWidget(self) self.c.l = l2 = QGridLayout(self.c) self.c.setLayout(l2) l2.addWidget(self.details, 0, 0, 1, -1) self.splitter.addWidget(self.c) self.fit_cover = QCheckBox(_('Fit &cover within view'), self) self.fit_cover.setChecked( gprefs.get('book_info_dialog_fit_cover', True)) l2.addWidget(self.fit_cover, l2.rowCount(), 0, 1, -1) self.previous_button = QPushButton(QIcon(I('previous.png')), _('&Previous'), self) self.previous_button.clicked.connect(self.previous) l2.addWidget(self.previous_button, l2.rowCount(), 0) self.next_button = QPushButton(QIcon(I('next.png')), _('&Next'), self) self.next_button.clicked.connect(self.next) l2.addWidget(self.next_button, l2.rowCount() - 1, 1) self.view = view self.current_row = None self.refresh(row) self.view.model().new_bookdisplay_data.connect(self.slave) self.fit_cover.stateChanged.connect(self.toggle_cover_fit) self.ns = QShortcut(QKeySequence('Alt+Right'), self) self.ns.activated.connect(self.next) self.ps = QShortcut(QKeySequence('Alt+Left'), self) self.ps.activated.connect(self.previous) self.next_button.setToolTip( _('Next [%s]') % unicode(self.ns.key().toString(QKeySequence.NativeText))) self.previous_button.setToolTip( _('Previous [%s]') % unicode(self.ps.key().toString(QKeySequence.NativeText))) geom = QCoreApplication.instance().desktop().availableGeometry(self) screen_height = geom.height() - 100 screen_width = geom.width() - 100 self.resize(max(int(screen_width / 2), 700), screen_height) saved_layout = gprefs.get('book_info_dialog_layout', None) if saved_layout is not None: try: self.restoreGeometry(saved_layout[0]) self.splitter.restoreState(saved_layout[1]) except Exception: pass def link_clicked(self, qurl): link = unicode(qurl.toString(QUrl.None)) self.link_delegate(link) def done(self, r): saved_layout = (bytearray(self.saveGeometry()), bytearray(self.splitter.saveState())) gprefs.set('book_info_dialog_layout', saved_layout) ret = QDialog.done(self, r) self.view.model().new_bookdisplay_data.disconnect(self.slave) self.view = self.link_delegate = self.gui = None self.closed.emit(self) return ret def cover_changed(self, data): if self.current_row is not None: id_ = self.view.model().id(self.current_row) self.view.model().db.set_cover(id_, data) if self.gui.cover_flow: self.gui.cover_flow.dataChanged() ci = self.view.currentIndex() if ci.isValid(): self.view.model().current_changed(ci, ci) def details_size_hint(self): return QSize(350, 550) def toggle_cover_fit(self, state): gprefs.set('book_info_dialog_fit_cover', self.fit_cover.isChecked()) self.resize_cover() def cover_view_resized(self, event): QTimer.singleShot(1, self.resize_cover) def slave(self, mi): self.refresh(mi.row_number, mi) def move(self, delta=1): idx = self.view.currentIndex() if idx.isValid(): m = self.view.model() ni = m.index(idx.row() + delta, idx.column()) if ni.isValid(): if self.view.isVisible(): self.view.scrollTo(ni) self.view.setCurrentIndex(ni) def next(self): self.move() def previous(self): self.move(-1) def resize_cover(self): if self.cover_pixmap is None: return pixmap = self.cover_pixmap if self.fit_cover.isChecked(): scaled, new_width, new_height = fit_image( pixmap.width(), pixmap.height(), self.cover.size().width() - 10, self.cover.size().height() - 10) if scaled: pixmap = pixmap.scaled(new_width, new_height, Qt.KeepAspectRatio, Qt.SmoothTransformation) self.cover.set_pixmap(pixmap) self.update_cover_tooltip() def update_cover_tooltip(self): tt = '' if self.marked: tt = _('This book is marked') if self.marked in { True, 'true' } else _('This book is marked as: %s') % self.marked tt += '\n\n' if self.cover_pixmap is not None: sz = self.cover_pixmap.size() tt += _('Cover size: %(width)d x %(height)d') % dict( width=sz.width(), height=sz.height()) self.cover.setToolTip(tt) def refresh(self, row, mi=None): if isinstance(row, QModelIndex): row = row.row() if row == self.current_row and mi is None: return mi = self.view.model().get_book_display_info(row) if mi is None else mi if mi is None: # Indicates books was deleted from library, or row numbers have # changed return self.previous_button.setEnabled(False if row == 0 else True) self.next_button.setEnabled(False if row == self.view.model().rowCount(QModelIndex()) - 1 else True) self.current_row = row self.setWindowTitle(mi.title) self.cover_pixmap = QPixmap.fromImage(mi.cover_data[1]) self.resize_cover() html = render_html(mi, self.css, True, self, all_fields=True) self.details.setHtml(html) self.marked = mi.marked self.cover.setBackgroundBrush( self.marked_brush if mi.marked else self.normal_brush) self.update_cover_tooltip()