%PDF- %PDF-
Direktori : /lib/calibre/calibre/gui2/ |
Current File : //lib/calibre/calibre/gui2/cover_flow.py |
#!/usr/bin/env python3 __license__ = 'GPL v3' __copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net' __docformat__ = 'restructuredtext en' ''' Module to implement the Cover Flow feature ''' import os import sys import time from qt.core import ( QAction, QApplication, QDialog, QFont, QImage, QItemSelectionModel, QKeySequence, QLabel, QSize, QSizePolicy, QStackedLayout, Qt, QTimer, pyqtSignal ) from calibre.constants import islinux from calibre.ebooks.metadata import rating_to_stars, authors_to_string from calibre.gui2 import config, gprefs, rating_font from calibre_extensions import pictureflow class EmptyImageList(pictureflow.FlowImages): def __init__(self): pictureflow.FlowImages.__init__(self) class FileSystemImages(pictureflow.FlowImages): def __init__(self, dirpath): pictureflow.FlowImages.__init__(self) self.images = [] self.captions = [] self.subtitles = [] for f in os.listdir(dirpath): f = os.path.join(dirpath, f) img = QImage(f) if not img.isNull(): self.images.append(img) self.captions.append(os.path.basename(f)) self.subtitles.append('%d bytes'%os.stat(f).st_size) def count(self): return len(self.images) def image(self, index): return self.images[index] def caption(self, index): return self.captions[index] def subtitle(self, index): return self.subtitles[index] def currentChanged(self, index): print('current changed:', index) class DummyImageList(pictureflow.FlowImages): def __init__(self): pictureflow.FlowImages.__init__(self) self.num = 40000 i1, i2 = QImage(300, 400, QImage.Format.Format_RGB32), QImage(300, 400, QImage.Format.Format_RGB32) i1.fill(Qt.GlobalColor.green), i2.fill(Qt.GlobalColor.blue) self.images = [i1, i2] def count(self): return self.num def image(self, index): return self.images[index%2] def caption(self, index): return 'Number: %d'%index def subtitle(self, index): return '' class DatabaseImages(pictureflow.FlowImages): def __init__(self, model, is_cover_browser_visible): pictureflow.FlowImages.__init__(self) self.model = model self.is_cover_browser_visible = is_cover_browser_visible self.model.modelReset.connect(self.reset, type=Qt.ConnectionType.QueuedConnection) self.ignore_image_requests = True self.template_inited = False self.subtitle_error_reported = False def init_template(self, db): self.template_cache = {} self.template_error_reported = False self.template = db.pref('cover_browser_title_template', '{title}') or '' self.template_is_title = self.template == '{title}' self.template_is_empty = not self.template.strip() def count(self): return self.model.count() def render_template(self, template, index, db): book_id = self.model.id(index) mi = db.get_proxy_metadata(book_id) return mi.formatter.safe_format(template, mi, _('TEMPLATE ERROR'), mi, template_cache=self.template_cache) def caption(self, index): if self.ignore_image_requests: return '' ans = '' try: db = self.model.db.new_api if not self.template_inited: self.init_template(db) if self.template_is_title: ans = self.model.title(index) elif self.template_is_empty: ans = '' else: try: ans = self.render_template(self.template, index, db) except Exception: if not self.template_error_reported: self.template_error_reported = True import traceback traceback.print_exc() ans = '' ans = (ans or '').replace('&', '&&') except Exception: return '' return ans def subtitle(self, index): try: db = self.model.db.new_api if not self.template_inited: self.init_template(db) field = db.pref('cover_browser_subtitle_field', 'rating') if field and field != 'none': book_id = self.model.id(index) fm = db.field_metadata[field] if fm['datatype'] == 'rating': val = db.field_for(field, book_id, default_value=0) if val: return rating_to_stars(val, allow_half_stars=db.field_metadata[field]['display'].get('allow_half_stars')) else: if field == 'authors': book_id = self.model.id(index) val = db.field_for(field, book_id, default_value=0) if val == (_('Unknown'),): val = '' elif val: val = authors_to_string(val).replace('&', '&&') else: val = '' return val return self.render_template('{%s}' % field, index, db).replace('&', '&&') except Exception: if not self.subtitle_error_reported: self.subtitle_error_reported = True import traceback traceback.print_exc() return '' def reset(self): self.beginResetModel(), self.endResetModel() def beginResetModel(self): if self.is_cover_browser_visible(): self.dataChanged.emit() def endResetModel(self): pass def image(self, index): if self.ignore_image_requests: return QImage() return self.model.cover(index) class CoverFlow(pictureflow.PictureFlow): dc_signal = pyqtSignal() context_menu_requested = pyqtSignal() def __init__(self, parent=None): pictureflow.PictureFlow.__init__(self, parent, config['cover_flow_queue_length']+1) self.setMinimumSize(QSize(300, 150)) self.setFocusPolicy(Qt.FocusPolicy.WheelFocus) self.setSizePolicy(QSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)) self.dc_signal.connect(self._data_changed, type=Qt.ConnectionType.QueuedConnection) self.context_menu = None self.setContextMenuPolicy(Qt.ContextMenuPolicy.DefaultContextMenu) self.setPreserveAspectRatio(gprefs['cb_preserve_aspect_ratio']) if not gprefs['cover_browser_reflections']: self.setShowReflections(False) if gprefs['cb_double_click_to_activate']: self.setActivateOnDoubleClick(True) def one_auto_scroll(self): if self.currentSlide() >= self.count() - 1: self.setCurrentSlide(0) else: self.showNext() def set_subtitle_font(self, for_ratings=True): if for_ratings: self.setSubtitleFont(QFont(rating_font())) else: self.setSubtitleFont(self.font()) def set_context_menu(self, cm): self.context_menu = cm def contextMenuEvent(self, event): if self.context_menu is not None: from calibre.gui2.main_window import clone_menu self.context_menu_requested.emit() m = clone_menu(self.context_menu) if islinux else self.context_menu m.popup(event.globalPos()) event.accept() def sizeHint(self): return self.minimumSize() def wheelEvent(self, ev): d = ev.angleDelta().y() if abs(d) > 0: ev.accept() (self.showNext if d < 0 else self.showPrevious)() def dataChanged(self): self.dc_signal.emit() def _data_changed(self): pictureflow.PictureFlow.dataChanged(self) def setCurrentSlide(self, num): pictureflow.PictureFlow.setCurrentSlide(self, num) class CBDialog(QDialog): closed = pyqtSignal() def __init__(self, gui, cover_flow): QDialog.__init__(self, gui) self._layout = QStackedLayout() self.setLayout(self._layout) self.setWindowTitle(_('Browse by covers')) self.layout().addWidget(cover_flow) geom = gprefs.get('cover_browser_dialog_geometry', None) if not geom or not QApplication.instance().safe_restore_geometry(self, geom): sz = self.screen().availableSize() h, w = sz.height()-60, int(sz.width()/1.5) self.resize(w, h) self.action_fs_toggle = a = QAction(self) self.addAction(a) a.setShortcuts([QKeySequence(QKeySequence.StandardKey.FullScreen)]) a.triggered.connect(self.toggle_fullscreen) self.action_esc_fs = a = QAction(self) a.triggered.connect(self.show_normal) self.addAction(a) a.setShortcuts([QKeySequence('Esc', QKeySequence.SequenceFormat.PortableText)]) self.pre_fs_geom = None cover_flow.setFocus(Qt.FocusReason.OtherFocusReason) self.view_action = a = QAction(self) iactions = gui.iactions self.addAction(a) a.setShortcuts(list(iactions['View'].menuless_qaction.shortcuts())+ [QKeySequence(Qt.Key.Key_Space)]) a.triggered.connect(iactions['View'].menuless_qaction.trigger) self.auto_scroll_action = a = QAction(self) a.setShortcuts(list(iactions['Autoscroll Books'].menuless_qaction.shortcuts())) self.addAction(a) a.triggered.connect(iactions['Autoscroll Books'].menuless_qaction.trigger) self.sd_action = a = QAction(self) self.addAction(a) a.setShortcuts(list(iactions['Send To Device']. menuless_qaction.shortcuts())) a.triggered.connect(iactions['Send To Device'].menuless_qaction.trigger) def closeEvent(self, *args): if not self.isFullScreen(): geom = bytearray(self.saveGeometry()) gprefs['cover_browser_dialog_geometry'] = geom self.closed.emit() def show_normal(self): self.showNormal() if self.pre_fs_geom is not None: QApplication.instance().safe_restore_geometry(self, self.pre_fs_geom) self.pre_fs_geom = None def show_fullscreen(self): self.pre_fs_geom = bytearray(self.saveGeometry()) self.showFullScreen() def toggle_fullscreen(self, *args): if self.isFullScreen(): self.show_normal() else: self.show_fullscreen() class CoverFlowMixin: disable_cover_browser_refresh = False def __init__(self, *args, **kwargs): pass def one_auto_scroll(self): cb_visible = self.cover_flow is not None and self.cb_splitter.button.isChecked() if cb_visible: self.cover_flow.one_auto_scroll() else: self.library_view.show_next_book() def toggle_auto_scroll(self): if not hasattr(self, 'auto_scroll_timer'): self.auto_scroll_timer = t = QTimer(self) t.timeout.connect(self.one_auto_scroll) if self.auto_scroll_timer.isActive(): self.auto_scroll_timer.stop() else: self.one_auto_scroll() self.auto_scroll_timer.start(int(1000 * gprefs['books_autoscroll_time'])) def update_auto_scroll_timeout(self): if hasattr(self, 'auto_scroll_timer') and self.auto_scroll_timer.isActive(): self.auto_scroll_timer.stop() self.toggle_auto_scroll() def init_cover_flow_mixin(self): self.cover_flow = None self.cf_last_updated_at = None self.cover_flow_syncing_enabled = False self.cover_flow_sync_flag = True self.cover_flow = CoverFlow(parent=self) self.cover_flow.currentChanged.connect(self.sync_listview_to_cf) self.cover_flow.context_menu_requested.connect(self.cf_context_menu_requested) self.library_view.selectionModel().currentRowChanged.connect(self.sync_cf_to_listview) self.db_images = DatabaseImages(self.library_view.model(), self.is_cover_browser_visible) self.cover_flow.setImages(self.db_images) self.cover_flow.itemActivated.connect(self.iactions['View'].view_specific_book) self.update_cover_flow_subtitle_font() if config['separate_cover_flow']: self.separate_cover_browser = True self.cb_splitter.button.clicked.connect(self.toggle_cover_browser) self.cb_splitter.button.set_state_to_show() self.cb_splitter.action_toggle.triggered.connect(self.toggle_cover_browser) if CoverFlow is not None: self.cover_flow.stop.connect(self.hide_cover_browser) self.cover_flow.setVisible(False) else: self.separate_cover_browser = False self.cb_splitter.insertWidget(self.cb_splitter.side_index, self.cover_flow) if CoverFlow is not None: self.cover_flow.stop.connect(self.cb_splitter.hide_side_pane) self.cb_splitter.button.toggled.connect(self.cover_browser_toggled, type=Qt.ConnectionType.QueuedConnection) def update_cover_flow_subtitle_font(self): db = self.current_db.new_api field = db.pref('cover_browser_subtitle_field', 'rating') try: is_rating = db.field_metadata[field]['datatype'] == 'rating' except Exception: is_rating = False if hasattr(self.cover_flow, 'set_subtitle_font'): self.cover_flow.set_subtitle_font(is_rating) def toggle_cover_browser(self, *args): cbd = getattr(self, 'cb_dialog', None) if cbd is not None: self.hide_cover_browser() else: self.show_cover_browser() def cover_browser_toggled(self, *args): if self.cb_splitter.button.isChecked(): self.cover_browser_shown() else: self.cover_browser_hidden() def cover_browser_shown(self): self.cover_flow.setFocus(Qt.FocusReason.OtherFocusReason) if CoverFlow is not None: if self.db_images.ignore_image_requests: self.db_images.ignore_image_requests = False self.db_images.dataChanged.emit() self.cover_flow.setCurrentSlide(self.library_view.currentIndex().row()) self.cover_flow_syncing_enabled = True QTimer.singleShot(500, self.cover_flow_do_sync) self.library_view.setCurrentIndex( self.library_view.currentIndex()) self.library_view.scroll_to_row(self.library_view.currentIndex().row()) def cover_browser_hidden(self): if CoverFlow is not None: self.cover_flow_syncing_enabled = False idx = self.library_view.model().index(self.cover_flow.currentSlide(), 0) if idx.isValid(): sm = self.library_view.selectionModel() sm.select(idx, QItemSelectionModel.SelectionFlag.ClearAndSelect|QItemSelectionModel.SelectionFlag.Rows) self.library_view.setCurrentIndex(idx) self.library_view.scroll_to_row(idx.row()) def show_cover_browser(self): d = CBDialog(self, self.cover_flow) d.addAction(self.cb_splitter.action_toggle) self.cover_flow.setVisible(True) self.cover_flow.setFocus(Qt.FocusReason.OtherFocusReason) d.show_fullscreen() if gprefs['cb_fullscreen'] else d.show() self.cb_splitter.button.set_state_to_hide() d.closed.connect(self.cover_browser_closed) self.cb_dialog = d self.cb_splitter.button.set_state_to_hide() def cover_browser_closed(self, *args): self.cb_dialog = None self.cb_splitter.button.set_state_to_show() def hide_cover_browser(self, *args): cbd = getattr(self, 'cb_dialog', None) if cbd is not None: cbd.accept() self.cb_dialog = None self.cb_splitter.button.set_state_to_show() def is_cover_browser_visible(self): try: if self.separate_cover_browser: return self.cover_flow.isVisible() except AttributeError: return False # called before init_cover_flow_mixin return not self.cb_splitter.is_side_index_hidden def refresh_cover_browser(self): if self.disable_cover_browser_refresh: return try: if self.is_cover_browser_visible() and not isinstance(self.cover_flow, QLabel): self.db_images.ignore_image_requests = False self.cover_flow.dataChanged() except AttributeError: pass # called before init_cover_flow_mixin def sync_cf_to_listview(self, current, previous): if (self.cover_flow_sync_flag and self.is_cover_browser_visible() and self.cover_flow.currentSlide() != current.row()): self.cover_flow.setCurrentSlide(current.row()) self.cover_flow_sync_flag = True def cf_context_menu_requested(self): row = self.cover_flow.currentSlide() m = self.library_view.model() index = m.index(row, 0) sm = self.library_view.selectionModel() sm.select(index, QItemSelectionModel.SelectionFlag.ClearAndSelect|QItemSelectionModel.SelectionFlag.Rows) self.library_view.setCurrentIndex(index) def cover_flow_do_sync(self): self.cover_flow_sync_flag = True try: if (self.is_cover_browser_visible() and self.cf_last_updated_at is not None and time.time() - self.cf_last_updated_at > 0.5): self.cf_last_updated_at = None row = self.cover_flow.currentSlide() m = self.library_view.model() index = m.index(row, 0) if self.library_view.currentIndex().row() != row and index.isValid(): self.cover_flow_sync_flag = False self.library_view.select_rows([row], using_ids=False) except: import traceback traceback.print_exc() if self.cover_flow_syncing_enabled: QTimer.singleShot(500, self.cover_flow_do_sync) def sync_listview_to_cf(self, row): self.cf_last_updated_at = time.time() def test(): from qt.core import QMainWindow app = QApplication([]) w = QMainWindow() cf = CoverFlow() w.resize(cf.size()+QSize(30, 20)) model = DummyImageList() cf.setImages(model) cf.setCurrentSlide(39000) w.setCentralWidget(cf) w.show() cf.setFocus(Qt.FocusReason.OtherFocusReason) sys.exit(app.exec()) def main(args=sys.argv): return 0 if __name__ == '__main__': from qt.core import QMainWindow app = QApplication([]) w = QMainWindow() cf = CoverFlow() w.resize(cf.size()+QSize(30, 20)) path = sys.argv[1] model = FileSystemImages(sys.argv[1]) cf.currentChanged[int].connect(model.currentChanged) cf.setImages(model) w.setCentralWidget(cf) w.show() cf.setFocus(Qt.FocusReason.OtherFocusReason) sys.exit(app.exec())