def __init__(self, shortcuts, parent=None, debug_javascript=False): QWebPage.__init__(self, parent) self.setObjectName("py_bridge") self.in_paged_mode = False # Use this to pass arbitrary JSON encodable objects between python and # javascript. In python get/set the value as: self.bridge_value. In # javascript, get/set the value as: py_bridge.value self.bridge_value = None self.first_load = True self.debug_javascript = debug_javascript self.anchor_positions = {} self.index_anchors = set() self.current_language = None self.loaded_javascript = False self.js_loader = JavaScriptLoader( dynamic_coffeescript=self.debug_javascript) self.in_fullscreen_mode = False self.setLinkDelegationPolicy(self.DelegateAllLinks) self.scroll_marks = [] self.shortcuts = shortcuts pal = self.palette() pal.setBrush(QPalette.Background, QColor(0xee, 0xee, 0xee)) self.setPalette(pal) self.page_position = PagePosition(self) settings = self.settings() # Fonts self.all_viewer_plugins = tuple(all_viewer_plugins()) for pl in self.all_viewer_plugins: pl.load_fonts() opts = config().parse() self.set_font_settings(opts) # Security settings.setAttribute(QWebSettings.JavaEnabled, False) settings.setAttribute(QWebSettings.PluginsEnabled, False) settings.setAttribute(QWebSettings.JavascriptCanOpenWindows, False) settings.setAttribute(QWebSettings.JavascriptCanAccessClipboard, False) # Miscellaneous settings.setAttribute(QWebSettings.LinksIncludedInFocusChain, True) settings.setAttribute(QWebSettings.DeveloperExtrasEnabled, True) self.set_user_stylesheet(opts) self.misc_config(opts) # Load javascript self.mainFrame().javaScriptWindowObjectCleared.connect( self.add_window_objects) self.turn_off_internal_scrollbars()
def __init__(self, shortcuts, parent=None, debug_javascript=False): QWebPage.__init__(self, parent) self.setObjectName("py_bridge") self.in_paged_mode = False # Use this to pass arbitrary JSON encodable objects between python and # javascript. In python get/set the value as: self.bridge_value. In # javascript, get/set the value as: py_bridge.value self.bridge_value = None self.first_load = True self.debug_javascript = debug_javascript self.anchor_positions = {} self.index_anchors = set() self.current_language = None self.loaded_javascript = False self.js_loader = JavaScriptLoader( dynamic_coffeescript=self.debug_javascript) self.in_fullscreen_mode = False self.setLinkDelegationPolicy(self.DelegateAllLinks) self.scroll_marks = [] self.shortcuts = shortcuts pal = self.palette() pal.setBrush(QPalette.Background, QColor(0xee, 0xee, 0xee)) self.setPalette(pal) self.page_position = PagePosition(self) settings = self.settings() # Fonts load_builtin_fonts() self.all_viewer_plugins = tuple(all_viewer_plugins()) for pl in self.all_viewer_plugins: pl.load_fonts() opts = config().parse() self.set_font_settings(opts) # Security settings.setAttribute(QWebSettings.JavaEnabled, False) settings.setAttribute(QWebSettings.PluginsEnabled, False) settings.setAttribute(QWebSettings.JavascriptCanOpenWindows, False) settings.setAttribute(QWebSettings.JavascriptCanAccessClipboard, False) # Miscellaneous settings.setAttribute(QWebSettings.LinksIncludedInFocusChain, True) settings.setAttribute(QWebSettings.DeveloperExtrasEnabled, True) self.set_user_stylesheet(opts) self.misc_config(opts) # Load javascript self.mainFrame().javaScriptWindowObjectCleared.connect( self.add_window_objects) self.turn_off_internal_scrollbars()
class Document(QWebPage): # {{{ page_turn = pyqtSignal(object) mark_element = pyqtSignal(QWebElement) settings_changed = pyqtSignal() def set_font_settings(self, opts): settings = self.settings() apply_settings(settings, opts) def do_config(self, parent=None): d = ConfigDialog(self.shortcuts, parent) if d.exec_() == QDialog.Accepted: opts = config().parse() self.apply_settings(opts) def apply_settings(self, opts): with self.page_position: self.set_font_settings(opts) self.set_user_stylesheet(opts) self.misc_config(opts) self.settings_changed.emit() self.after_load() def __init__(self, shortcuts, parent=None, debug_javascript=False): QWebPage.__init__(self, parent) self.setObjectName("py_bridge") self.in_paged_mode = False # Use this to pass arbitrary JSON encodable objects between python and # javascript. In python get/set the value as: self.bridge_value. In # javascript, get/set the value as: py_bridge.value self.bridge_value = None self.first_load = True self.debug_javascript = debug_javascript self.anchor_positions = {} self.index_anchors = set() self.current_language = None self.loaded_javascript = False self.js_loader = JavaScriptLoader( dynamic_coffeescript=self.debug_javascript) self.in_fullscreen_mode = False self.setLinkDelegationPolicy(self.DelegateAllLinks) self.scroll_marks = [] self.shortcuts = shortcuts pal = self.palette() pal.setBrush(QPalette.Background, QColor(0xee, 0xee, 0xee)) self.setPalette(pal) self.page_position = PagePosition(self) settings = self.settings() # Fonts self.all_viewer_plugins = tuple(all_viewer_plugins()) for pl in self.all_viewer_plugins: pl.load_fonts() opts = config().parse() self.set_font_settings(opts) # Security settings.setAttribute(QWebSettings.JavaEnabled, False) settings.setAttribute(QWebSettings.PluginsEnabled, False) settings.setAttribute(QWebSettings.JavascriptCanOpenWindows, False) settings.setAttribute(QWebSettings.JavascriptCanAccessClipboard, False) # Miscellaneous settings.setAttribute(QWebSettings.LinksIncludedInFocusChain, True) settings.setAttribute(QWebSettings.DeveloperExtrasEnabled, True) self.set_user_stylesheet(opts) self.misc_config(opts) # Load javascript self.mainFrame().javaScriptWindowObjectCleared.connect( self.add_window_objects) self.turn_off_internal_scrollbars() def turn_off_internal_scrollbars(self): mf = self.mainFrame() mf.setScrollBarPolicy(Qt.Vertical, Qt.ScrollBarAlwaysOff) mf.setScrollBarPolicy(Qt.Horizontal, Qt.ScrollBarAlwaysOff) def set_user_stylesheet(self, opts): bg = opts.background_color or 'white' brules = ['background-color: %s !important'%bg] prefix = ''' body { %s } '''%('; '.join(brules)) if opts.text_color: prefix += '\n\nbody, p, div { color: %s !important }'%opts.text_color raw = prefix + opts.user_css raw = '::selection {background:#ffff00; color:#000;}\n'+raw data = 'data:text/css;charset=utf-8;base64,' data += b64encode(raw.encode('utf-8')) self.settings().setUserStyleSheetUrl(QUrl(data)) def findText(self, q, flags): if self.hyphenatable: q = unicode(q) hyphenated_q = self.javascript( 'hyphenate_text(%s, "%s")' % (json.dumps(q, ensure_ascii=False), self.loaded_lang), typ='string') if QWebPage.findText(self, hyphenated_q, flags): return True return QWebPage.findText(self, q, flags) def misc_config(self, opts): self.hyphenate = opts.hyphenate self.hyphenate_default_lang = opts.hyphenate_default_lang self.do_fit_images = opts.fit_images self.page_flip_duration = opts.page_flip_duration self.enable_page_flip = self.page_flip_duration > 0.1 self.font_magnification_step = opts.font_magnification_step self.wheel_flips_pages = opts.wheel_flips_pages self.line_scrolling_stops_on_pagebreaks = opts.line_scrolling_stops_on_pagebreaks screen_width = QApplication.desktop().screenGeometry().width() # Leave some space for the scrollbar and some border self.max_fs_width = min(opts.max_fs_width, screen_width-50) self.fullscreen_clock = opts.fullscreen_clock self.fullscreen_scrollbar = opts.fullscreen_scrollbar self.fullscreen_pos = opts.fullscreen_pos self.start_in_fullscreen = opts.start_in_fullscreen self.show_fullscreen_help = opts.show_fullscreen_help self.use_book_margins = opts.use_book_margins self.cols_per_screen = opts.cols_per_screen self.side_margin = opts.side_margin self.top_margin, self.bottom_margin = opts.top_margin, opts.bottom_margin self.show_controls = opts.show_controls def fit_images(self): if self.do_fit_images and not self.in_paged_mode: self.javascript('setup_image_scaling_handlers()') def add_window_objects(self): self.mainFrame().addToJavaScriptWindowObject("py_bridge", self) self.javascript(''' 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); }); ''') self.loaded_javascript = False def load_javascript_libraries(self): if self.loaded_javascript: return self.loaded_javascript = True evaljs = self.mainFrame().evaluateJavaScript self.loaded_lang = self.js_loader(evaljs, self.current_language, self.hyphenate_default_lang) mjpath = P(u'viewer/mathjax').replace(os.sep, '/') if iswindows: mjpath = u'/' + mjpath self.javascript(u'window.mathjax.base = %s'%(json.dumps(mjpath, ensure_ascii=False))) for pl in self.all_viewer_plugins: pl.load_javascript(evaljs) evaljs('py_bridge.mark_element.connect(window.calibre_extract.mark)') @pyqtSignature("") def animated_scroll_done(self): self.emit(SIGNAL('animated_scroll_done()')) @property def hyphenatable(self): # Qt fails to render soft hyphens correctly on windows xp return not isxp and self.hyphenate and getattr(self, 'loaded_lang', '') @pyqtSignature("") def init_hyphenate(self): if self.hyphenatable: self.javascript('do_hyphenation("%s")'%self.loaded_lang) @pyqtSlot(int) def page_turn_requested(self, backwards): self.page_turn.emit(bool(backwards)) def _pass_json_value_getter(self): val = json.dumps(self.bridge_value) return QString(val) def _pass_json_value_setter(self, value): self.bridge_value = json.loads(unicode(value)) _pass_json_value = pyqtProperty(QString, fget=_pass_json_value_getter, fset=_pass_json_value_setter) def after_load(self, last_loaded_path=None): self.javascript('window.paged_display.read_document_margins()') self.set_bottom_padding(0) self.fit_images() self.init_hyphenate() self.javascript('full_screen.save_margins()') if self.in_fullscreen_mode: self.switch_to_fullscreen_mode() if self.in_paged_mode: self.switch_to_paged_mode(last_loaded_path=last_loaded_path) self.read_anchor_positions(use_cache=False) evaljs = self.mainFrame().evaluateJavaScript for pl in self.all_viewer_plugins: pl.run_javascript(evaljs) self.javascript('window.mathjax.check_for_math()') self.first_load = False def colors(self): self.javascript(''' bs = getComputedStyle(document.body); py_bridge.value = [bs.backgroundColor, bs.color] ''') ans = self.bridge_value return (ans if isinstance(ans, list) else ['white', 'black']) def read_anchor_positions(self, use_cache=True): self.bridge_value = tuple(self.index_anchors) self.javascript(u''' py_bridge.value = book_indexing.anchor_positions(py_bridge.value, %s); '''%('true' if use_cache else 'false')) self.anchor_positions = self.bridge_value if not isinstance(self.anchor_positions, dict): # Some weird javascript error happened self.anchor_positions = {} return {k:tuple(v) for k, v in self.anchor_positions.iteritems()} def switch_to_paged_mode(self, onresize=False, last_loaded_path=None): if onresize and not self.loaded_javascript: return self.javascript(''' window.paged_display.use_document_margins = %s; window.paged_display.set_geometry(%d, %d, %d, %d); '''%( ('true' if self.use_book_margins else 'false'), self.cols_per_screen, self.top_margin, self.side_margin, self.bottom_margin )) force_fullscreen_layout = bool(getattr(last_loaded_path, 'is_single_page', False)) f = 'true' if force_fullscreen_layout else 'false' side_margin = self.javascript('window.paged_display.layout(%s)'%f, typ=int) # Setup the contents size to ensure that there is a right most margin. # Without this WebKit renders the final column with no margin, as the # columns extend beyond the boundaries (and margin) of body mf = self.mainFrame() sz = mf.contentsSize() scroll_width = self.javascript('document.body.scrollWidth', int) # At this point sz.width() is not reliable, presumably because Qt # has not yet been updated if scroll_width > self.window_width: sz.setWidth(scroll_width+side_margin) self.setPreferredContentsSize(sz) self.javascript('window.paged_display.fit_images()') @property def column_boundaries(self): if not self.loaded_javascript: return (0, 1) self.javascript(u'py_bridge.value = paged_display.column_boundaries()') return tuple(self.bridge_value) def after_resize(self): if self.in_paged_mode: self.setPreferredContentsSize(QSize()) self.switch_to_paged_mode(onresize=True) self.javascript('window.mathjax.after_resize()') def switch_to_fullscreen_mode(self): self.in_fullscreen_mode = True self.javascript('full_screen.on(%d, %s)'%(self.max_fs_width, 'true' if self.in_paged_mode else 'false')) def switch_to_window_mode(self): self.in_fullscreen_mode = False self.javascript('full_screen.off(%s)'%('true' if self.in_paged_mode else 'false')) @pyqtSignature("QString") def debug(self, msg): prints(msg) def reference_mode(self, enable): self.javascript(('enter' if enable else 'leave')+'_reference_mode()') def set_reference_prefix(self, prefix): self.javascript('reference_prefix = "%s"'%prefix) def goto(self, ref): self.javascript('goto_reference("%s")'%ref) def goto_bookmark(self, bm): if bm['type'] == 'legacy': bm = bm['pos'] bm = bm.strip() if bm.startswith('>'): bm = bm[1:].strip() self.javascript('scroll_to_bookmark("%s")'%bm) elif bm['type'] == 'cfi': self.page_position.to_pos(bm['pos']) def javascript(self, string, typ=None): ans = self.mainFrame().evaluateJavaScript(string) if typ in {'int', int}: ans = ans.toInt() if ans[1]: return ans[0] return 0 if typ in {'float', float}: ans = ans.toReal() return ans[0] if ans[1] else 0.0 if typ == 'string': return unicode(ans.toString()) return ans def javaScriptConsoleMessage(self, msg, lineno, msgid): if self.debug_javascript: prints(msg) else: return QWebPage.javaScriptConsoleMessage(self, msg, lineno, msgid) def javaScriptAlert(self, frame, msg): if self.debug_javascript: prints(msg) else: return QWebPage.javaScriptAlert(self, frame, msg) def scroll_by(self, dx=0, dy=0): self.mainFrame().scroll(dx, dy) def scroll_to(self, x=0, y=0): self.mainFrame().setScrollPosition(QPoint(x, y)) def jump_to_anchor(self, anchor): if not self.loaded_javascript: return self.javascript('window.paged_display.jump_to_anchor("%s")'%anchor) def element_ypos(self, elem): ans, ok = elem.evaluateJavaScript('$(this).offset().top').toInt() if not ok: raise ValueError('No ypos found') return ans def elem_outer_xml(self, elem): return unicode(elem.toOuterXml()) def bookmark(self): pos = self.page_position.current_pos return {'type':'cfi', 'pos':pos} @property def at_bottom(self): return self.height - self.ypos <= self.window_height @property def at_top(self): return self.ypos <=0 def test(self): pass @property def ypos(self): return self.mainFrame().scrollPosition().y() @property def window_height(self): return self.javascript('window.innerHeight', 'int') @property def window_width(self): return self.javascript('window.innerWidth', 'int') @property def xpos(self): return self.mainFrame().scrollPosition().x() @dynamic_property def scroll_fraction(self): def fget(self): if self.in_paged_mode: return self.javascript(''' ans = 0.0; if (window.paged_display) { ans = window.paged_display.current_pos(); } ans;''', typ='float') else: try: return abs(float(self.ypos)/(self.height-self.window_height)) except ZeroDivisionError: return 0. def fset(self, val): if self.in_paged_mode and self.loaded_javascript: self.javascript('paged_display.scroll_to_pos(%f)'%val) else: npos = val * (self.height - self.window_height) if npos < 0: npos = 0 self.scroll_to(x=self.xpos, y=npos) return property(fget=fget, fset=fset) @property def hscroll_fraction(self): try: return float(self.xpos)/self.width except ZeroDivisionError: return 0. @property def height(self): # Note that document.body.offsetHeight does not include top and bottom # margins on body and in some cases does not include the top margin on # the first element inside body either. See ticket #8791 for an example # of the latter. q = self.mainFrame().contentsSize().height() if q < 0: # Don't know if this is still needed, but it can't hurt j = self.javascript('document.body.offsetHeight', 'int') if j >= 0: q = j return q @property def width(self): return self.mainFrame().contentsSize().width() # offsetWidth gives inaccurate results def set_bottom_padding(self, amount): s = QSize(-1, -1) if amount == 0 else QSize(self.viewportSize().width(), self.height+amount) self.setPreferredContentsSize(s) def extract_node(self): return unicode(self.mainFrame().evaluateJavaScript( 'window.calibre_extract.extract()').toString())