class TabBarTabMetrics(QWidget): def __init__(self, parent=None): super().__init__(parent) self._metrics = {} # QHash<int, int> def init(self): if self._metrics: return self._metrics[0] = 250 # normalMaxWidth self._metrics[1] = 100 # normalMinWidth self._metrics[2] = 100 # activeMinWidth self._metrics[3] = 100 # overflowedWidth self._metrics[4] = -1 # pinnedWidth Will be initialized from TabBar def _normalMaxWidth(self): return self._metrics[0] def setNormalMaxWidth(self, value): self._metrics[0] = value normalMaxWidth = pyqtProperty(int, _normalMaxWidth, setNormalMaxWidth) def _normalMinWidth(self): return self._metrics[1] def setNormalMinWidth(self, value): self._metrics[1] = value normalMinWidth = pyqtProperty(int, _normalMinWidth, setNormalMinWidth) def _activeMinWidth(self): return self._metrics[2] def setActiveMinWidth(self, value): self._metrics[2] = value activeMinWidth = pyqtProperty(int, _activeMinWidth, setActiveMinWidth) def _overflowedWidth(self): return self._metrics[3] def setOverflowedWidth(self, value): self._metrics[3] = value overflowedWidth = pyqtProperty(int, _overflowedWidth, setOverflowedWidth) def _pinnedWidth(self): return self._metrics[4] def setPinnedWidth(self, value): self._metrics[4] = value pinnedWidth = pyqtProperty(int, _pinnedWidth, setPinnedWidth)
def quick_property(property_type, property_name): def getter(self): return getattr(self, '_{}'.format(property_name)) def setter(self, val): return setattr(self, '_{}'.format(property_name), val) return pyqtProperty(property_type, fget=getter, fset=setter)
class Page(QWebPage): # {{{ def __init__(self, log): self.log = log QWebPage.__init__(self) self.js = None self.evaljs = self.mainFrame().evaluateJavaScript self.bridge_value = None nam = self.networkAccessManager() nam.setNetworkAccessible(nam.NotAccessible) self.longjs_counter = 0 def javaScriptConsoleMessage(self, msg, lineno, msgid): self.log(u'JS:', unicode(msg)) def javaScriptAlert(self, frame, msg): self.log(unicode(msg)) @pyqtSlot(result=bool) def shouldInterruptJavaScript(self): if self.longjs_counter < 5: self.log('Long running javascript, letting it proceed') self.longjs_counter += 1 return False self.log.warn('Long running javascript, aborting it') return True def _pass_json_value_getter(self): val = json.dumps(self.bridge_value) return val def _pass_json_value_setter(self, value): # Qt WebKit in Qt 4.x adds extra null bytes to the end of the string # if the JSON contains non-BMP characters self.bridge_value = json.loads(unicode(value).rstrip('\0')) _pass_json_value = pyqtProperty(str, fget=_pass_json_value_getter, fset=_pass_json_value_setter) def load_js(self): self.longjs_counter = 0 if self.js is None: from calibre.utils.resources import compiled_coffeescript self.js = compiled_coffeescript('ebooks.oeb.display.utils') self.js += compiled_coffeescript('ebooks.oeb.polish.font_stats') self.mainFrame().addToJavaScriptWindowObject("py_bridge", self) self.evaljs(self.js) self.evaljs(''' py_bridge.__defineGetter__('value', function() { return JSON.parse(this._pass_json_value); }); py_bridge.__defineSetter__('value', function(val) { this._pass_json_value = JSON.stringify(val); }); ''')
def proxy_property(child_name, child_type, child_property): meta_object = child_type.staticMetaObject meta_property = meta_object.property(meta_object.indexOfProperty(child_property)) def getter(self): return meta_property.read(getattr(self, child_name)) def setter(self, val): return meta_property.write(getattr(self, child_name), val) return pyqtProperty(meta_property.typeName(), fget=getter, fset=setter)
class MacToolButton(QPushButton): def __init__(self, parent=None): super().__init__(parent) self._autoRaise = False self._buttonFixedSize = QSize(18, 18) def setIconSize(self, size): super().setIconSize(size) self._buttonFixedSize = QSize(size.width() + 2, size.height() + 2) def setAutoRaise(self, enable): self._autoRaise = enable self.setFlat(enable) if enable: self.setFixedSize(self._buttonFixedSize) def _autoRaise(self): return self._autoRaise autoRaise = pyqtProperty(bool, _autoRaise, setAutoRaise)
class PDFWriter(QObject): def _pass_json_value_getter(self): val = json.dumps(self.bridge_value) return val def _pass_json_value_setter(self, value): self.bridge_value = json.loads(unicode(value)) _pass_json_value = pyqtProperty(str, fget=_pass_json_value_getter, fset=_pass_json_value_setter) @pyqtSlot(result=unicode) def title(self): return self.doc_title @pyqtSlot(result=unicode) def author(self): return self.doc_author @pyqtSlot(result=unicode) def section(self): return self.current_section @pyqtSlot(result=unicode) def tl_section(self): return self.current_tl_section def __init__(self, opts, log, cover_data=None, toc=None): from calibre.gui2 import must_use_qt must_use_qt() QObject.__init__(self) self.logger = self.log = log self.opts = opts self.cover_data = cover_data self.paged_js = None self.toc = toc self.loop = QEventLoop() self.view = QWebView() self.page = Page(opts, self.log) self.view.setPage(self.page) self.view.setRenderHints(QPainter.Antialiasing| QPainter.TextAntialiasing|QPainter.SmoothPixmapTransform) self.view.loadFinished.connect(self.render_html, type=Qt.QueuedConnection) for x in (Qt.Horizontal, Qt.Vertical): self.view.page().mainFrame().setScrollBarPolicy(x, Qt.ScrollBarAlwaysOff) self.report_progress = lambda x, y: x self.current_section = '' self.current_tl_section = '' def dump(self, items, out_stream, pdf_metadata): opts = self.opts page_size = get_page_size(self.opts) xdpi, ydpi = self.view.logicalDpiX(), self.view.logicalDpiY() # We cannot set the side margins in the webview as there is no right # margin for the last page (the margins are implemented with # -webkit-column-gap) ml, mr = opts.margin_left, opts.margin_right self.doc = PdfDevice(out_stream, page_size=page_size, left_margin=ml, top_margin=0, right_margin=mr, bottom_margin=0, xdpi=xdpi, ydpi=ydpi, errors=self.log.error, debug=self.log.debug, compress=not opts.uncompressed_pdf, opts=opts, mark_links=opts.pdf_mark_links) self.footer = opts.pdf_footer_template if self.footer: self.footer = self.footer.strip() if not self.footer and opts.pdf_page_numbers: self.footer = '<p style="text-align:center; text-indent: 0">_PAGENUM_</p>' self.header = opts.pdf_header_template if self.header: self.header = self.header.strip() min_margin = 1.5 * opts._final_base_font_size if self.footer and opts.margin_bottom < min_margin: self.log.warn('Bottom margin is too small for footer, increasing it to %.1fpts' % min_margin) opts.margin_bottom = min_margin if self.header and opts.margin_top < min_margin: self.log.warn('Top margin is too small for header, increasing it to %.1fpts' % min_margin) opts.margin_top = min_margin self.page.setViewportSize(QSize(self.doc.width(), self.doc.height())) self.render_queue = items self.total_items = len(items) mt, mb = map(self.doc.to_px, (opts.margin_top, opts.margin_bottom)) self.margin_top, self.margin_bottom = map(lambda x:int(floor(x)), (mt, mb)) self.painter = QPainter(self.doc) self.doc.set_metadata(title=pdf_metadata.title, author=pdf_metadata.author, tags=pdf_metadata.tags, mi=pdf_metadata.mi) self.doc_title = pdf_metadata.title self.doc_author = pdf_metadata.author self.painter.save() try: if self.cover_data is not None: p = QPixmap() try: p.loadFromData(self.cover_data) except TypeError: self.log.warn('This ebook does not have a raster cover, cannot generate cover for PDF' '. Cover type: %s' % type(self.cover_data)) if not p.isNull(): self.doc.init_page() draw_image_page(QRect(*self.doc.full_page_rect), self.painter, p, preserve_aspect_ratio=self.opts.preserve_cover_aspect_ratio) self.doc.end_page() finally: self.painter.restore() QTimer.singleShot(0, self.render_book) if self.loop.exec_() == 1: raise Exception('PDF Output failed, see log for details') if self.toc is not None and len(self.toc) > 0: self.doc.add_outline(self.toc) self.painter.end() if self.doc.errors_occurred: raise Exception('PDF Output failed, see log for details') def render_inline_toc(self): self.rendered_inline_toc = True from calibre.ebooks.pdf.render.toc import toc_as_html raw = toc_as_html(self.toc, self.doc, self.opts) pt = PersistentTemporaryFile('_pdf_itoc.htm') pt.write(raw) pt.close() self.render_queue.append(pt.name) self.render_next() def render_book(self): if self.doc.errors_occurred: return self.loop.exit(1) try: if not self.render_queue: if self.opts.pdf_add_toc and self.toc is not None and len(self.toc) > 0 and not hasattr(self, 'rendered_inline_toc'): return self.render_inline_toc() self.loop.exit() else: self.render_next() except: self.logger.exception('Rendering failed') self.loop.exit(1) def render_next(self): item = unicode(self.render_queue.pop(0)) self.logger.debug('Processing %s...' % item) self.current_item = item load_html(item, self.view) def render_html(self, ok): if ok: try: self.do_paged_render() except: self.log.exception('Rendering failed') self.loop.exit(1) return else: # The document is so corrupt that we can't render the page. self.logger.error('Document cannot be rendered.') self.loop.exit(1) return done = self.total_items - len(self.render_queue) self.report_progress(done/self.total_items, _('Rendered %s'%os.path.basename(self.current_item))) self.render_book() @property def current_page_num(self): return self.doc.current_page_num def load_mathjax(self): evaljs = self.view.page().mainFrame().evaluateJavaScript mjpath = P(u'viewer/mathjax').replace(os.sep, '/') if iswindows: mjpath = u'/' + mjpath if bool(evaljs(''' window.mathjax.base = %s; mathjax.check_for_math(); mathjax.math_present '''%(json.dumps(mjpath, ensure_ascii=False)))): self.log.debug('Math present, loading MathJax') while not bool(evaljs('mathjax.math_loaded')): self.loop.processEvents(self.loop.ExcludeUserInputEvents) evaljs('document.getElementById("MathJax_Message").style.display="none";') def get_sections(self, anchor_map, only_top_level=False): sections = defaultdict(list) ci = os.path.abspath(os.path.normcase(self.current_item)) if self.toc is not None: tocentries = self.toc.top_level_items() if only_top_level else self.toc.flat() for toc in tocentries: path = toc.abspath or None frag = toc.fragment or None if path is None: continue path = os.path.abspath(os.path.normcase(path)) if path == ci: col = 0 if frag and frag in anchor_map: col = anchor_map[frag]['column'] sections[col].append(toc.text or _('Untitled')) return sections def do_paged_render(self): if self.paged_js is None: import uuid from calibre.utils.resources import compiled_coffeescript as cc self.paged_js = cc('ebooks.oeb.display.utils') self.paged_js += cc('ebooks.oeb.display.indexing') self.paged_js += cc('ebooks.oeb.display.paged') self.paged_js += cc('ebooks.oeb.display.mathjax') self.hf_uuid = str(uuid.uuid4()).replace('-', '') self.view.page().mainFrame().addToJavaScriptWindowObject("py_bridge", self) self.view.page().longjs_counter = 0 evaljs = self.view.page().mainFrame().evaluateJavaScript evaljs(self.paged_js) self.load_mathjax() evaljs(''' Object.defineProperty(py_bridge, 'value', { get : function() { return JSON.parse(this._pass_json_value); }, set : function(val) { this._pass_json_value = JSON.stringify(val); } }); document.body.style.backgroundColor = "white"; paged_display.set_geometry(1, %d, %d, %d); paged_display.layout(); paged_display.fit_images(); py_bridge.value = book_indexing.all_links_and_anchors(); window.scrollTo(0, 0); // This is needed as getting anchor positions could have caused the viewport to scroll '''%(self.margin_top, 0, self.margin_bottom)) amap = self.bridge_value if not isinstance(amap, dict): amap = {'links':[], 'anchors':{}} # Some javascript error occurred sections = self.get_sections(amap['anchors']) tl_sections = self.get_sections(amap['anchors'], True) col = 0 if self.header: self.bridge_value = self.header evaljs('paged_display.header_template = py_bridge.value') if self.footer: self.bridge_value = self.footer evaljs('paged_display.footer_template = py_bridge.value') if self.header or self.footer: evaljs('paged_display.create_header_footer("%s");'%self.hf_uuid) start_page = self.current_page_num mf = self.view.page().mainFrame() def set_section(col, sections, attr): # If this page has no section, use the section from the previous page idx = col if col in sections else col - 1 if col - 1 in sections else None if idx is not None: setattr(self, attr, sections[idx][0]) while True: set_section(col, sections, 'current_section') set_section(col, tl_sections, 'current_tl_section') self.doc.init_page() if self.header or self.footer: evaljs('paged_display.update_header_footer(%d)'%self.current_page_num) self.painter.save() mf.render(self.painter) self.painter.restore() try: nsl = int(evaljs('paged_display.next_screen_location()')) except (TypeError, ValueError): break self.doc.end_page(nsl <= 0) if nsl <= 0: break evaljs('window.scrollTo(%d, 0); paged_display.position_header_footer();'%nsl) if self.doc.errors_occurred: break col += 1 if not self.doc.errors_occurred: self.doc.add_links(self.current_item, start_page, amap['links'], amap['anchors'])
class PDFWriter(QObject): # {{{ def __init__(self, opts, log, cover_data=None, toc=None): from calibre.gui2 import must_use_qt from calibre.utils.podofo import get_podofo must_use_qt() QObject.__init__(self) self.logger = self.log = log self.podofo = get_podofo() self.doc = self.podofo.PDFDoc() self.loop = QEventLoop() self.view = QWebView() self.page = Page(opts, self.log) self.view.setPage(self.page) self.view.setRenderHints(QPainter.Antialiasing | QPainter.TextAntialiasing | QPainter.SmoothPixmapTransform) self.view.loadFinished.connect(self._render_html, type=Qt.QueuedConnection) for x in (Qt.Horizontal, Qt.Vertical): self.view.page().mainFrame().setScrollBarPolicy( x, Qt.ScrollBarAlwaysOff) self.render_queue = [] self.combine_queue = [] self.tmp_path = PersistentTemporaryDirectory(u'_pdf_output_parts') self.opts = opts self.cover_data = cover_data self.paged_js = None self.toc = toc def dump(self, items, out_stream, pdf_metadata): self.metadata = pdf_metadata self._delete_tmpdir() self.outline = Outline(self.toc, items) self.render_queue = items self.combine_queue = [] self.out_stream = out_stream self.insert_cover() self.render_succeeded = False self.current_page_num = self.doc.page_count() self.combine_queue.append( os.path.join(self.tmp_path, 'qprinter_out.pdf')) self.first_page = True self.setup_printer(self.combine_queue[-1]) QTimer.singleShot(0, self._render_book) self.loop.exec_() if self.painter is not None: self.painter.end() if self.printer is not None: self.printer.abort() if not self.render_succeeded: raise Exception('Rendering HTML to PDF failed') def _render_book(self): try: if len(self.render_queue) == 0: self._write() else: self._render_next() except: self.logger.exception('Rendering failed') self.loop.exit(1) def _render_next(self): item = unicode(self.render_queue.pop(0)) self.logger.debug('Processing %s...' % item) self.current_item = item load_html(item, self.view) def _render_html(self, ok): if ok: self.do_paged_render() else: # The document is so corrupt that we can't render the page. self.logger.error('Document cannot be rendered.') self.loop.exit(0) return self._render_book() def _pass_json_value_getter(self): val = json.dumps(self.bridge_value) return val def _pass_json_value_setter(self, value): self.bridge_value = json.loads(unicode(value)) _pass_json_value = pyqtProperty(str, fget=_pass_json_value_getter, fset=_pass_json_value_setter) def setup_printer(self, outpath): self.printer = self.painter = None printer = get_pdf_printer(self.opts, output_file_name=outpath) painter = QPainter(printer) zoomx = printer.logicalDpiX() / self.view.logicalDpiX() zoomy = printer.logicalDpiY() / self.view.logicalDpiY() painter.scale(zoomx, zoomy) pr = printer.pageRect() self.printer, self.painter = printer, painter self.viewport_size = QSize(pr.width() / zoomx, pr.height() / zoomy) self.page.setViewportSize(self.viewport_size) def do_paged_render(self): if self.paged_js is None: from calibre.utils.resources import compiled_coffeescript self.paged_js = compiled_coffeescript('ebooks.oeb.display.utils') self.paged_js += compiled_coffeescript( 'ebooks.oeb.display.indexing') self.paged_js += compiled_coffeescript('ebooks.oeb.display.paged') self.view.page().mainFrame().addToJavaScriptWindowObject( "py_bridge", self) evaljs = self.view.page().mainFrame().evaluateJavaScript evaljs(self.paged_js) evaljs(''' Object.defineProperty(py_bridge, 'value', { get : function() { return JSON.parse(this._pass_json_value); }, set : function(val) { this._pass_json_value = JSON.stringify(val); } }); document.body.style.backgroundColor = "white"; paged_display.set_geometry(1, 0, 0, 0); paged_display.layout(); paged_display.fit_images(); ''') mf = self.view.page().mainFrame() start_page = self.current_page_num if not self.first_page: start_page += 1 while True: if not self.first_page: if self.printer.newPage(): self.current_page_num += 1 self.first_page = False mf.render(self.painter) try: nsl = int(evaljs('paged_display.next_screen_location()')) except (TypeError, ValueError): break if nsl <= 0: break evaljs('window.scrollTo(%d, 0)' % nsl) self.bridge_value = tuple(self.outline.anchor_map[self.current_item]) evaljs( 'py_bridge.value = book_indexing.anchor_positions(py_bridge.value)' ) amap = self.bridge_value if not isinstance(amap, dict): amap = {} # Some javascript error occurred self.outline.set_pos(self.current_item, None, start_page, 0) for anchor, x in amap.iteritems(): pagenum, ypos = x self.outline.set_pos(self.current_item, anchor, start_page + pagenum, ypos) def append_doc(self, outpath): doc = self.podofo.PDFDoc() with open(outpath, 'rb') as f: raw = f.read() doc.load(raw) self.doc.append(doc) def _delete_tmpdir(self): if os.path.exists(self.tmp_path): shutil.rmtree(self.tmp_path, True) self.tmp_path = PersistentTemporaryDirectory('_pdf_output_parts') def insert_cover(self): if not isinstance(self.cover_data, bytes): return item_path = os.path.join(self.tmp_path, 'cover.pdf') printer = get_pdf_printer(self.opts, output_file_name=item_path, for_comic=True) self.combine_queue.insert(0, item_path) p = QPixmap() p.loadFromData(self.cover_data) if not p.isNull(): painter = QPainter(printer) draw_image_page( printer, painter, p, preserve_aspect_ratio=self.opts.preserve_cover_aspect_ratio) painter.end() self.append_doc(item_path) printer.abort() def _write(self): self.painter.end() self.printer.abort() self.painter = self.printer = None self.append_doc(self.combine_queue[-1]) try: self.doc.creator = u'%s %s [http://calibre-ebook.com]' % ( __appname__, __version__) self.doc.title = self.metadata.title self.doc.author = self.metadata.author if self.metadata.tags: self.doc.keywords = self.metadata.tags self.outline(self.doc) self.doc.save_to_fileobj(self.out_stream) self.render_succeeded = True finally: self._delete_tmpdir() self.loop.exit(0)
class RecoveryJsObject(QObject): def __init__(self, manager): ''' @param: manager RestoreManager ''' super().__init__() self._manager = manager # RestoreManager self._page = None # WebPage def setPage(self, page): ''' @param: page WebPage ''' assert (page) self._page = page def restoreData(self): ''' @return: QJsonArray ''' out = [] idx = 0 for window in self._manager.restoreData().windows: jdx = 0 tabs = [] for tab in window.tabs: icon = tab.icon.isNull() and IconProvider.emptyWebIcon( ) or tab.icon item = {} item['tab'] = jdx item['icon'] = gVar.appTools.pixmapToDataUrl(icon.pixmap(16)) item['title'] = tab.title item['url'] = tab.url.toString() item['pinned'] = tab.isPinned item['current'] = window.currentTab == jdx tabs.append(item) jdx += 1 window = {} window['window'] = idx idx += 1 window['tabs'] = tabs out.append(window) return out restoreData = pyqtProperty(list, restoreData, constant=True) # public Q_SLOTS: @pyqtSlot() def startNewSession(self): gVar.app.restoreManager().clearRestoreData() gVar.app.destroyRestoreManager() view = self._page.view() view.loadByUrl(QUrl('app:start')) @pyqtSlot(list, list) def restoreSession(self, excludeWin, excludeTab): ''' @param excludeWin QStringList @param excludeTab QStringList ''' assert (len(excludeWin) == len(excludeTab)) # This assumes that excludeWin and excludeTab are sorted in descending order # RestoreData data = self._manager.restoreData() for idx in range(len(excludeWin)): win = excludeWin[idx] tab = excludeTab[idx] if not gVar.appTools.containsIndex(data.windows, win) or \ gVar.appTools.containsIndex(data.windows[win].tabs, tab): continue wd = data.windows[win] wd.tabs.remove(tab) if wd.currentTab >= tab: wd.currentTab -= 1 if not wd.tabs: data.windows.remove(win) continue if wd.currentTab < 0: wd.currentTab = len(wd.tabs) - 1 if gVar.app.restoreSession(None, data): self._closeTab() else: self.startNewSession() # private: def _closeTab(self): # TabbedWebView view = self._page.view() if not isinstance(view, TabbedWebView): return if view.browserWindow().tabCount() > 1: view.closeView() else: view.browserWindow().close()
class CoverView(QWidget): # {{{ cover_changed = pyqtSignal(object, object) cover_removed = pyqtSignal(object) def __init__(self, vertical, parent=None): QWidget.__init__(self, parent) self._current_pixmap_size = QSize(120, 120) self.vertical = vertical self.animation = QPropertyAnimation(self, 'current_pixmap_size', self) self.animation.setEasingCurve(QEasingCurve(QEasingCurve.OutExpo)) self.animation.setDuration(1000) self.animation.setStartValue(QSize(0, 0)) self.animation.valueChanged.connect(self.value_changed) self.setSizePolicy( QSizePolicy.Expanding if vertical else QSizePolicy.Minimum, QSizePolicy.Expanding) self.default_pixmap = QPixmap(I('book.png')) self.pixmap = self.default_pixmap self.pwidth = self.pheight = None self.data = {} self.do_layout() def value_changed(self, val): self.update() def setCurrentPixmapSize(self, val): self._current_pixmap_size = val def do_layout(self): if self.rect().width() == 0 or self.rect().height() == 0: return pixmap = self.pixmap pwidth, pheight = pixmap.width(), pixmap.height() try: self.pwidth, self.pheight = fit_image(pwidth, pheight, self.rect().width(), self.rect().height())[1:] except: self.pwidth, self.pheight = self.rect().width()-1, \ self.rect().height()-1 self.current_pixmap_size = QSize(self.pwidth, self.pheight) self.animation.setEndValue(self.current_pixmap_size) def show_data(self, data): self.animation.stop() same_item = getattr(data, 'id', True) == self.data.get('id', False) self.data = {'id':data.get('id', None)} if data.cover_data[1]: self.pixmap = QPixmap.fromImage(data.cover_data[1]) if self.pixmap.isNull() or self.pixmap.width() < 5 or \ self.pixmap.height() < 5: self.pixmap = self.default_pixmap else: self.pixmap = self.default_pixmap self.do_layout() self.update() if (not same_item and not config['disable_animations'] and self.isVisible()): self.animation.start() def paintEvent(self, event): canvas_size = self.rect() width = self.current_pixmap_size.width() extrax = canvas_size.width() - width if extrax < 0: extrax = 0 x = int(extrax/2.) height = self.current_pixmap_size.height() extray = canvas_size.height() - height if extray < 0: extray = 0 y = int(extray/2.) target = QRect(x, y, width, height) p = QPainter(self) p.setRenderHints(QPainter.Antialiasing | QPainter.SmoothPixmapTransform) p.drawPixmap(target, self.pixmap.scaled(target.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation)) if gprefs['bd_overlay_cover_size']: sztgt = target.adjusted(0, 0, 0, -4) f = p.font() f.setBold(True) p.setFont(f) sz = u'\u00a0%d x %d\u00a0'%(self.pixmap.width(), self.pixmap.height()) flags = Qt.AlignBottom|Qt.AlignRight|Qt.TextSingleLine szrect = p.boundingRect(sztgt, flags, sz) p.fillRect(szrect.adjusted(0, 0, 0, 4), QColor(0, 0, 0, 200)) p.setPen(QPen(QColor(255,255,255))) p.drawText(sztgt, flags, sz) p.end() current_pixmap_size = pyqtProperty('QSize', fget=lambda self: self._current_pixmap_size, fset=setCurrentPixmapSize ) def contextMenuEvent(self, ev): cm = QMenu(self) paste = cm.addAction(_('Paste Cover')) copy = cm.addAction(_('Copy Cover')) remove = cm.addAction(_('Remove Cover')) if not QApplication.instance().clipboard().mimeData().hasImage(): paste.setEnabled(False) copy.triggered.connect(self.copy_to_clipboard) paste.triggered.connect(self.paste_from_clipboard) remove.triggered.connect(self.remove_cover) cm.exec_(ev.globalPos()) def copy_to_clipboard(self): QApplication.instance().clipboard().setPixmap(self.pixmap) def paste_from_clipboard(self, pmap=None): if not isinstance(pmap, QPixmap): cb = QApplication.instance().clipboard() pmap = cb.pixmap() if pmap.isNull() and cb.supportsSelection(): pmap = cb.pixmap(cb.Selection) if not pmap.isNull(): self.pixmap = pmap self.do_layout() self.update() self.update_tooltip(getattr(self.parent(), 'current_path', '')) if not config['disable_animations']: self.animation.start() id_ = self.data.get('id', None) if id_ is not None: self.cover_changed.emit(id_, pixmap_to_data(pmap)) def remove_cover(self): id_ = self.data.get('id', None) self.pixmap = self.default_pixmap self.do_layout() self.update() if id_ is not None: self.cover_removed.emit(id_) def update_tooltip(self, current_path): try: sz = self.pixmap.size() except: sz = QSize(0, 0) self.setToolTip( '<p>'+_('Double-click to open Book Details window') + '<br><br>' + _('Path') + ': ' + current_path + '<br><br>' + _('Cover size: %(width)d x %(height)d')%dict( width=sz.width(), height=sz.height()) )
class FancyTabWidget(QWidget): # Values are persisted = only add to the end # enum Mode Mode_None = 0 Mode_LargeSidebar = 1 Mode_SmallSidebar = 2 Mode_Tabs = 3 Mode_IconOnlyTabs = 4 Mode_PlainSidebar = 5 def __init__(self, parent=None): super().__init__(parent) self._mode = self.Mode_None self._items = [] # QList<Item> self._tab_bar = None # QWidget self._stack = QStackedLayout() # QStackedLayout self._background_pixmap = QPixmap() self._side_widget = QWidget() # QWidget self._side_layout = QVBoxLayout() # QVBoxLayout self._top_layout = QVBoxLayout() # QVBoxLayout self._use_background = False # bool self._menu = None # QMenu self._proxy_style = FancyTabProxyStyle() # FancyTabProxyStyle self._side_layout.setSpacing(0) self._side_layout.setContentsMargins(0, 0, 0, 0) self._side_layout.addSpacerItem( QSpacerItem(0, 0, QSizePolicy.Fixed, QSizePolicy.Expanding)) self._side_widget.setLayout(self._side_layout) self._side_widget.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Expanding) self._top_layout.setSpacing(0) self._top_layout.setContentsMargins(0, 0, 0, 0) self._top_layout.addLayout(self._stack) main_layout = QHBoxLayout() main_layout.setContentsMargins(0, 0, 0, 0) main_layout.setSpacing(1) main_layout.addWidget(self._side_widget) main_layout.addLayout(self._top_layout) self.setLayout(main_layout) class Item: # enum Type Type_Tab = 0 Type_Spacer = 1 def __init__(self, icon, label): ''' @param: icon QIcon @param: label QString ''' self.type = self.Type_Tab # Type self.tab_label = label # QString self.tab_icon = icon # QIcon self.spacer_size = 0 def AddTab(self, tab, icon, label): ''' @param: tab QWidget @param: icon Qicon @param: label QString ''' self._stack.addWidget(tab) self._items.append(self.Item(icon, label)) def AddSpacer(self, size=40): self._items.append(self.Item(size)) def SetBackgroundPixmap(self, pixmap): ''' @param: pixmap QPixmap ''' self._background_pixmap = pixmap self.update() def AddBottomWidget(self, widget): ''' @param: widget QWidget ''' self._top_layout.addWidget(widget) def current_index(self): ''' @return: int ''' return self._stack.currentIndex() def mode(self): ''' @return: Mode ''' return self._mode def bgPixmap(self): ''' @return: QPixmap ''' return self._background_pixmap bgPixmap = pyqtProperty(QPixmap, bgPixmap, SetBackgroundPixmap) # public Q_SLOTS: def SetCurrentIndex(self, index): bar = self._tab_bar if isinstance(bar, FancyTabBar): bar.setCurrentIndex(index) elif isinstance(bar, QTabBar): bar.setCurrentIndex(index) else: self._stack.setCurrentIndex(index) def SetMode(self, mode): # Remove previous tab bar del self._tab_bar self._tab_bar = None self._use_background = False # Create new tab bar if mode == self.Mode_None: pass elif mode == self.Mode_LargeSidebar: bar = FancyTabBar(self) self._side_layout.insertWidget(0, bar) self._tab_bar = bar for item in self._items: if item.type == self.Item.Type_Spacer: bar.addSpacer(item.spacer_size) else: bar.addTab(item.tab_icon, item.tab_label) bar.setCurrentIndex(self._stack.currentIndex()) bar.currentChanged.connect(self._ShowWidget) self._use_background = True elif mode == self.Mode_Tabs: self._MakeTabBar(QTabBar.RoundedNorth, True, False, False) elif mode == self.Mode_IconOnlyTabs: self._MakeTabBar(QTabBar.RoundedNorth, False, True, False) elif mode == self.Mode_SmallSidebar: self._MakeTabBar(QTabBar.RoundedWest, True, True, True) self._use_background = True elif mode == self.Mode_PlainSidebar: self._MakeTabBar(QTabBar.RoundedWest, True, True, False) else: print('DEBUG: Unknown fancy tab mode %s' % mode) self._tab_bar.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred) self._mode = mode self.ModeChanged.emit(mode) self.update() # Q_SIGNALS: CurrentChanged = pyqtSignal(int) # index ModeChanged = pyqtSignal(int) # mode FancyTabWidget::Mode # protected: # override def paintEvent(self, event): ''' @param: event QPaintEvent ''' if not self._use_background: return painter = QPainter(self) rect = self._side_widget.rect().adjusted(0, 0, 1, 0) rect = self.style().visualRect(self.layoutDirection(), self.geometry(), rect) styleHelper.verticalGradient(painter, rect, rect) if not self._background_pixmap.isNull(): pixmap_rect = QRect(self._background_pixmap.rect()) pixmap_rect.moveTo(rect.topLeft()) while pixmap_rect.top() < rect.bottom(): source_rect = QRect(pixmap_rect.intersected(rect)) source_rect.moveTo(0, 0) painter.drawPixmap(pixmap_rect.topLeft(), self._background_pixmap, source_rect) pixmap_rect.moveTop(pixmap_rect.bottom() - 10) painter.setPen(styleHelper.borderColor()) painter.drawLine(rect.topRight(), rect.bottomRight()) # QColor light = styleHelper.sidebarHighlight() painter.setPen(light) painter.drawLine(rect.bottomLeft(), rect.bottomRight()) # override def contextMenuEvent(self, event): ''' @param: event QContextMenuEvent ''' pass # private Q_SLOTS: def _ShowWidget(self, index): self._stack.setCurrentIndex(index) self.CurrentChanged.emit(index) # private: def _MakeTabBar(self, shap, text, icons, fancy): ''' @param: shap QTabBar::Shap @param: text bool @param: icons bool @param: fancy bool ''' bar = QTabBar(self) bar.setShape(shap) bar.setDocumentMode(True) bar.setUsesScrollButtons(True) if shap == QTabBar.RoundedWest: bar.setIconSize(QSize(22, 22)) if fancy: bar.setStyle(self._proxy_style) if shap == QTabBar.RoundedNorth: self._top_layout.insertWidget(0, bar) else: self._side_layout.insertWidget(0, bar) # Item for item in self._items: if item.type != self.Item.Type_Tab: continue label = item.tab_label if shap == QTabBar.RoundedWest: label = QFontMetrics(self.font()).elidedText( label, Qt.ElideMiddle, 100) tab_id = -1 if icons and text: tab_id = bar.addTab(item.tab_icon, label) elif icons: tab_id = bar.addTab(item.tab_icon, '') elif text: tab_id = bar.addTab(label) bar.setTabToolTip(tab_id, item.tab_label) bar.setCurrentIndex(self._stack.currentIndex()) bar.currentChanged.connect(self._ShowWidget) self._tab_bar = bar def _AddMenuItem(self, mapper, group, text, mode): ''' @param: mapper QSignalMapper @param: group QActionGroup @param: text QString @param: mode Mode ''' # QAction action = group.addAction(text) action.setCheckable(True) mapper.setMapping(action, mode) action.triggered.connect(mapper.map) if mode == self._mode: action.setChecked(True)
class ToolButton(QToolButton): _MultiIconOption = 1 _ShowMenuInsideOption = 2 _ToolBarLookOption = 4 _ShowMenuOnRightClick = 8 def __init__(self, parent=None): super(ToolButton, self).__init__(parent) self._multiIcon = QImage() self._themeIcon = '' self._pressTimer = QTimer() self._menu = None # QMenu self._options = 0 self.setMinimumWidth(16) opt = QStyleOptionToolButton() self.initStyleOption(opt) self._pressTimer.setSingleShot(True) self._pressTimer.setInterval(QApplication.style().styleHint( QStyle.SH_ToolButton_PopupDelay, opt, self )) self._pressTimer.timeout.connect(self._showMenu) def size(self): return super().size() def setFixedSize(self, size): return super().setFixedSize(size) fixedsize = pyqtProperty(QSize, size, setFixedSize) def width(self): return super().width() def setFixedWidth(self, width): return super().setFixedWidth(width) fixedwidth = pyqtProperty(int, width, setFixedWidth) def height(self): return super().height() def setFixedHeight(self, height): return super().setFixedHeight(height) fixedheight = pyqtProperty(int, height, setFixedHeight) def multiIcon(self): ''' @brief: MultiIcon - Image containing pixmaps for all button states @return: QImage ''' return self._multiIcon def setMultiIcon(self, image): ''' @param: image QImage ''' self._options |= self._MultiIconOption self._multiIcon = image self.setFixedSize(self._multiIcon.width(), self._multiIcon.height()) self.update() multiIcon = pyqtProperty(QImage, multiIcon, setMultiIcon) def icon(self): ''' @brief: Icon - Standard QToolButton with icon @return: QIcon ''' return super().icon() def setIcon(self, icon): ''' @param: QIcon ''' if self._options & self._MultiIconOption: self.setFixedSize(self.sizeHint()) self._options &= ~self._MultiIconOption if not isinstance(icon, QIcon): icon = QIcon(icon) super().setIcon(icon) icon = pyqtProperty(QIcon, icon, setIcon) def themeIcon(self): ''' @brief: ThemeIcon - Standard QToolButton with theme icon @return: QString ''' return self._themeIcon def setThemeIcon(self, icon): ''' @param: icon QString ''' # QIcon ic ic = QIcon.fromTheme(icon) if not ic.isNull(): self._themeIcon = icon self.setIcon(QIcon.fromTheme(self._themeIcon)) themeIcon = pyqtProperty(str, themeIcon, setThemeIcon) def fallbackIcon(self): ''' @brief: FallbackIcon - In case theme doesn't contain ThemeIcon @return: QIcon ''' return self.icon def setFallbackIcon(self, fallbackIcon): ''' @param: fallbackIcon QIcon ''' if self.icon.isNull(): self.setIcon(fallbackIcon) fallbackIcon = pyqtProperty(QIcon, fallbackIcon, setFallbackIcon) def menu(self): ''' @note: Menu - Menu is handled in ToolButton and is not passed to QToolButton There won't be menu indicator shown in the button QToolButton::MenuButtonPopup is not supported @return: QMenu ''' return self._menu def setMenu(self, menu): ''' @param: menu QMenu ''' assert(menu) if self._menu: self._menu.aboutToHide.disconnect(self._menuAboutToHide) self._menu = menu self._menu.aboutToHide.connect(self._menuAboutToHide) def showMenuInside(self): ''' @brief: Align the right corner of menu to the right corner of button ''' return self._options & self._ShowMenuInsideOption def setShowMenuInside(self, enable): if enable: self._options |= self._ShowMenuInsideOption else: self._options &= ~self._ShowMenuInsideOption def showMenuOnRightClick(self): ''' @brief: Show button menu on right click ''' return self._options & self._ShowMenuOnRightClick def setShowMenuOnRightClick(self, enable): if enable: self._options |= self._ShowMenuOnRightClick else: self._options &= ~self._ShowMenuOnRightClick def toolbarButtonLook(self): ''' @brief: Set the button to look as it was in toolbar (it now only sets the correct icon size) @return: bool ''' return self._options & self._ToolBarLookOption def setToolbarButtonLook(self, enable): if enable: self._options |= self._ToolBarLookOption opt = QStyleOption() opt.initFrom(self) size = self.style().pixelMetric(QStyle.PM_ToolBarIconSize, opt, self) self.setIconSize(QSize(size, size)) else: self._options &= ~self._ToolBarLookOption self.setProperty('toolbar-look', enable) self.style().unpolish(self) self.style().polish(self) # Q_SIGNALS middleMouseClicked = pyqtSignal() controlClicked = pyqtSignal() doubleClicked = pyqtSignal() # It is needed to use these signals with ShowMenuInside aboutToShowMenu = pyqtSignal() aboutToHideMenu = pyqtSignal() # private Q_SLOTS def _menuAboutToHide(self): self.setDown(False) self.aboutToHideMenu.emit() def _showMenu(self): if not self._menu or self._menu.isVisible(): return self.aboutToShowMenu.emit() pos = QPoint() if self._options & self._ShowMenuInsideOption: pos = self.mapToGlobal(self.rect().bottomRight()) if QApplication.layoutDirection() == Qt.RightToLeft: pos.setX(pos.x() - self.rect().width()) else: pos.setX(pos.x() - self._menu.sizeHint().width()) else: pos = self.mapToGlobal(self.rect().bottomLeft()) self._menu.popup(pos) # protected: # override def mousePressEvent(self, event): ''' @param: event QMouseEvent ''' buttons = event.buttons() if buttons == Qt.LeftButton and self.popupMode() == QToolButton.DelayedPopup: self._pressTimer.start() if buttons == Qt.LeftButton and self.menu() and self.popupMode() == QToolButton.InstantPopup: self.setDown(True) self._showMenu() elif buttons == Qt.RightButton and self.menu() and self._options & self.showMenuOnRightClick: self.setDown(True) self._showMenu() else: super().mousePressEvent(event) # override def mouseReleaseEvent(self, event): ''' @param: event QMouseEvent ''' self._pressTimer.stop() button = event.button() if button == Qt.MiddleButton and self.rect().contains(event.pos()): self.middleMouseClicked.emit() self.setDown(False) elif button == Qt.LeftButton and self.rect().contains(event.pos()) and \ event.modifiers() == Qt.ControlModifier: self.controlClicked.emit() self.setDown(False) else: super().mouseReleaseEvent(event) # override def mouseDoubleClickEvent(self, event): ''' @param: event QMouseEvent ''' super().mouseDoubleClickEvent(event) self._pressTimer.stop() if event.buttons() == Qt.LeftButton: self.doubleClicked.emit() # override def contextMenuEvent(self, event): ''' @param: event QContextMenuEvent ''' # Block to prevent showing both context menu and button menu if self.menu() and self._options & self._ShowMenuOnRightClick: return super().contextMenuEvent(event) # override def paintEvent(self, event): ''' @param: event QPaintEvent ''' if not (self._options & self._MultiIconOption): super().paintEvent(event) return p = QPainter(self) w = self._multiIcon.width() h4 = self._multiIcon.height() / 4 if not self.isEnabled(): p.drawImage(0, 0, self._multiIcon, 0, h4 * 3, w, h4) elif self.isDown(): p.drawImage(0, 0, self._multiIcon, 0, h4 * 2, w, h4) elif self.underMouse(): p.drawImage(0, 0, self._multiIcon, 0, h4 * 1, w, h4) else: p.drawImage(0, 0, self._multiIcon, 0, h4 * 0, w, h4)
class SlideFlip(QWidget): # API {{{ # In addition the isVisible() and setVisible() methods must be present def __init__(self, parent): QWidget.__init__(self, parent) self.setGeometry(0, 0, 1, 1) self._current_width = 0 self.before_image = self.after_image = None self.animation = QPropertyAnimation(self, b'current_width', self) self.setVisible(False) self.animation.valueChanged.connect(self.update) self.animation.finished.connect(self.finished) self.flip_forwards = True self.setAttribute(Qt.WA_OpaquePaintEvent) @property def running(self): 'True iff animation is currently running' return self.animation.state() == self.animation.Running def initialize(self, image, forwards=True): ''' Initialize the flipper, causes the flipper to show itself displaying the full `image`. :param image: The image to display as background :param forwards: If True flipper will flip forwards, otherwise backwards ''' self.flip_forwards = forwards self.before_image = QPixmap.fromImage(image) self.after_image = None self.setGeometry(0, 0, image.width(), image.height()) self.setVisible(True) def __call__(self, image, duration=0.5): ''' Start the animation. You must have called :meth:`initialize` first. :param duration: Animation duration in seconds. ''' if self.running: return self.after_image = QPixmap.fromImage(image) if self.flip_forwards: self.animation.setStartValue(image.width()) self.animation.setEndValue(0) t = self.before_image self.before_image = self.after_image self.after_image = t self.animation.setEasingCurve(QEasingCurve(QEasingCurve.InExpo)) else: self.animation.setStartValue(0) self.animation.setEndValue(image.width()) self.animation.setEasingCurve(QEasingCurve(QEasingCurve.OutExpo)) self.animation.setDuration(duration * 1000) self.animation.start() # }}} def finished(self): self.setVisible(False) self.before_image = self.after_image = None def paintEvent(self, ev): if self.before_image is None: return canvas_size = self.rect() p = QPainter(self) p.setRenderHints(QPainter.Antialiasing | QPainter.SmoothPixmapTransform) p.drawPixmap(canvas_size, self.before_image, self.before_image.rect()) if self.after_image is not None: width = self._current_width iw = self.after_image.width() sh = min(self.after_image.height(), canvas_size.height()) if self.flip_forwards: source = QRect(max(0, iw - width), 0, width, sh) else: source = QRect(0, 0, width, sh) target = QRect(source) target.moveLeft(0) p.drawPixmap(target, self.after_image, source) p.end() def set_current_width(self, val): self._current_width = val current_width = pyqtProperty('int', fget=lambda self: self._current_width, fset=set_current_width)
class IconProvider(QWidget): _instance = None def __init__(self): super().__init__() self._emptyWebImage = QImage() self._bookmarkIcon = QIcon() self._iconBuffer = [] # QVector<[QUrl, QImage]> # TODO: cache limit size self._urlImageCache = {} # QCache<QByteArray, QImage> self._iconCacheMutex = Lock() # QMutex self._autoSaver = AutoSaver(self) # AutoSaver self._autoSaver.save.connect(self.saveIconsToDatabase) def saveIcon(self, view): ''' @param: view WebView ''' # Don't save icons in private mode if gVar.app.isPrivate(): return icon = view.icon(True) if icon.isNull(): return ignoredSchemes = ['app', 'ftp', 'file', 'view-source', 'data', 'about'] if view.url().scheme() in ignoredSchemes: return for idx in range(len(self._iconBuffer)): if self._iconBuffer[idx][0] == view.url(): self._iconBuffer.pop(idx) break item = (view.url(), icon.pixmap(16).toImage()) self._autoSaver.changeOccurred() self._iconBuffer.append(item) def _bookmarkIcon(self): ''' @return: QIcon ''' return QIcon.fromTheme('bookmarks', self._bookmarkIcon) def setBookmarkIcon(self, icon): ''' @param: icon QIcon ''' self._bookmarkIcon = icon bookmarkIcon = pyqtProperty(QIcon, _bookmarkIcon, setBookmarkIcon) # QStyle equivalent @classmethod # noqa C901 def standardIcon(cls, icon): ''' @param: icon QStyle::StandardPixmap @return: QIcon ''' defIcon = QApplication.style().standardIcon(icon) if icon == QStyle.SP_MessageBoxCritical: return QIcon.fromTheme('dialog-error', defIcon) elif icon == QStyle.SP_MessageBoxInformation: return QIcon.fromTheme('dialog-information', defIcon) elif icon == QStyle.SP_MessageBoxQuestion: return QIcon.fromTheme('dialog-question', defIcon) elif icon == QStyle.SP_MessageBoxWarning: return QIcon.fromTheme('dialog-warning', defIcon) elif icon == QStyle.SP_DialogCloseButton: return QIcon.fromTheme('dialog-close', defIcon) elif icon == QStyle.SP_BrowserStop: return QIcon.fromTheme('progress-stop', defIcon) elif icon == QStyle.SP_BrowserReload: return QIcon.fromTheme('view-refresh', defIcon) elif icon == QStyle.SP_FileDialogToParent: return QIcon.fromTheme('go-up', defIcon) elif icon == QStyle.SP_ArrowUp: return QIcon.fromTheme('go-up', defIcon) elif icon == QStyle.SP_ArrowDown: return QIcon.fromTheme('go-down', defIcon) elif icon == QStyle.SP_ArrowForward: if QApplication.layoutDirection() == Qt.RightToLeft: return QIcon.fromTheme('go-previous', defIcon) else: return QIcon.fromTheme('go-next', defIcon) elif icon == QStyle.SP_ArrowBack: if QApplication.layoutDirection() == Qt.RightToLeft: return QIcon.fromTheme('go-next', defIcon) else: return QIcon.fromTheme('go-previous', defIcon) else: return defIcon @classmethod def newTabIcon(cls): ''' @return: QIcon ''' return QIcon.fromTheme('tab-new', QIcon(':/icons/menu/tab-new.svg')) @classmethod def newWindowIcon(cls): ''' @return: QIcon ''' return QIcon.fromTheme('window-new', QIcon(':/icons/menu/window-new.svg')) @classmethod def privateBrowsingIcon(cls): ''' @return: QIcon ''' return QIcon.fromTheme('view-private-symbolic', QIcon(':/icons/menu/privatebrowsing.png')) @classmethod def settingsIcon(cls): ''' @return: QIcon ''' return QIcon.fromTheme('configure', QIcon(':/icons/menu/settings.svg')) # Icon for empty page @classmethod def emptyWebIcon(cls): ''' @return: QIcon ''' return QIcon(QPixmap.fromImage(cls.emptyWebImage())) @classmethod def emptyWebImage(cls): ''' @return: QIcon ''' if cls.instance()._emptyWebImage.isNull(): cls.instance()._emptyWebImage = QIcon( ':/icons/other/webpage.svg').pixmap(16).toImage() return cls.instance()._emptyWebImage # Icon for url (only available for urls in history) @classmethod def iconForUrl(cls, url, allowNull=False): ''' @param: url QUrl @return: QIcon ''' return cls.instance()._iconFromImage(cls.imageForUrl(url, allowNull)) @classmethod def imageForUrl(cls, url, allowNull=False): ''' @param: url QUrl @return: QImage ''' if not url.path(): return allowNull and QImage() or cls.emptyWebImage() with cls.instance()._iconCacheMutex: encUrl = encodeUrl(url) # find in urlImageCache img = cls.instance()._urlImageCache.get(encUrl, None) if img: if not img.isNull(): return img if not allowNull: return cls.emptyWebImage() return img # find from icon buffer for url0, img in cls.instance()._iconBuffer: if encodeUrl(url0) == encUrl: return img # TODO: is it necessary to use escapeSqlGlobString escapedUrl = gVar.appTools.escapeSqlGlobString( encUrl.data().decode()) urlPattern = '%s*' % escapedUrl icon = IconsDbModel.select().filter( IconsDbModel.url.contains(urlPattern)).first() img = QImage() if icon: img.loadFromData(icon.icon) cls.instance()._urlImageCache[encUrl] = img if not img.isNull(): return img if not allowNull: return cls.emptyWebImage() return img # Icon for domain (only available for urls in history) @classmethod def iconForDomain(cls, url, allowNull=False): ''' @param: url QUrl @return: QIcon ''' return cls.instance()._iconFromImage(cls.imageForDomain( url, allowNull)) @classmethod def imageForDomain(cls, url, allowNull=False): ''' @param: url QUrl @return: QIcon ''' if not url.host(): if allowNull: return QImage() return cls.emptyWebImage() with cls.instance()._iconCacheMutex: for url0, img in cls.instance()._iconBuffer: if url0.host() == url.host(): return img # TODO: is it necessary to use escapeSqlGlobString escapedHost = gVar.appTools.escapeSqlGlobString(url.host()) hostPattern = '*%s*' % escapedHost icon = IconsDbModel.select().filter( IconsDbModel.url.contains(hostPattern)).first() img = QImage() if icon: img.loadFromData(icon.icon) if not img.isNull(): return img if not allowNull: return cls.emptyWebImage() return img # public Q_SLOTS: def saveIconsToDatabase(self): gVar.executor.submit(self._saveIconsToDatabase) def _saveIconsToDatabase(self): with self._iconCacheMutex: for url, img in self._iconBuffer: ba = QByteArray() buff = QBuffer(ba) buff.open(QIODevice.WriteOnly) img.save(buff, 'PNG') # QByteArray encodedUrl = encodeUrl(url) self._urlImageCache.pop(encodedUrl, None) IconsDbModel.insert(icon=buff.data(), url=encodedUrl.data().decode()) \ .on_conflict('replace') \ .execute() self._iconBuffer.clear() def clearOldIconsInDatabase(self): # Delete icons for entries older than 6 months gVar.executor.submit(self._clearOldIconsInDatabase) def _clearOldIconsInDatabase(self): date = QDateTime.currentDateTime().addMonths(-6) urls = HistoryDbModel.select().where( HistoryDbModel.date < date.toMSecsSinceEpoch()) IconsDbModel.delete().where(IconsDbModel.url.in_(urls)).execute() # private: def _iconFromImage(self, image): ''' @param: image QImage @return: QIcon ''' return QIcon(QPixmap.fromImage(image)) @classmethod def instance(cls): if not cls._instance: cls._instance = cls() return cls._instance
class ClickableLabel(QLabel): def __init__(self, parent=None): super().__init__(parent) self._themeIcon = '' self._fallbackIcon = QIcon() def size(self): return super().size() def setFixedSize(self, sz): super().setFixedSize(sz) fixedsize = pyqtProperty(QSize, size, setFixedSize) def width(self): return super().width() def setFixedWidth(self, width): super().setFixedWidth(width) fixedwidth = pyqtProperty(int, width, setFixedWidth) def height(self): return super().height() def setFixedHeight(self, height): super().setFixedHeight(height) fixedheight = pyqtProperty(int, height, setFixedHeight) def themeIcon(self): ''' @return: QString ''' return self._themeIcon def setThemeIcon(self, name): ''' @param: name QString ''' self._themeIcon = name self._updateIcon() themeIcon = pyqtProperty(str, themeIcon, setThemeIcon) def fallbackIcon(self): ''' @return: QIcon ''' return self._fallbackIcon def setFallbackIcon(self, fallbackIcon): ''' @param: fallbackIcon QIcon ''' self._fallbackIcon = fallbackIcon self._updateIcon() fallbackIcon = pyqtProperty(QIcon, fallbackIcon, setFallbackIcon) # public Q_SIGNALS clicked = pyqtSignal(QPoint) middleClicked = pyqtSignal(QPoint) # private: def _updateIcon(self): if self._themeIcon: icon = QIcon.fromTheme(self._themeIcon) if not icon.isNull(): self.setPixmap(icon.pixmap(self.size())) return if self._fallbackIcon: self.setPixmap(self._fallbackIcon.pixmap(self.size())) # override def resizeEvent(self, event): ''' @param: event QResizeEvent ''' super().resizeEvent(event) self._updateIcon() def mouseReleaseEvent(self, event): ''' @param: event QMouseEvent ''' evtBtn = event.button() if evtBtn == Qt.LeftButton and self.rect().contains(event.pos()): if event.modifiers() == Qt.ControlModifier: self.middleClicked.emit(event.globalPos()) else: self.clicked.emit(event.globalPos()) elif evtBtn == Qt.MiddleButton and self.rect().contains(event.pos()): self.middleClicked.emit(event.globalPos()) else: super().mouseReleaseEvent(event)
class OpenSearchEngine(QObject): # Q_SIGNALS: imageChanged = pyqtSignal() suggestions = pyqtSignal([]) # suggestions # typedef QPair<QString, QString> Parameter # typedef QList<Parameter> Parameters def __init__(self, parent=None): super().__init__(parent) self._name = '' self._description = '' self._imageUrl = '' self._image = QImage() self._searchUrlTemplate = '' self._suggestionsUrlTemplate = '' self._searchParameters = [] # QList<Parameter> self._suggestionsParameters = [] # QList<Parameter> self._searchMethod = '' self._suggestionsMethod = '' self._preparedSuggestionsParameters = QByteArray() self._preparedSuggestionsUrl = '' self._requestMethods = { } # QMap<QString, QNetworkAccessManager::Operation> self._networkAccessManager = None # QNetworkAccessManager self._suggestionsReply = None # QNetworkReply self._delegate = None # OpenSearchEngineDelegate # public: def name(self): ''' @return: QString ''' pass def setName(self, name): ''' @param: name QString ''' pass def description(self): ''' @return: QString ''' pass def setDescription(self, description): ''' @param: description QString ''' pass def searchUrlTemplate(self): ''' @return: QString ''' pass def setSearchUrlTemplate(self, searchUrl): ''' @param: searchUrl QString ''' pass def searchUrl(self, searchTerm): ''' @param: searchTerm QString @return: QUrl ''' pass def getPostData(self, searchTerm): ''' @param: searchTerm QString @return: QByteArray ''' pass def providesSuggestions(self): ''' @return: bool ''' pass def suggestionsUrlTemplate(self): ''' @return: QString ''' pass def setSuggestionsUrlTemplate(self, suggestionsUrl): ''' @param: suggestionsUrl QString ''' pass def suggestionsUrl(self, searchTerm): ''' @param: searchTerm QString ''' pass def searchParameters(self): ''' @return: QList<Parameter> (which Parameter is QList<QString, QString>) ''' pass def setSearchParameters(self, searchParameters): ''' @param: searchParameters QList<Parameter> (which Parameter is QList<QString, QString>) ''' pass def suggestionsParameters(self): ''' @return: QList<Parameter> ''' pass def setSuggestionsParameters(self, suggestionsParameters): ''' @param: suggestionsParameters QList<Parameter> ''' pass def searchMethod(self): ''' @return: QString ''' pass def setSearchMethod(self, method): ''' @param: method QString ''' pass def suggestionsMethod(self): ''' @return: QString ''' pass def setSuggestionsMethod(self, method): ''' @param: method QString ''' pass def imageUrl(self): ''' @return: QString ''' pass def setImageUrl(self, url): ''' @param: url QString ''' pass def image(self): ''' @return: QImage ''' pass def setImage(self, image): ''' @param: image QImage ''' pass def isValid(self): ''' @return: bool ''' pass def setSuggestionsUrl(self, string): ''' @param: string QString ''' pass def setSuggestionsParametersByBytes(self, parameters): ''' @param: parameters QByteArray ''' pass def getSuggestionsUrl(self): ''' @return: QString ''' pass def getSuggestionsParameters(self): ''' @return: QByteArray ''' pass def networkAccessManager(self): ''' @return: QNetworkAccessManager ''' pass def setNetworkAccessManager(self, networkAccessManager): ''' @param: networkAccessManager QNetworkAccessManager ''' pass def delegate(self): ''' @return: OpenSearchEngineDelegate ''' pass def setDelegate(self, delegate): ''' @param: delegate OpenSearchEngineDelegate ''' pass def __eq__(self, other): pass def __lt__(self, other): pass # public Q_SLOTS: def requestSuggestions(self, searchTerm): ''' @param: searchTerm QString ''' pass def requestSearchResults(self, searchTerm): ''' @param: searchTerm QString ''' pass name = pyqtProperty(str, name, setName) description = pyqtProperty(str, description, setDescription) searchUrlTemplate = pyqtProperty(str, searchUrlTemplate, setSearchUrlTemplate) searchParameters = pyqtProperty(list, searchParameters, setSearchParameters) searchMethod = pyqtProperty(str, searchMethod, setSearchMethod) suggestionsUrlTemplate = pyqtProperty(str, suggestionsUrlTemplate, setSuggestionsUrlTemplate) suggestionsParameters = pyqtProperty(list, suggestionsParameters, setSuggestionsParameters) suggestionsMethod = pyqtProperty(str, suggestionsMethod, setSuggestionsMethod) providesSuggestions = pyqtProperty(bool, providesSuggestions) imageUrl = pyqtProperty(str, imageUrl, setImageUrl) valid = pyqtProperty(bool, isValid) networkAccessManager = pyqtProperty(QNetworkAccessManager, networkAccessManager, setNetworkAccessManager) # protected: @staticmethod def _parseTemplate(cls, searchTerm, searchTemplate): pass def _loadImage(self): pass # private Q_SLOTS: def _imageObtained(self): pass def _suggestionsObtained(self): pass
class ExternalJsObject(QObject): _s_extraObjects = {} # QHash<QString, QObject> def __init__(self, page): ''' @param: page WebPage ''' super().__init__(page) self._page = page # WebPage self._autoFill = None # AutoFillJsObject self._autoFill = AutoFillJsObject(self) def page(self): ''' @return: WebPage ''' return self._page @classmethod def setupWebChannel(cls, webChannel, page): ''' @param: webChannel QWebChannel @param: page WebPage ''' webChannel.registerObject('app_object', ExternalJsObject(page)) for key, val in cls._s_extraObjects.items(): webChannel.registerObject('app_' + key, val) @classmethod def registerExtraObject(cls, id_, object_): ''' @param: id_ QString @param: object_ QObject ''' cls._s_extraObjects[id_] = object_ @classmethod def unregisterExtraObject(cls, object_): ''' @param: object_ QObject ''' removeKey = None for key, val in cls._s_extraObjects.items(): if val == object_: removeKey = key break if removeKey is not None: cls._s_extraObjects.pop(removeKey) # private: def _speedDial(self): ''' @return: QObject ''' if self._page.url().toString() != 'app:speeddial': return None return gVar.app.plugins().speedDial() speedDial = pyqtProperty(QObject, _speedDial, constant=True) def _autoFill(self): ''' @return: QObject ''' return self._autoFill autoFill = pyqtProperty(QObject, _autoFill, constant=True) def _recovery(self): ''' @return: QObject ''' if not gVar.app.restoreManager() or self._page.url().toString() != 'app:restore': return None return gVar.app.restoreManager().recoveryObject(self._page) recovery = pyqtProperty(QObject, _recovery, constant=True)
class CoverView(QWidget): # {{{ cover_changed = pyqtSignal(object, object) cover_removed = pyqtSignal(object) open_cover_with = pyqtSignal(object, object) search_internet = pyqtSignal(object) def __init__(self, vertical, parent=None): QWidget.__init__(self, parent) self._current_pixmap_size = QSize(120, 120) self.vertical = vertical self.animation = QPropertyAnimation(self, b'current_pixmap_size', self) self.animation.setEasingCurve(QEasingCurve(QEasingCurve.OutExpo)) self.animation.setDuration(1000) self.animation.setStartValue(QSize(0, 0)) self.animation.valueChanged.connect(self.value_changed) self.setSizePolicy( QSizePolicy.Expanding if vertical else QSizePolicy.Minimum, QSizePolicy.Expanding) self.default_pixmap = QPixmap(I('default_cover.png')) self.pixmap = self.default_pixmap self.pwidth = self.pheight = None self.data = {} self.do_layout() def value_changed(self, val): self.update() def setCurrentPixmapSize(self, val): self._current_pixmap_size = val def do_layout(self): if self.rect().width() == 0 or self.rect().height() == 0: return pixmap = self.pixmap pwidth, pheight = pixmap.width(), pixmap.height() try: self.pwidth, self.pheight = fit_image(pwidth, pheight, self.rect().width(), self.rect().height())[1:] except: self.pwidth, self.pheight = self.rect().width()-1, \ self.rect().height()-1 self.current_pixmap_size = QSize(self.pwidth, self.pheight) self.animation.setEndValue(self.current_pixmap_size) def show_data(self, data): self.animation.stop() same_item = getattr(data, 'id', True) == self.data.get('id', False) self.data = {'id':data.get('id', None)} if data.cover_data[1]: self.pixmap = QPixmap.fromImage(data.cover_data[1]) if self.pixmap.isNull() or self.pixmap.width() < 5 or \ self.pixmap.height() < 5: self.pixmap = self.default_pixmap else: self.pixmap = self.default_pixmap self.do_layout() self.update() if (not same_item and not config['disable_animations'] and self.isVisible()): self.animation.start() def paintEvent(self, event): canvas_size = self.rect() width = self.current_pixmap_size.width() extrax = canvas_size.width() - width if extrax < 0: extrax = 0 x = int(extrax/2.) height = self.current_pixmap_size.height() extray = canvas_size.height() - height if extray < 0: extray = 0 y = int(extray/2.) target = QRect(x, y, width, height) p = QPainter(self) p.setRenderHints(QPainter.Antialiasing | QPainter.SmoothPixmapTransform) try: dpr = self.devicePixelRatioF() except AttributeError: dpr = self.devicePixelRatio() spmap = self.pixmap.scaled(target.size() * dpr, Qt.KeepAspectRatio, Qt.SmoothTransformation) spmap.setDevicePixelRatio(dpr) p.drawPixmap(target, spmap) if gprefs['bd_overlay_cover_size']: sztgt = target.adjusted(0, 0, 0, -4) f = p.font() f.setBold(True) p.setFont(f) sz = u'\u00a0%d x %d\u00a0'%(self.pixmap.width(), self.pixmap.height()) flags = Qt.AlignBottom|Qt.AlignRight|Qt.TextSingleLine szrect = p.boundingRect(sztgt, flags, sz) p.fillRect(szrect.adjusted(0, 0, 0, 4), QColor(0, 0, 0, 200)) p.setPen(QPen(QColor(255,255,255))) p.drawText(sztgt, flags, sz) p.end() current_pixmap_size = pyqtProperty('QSize', fget=lambda self: self._current_pixmap_size, fset=setCurrentPixmapSize ) def contextMenuEvent(self, ev): from calibre.gui2.open_with import populate_menu, edit_programs cm = QMenu(self) paste = cm.addAction(_('Paste cover')) copy = cm.addAction(_('Copy cover')) remove = cm.addAction(_('Remove cover')) gc = cm.addAction(_('Generate cover from metadata')) cm.addSeparator() if not QApplication.instance().clipboard().mimeData().hasImage(): paste.setEnabled(False) copy.triggered.connect(self.copy_to_clipboard) paste.triggered.connect(self.paste_from_clipboard) remove.triggered.connect(self.remove_cover) gc.triggered.connect(self.generate_cover) m = QMenu(_('Open cover with...')) populate_menu(m, self.open_with, 'cover_image') if len(m.actions()) == 0: cm.addAction(_('Open cover with...'), self.choose_open_with) else: m.addSeparator() m.addAction(_('Add another application to open cover...'), self.choose_open_with) m.addAction(_('Edit Open with applications...'), partial(edit_programs, 'cover_image', self)) cm.ocw = m cm.addMenu(m) cm.si = m = create_search_internet_menu(self.search_internet.emit) cm.addMenu(m) cm.exec_(ev.globalPos()) def open_with(self, entry): id_ = self.data.get('id', None) if id_ is not None: self.open_cover_with.emit(id_, entry) def choose_open_with(self): from calibre.gui2.open_with import choose_program entry = choose_program('cover_image', self) if entry is not None: self.open_with(entry) def copy_to_clipboard(self): QApplication.instance().clipboard().setPixmap(self.pixmap) def paste_from_clipboard(self, pmap=None): if not isinstance(pmap, QPixmap): cb = QApplication.instance().clipboard() pmap = cb.pixmap() if pmap.isNull() and cb.supportsSelection(): pmap = cb.pixmap(cb.Selection) if not pmap.isNull(): self.update_cover(pmap) def update_cover(self, pmap=None, cdata=None): if pmap is None: pmap = QPixmap() pmap.loadFromData(cdata) if pmap.isNull(): return if pmap.hasAlphaChannel(): pmap = QPixmap.fromImage(blend_image(image_from_x(pmap))) self.pixmap = pmap self.do_layout() self.update() self.update_tooltip(getattr(self.parent(), 'current_path', '')) if not config['disable_animations']: self.animation.start() id_ = self.data.get('id', None) if id_ is not None: self.cover_changed.emit(id_, cdata or pixmap_to_data(pmap)) def generate_cover(self, *args): book_id = self.data.get('id') if book_id is not None: from calibre.ebooks.covers import generate_cover from calibre.gui2.ui import get_gui mi = get_gui().current_db.new_api.get_metadata(book_id) cdata = generate_cover(mi) self.update_cover(cdata=cdata) def remove_cover(self): id_ = self.data.get('id', None) self.pixmap = self.default_pixmap self.do_layout() self.update() if id_ is not None: self.cover_removed.emit(id_) def update_tooltip(self, current_path): try: sz = self.pixmap.size() except: sz = QSize(0, 0) self.setToolTip( '<p>'+_('Double click to open the Book details window') + '<br><br>' + _('Path') + ': ' + current_path + '<br><br>' + _('Cover size: %(width)d x %(height)d pixels')%dict( width=sz.width(), height=sz.height()) )
class FancyTab(QWidget): def __init__(self, tabbar): ''' @param: tabbar QWidget ''' super().__init__(tabbar) self.icon = QIcon() self.text = '' self._animator = QPropertyAnimation() self._tabbar = tabbar # QWidget self._fader = 0.0 self._animator.setPropertyName(b'fader') self._animator.setTargetObject(self) self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Maximum) def fader(self): ''' @return: float ''' return self._fader def setFader(self, value): ''' @param: value float ''' self._fader = value self._tabbar.update() fader = pyqtProperty(float, fader, setFader) # override def sizeHint(self): ''' @return: QSize ''' boldFont = QFont(self.font()) boldFont.setPointSizeF(styleHelper.sidebarFontSize()) boldFont.setBold(True) fm = QFontMetrics(boldFont) spacing = 8 width = 60 + spacing + 2 iconHeight = 32 ret = QSize(width, iconHeight + spacing + fm.height()) return ret def fadeIn(self): self._animator.stop() self._animator.setDuration(80) self._animator.setEndValue(40) self._animator.start() def fadeOut(self): self._animator.stop() self._animator.setDuration(160) self._animator.setEndValue(0) self._animator.start() # protected: # override def enterEvent(self, event): self.fadeIn() # override def leaveEvent(self, event): self.fadeOut()