def create_action(self, spec=None, attr='qaction', shortcut_name=None): if spec is None: spec = self.action_spec text, icon, tooltip, shortcut = spec if icon is not None: action = QAction(QIcon(I(icon)), text, self.gui) else: action = QAction(text, self.gui) if attr == 'qaction': mt = (action.text() if self.action_menu_clone_qaction is True else unicode(self.action_menu_clone_qaction)) self.menuless_qaction = ma = QAction(action.icon(), mt, self.gui) ma.triggered.connect(action.trigger) for a in ((action, ma) if attr == 'qaction' else (action, )): a.setAutoRepeat(self.auto_repeat) text = tooltip if tooltip else text a.setToolTip(text) a.setStatusTip(text) a.setWhatsThis(text) shortcut_action = action desc = tooltip if tooltip else None if attr == 'qaction': shortcut_action = ma if shortcut is not None: keys = ((shortcut, ) if isinstance(shortcut, basestring) else tuple(shortcut)) if shortcut_name is None and spec[0]: shortcut_name = unicode(spec[0]) if shortcut_name and self.action_spec[0] and not ( attr == 'qaction' and self.popup_type == QToolButton.InstantPopup): try: self.gui.keyboard.register_shortcut( self.unique_name + ' - ' + attr, shortcut_name, default_keys=keys, action=shortcut_action, description=desc, group=self.action_spec[0]) except NameConflict as e: try: prints(unicode(e)) except: pass shortcut_action.setShortcuts([ QKeySequence(key, QKeySequence.PortableText) for key in keys ]) if attr is not None: setattr(self, attr, action) if attr == 'qaction' and self.action_add_menu: menu = QMenu() action.setMenu(menu) if self.action_menu_clone_qaction: menu.addAction(self.menuless_qaction) return action
class MenuBar(QMenuBar): # {{{ def __init__(self, location_manager, parent): QMenuBar.__init__(self, parent) self.gui = parent self.setNativeMenuBar(True) self.location_manager = location_manager self.added_actions = [] self.donate_action = QAction(_('Donate'), self) self.donate_menu = QMenu() self.donate_menu.addAction(self.gui.donate_action) self.donate_action.setMenu(self.donate_menu) def update_lm_actions(self): for ac in self.added_actions: clone = getattr(ac, 'clone', None) if clone is not None and clone in self.location_manager.all_actions: ac.setVisible(clone in self.location_manager.available_actions) def init_bar(self, actions): for ac in self.added_actions: m = ac.menu() if m is not None: m.setVisible(False) self.clear() self.added_actions = [] for what in actions: if what is None: continue elif what == 'Location Manager': for ac in self.location_manager.all_actions: ac = self.build_menu(ac) self.addAction(ac) self.added_actions.append(ac) ac.setVisible(False) elif what == 'Donate': self.addAction(self.donate_action) elif what in self.gui.iactions: action = self.gui.iactions[what] ac = self.build_menu(action.qaction) self.addAction(ac) self.added_actions.append(ac) def build_menu(self, action): m = action.menu() ac = MenuAction(action, self) if m is None: m = QMenu() m.addAction(action) ac.setMenu(m) return ac
def create_action(self, spec=None, attr='qaction', shortcut_name=None): if spec is None: spec = self.action_spec text, icon, tooltip, shortcut = spec if icon is not None: action = QAction(QIcon(I(icon)), text, self.gui) else: action = QAction(text, self.gui) if attr == 'qaction': mt = (action.text() if self.action_menu_clone_qaction is True else unicode(self.action_menu_clone_qaction)) self.menuless_qaction = ma = QAction(action.icon(), mt, self.gui) ma.triggered.connect(action.trigger) for a in ((action, ma) if attr == 'qaction' else (action,)): a.setAutoRepeat(self.auto_repeat) text = tooltip if tooltip else text a.setToolTip(text) a.setStatusTip(text) a.setWhatsThis(text) shortcut_action = action desc = tooltip if tooltip else None if attr == 'qaction': shortcut_action = ma if shortcut is not None: keys = ((shortcut,) if isinstance(shortcut, basestring) else tuple(shortcut)) if shortcut_name is None and spec[0]: shortcut_name = unicode(spec[0]) if shortcut_name and self.action_spec[0] and not ( attr == 'qaction' and self.popup_type == QToolButton.InstantPopup): try: self.gui.keyboard.register_shortcut(self.unique_name + ' - ' + attr, shortcut_name, default_keys=keys, action=shortcut_action, description=desc, group=self.action_spec[0]) except NameConflict as e: try: prints(unicode(e)) except: pass shortcut_action.setShortcuts([QKeySequence(key, QKeySequence.PortableText) for key in keys]) if attr is not None: setattr(self, attr, action) if attr == 'qaction' and self.action_add_menu: menu = QMenu() action.setMenu(menu) if self.action_menu_clone_qaction: menu.addAction(self.menuless_qaction) return action
class DocumentView(QWebView): # {{{ magnification_changed = pyqtSignal(object) DISABLED_BRUSH = QBrush(Qt.lightGray, Qt.Dense5Pattern) def initialize_view(self, debug_javascript=False): self.setRenderHints(QPainter.Antialiasing|QPainter.TextAntialiasing|QPainter.SmoothPixmapTransform) self.flipper = SlideFlip(self) self.is_auto_repeat_event = False self.debug_javascript = debug_javascript self.shortcuts = Shortcuts(SHORTCUTS, 'shortcuts/viewer') self.setSizePolicy(QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)) self._size_hint = QSize(510, 680) self.initial_pos = 0.0 self.to_bottom = False self.document = Document(self.shortcuts, parent=self, debug_javascript=debug_javascript) self.setPage(self.document) self.inspector = WebInspector(self, self.document) self.manager = None self._reference_mode = False self._ignore_scrollbar_signals = False self.loading_url = None self.loadFinished.connect(self.load_finished) self.connect(self.document, SIGNAL('linkClicked(QUrl)'), self.link_clicked) self.connect(self.document, SIGNAL('linkHovered(QString,QString,QString)'), self.link_hovered) self.connect(self.document, SIGNAL('selectionChanged()'), self.selection_changed) self.connect(self.document, SIGNAL('animated_scroll_done()'), self.animated_scroll_done, Qt.QueuedConnection) self.document.page_turn.connect(self.page_turn_requested) copy_action = self.pageAction(self.document.Copy) copy_action.setIcon(QIcon(I('convert.png'))) d = self.document self.unimplemented_actions = list(map(self.pageAction, [d.DownloadImageToDisk, d.OpenLinkInNewWindow, d.DownloadLinkToDisk, d.OpenImageInNewWindow, d.OpenLink, d.Reload, d.InspectElement])) self.search_online_action = QAction(QIcon(I('search.png')), '', self) self.search_online_action.triggered.connect(self.search_online) self.addAction(self.search_online_action) self.dictionary_action = QAction(QIcon(I('dictionary.png')), _('&Lookup in dictionary'), self) self.dictionary_action.triggered.connect(self.lookup) self.addAction(self.dictionary_action) self.image_popup = ImagePopup(self) self.table_popup = TablePopup(self) self.view_image_action = QAction(QIcon(I('view-image.png')), _('View &image...'), self) self.view_image_action.triggered.connect(self.image_popup) self.view_table_action = QAction(QIcon(I('view.png')), _('View &table...'), self) self.view_table_action.triggered.connect(self.popup_table) self.search_action = QAction(QIcon(I('dictionary.png')), _('&Search for next occurrence'), self) self.search_action.triggered.connect(self.search_next) self.addAction(self.search_action) self.goto_location_action = QAction(_('Go to...'), self) self.goto_location_menu = m = QMenu(self) self.goto_location_actions = a = { 'Next Page': self.next_page, 'Previous Page': self.previous_page, 'Section Top' : partial(self.scroll_to, 0), 'Document Top': self.goto_document_start, 'Section Bottom':partial(self.scroll_to, 1), 'Document Bottom': self.goto_document_end, 'Next Section': self.goto_next_section, 'Previous Section': self.goto_previous_section, } for name, key in [(_('Next Section'), 'Next Section'), (_('Previous Section'), 'Previous Section'), (None, None), (_('Document Start'), 'Document Top'), (_('Document End'), 'Document Bottom'), (None, None), (_('Section Start'), 'Section Top'), (_('Section End'), 'Section Bottom'), (None, None), (_('Next Page'), 'Next Page'), (_('Previous Page'), 'Previous Page')]: if key is None: m.addSeparator() else: m.addAction(name, a[key], self.shortcuts.get_sequences(key)[0]) self.goto_location_action.setMenu(self.goto_location_menu) self.grabGesture(Qt.SwipeGesture) self.restore_fonts_action = QAction(_('Default font size'), self) self.restore_fonts_action.setCheckable(True) self.restore_fonts_action.triggered.connect(self.restore_font_size) def goto_next_section(self, *args): if self.manager is not None: self.manager.goto_next_section() def goto_previous_section(self, *args): if self.manager is not None: self.manager.goto_previous_section() def goto_document_start(self, *args): if self.manager is not None: self.manager.goto_start() def goto_document_end(self, *args): if self.manager is not None: self.manager.goto_end() @property def copy_action(self): return self.pageAction(self.document.Copy) def animated_scroll_done(self): if self.manager is not None: self.manager.scrolled(self.document.scroll_fraction) def reference_mode(self, enable): self._reference_mode = enable self.document.reference_mode(enable) def goto(self, ref): self.document.goto(ref) def goto_bookmark(self, bm): self.document.goto_bookmark(bm) def config(self, parent=None): self.document.do_config(parent) if self.document.in_fullscreen_mode: self.document.switch_to_fullscreen_mode() self.setFocus(Qt.OtherFocusReason) def load_theme(self, theme_id): themes = load_themes() theme = themes[theme_id] opts = config(theme).parse() self.document.apply_settings(opts) if self.document.in_fullscreen_mode: self.document.switch_to_fullscreen_mode() self.setFocus(Qt.OtherFocusReason) def bookmark(self): return self.document.bookmark() def selection_changed(self): if self.manager is not None: self.manager.selection_changed(unicode(self.document.selectedText())) def _selectedText(self): t = unicode(self.selectedText()).strip() if not t: return u'' if len(t) > 40: t = t[:40] + u'...' t = t.replace(u'&', u'&&') return _("S&earch Google for '%s'")%t def popup_table(self): html = self.document.extract_node() self.table_popup(html, QUrl.fromLocalFile(self.last_loaded_path), self.document.font_magnification_step) def contextMenuEvent(self, ev): mf = self.document.mainFrame() r = mf.hitTestContent(ev.pos()) img = r.pixmap() elem = r.element() if elem.isNull(): elem = r.enclosingBlockElement() table = None parent = elem while not parent.isNull(): if (unicode(parent.tagName()) == u'table' or unicode(parent.localName()) == u'table'): table = parent break parent = parent.parent() self.image_popup.current_img = img self.image_popup.current_url = r.imageUrl() menu = self.document.createStandardContextMenu() for action in self.unimplemented_actions: menu.removeAction(action) if not img.isNull(): menu.addAction(self.view_image_action) if table is not None: self.document.mark_element.emit(table) menu.addAction(self.view_table_action) text = self._selectedText() if text and img.isNull(): self.search_online_action.setText(text) for x, sc in (('search_online', 'Search online'), ('dictionary', 'Lookup word'), ('search', 'Next occurrence')): ac = getattr(self, '%s_action' % x) menu.addAction(ac.icon(), '%s [%s]' % (unicode(ac.text()), ','.join(self.shortcuts.get_shortcuts(sc))), ac.trigger) if not text and img.isNull(): menu.addSeparator() if self.manager.action_back.isEnabled(): menu.addAction(self.manager.action_back) if self.manager.action_forward.isEnabled(): menu.addAction(self.manager.action_forward) menu.addAction(self.goto_location_action) if self.manager is not None: menu.addSeparator() menu.addAction(self.manager.action_table_of_contents) menu.addSeparator() menu.addAction(self.manager.action_font_size_larger) self.restore_fonts_action.setChecked(self.multiplier == 1) menu.addAction(self.restore_fonts_action) menu.addAction(self.manager.action_font_size_smaller) menu.addSeparator() menu.addAction(_('Inspect'), self.inspect) if not text and img.isNull() and self.manager is not None: menu.addSeparator() if (not self.document.show_controls or self.document.in_fullscreen_mode) and self.manager is not None: menu.addAction(self.manager.toggle_toolbar_action) menu.addAction(self.manager.action_full_screen) menu.addSeparator() menu.addAction(self.manager.action_quit) menu.exec_(ev.globalPos()) def inspect(self): self.inspector.show() self.inspector.raise_() self.pageAction(self.document.InspectElement).trigger() def lookup(self, *args): if self.manager is not None: t = unicode(self.selectedText()).strip() if t: self.manager.lookup(t.split()[0]) def search_next(self): if self.manager is not None: t = unicode(self.selectedText()).strip() if t: self.manager.search.set_search_string(t) def search_online(self): t = unicode(self.selectedText()).strip() if t: url = 'https://www.google.com/search?q=' + QUrl().toPercentEncoding(t) open_url(QUrl.fromEncoded(url)) def set_manager(self, manager): self.manager = manager self.scrollbar = manager.horizontal_scrollbar self.connect(self.scrollbar, SIGNAL('valueChanged(int)'), self.scroll_horizontally) def scroll_horizontally(self, amount): self.document.scroll_to(y=self.document.ypos, x=amount) @property def scroll_pos(self): return (self.document.ypos, self.document.ypos + self.document.window_height) @property def viewport_rect(self): # (left, top, right, bottom) of the viewport in document co-ordinates # When in paged mode, left and right are the numbers of the columns # at the left edge and *after* the right edge of the viewport d = self.document if d.in_paged_mode: try: l, r = d.column_boundaries except ValueError: l, r = (0, 1) else: l, r = d.xpos, d.xpos + d.window_width return (l, d.ypos, r, d.ypos + d.window_height) def link_hovered(self, link, text, context): link, text = unicode(link), unicode(text) if link: self.setCursor(Qt.PointingHandCursor) else: self.unsetCursor() def link_clicked(self, url): if self.manager is not None: self.manager.link_clicked(url) def sizeHint(self): return self._size_hint @dynamic_property def scroll_fraction(self): def fget(self): return self.document.scroll_fraction def fset(self, val): self.document.scroll_fraction = float(val) return property(fget=fget, fset=fset) @property def hscroll_fraction(self): return self.document.hscroll_fraction @property def content_size(self): return self.document.width, self.document.height @dynamic_property def current_language(self): def fget(self): return self.document.current_language def fset(self, val): self.document.current_language = val return property(fget=fget, fset=fset) def search(self, text, backwards=False): flags = self.document.FindBackward if backwards else self.document.FindFlags(0) found = self.document.findText(text, flags) if found and self.document.in_paged_mode: self.document.javascript('paged_display.snap_to_selection()') return found def path(self): return os.path.abspath(unicode(self.url().toLocalFile())) def load_path(self, path, pos=0.0): self.initial_pos = pos self.last_loaded_path = path def callback(lu): self.loading_url = lu if self.manager is not None: self.manager.load_started() load_html(path, self, codec=getattr(path, 'encoding', 'utf-8'), mime_type=getattr(path, 'mime_type', 'text/html'), pre_load_callback=callback) entries = set() for ie in getattr(path, 'index_entries', []): if ie.start_anchor: entries.add(ie.start_anchor) if ie.end_anchor: entries.add(ie.end_anchor) self.document.index_anchors = entries def initialize_scrollbar(self): if getattr(self, 'scrollbar', None) is not None: if self.document.in_paged_mode: self.scrollbar.setVisible(False) return delta = self.document.width - self.size().width() if delta > 0: self._ignore_scrollbar_signals = True self.scrollbar.blockSignals(True) self.scrollbar.setRange(0, delta) self.scrollbar.setValue(0) self.scrollbar.setSingleStep(1) self.scrollbar.setPageStep(int(delta/10.)) self.scrollbar.setVisible(delta > 0) self.scrollbar.blockSignals(False) self._ignore_scrollbar_signals = False def load_finished(self, ok): if self.loading_url is None: # An <iframe> finished loading return self.loading_url = None self.document.load_javascript_libraries() self.document.after_load(self.last_loaded_path) self._size_hint = self.document.mainFrame().contentsSize() scrolled = False if self.to_bottom: self.to_bottom = False self.initial_pos = 1.0 if self.initial_pos > 0.0: scrolled = True self.scroll_to(self.initial_pos, notify=False) self.initial_pos = 0.0 self.update() self.initialize_scrollbar() self.document.reference_mode(self._reference_mode) if self.manager is not None: spine_index = self.manager.load_finished(bool(ok)) if spine_index > -1: self.document.set_reference_prefix('%d.'%(spine_index+1)) if scrolled: self.manager.scrolled(self.document.scroll_fraction, onload=True) if self.flipper.isVisible(): if self.flipper.running: self.flipper.setVisible(False) else: self.flipper(self.current_page_image(), duration=self.document.page_flip_duration) @classmethod def test_line(cls, img, y): 'Test if line contains pixels of exactly the same color' start = img.pixel(0, y) for i in range(1, img.width()): if img.pixel(i, y) != start: return False return True def current_page_image(self, overlap=-1): if overlap < 0: overlap = self.height() img = QImage(self.width(), overlap, QImage.Format_ARGB32_Premultiplied) painter = QPainter(img) painter.setRenderHints(self.renderHints()) self.document.mainFrame().render(painter, QRegion(0, 0, self.width(), overlap)) painter.end() return img def find_next_blank_line(self, overlap): img = self.current_page_image(overlap) for i in range(overlap-1, -1, -1): if self.test_line(img, i): self.scroll_by(y=i, notify=False) return self.scroll_by(y=overlap) def previous_page(self): if self.flipper.running and not self.is_auto_repeat_event: return if self.loading_url is not None: return epf = self.document.enable_page_flip and not self.is_auto_repeat_event if self.document.in_paged_mode: loc = self.document.javascript( 'paged_display.previous_screen_location()', typ='int') if loc < 0: if self.manager is not None: if epf: self.flipper.initialize(self.current_page_image(), forwards=False) self.manager.previous_document() else: if epf: self.flipper.initialize(self.current_page_image(), forwards=False) self.document.scroll_to(x=loc, y=0) if epf: self.flipper(self.current_page_image(), duration=self.document.page_flip_duration) if self.manager is not None: self.manager.scrolled(self.scroll_fraction) return delta_y = self.document.window_height - 25 if self.document.at_top: if self.manager is not None: self.to_bottom = True if epf: self.flipper.initialize(self.current_page_image(), False) self.manager.previous_document() else: opos = self.document.ypos upper_limit = opos - delta_y if upper_limit < 0: upper_limit = 0 if upper_limit < opos: if epf: self.flipper.initialize(self.current_page_image(), forwards=False) self.document.scroll_to(self.document.xpos, upper_limit) if epf: self.flipper(self.current_page_image(), duration=self.document.page_flip_duration) if self.manager is not None: self.manager.scrolled(self.scroll_fraction) def next_page(self): if self.flipper.running and not self.is_auto_repeat_event: return if self.loading_url is not None: return epf = self.document.enable_page_flip and not self.is_auto_repeat_event if self.document.in_paged_mode: loc = self.document.javascript( 'paged_display.next_screen_location()', typ='int') if loc < 0: if self.manager is not None: if epf: self.flipper.initialize(self.current_page_image()) self.manager.next_document() else: if epf: self.flipper.initialize(self.current_page_image()) self.document.scroll_to(x=loc, y=0) if epf: self.flipper(self.current_page_image(), duration=self.document.page_flip_duration) if self.manager is not None: self.manager.scrolled(self.scroll_fraction) return window_height = self.document.window_height document_height = self.document.height ddelta = document_height - window_height # print '\nWindow height:', window_height # print 'Document height:', self.document.height delta_y = window_height - 25 if self.document.at_bottom or ddelta <= 0: if self.manager is not None: if epf: self.flipper.initialize(self.current_page_image()) self.manager.next_document() elif ddelta < 25: self.scroll_by(y=ddelta) return else: oopos = self.document.ypos # print 'Original position:', oopos self.document.set_bottom_padding(0) opos = self.document.ypos # print 'After set padding=0:', self.document.ypos if opos < oopos: if self.manager is not None: if epf: self.flipper.initialize(self.current_page_image()) self.manager.next_document() return # oheight = self.document.height lower_limit = opos + delta_y # Max value of top y co-ord after scrolling max_y = self.document.height - window_height # The maximum possible top y co-ord if max_y < lower_limit: padding = lower_limit - max_y if padding == window_height: if self.manager is not None: if epf: self.flipper.initialize(self.current_page_image()) self.manager.next_document() return # print 'Setting padding to:', lower_limit - max_y self.document.set_bottom_padding(lower_limit - max_y) if epf: self.flipper.initialize(self.current_page_image()) # print 'Document height:', self.document.height # print 'Height change:', (self.document.height - oheight) max_y = self.document.height - window_height lower_limit = min(max_y, lower_limit) # print 'Scroll to:', lower_limit if lower_limit > opos: self.document.scroll_to(self.document.xpos, lower_limit) actually_scrolled = self.document.ypos - opos # print 'After scroll pos:', self.document.ypos # print 'Scrolled by:', self.document.ypos - opos self.find_next_blank_line(window_height - actually_scrolled) # print 'After blank line pos:', self.document.ypos if epf: self.flipper(self.current_page_image(), duration=self.document.page_flip_duration) if self.manager is not None: self.manager.scrolled(self.scroll_fraction) # print 'After all:', self.document.ypos def page_turn_requested(self, backwards): if backwards: self.previous_page() else: self.next_page() def scroll_by(self, x=0, y=0, notify=True): old_pos = (self.document.xpos if self.document.in_paged_mode else self.document.ypos) self.document.scroll_by(x, y) new_pos = (self.document.xpos if self.document.in_paged_mode else self.document.ypos) if notify and self.manager is not None and new_pos != old_pos: self.manager.scrolled(self.scroll_fraction) def scroll_to(self, pos, notify=True): if self._ignore_scrollbar_signals: return old_pos = (self.document.xpos if self.document.in_paged_mode else self.document.ypos) if self.document.in_paged_mode: if isinstance(pos, basestring): self.document.jump_to_anchor(pos) else: self.document.scroll_fraction = pos else: if isinstance(pos, basestring): self.document.jump_to_anchor(pos) else: if pos >= 1: self.document.scroll_to(0, self.document.height) else: y = int(math.ceil( pos*(self.document.height-self.document.window_height))) self.document.scroll_to(0, y) new_pos = (self.document.xpos if self.document.in_paged_mode else self.document.ypos) if notify and self.manager is not None and new_pos != old_pos: self.manager.scrolled(self.scroll_fraction) @dynamic_property def multiplier(self): def fget(self): return self.zoomFactor() def fset(self, val): self.setZoomFactor(val) self.magnification_changed.emit(val) return property(fget=fget, fset=fset) def magnify_fonts(self, amount=None): if amount is None: amount = self.document.font_magnification_step with self.document.page_position: self.multiplier += amount return self.document.scroll_fraction def shrink_fonts(self, amount=None): if amount is None: amount = self.document.font_magnification_step if self.multiplier >= amount: with self.document.page_position: self.multiplier -= amount return self.document.scroll_fraction def restore_font_size(self): with self.document.page_position: self.multiplier = 1 return self.document.scroll_fraction def changeEvent(self, event): if event.type() == event.EnabledChange: self.update() return QWebView.changeEvent(self, event) def paintEvent(self, event): painter = QPainter(self) painter.setRenderHints(self.renderHints()) self.document.mainFrame().render(painter, event.region()) if not self.isEnabled(): painter.fillRect(event.region().boundingRect(), self.DISABLED_BRUSH) painter.end() def wheelEvent(self, event): mods = event.modifiers() if mods & Qt.CTRL: if self.manager is not None and event.delta() != 0: (self.manager.font_size_larger if event.delta() > 0 else self.manager.font_size_smaller)() return if self.document.in_paged_mode: if abs(event.delta()) < 15: return typ = 'screen' if self.document.wheel_flips_pages else 'col' direction = 'next' if event.delta() < 0 else 'previous' loc = self.document.javascript('paged_display.%s_%s_location()'%( direction, typ), typ='int') if loc > -1: self.document.scroll_to(x=loc, y=0) if self.manager is not None: self.manager.scrolled(self.scroll_fraction) event.accept() elif self.manager is not None: if direction == 'next': self.manager.next_document() else: self.manager.previous_document() event.accept() return if event.delta() < -14: if self.document.wheel_flips_pages: self.next_page() event.accept() return if self.document.at_bottom: self.scroll_by(y=15) # at_bottom can lie on windows if self.manager is not None: self.manager.next_document() event.accept() return elif event.delta() > 14: if self.document.wheel_flips_pages: self.previous_page() event.accept() return if self.document.at_top: if self.manager is not None: self.manager.previous_document() event.accept() return ret = QWebView.wheelEvent(self, event) scroll_amount = (event.delta() / 120.0) * .2 * -1 if event.orientation() == Qt.Vertical: self.scroll_by(0, self.document.viewportSize().height() * scroll_amount) else: self.scroll_by(self.document.viewportSize().width() * scroll_amount, 0) if self.manager is not None: self.manager.scrolled(self.scroll_fraction) return ret def keyPressEvent(self, event): if not self.handle_key_press(event): return QWebView.keyPressEvent(self, event) def paged_col_scroll(self, forward=True, scroll_past_end=True): dir = 'next' if forward else 'previous' loc = self.document.javascript( 'paged_display.%s_col_location()'%dir, typ='int') if loc > -1: self.document.scroll_to(x=loc, y=0) self.manager.scrolled(self.document.scroll_fraction) elif scroll_past_end: (self.manager.next_document() if forward else self.manager.previous_document()) def handle_key_press(self, event): handled = True key = self.shortcuts.get_match(event) func = self.goto_location_actions.get(key, None) if func is not None: self.is_auto_repeat_event = event.isAutoRepeat() try: func() finally: self.is_auto_repeat_event = False elif key == 'Down': if self.document.in_paged_mode: self.paged_col_scroll(scroll_past_end=not self.document.line_scrolling_stops_on_pagebreaks) else: if (not self.document.line_scrolling_stops_on_pagebreaks and self.document.at_bottom): self.manager.next_document() else: self.scroll_by(y=15) elif key == 'Up': if self.document.in_paged_mode: self.paged_col_scroll(forward=False, scroll_past_end=not self.document.line_scrolling_stops_on_pagebreaks) else: if (not self.document.line_scrolling_stops_on_pagebreaks and self.document.at_top): self.manager.previous_document() else: self.scroll_by(y=-15) elif key == 'Left': if self.document.in_paged_mode: self.paged_col_scroll(forward=False) else: self.scroll_by(x=-15) elif key == 'Right': if self.document.in_paged_mode: self.paged_col_scroll() else: self.scroll_by(x=15) elif key == 'Back': if self.manager is not None: self.manager.back(None) elif key == 'Forward': if self.manager is not None: self.manager.forward(None) else: handled = False return handled def resizeEvent(self, event): if self.manager is not None: self.manager.viewport_resize_started(event) return QWebView.resizeEvent(self, event) def event(self, ev): if ev.type() == ev.Gesture: swipe = ev.gesture(Qt.SwipeGesture) if swipe is not None: self.handle_swipe(swipe) return True return QWebView.event(self, ev) def handle_swipe(self, swipe): if swipe.state() == Qt.GestureFinished: if swipe.horizontalDirection() == QSwipeGesture.Left: self.previous_page() elif swipe.horizontalDirection() == QSwipeGesture.Right: self.next_page() elif swipe.verticalDirection() == QSwipeGesture.Up: self.goto_previous_section() elif swipe.horizontalDirection() == QSwipeGesture.Down: self.goto_next_section() def mouseReleaseEvent(self, ev): opos = self.document.ypos ret = QWebView.mouseReleaseEvent(self, ev) if self.manager is not None and opos != self.document.ypos: self.manager.internal_link_clicked(opos) self.manager.scrolled(self.scroll_fraction) return ret
class EditorWidget(QWebView): # {{{ def __init__(self, parent=None): QWebView.__init__(self, parent) self.readonly = False self.comments_pat = re.compile(r'<!--.*?-->', re.DOTALL) extra_shortcuts = { 'ToggleBold': 'Bold', 'ToggleItalic': 'Italic', 'ToggleUnderline': 'Underline', } for wac, name, icon, text, checkable in [ ('ToggleBold', 'bold', 'format-text-bold', _('Bold'), True), ('ToggleItalic', 'italic', 'format-text-italic', _('Italic'), True), ('ToggleUnderline', 'underline', 'format-text-underline', _('Underline'), True), ('ToggleStrikethrough', 'strikethrough', 'format-text-strikethrough', _('Strikethrough'), True), ('ToggleSuperscript', 'superscript', 'format-text-superscript', _('Superscript'), True), ('ToggleSubscript', 'subscript', 'format-text-subscript', _('Subscript'), True), ('InsertOrderedList', 'ordered_list', 'format-list-ordered', _('Ordered list'), True), ('InsertUnorderedList', 'unordered_list', 'format-list-unordered', _('Unordered list'), True), ('AlignLeft', 'align_left', 'format-justify-left', _('Align left'), False), ('AlignCenter', 'align_center', 'format-justify-center', _('Align center'), False), ('AlignRight', 'align_right', 'format-justify-right', _('Align right'), False), ('AlignJustified', 'align_justified', 'format-justify-fill', _('Align justified'), False), ('Undo', 'undo', 'edit-undo', _('Undo'), False), ('Redo', 'redo', 'edit-redo', _('Redo'), False), ('RemoveFormat', 'remove_format', 'trash', _('Remove formatting'), False), ('Copy', 'copy', 'edit-copy', _('Copy'), False), ('Paste', 'paste', 'edit-paste', _('Paste'), False), ('Cut', 'cut', 'edit-cut', _('Cut'), False), ('Indent', 'indent', 'format-indent-more', _('Increase Indentation'), False), ('Outdent', 'outdent', 'format-indent-less', _('Decrease Indentation'), False), ('SelectAll', 'select_all', 'edit-select-all', _('Select all'), False), ]: ac = PageAction(wac, icon, text, checkable, self) setattr(self, 'action_'+name, ac) ss = extra_shortcuts.get(wac, None) if ss: ac.setShortcut(QKeySequence(getattr(QKeySequence, ss))) if wac == 'RemoveFormat': ac.triggered.connect(self.remove_format_cleanup, type=Qt.QueuedConnection) self.action_color = QAction(QIcon(I('format-text-color')), _('Foreground color'), self) self.action_color.triggered.connect(self.foreground_color) self.action_background = QAction(QIcon(I('format-fill-color')), _('Background color'), self) self.action_background.triggered.connect(self.background_color) self.action_block_style = QAction(QIcon(I('format-text-heading')), _('Style text block'), self) self.action_block_style.setToolTip( _('Style the selected text block')) self.block_style_menu = QMenu(self) self.action_block_style.setMenu(self.block_style_menu) self.block_style_actions = [] for text, name in [ (_('Normal'), 'p'), (_('Heading') +' 1', 'h1'), (_('Heading') +' 2', 'h2'), (_('Heading') +' 3', 'h3'), (_('Heading') +' 4', 'h4'), (_('Heading') +' 5', 'h5'), (_('Heading') +' 6', 'h6'), (_('Pre-formatted'), 'pre'), (_('Blockquote'), 'blockquote'), (_('Address'), 'address'), ]: ac = BlockStyleAction(text, name, self) self.block_style_menu.addAction(ac) self.block_style_actions.append(ac) self.action_insert_link = QAction(QIcon(I('insert-link.png')), _('Insert link or image'), self) self.action_insert_link.triggered.connect(self.insert_link) self.pageAction(QWebPage.ToggleBold).changed.connect(self.update_link_action) self.action_insert_link.setEnabled(False) self.action_clear = QAction(QIcon(I('edit-clear')), _('Clear'), self) self.action_clear.triggered.connect(self.clear_text) self.page().setLinkDelegationPolicy(QWebPage.DelegateAllLinks) self.page().linkClicked.connect(self.link_clicked) self.setHtml('') self.set_readonly(False) def update_link_action(self): wac = self.pageAction(QWebPage.ToggleBold) self.action_insert_link.setEnabled(wac.isEnabled()) def set_readonly(self, what): self.readonly = what self.page().setContentEditable(not self.readonly) def clear_text(self, *args): us = self.page().undoStack() us.beginMacro('clear all text') self.action_select_all.trigger() self.action_remove_format.trigger() self.exec_command('delete') us.endMacro() self.set_font_style() self.setFocus(Qt.OtherFocusReason) def link_clicked(self, url): open_url(url) def foreground_color(self): col = QColorDialog.getColor(Qt.black, self, _('Choose foreground color'), QColorDialog.ShowAlphaChannel) if col.isValid(): self.exec_command('foreColor', unicode(col.name())) def background_color(self): col = QColorDialog.getColor(Qt.white, self, _('Choose background color'), QColorDialog.ShowAlphaChannel) if col.isValid(): self.exec_command('hiliteColor', unicode(col.name())) def insert_link(self, *args): link, name, is_image = self.ask_link() if not link: return url = self.parse_link(link) if url.isValid(): url = unicode(url.toString()) self.setFocus(Qt.OtherFocusReason) if is_image: self.exec_command('insertHTML', '<img src="%s" alt="%s"></img>'%(prepare_string_for_xml(url, True), prepare_string_for_xml(name or _('Image'), True))) elif name: self.exec_command('insertHTML', '<a href="%s">%s</a>'%(prepare_string_for_xml(url, True), prepare_string_for_xml(name))) else: self.exec_command('createLink', url) else: error_dialog(self, _('Invalid URL'), _('The url %r is invalid') % link, show=True) def ask_link(self): d = QDialog(self) d.setWindowTitle(_('Create link')) l = QFormLayout() d.setLayout(l) d.url = QLineEdit(d) d.name = QLineEdit(d) d.setMinimumWidth(600) d.bb = QDialogButtonBox(QDialogButtonBox.Ok|QDialogButtonBox.Cancel) d.br = b = QPushButton(_('&Browse')) b.setIcon(QIcon(I('document_open.png'))) def cf(): files = choose_files(d, 'select link file', _('Choose file'), select_only_single_file=True) if files: d.url.setText(files[0]) b.clicked.connect(cf) d.la = la = QLabel(_( 'Enter a URL. You can also choose to create a link to a file on ' 'your computer. If the selected file is an image, it will be ' 'inserted as an image. Note that if you create a link to a file on ' 'your computer, it will stop working if the file is moved.')) la.setWordWrap(True) la.setStyleSheet('QLabel { margin-bottom: 1.5ex }') l.setWidget(0, l.SpanningRole, la) l.addRow(_('Enter &URL:'), d.url) l.addRow(_('Enter &name (optional):'), d.name) l.addRow(_('Choose a file on your computer:'), d.br) l.addRow(d.bb) d.bb.accepted.connect(d.accept) d.bb.rejected.connect(d.reject) d.resize(d.sizeHint()) link, name, is_image = None, None, False if d.exec_() == d.Accepted: link, name = unicode(d.url.text()).strip(), unicode(d.name.text()).strip() if link and os.path.exists(link): with lopen(link, 'rb') as f: q = what(f) is_image = q in {'jpeg', 'png', 'gif'} return link, name, is_image def parse_link(self, link): link = link.strip() if link and os.path.exists(link): return QUrl.fromLocalFile(link) has_schema = re.match(r'^[a-zA-Z]+:', link) if has_schema is not None: url = QUrl(link, QUrl.TolerantMode) if url.isValid(): return url if os.path.exists(link): return QUrl.fromLocalFile(link) if has_schema is None: first, _, rest = link.partition('.') prefix = 'http' if first == 'ftp': prefix = 'ftp' url = QUrl(prefix +'://'+link, QUrl.TolerantMode) if url.isValid(): return url return QUrl(link, QUrl.TolerantMode) def sizeHint(self): return QSize(150, 150) def exec_command(self, cmd, arg=None): frame = self.page().mainFrame() if arg is not None: js = 'document.execCommand("%s", false, %s);' % (cmd, json.dumps(unicode(arg))) else: js = 'document.execCommand("%s", false, null);' % cmd frame.evaluateJavaScript(js) def remove_format_cleanup(self): self.html = self.html @dynamic_property def html(self): def fget(self): ans = u'' try: check = unicode(self.page().mainFrame().toPlainText()).strip() raw = unicode(self.page().mainFrame().toHtml()) raw = xml_to_unicode(raw, strip_encoding_pats=True, resolve_entities=True)[0] raw = self.comments_pat.sub('', raw) if not check and '<img' not in raw.lower(): return ans try: root = html.fromstring(raw) except: root = fromstring(raw) elems = [] for body in root.xpath('//body'): if body.text: elems.append(body.text) elems += [html.tostring(x, encoding=unicode) for x in body if x.tag not in ('script', 'style')] if len(elems) > 1: ans = u'<div>%s</div>'%(u''.join(elems)) else: ans = u''.join(elems) if not ans.startswith('<'): ans = '<p>%s</p>'%ans ans = xml_replace_entities(ans) except: import traceback traceback.print_exc() return ans def fset(self, val): self.setHtml(val) self.set_font_style() return property(fget=fget, fset=fset) def set_font_style(self): fi = QFontInfo(QApplication.font(self)) f = fi.pixelSize() + 1 + int(tweaks['change_book_details_font_size_by']) fam = unicode(fi.family()).strip().replace('"', '') if not fam: fam = 'sans-serif' style = 'font-size: %fpx; font-family:"%s",sans-serif;' % (f, fam) # toList() is needed because PyQt on Debian is old/broken for body in self.page().mainFrame().documentElement().findAll('body').toList(): body.setAttribute('style', style) self.page().setContentEditable(not self.readonly) def keyPressEvent(self, ev): if ev.key() in (Qt.Key_Tab, Qt.Key_Escape, Qt.Key_Backtab): ev.ignore() else: return QWebView.keyPressEvent(self, ev) def keyReleaseEvent(self, ev): if ev.key() in (Qt.Key_Tab, Qt.Key_Escape, Qt.Key_Backtab): ev.ignore() else: return QWebView.keyReleaseEvent(self, ev) def contextMenuEvent(self, ev): menu = self.page().createStandardContextMenu() paste = self.pageAction(QWebPage.Paste) for action in menu.actions(): if action == paste: menu.insertAction(action, self.pageAction(QWebPage.PasteAndMatchStyle)) menu.exec_(ev.globalPos())
class DocumentView(QWebView): # {{{ magnification_changed = pyqtSignal(object) DISABLED_BRUSH = QBrush(Qt.lightGray, Qt.Dense5Pattern) def initialize_view(self, debug_javascript=False): self.setRenderHints(QPainter.Antialiasing|QPainter.TextAntialiasing|QPainter.SmoothPixmapTransform) self.flipper = SlideFlip(self) self.is_auto_repeat_event = False self.debug_javascript = debug_javascript self.shortcuts = Shortcuts(SHORTCUTS, 'shortcuts/viewer') self.setSizePolicy(QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)) self._size_hint = QSize(510, 680) self.initial_pos = 0.0 self.to_bottom = False self.document = Document(self.shortcuts, parent=self, debug_javascript=debug_javascript) self.setPage(self.document) self.inspector = WebInspector(self, self.document) self.manager = None self._reference_mode = False self._ignore_scrollbar_signals = False self.loading_url = None self.loadFinished.connect(self.load_finished) self.connect(self.document, SIGNAL('linkClicked(QUrl)'), self.link_clicked) self.connect(self.document, SIGNAL('linkHovered(QString,QString,QString)'), self.link_hovered) self.connect(self.document, SIGNAL('selectionChanged()'), self.selection_changed) self.connect(self.document, SIGNAL('animated_scroll_done()'), self.animated_scroll_done, Qt.QueuedConnection) self.document.page_turn.connect(self.page_turn_requested) copy_action = self.pageAction(self.document.Copy) copy_action.setIcon(QIcon(I('convert.png'))) d = self.document self.unimplemented_actions = list(map(self.pageAction, [d.DownloadImageToDisk, d.OpenLinkInNewWindow, d.DownloadLinkToDisk, d.OpenImageInNewWindow, d.OpenLink, d.Reload, d.InspectElement])) self.search_online_action = QAction(QIcon(I('search.png')), '', self) self.search_online_action.triggered.connect(self.search_online) self.addAction(self.search_online_action) self.dictionary_action = QAction(QIcon(I('dictionary.png')), _('&Lookup in dictionary'), self) self.dictionary_action.triggered.connect(self.lookup) self.addAction(self.dictionary_action) self.image_popup = ImagePopup(self) self.table_popup = TablePopup(self) self.view_image_action = QAction(QIcon(I('view-image.png')), _('View &image...'), self) self.view_image_action.triggered.connect(self.image_popup) self.view_table_action = QAction(QIcon(I('view.png')), _('View &table...'), self) self.view_table_action.triggered.connect(self.popup_table) self.search_action = QAction(QIcon(I('dictionary.png')), _('&Search for next occurrence'), self) self.search_action.triggered.connect(self.search_next) self.addAction(self.search_action) self.goto_location_action = QAction(_('Go to...'), self) self.goto_location_menu = m = QMenu(self) self.goto_location_actions = a = { 'Next Page': self.next_page, 'Previous Page': self.previous_page, 'Section Top' : partial(self.scroll_to, 0), 'Document Top': self.goto_document_start, 'Section Bottom':partial(self.scroll_to, 1), 'Document Bottom': self.goto_document_end, 'Next Section': self.goto_next_section, 'Previous Section': self.goto_previous_section, } for name, key in [(_('Next Section'), 'Next Section'), (_('Previous Section'), 'Previous Section'), (None, None), (_('Document Start'), 'Document Top'), (_('Document End'), 'Document Bottom'), (None, None), (_('Section Start'), 'Section Top'), (_('Section End'), 'Section Bottom'), (None, None), (_('Next Page'), 'Next Page'), (_('Previous Page'), 'Previous Page')]: if key is None: m.addSeparator() else: m.addAction(name, a[key], self.shortcuts.get_sequences(key)[0]) self.goto_location_action.setMenu(self.goto_location_menu) self.grabGesture(Qt.SwipeGesture) self.restore_fonts_action = QAction(_('Default font size'), self) self.restore_fonts_action.setCheckable(True) self.restore_fonts_action.triggered.connect(self.restore_font_size) def goto_next_section(self, *args): if self.manager is not None: self.manager.goto_next_section() def goto_previous_section(self, *args): if self.manager is not None: self.manager.goto_previous_section() def goto_document_start(self, *args): if self.manager is not None: self.manager.goto_start() def goto_document_end(self, *args): if self.manager is not None: self.manager.goto_end() @property def copy_action(self): return self.pageAction(self.document.Copy) def animated_scroll_done(self): if self.manager is not None: self.manager.scrolled(self.document.scroll_fraction) def reference_mode(self, enable): self._reference_mode = enable self.document.reference_mode(enable) def goto(self, ref): self.document.goto(ref) def goto_bookmark(self, bm): self.document.goto_bookmark(bm) def config(self, parent=None): self.document.do_config(parent) if self.document.in_fullscreen_mode: self.document.switch_to_fullscreen_mode() self.setFocus(Qt.OtherFocusReason) def load_theme(self, theme_id): themes = load_themes() theme = themes[theme_id] opts = config(theme).parse() self.document.apply_settings(opts) if self.document.in_fullscreen_mode: self.document.switch_to_fullscreen_mode() self.setFocus(Qt.OtherFocusReason) def bookmark(self): return self.document.bookmark() def selection_changed(self): if self.manager is not None: self.manager.selection_changed(unicode(self.document.selectedText())) def _selectedText(self): t = unicode(self.selectedText()).strip() if not t: return u'' if len(t) > 40: t = t[:40] + u'...' t = t.replace(u'&', u'&&') return _("S&earch Google for '%s'")%t def popup_table(self): html = self.document.extract_node() self.table_popup(html, QUrl.fromLocalFile(self.last_loaded_path), self.document.font_magnification_step) def contextMenuEvent(self, ev): mf = self.document.mainFrame() r = mf.hitTestContent(ev.pos()) img = r.pixmap() elem = r.element() if elem.isNull(): elem = r.enclosingBlockElement() table = None parent = elem while not parent.isNull(): if (unicode(parent.tagName()) == u'table' or unicode(parent.localName()) == u'table'): table = parent break parent = parent.parent() self.image_popup.current_img = img self.image_popup.current_url = r.imageUrl() menu = self.document.createStandardContextMenu() for action in self.unimplemented_actions: menu.removeAction(action) if not img.isNull(): menu.addAction(self.view_image_action) if table is not None: self.document.mark_element.emit(table) menu.addAction(self.view_table_action) text = self._selectedText() if text and img.isNull(): self.search_online_action.setText(text) for x, sc in (('search_online', 'Search online'), ('dictionary', 'Lookup word'), ('search', 'Next occurrence')): ac = getattr(self, '%s_action' % x) menu.addAction(ac.icon(), '%s [%s]' % (unicode(ac.text()), ','.join(self.shortcuts.get_shortcuts(sc))), ac.trigger) if not text and img.isNull(): menu.addSeparator() if self.manager.action_back.isEnabled(): menu.addAction(self.manager.action_back) if self.manager.action_forward.isEnabled(): menu.addAction(self.manager.action_forward) menu.addAction(self.goto_location_action) if self.manager is not None: menu.addSeparator() menu.addAction(self.manager.action_table_of_contents) menu.addSeparator() menu.addAction(self.manager.action_font_size_larger) self.restore_fonts_action.setChecked(self.multiplier == 1) menu.addAction(self.restore_fonts_action) menu.addAction(self.manager.action_font_size_smaller) menu.addSeparator() menu.addAction(_('Inspect'), self.inspect) if not text and img.isNull() and self.manager is not None: menu.addSeparator() if (not self.document.show_controls or self.document.in_fullscreen_mode) and self.manager is not None: menu.addAction(self.manager.toggle_toolbar_action) menu.addAction(self.manager.action_full_screen) menu.addSeparator() menu.addAction(self.manager.action_quit) for plugin in self.document.all_viewer_plugins: plugin.customize_context_menu(menu, ev, r) menu.exec_(ev.globalPos()) def inspect(self): self.inspector.show() self.inspector.raise_() self.pageAction(self.document.InspectElement).trigger() def lookup(self, *args): if self.manager is not None: t = unicode(self.selectedText()).strip() if t: self.manager.lookup(t.split()[0]) def search_next(self): if self.manager is not None: t = unicode(self.selectedText()).strip() if t: self.manager.search.set_search_string(t) def search_online(self): t = unicode(self.selectedText()).strip() if t: url = 'https://www.google.com/search?q=' + QUrl().toPercentEncoding(t) open_url(QUrl.fromEncoded(url)) def set_manager(self, manager): self.manager = manager self.scrollbar = manager.horizontal_scrollbar self.connect(self.scrollbar, SIGNAL('valueChanged(int)'), self.scroll_horizontally) def scroll_horizontally(self, amount): self.document.scroll_to(y=self.document.ypos, x=amount) @property def scroll_pos(self): return (self.document.ypos, self.document.ypos + self.document.window_height) @property def viewport_rect(self): # (left, top, right, bottom) of the viewport in document co-ordinates # When in paged mode, left and right are the numbers of the columns # at the left edge and *after* the right edge of the viewport d = self.document if d.in_paged_mode: try: l, r = d.column_boundaries except ValueError: l, r = (0, 1) else: l, r = d.xpos, d.xpos + d.window_width return (l, d.ypos, r, d.ypos + d.window_height) def link_hovered(self, link, text, context): link, text = unicode(link), unicode(text) if link: self.setCursor(Qt.PointingHandCursor) else: self.unsetCursor() def link_clicked(self, url): if self.manager is not None: self.manager.link_clicked(url) def sizeHint(self): return self._size_hint @dynamic_property def scroll_fraction(self): def fget(self): return self.document.scroll_fraction def fset(self, val): self.document.scroll_fraction = float(val) return property(fget=fget, fset=fset) @property def hscroll_fraction(self): return self.document.hscroll_fraction @property def content_size(self): return self.document.width, self.document.height @dynamic_property def current_language(self): def fget(self): return self.document.current_language def fset(self, val): self.document.current_language = val return property(fget=fget, fset=fset) def search(self, text, backwards=False): flags = self.document.FindBackward if backwards else self.document.FindFlags(0) found = self.document.findText(text, flags) if found and self.document.in_paged_mode: self.document.javascript('paged_display.snap_to_selection()') return found def path(self): return os.path.abspath(unicode(self.url().toLocalFile())) def load_path(self, path, pos=0.0): self.initial_pos = pos self.last_loaded_path = path def callback(lu): self.loading_url = lu if self.manager is not None: self.manager.load_started() load_html(path, self, codec=getattr(path, 'encoding', 'utf-8'), mime_type=getattr(path, 'mime_type', 'text/html'), pre_load_callback=callback) entries = set() for ie in getattr(path, 'index_entries', []): if ie.start_anchor: entries.add(ie.start_anchor) if ie.end_anchor: entries.add(ie.end_anchor) self.document.index_anchors = entries def initialize_scrollbar(self): if getattr(self, 'scrollbar', None) is not None: if self.document.in_paged_mode: self.scrollbar.setVisible(False) return delta = self.document.width - self.size().width() if delta > 0: self._ignore_scrollbar_signals = True self.scrollbar.blockSignals(True) self.scrollbar.setRange(0, delta) self.scrollbar.setValue(0) self.scrollbar.setSingleStep(1) self.scrollbar.setPageStep(int(delta/10.)) self.scrollbar.setVisible(delta > 0) self.scrollbar.blockSignals(False) self._ignore_scrollbar_signals = False def load_finished(self, ok): if self.loading_url is None: # An <iframe> finished loading return self.loading_url = None self.document.load_javascript_libraries() self.document.after_load(self.last_loaded_path) self._size_hint = self.document.mainFrame().contentsSize() scrolled = False if self.to_bottom: self.to_bottom = False self.initial_pos = 1.0 if self.initial_pos > 0.0: scrolled = True self.scroll_to(self.initial_pos, notify=False) self.initial_pos = 0.0 self.update() self.initialize_scrollbar() self.document.reference_mode(self._reference_mode) if self.manager is not None: spine_index = self.manager.load_finished(bool(ok)) if spine_index > -1: self.document.set_reference_prefix('%d.'%(spine_index+1)) if scrolled: self.manager.scrolled(self.document.scroll_fraction, onload=True) if self.flipper.isVisible(): if self.flipper.running: self.flipper.setVisible(False) else: self.flipper(self.current_page_image(), duration=self.document.page_flip_duration) @classmethod def test_line(cls, img, y): 'Test if line contains pixels of exactly the same color' start = img.pixel(0, y) for i in range(1, img.width()): if img.pixel(i, y) != start: return False return True def current_page_image(self, overlap=-1): if overlap < 0: overlap = self.height() img = QImage(self.width(), overlap, QImage.Format_ARGB32_Premultiplied) painter = QPainter(img) painter.setRenderHints(self.renderHints()) self.document.mainFrame().render(painter, QRegion(0, 0, self.width(), overlap)) painter.end() return img def find_next_blank_line(self, overlap): img = self.current_page_image(overlap) for i in range(overlap-1, -1, -1): if self.test_line(img, i): self.scroll_by(y=i, notify=False) return self.scroll_by(y=overlap) def previous_page(self): if self.flipper.running and not self.is_auto_repeat_event: return if self.loading_url is not None: return epf = self.document.enable_page_flip and not self.is_auto_repeat_event if self.document.in_paged_mode: loc = self.document.javascript( 'paged_display.previous_screen_location()', typ='int') if loc < 0: if self.manager is not None: if epf: self.flipper.initialize(self.current_page_image(), forwards=False) self.manager.previous_document() else: if epf: self.flipper.initialize(self.current_page_image(), forwards=False) self.document.scroll_to(x=loc, y=0) if epf: self.flipper(self.current_page_image(), duration=self.document.page_flip_duration) if self.manager is not None: self.manager.scrolled(self.scroll_fraction) return delta_y = self.document.window_height - 25 if self.document.at_top: if self.manager is not None: self.to_bottom = True if epf: self.flipper.initialize(self.current_page_image(), False) self.manager.previous_document() else: opos = self.document.ypos upper_limit = opos - delta_y if upper_limit < 0: upper_limit = 0 if upper_limit < opos: if epf: self.flipper.initialize(self.current_page_image(), forwards=False) self.document.scroll_to(self.document.xpos, upper_limit) if epf: self.flipper(self.current_page_image(), duration=self.document.page_flip_duration) if self.manager is not None: self.manager.scrolled(self.scroll_fraction) def next_page(self): if self.flipper.running and not self.is_auto_repeat_event: return if self.loading_url is not None: return epf = self.document.enable_page_flip and not self.is_auto_repeat_event if self.document.in_paged_mode: loc = self.document.javascript( 'paged_display.next_screen_location()', typ='int') if loc < 0: if self.manager is not None: if epf: self.flipper.initialize(self.current_page_image()) self.manager.next_document() else: if epf: self.flipper.initialize(self.current_page_image()) self.document.scroll_to(x=loc, y=0) if epf: self.flipper(self.current_page_image(), duration=self.document.page_flip_duration) if self.manager is not None: self.manager.scrolled(self.scroll_fraction) return window_height = self.document.window_height document_height = self.document.height ddelta = document_height - window_height # print '\nWindow height:', window_height # print 'Document height:', self.document.height delta_y = window_height - 25 if self.document.at_bottom or ddelta <= 0: if self.manager is not None: if epf: self.flipper.initialize(self.current_page_image()) self.manager.next_document() elif ddelta < 25: self.scroll_by(y=ddelta) return else: oopos = self.document.ypos # print 'Original position:', oopos self.document.set_bottom_padding(0) opos = self.document.ypos # print 'After set padding=0:', self.document.ypos if opos < oopos: if self.manager is not None: if epf: self.flipper.initialize(self.current_page_image()) self.manager.next_document() return # oheight = self.document.height lower_limit = opos + delta_y # Max value of top y co-ord after scrolling max_y = self.document.height - window_height # The maximum possible top y co-ord if max_y < lower_limit: padding = lower_limit - max_y if padding == window_height: if self.manager is not None: if epf: self.flipper.initialize(self.current_page_image()) self.manager.next_document() return # print 'Setting padding to:', lower_limit - max_y self.document.set_bottom_padding(lower_limit - max_y) if epf: self.flipper.initialize(self.current_page_image()) # print 'Document height:', self.document.height # print 'Height change:', (self.document.height - oheight) max_y = self.document.height - window_height lower_limit = min(max_y, lower_limit) # print 'Scroll to:', lower_limit if lower_limit > opos: self.document.scroll_to(self.document.xpos, lower_limit) actually_scrolled = self.document.ypos - opos # print 'After scroll pos:', self.document.ypos # print 'Scrolled by:', self.document.ypos - opos self.find_next_blank_line(window_height - actually_scrolled) # print 'After blank line pos:', self.document.ypos if epf: self.flipper(self.current_page_image(), duration=self.document.page_flip_duration) if self.manager is not None: self.manager.scrolled(self.scroll_fraction) # print 'After all:', self.document.ypos def page_turn_requested(self, backwards): if backwards: self.previous_page() else: self.next_page() def scroll_by(self, x=0, y=0, notify=True): old_pos = (self.document.xpos if self.document.in_paged_mode else self.document.ypos) self.document.scroll_by(x, y) new_pos = (self.document.xpos if self.document.in_paged_mode else self.document.ypos) if notify and self.manager is not None and new_pos != old_pos: self.manager.scrolled(self.scroll_fraction) def scroll_to(self, pos, notify=True): if self._ignore_scrollbar_signals: return old_pos = (self.document.xpos if self.document.in_paged_mode else self.document.ypos) if self.document.in_paged_mode: if isinstance(pos, basestring): self.document.jump_to_anchor(pos) else: self.document.scroll_fraction = pos else: if isinstance(pos, basestring): self.document.jump_to_anchor(pos) else: if pos >= 1: self.document.scroll_to(0, self.document.height) else: y = int(math.ceil( pos*(self.document.height-self.document.window_height))) self.document.scroll_to(0, y) new_pos = (self.document.xpos if self.document.in_paged_mode else self.document.ypos) if notify and self.manager is not None and new_pos != old_pos: self.manager.scrolled(self.scroll_fraction) @dynamic_property def multiplier(self): def fget(self): return self.zoomFactor() def fset(self, val): self.setZoomFactor(val) self.magnification_changed.emit(val) return property(fget=fget, fset=fset) def magnify_fonts(self, amount=None): if amount is None: amount = self.document.font_magnification_step with self.document.page_position: self.multiplier += amount return self.document.scroll_fraction def shrink_fonts(self, amount=None): if amount is None: amount = self.document.font_magnification_step if self.multiplier >= amount: with self.document.page_position: self.multiplier -= amount return self.document.scroll_fraction def restore_font_size(self): with self.document.page_position: self.multiplier = 1 return self.document.scroll_fraction def changeEvent(self, event): if event.type() == event.EnabledChange: self.update() return QWebView.changeEvent(self, event) def paintEvent(self, event): painter = QPainter(self) painter.setRenderHints(self.renderHints()) self.document.mainFrame().render(painter, event.region()) if not self.isEnabled(): painter.fillRect(event.region().boundingRect(), self.DISABLED_BRUSH) painter.end() def wheelEvent(self, event): mods = event.modifiers() if mods & Qt.CTRL: if self.manager is not None and event.delta() != 0: (self.manager.font_size_larger if event.delta() > 0 else self.manager.font_size_smaller)() return if self.document.in_paged_mode: if abs(event.delta()) < 15: return typ = 'screen' if self.document.wheel_flips_pages else 'col' direction = 'next' if event.delta() < 0 else 'previous' loc = self.document.javascript('paged_display.%s_%s_location()'%( direction, typ), typ='int') if loc > -1: self.document.scroll_to(x=loc, y=0) if self.manager is not None: self.manager.scrolled(self.scroll_fraction) event.accept() elif self.manager is not None: if direction == 'next': self.manager.next_document() else: self.manager.previous_document() event.accept() return if event.delta() < -14: if self.document.wheel_flips_pages: self.next_page() event.accept() return if self.document.at_bottom: self.scroll_by(y=15) # at_bottom can lie on windows if self.manager is not None: self.manager.next_document() event.accept() return elif event.delta() > 14: if self.document.wheel_flips_pages: self.previous_page() event.accept() return if self.document.at_top: if self.manager is not None: self.manager.previous_document() event.accept() return ret = QWebView.wheelEvent(self, event) scroll_amount = (event.delta() / 120.0) * .2 * -1 if event.orientation() == Qt.Vertical: self.scroll_by(0, self.document.viewportSize().height() * scroll_amount) else: self.scroll_by(self.document.viewportSize().width() * scroll_amount, 0) if self.manager is not None: self.manager.scrolled(self.scroll_fraction) return ret def keyPressEvent(self, event): if not self.handle_key_press(event): return QWebView.keyPressEvent(self, event) def paged_col_scroll(self, forward=True, scroll_past_end=True): dir = 'next' if forward else 'previous' loc = self.document.javascript( 'paged_display.%s_col_location()'%dir, typ='int') if loc > -1: self.document.scroll_to(x=loc, y=0) self.manager.scrolled(self.document.scroll_fraction) elif scroll_past_end: (self.manager.next_document() if forward else self.manager.previous_document()) def handle_key_press(self, event): handled = True key = self.shortcuts.get_match(event) func = self.goto_location_actions.get(key, None) if func is not None: self.is_auto_repeat_event = event.isAutoRepeat() try: func() finally: self.is_auto_repeat_event = False elif key == 'Down': if self.document.in_paged_mode: self.paged_col_scroll(scroll_past_end=not self.document.line_scrolling_stops_on_pagebreaks) else: if (not self.document.line_scrolling_stops_on_pagebreaks and self.document.at_bottom): self.manager.next_document() else: self.scroll_by(y=15) elif key == 'Up': if self.document.in_paged_mode: self.paged_col_scroll(forward=False, scroll_past_end=not self.document.line_scrolling_stops_on_pagebreaks) else: if (not self.document.line_scrolling_stops_on_pagebreaks and self.document.at_top): self.manager.previous_document() else: self.scroll_by(y=-15) elif key == 'Left': if self.document.in_paged_mode: self.paged_col_scroll(forward=False) else: self.scroll_by(x=-15) elif key == 'Right': if self.document.in_paged_mode: self.paged_col_scroll() else: self.scroll_by(x=15) elif key == 'Back': if self.manager is not None: self.manager.back(None) elif key == 'Forward': if self.manager is not None: self.manager.forward(None) else: handled = False return handled def resizeEvent(self, event): if self.manager is not None: self.manager.viewport_resize_started(event) return QWebView.resizeEvent(self, event) def event(self, ev): if ev.type() == ev.Gesture: swipe = ev.gesture(Qt.SwipeGesture) if swipe is not None: self.handle_swipe(swipe) return True return QWebView.event(self, ev) def handle_swipe(self, swipe): if swipe.state() == Qt.GestureFinished: if swipe.horizontalDirection() == QSwipeGesture.Left: self.previous_page() elif swipe.horizontalDirection() == QSwipeGesture.Right: self.next_page() elif swipe.verticalDirection() == QSwipeGesture.Up: self.goto_previous_section() elif swipe.horizontalDirection() == QSwipeGesture.Down: self.goto_next_section() def mouseReleaseEvent(self, ev): opos = self.document.ypos ret = QWebView.mouseReleaseEvent(self, ev) if self.manager is not None and opos != self.document.ypos: self.manager.internal_link_clicked(opos) self.manager.scrolled(self.scroll_fraction) return ret
class EditorWidget(QWebView): # {{{ def __init__(self, parent=None): QWebView.__init__(self, parent) self.comments_pat = re.compile(r'<!--.*?-->', re.DOTALL) extra_shortcuts = { 'ToggleBold': 'Bold', 'ToggleItalic': 'Italic', 'ToggleUnderline': 'Underline', } for wac, name, icon, text, checkable in [ ('ToggleBold', 'bold', 'format-text-bold', _('Bold'), True), ('ToggleItalic', 'italic', 'format-text-italic', _('Italic'), True), ('ToggleUnderline', 'underline', 'format-text-underline', _('Underline'), True), ('ToggleStrikethrough', 'strikethrough', 'format-text-strikethrough', _('Strikethrough'), True), ('ToggleSuperscript', 'superscript', 'format-text-superscript', _('Superscript'), True), ('ToggleSubscript', 'subscript', 'format-text-subscript', _('Subscript'), True), ('InsertOrderedList', 'ordered_list', 'format-list-ordered', _('Ordered list'), True), ('InsertUnorderedList', 'unordered_list', 'format-list-unordered', _('Unordered list'), True), ('AlignLeft', 'align_left', 'format-justify-left', _('Align left'), False), ('AlignCenter', 'align_center', 'format-justify-center', _('Align center'), False), ('AlignRight', 'align_right', 'format-justify-right', _('Align right'), False), ('AlignJustified', 'align_justified', 'format-justify-fill', _('Align justified'), False), ('Undo', 'undo', 'edit-undo', _('Undo'), False), ('Redo', 'redo', 'edit-redo', _('Redo'), False), ('RemoveFormat', 'remove_format', 'trash', _('Remove formatting'), False), ('Copy', 'copy', 'edit-copy', _('Copy'), False), ('Paste', 'paste', 'edit-paste', _('Paste'), False), ('Cut', 'cut', 'edit-cut', _('Cut'), False), ('Indent', 'indent', 'format-indent-more', _('Increase Indentation'), False), ('Outdent', 'outdent', 'format-indent-less', _('Decrease Indentation'), False), ('SelectAll', 'select_all', 'edit-select-all', _('Select all'), False), ]: ac = PageAction(wac, icon, text, checkable, self) setattr(self, 'action_' + name, ac) ss = extra_shortcuts.get(wac, None) if ss: ac.setShortcut(QKeySequence(getattr(QKeySequence, ss))) if wac == 'RemoveFormat': ac.triggered.connect(self.remove_format_cleanup, type=Qt.QueuedConnection) self.action_color = QAction(QIcon(I('format-text-color')), _('Foreground color'), self) self.action_color.triggered.connect(self.foreground_color) self.action_background = QAction(QIcon(I('format-fill-color')), _('Background color'), self) self.action_background.triggered.connect(self.background_color) self.action_block_style = QAction(QIcon(I('format-text-heading')), _('Style text block'), self) self.action_block_style.setToolTip(_('Style the selected text block')) self.block_style_menu = QMenu(self) self.action_block_style.setMenu(self.block_style_menu) self.block_style_actions = [] for text, name in [ (_('Normal'), 'p'), (_('Heading') + ' 1', 'h1'), (_('Heading') + ' 2', 'h2'), (_('Heading') + ' 3', 'h3'), (_('Heading') + ' 4', 'h4'), (_('Heading') + ' 5', 'h5'), (_('Heading') + ' 6', 'h6'), (_('Pre-formatted'), 'pre'), (_('Blockquote'), 'blockquote'), (_('Address'), 'address'), ]: ac = BlockStyleAction(text, name, self) self.block_style_menu.addAction(ac) self.block_style_actions.append(ac) self.action_insert_link = QAction(QIcon(I('insert-link.png')), _('Insert link'), self) self.action_insert_link.triggered.connect(self.insert_link) self.action_clear = QAction(QIcon(I('edit-clear')), _('Clear'), self) self.action_clear.triggered.connect(self.clear_text) self.page().setLinkDelegationPolicy(QWebPage.DelegateAllLinks) self.page().linkClicked.connect(self.link_clicked) self.setHtml('') self.page().setContentEditable(True) def clear_text(self, *args): us = self.page().undoStack() us.beginMacro('clear all text') self.action_select_all.trigger() self.action_remove_format.trigger() self.exec_command('delete') us.endMacro() self.set_font_style() self.setFocus(Qt.OtherFocusReason) def link_clicked(self, url): open_url(url) def foreground_color(self): col = QColorDialog.getColor(Qt.black, self, _('Choose foreground color'), QColorDialog.ShowAlphaChannel) if col.isValid(): self.exec_command('foreColor', unicode(col.name())) def background_color(self): col = QColorDialog.getColor(Qt.white, self, _('Choose background color'), QColorDialog.ShowAlphaChannel) if col.isValid(): self.exec_command('hiliteColor', unicode(col.name())) def insert_link(self, *args): link, ok = QInputDialog.getText(self, _('Create link'), _('Enter URL')) if not ok: return url = self.parse_link(unicode(link)) if url.isValid(): url = unicode(url.toString()) self.exec_command('createLink', url) def parse_link(self, link): link = link.strip() has_schema = re.match(r'^[a-zA-Z]+:', link) if has_schema is not None: url = QUrl(link, QUrl.TolerantMode) if url.isValid(): return url if os.path.exists(link): return QUrl.fromLocalFile(link) if has_schema is None: first, _, rest = link.partition('.') prefix = 'http' if first == 'ftp': prefix = 'ftp' url = QUrl(prefix + '://' + link, QUrl.TolerantMode) if url.isValid(): return url return QUrl(link, QUrl.TolerantMode) def sizeHint(self): return QSize(150, 150) def exec_command(self, cmd, arg=None): frame = self.page().mainFrame() if arg is not None: js = 'document.execCommand("%s", false, "%s");' % (cmd, arg) else: js = 'document.execCommand("%s", false, null);' % cmd frame.evaluateJavaScript(js) def remove_format_cleanup(self): self.html = self.html @dynamic_property def html(self): def fget(self): ans = u'' check = unicode(self.page().mainFrame().toPlainText()).strip() if not check: return ans try: raw = unicode(self.page().mainFrame().toHtml()) raw = xml_to_unicode(raw, strip_encoding_pats=True, resolve_entities=True)[0] raw = self.comments_pat.sub('', raw) try: root = html.fromstring(raw) except: root = fromstring(raw) elems = [] for body in root.xpath('//body'): if body.text: elems.append(body.text) elems += [ html.tostring(x, encoding=unicode) for x in body if x.tag not in ('script', 'style') ] if len(elems) > 1: ans = u'<div>%s</div>' % (u''.join(elems)) else: ans = u''.join(elems) if not ans.startswith('<'): ans = '<p>%s</p>' % ans ans = xml_replace_entities(ans) except: import traceback traceback.print_exc() return ans def fset(self, val): self.setHtml(val) self.set_font_style() return property(fget=fget, fset=fset) def set_font_style(self): fi = QFontInfo(QApplication.font(self)) f = fi.pixelSize() + 1 + int( tweaks['change_book_details_font_size_by']) fam = unicode(fi.family()).strip().replace('"', '') if not fam: fam = 'sans-serif' style = 'font-size: %fpx; font-family:"%s",sans-serif;' % (f, fam) # toList() is needed because PyQt on Debian is old/broken for body in self.page().mainFrame().documentElement().findAll( 'body').toList(): body.setAttribute('style', style) self.page().setContentEditable(True) def keyPressEvent(self, ev): if ev.key() in (Qt.Key_Tab, Qt.Key_Escape, Qt.Key_Backtab): ev.ignore() else: return QWebView.keyPressEvent(self, ev) def keyReleaseEvent(self, ev): if ev.key() in (Qt.Key_Tab, Qt.Key_Escape, Qt.Key_Backtab): ev.ignore() else: return QWebView.keyReleaseEvent(self, ev)
class EditorWidget(QWebView): # {{{ def __init__(self, parent=None): QWebView.__init__(self, parent) self.readonly = False self.comments_pat = re.compile(r'<!--.*?-->', re.DOTALL) extra_shortcuts = { 'ToggleBold': 'Bold', 'ToggleItalic': 'Italic', 'ToggleUnderline': 'Underline', } for wac, name, icon, text, checkable in [ ('ToggleBold', 'bold', 'format-text-bold', _('Bold'), True), ('ToggleItalic', 'italic', 'format-text-italic', _('Italic'), True), ('ToggleUnderline', 'underline', 'format-text-underline', _('Underline'), True), ('ToggleStrikethrough', 'strikethrough', 'format-text-strikethrough', _('Strikethrough'), True), ('ToggleSuperscript', 'superscript', 'format-text-superscript', _('Superscript'), True), ('ToggleSubscript', 'subscript', 'format-text-subscript', _('Subscript'), True), ('InsertOrderedList', 'ordered_list', 'format-list-ordered', _('Ordered list'), True), ('InsertUnorderedList', 'unordered_list', 'format-list-unordered', _('Unordered list'), True), ('AlignLeft', 'align_left', 'format-justify-left', _('Align left'), False), ('AlignCenter', 'align_center', 'format-justify-center', _('Align center'), False), ('AlignRight', 'align_right', 'format-justify-right', _('Align right'), False), ('AlignJustified', 'align_justified', 'format-justify-fill', _('Align justified'), False), ('Undo', 'undo', 'edit-undo', _('Undo'), False), ('Redo', 'redo', 'edit-redo', _('Redo'), False), ('RemoveFormat', 'remove_format', 'trash', _('Remove formatting'), False), ('Copy', 'copy', 'edit-copy', _('Copy'), False), ('Paste', 'paste', 'edit-paste', _('Paste'), False), ('Cut', 'cut', 'edit-cut', _('Cut'), False), ('Indent', 'indent', 'format-indent-more', _('Increase Indentation'), False), ('Outdent', 'outdent', 'format-indent-less', _('Decrease Indentation'), False), ('SelectAll', 'select_all', 'edit-select-all', _('Select all'), False), ]: ac = PageAction(wac, icon, text, checkable, self) setattr(self, 'action_'+name, ac) ss = extra_shortcuts.get(wac, None) if ss: ac.setShortcut(QKeySequence(getattr(QKeySequence, ss))) if wac == 'RemoveFormat': ac.triggered.connect(self.remove_format_cleanup, type=Qt.QueuedConnection) self.action_color = QAction(QIcon(I('format-text-color')), _('Foreground color'), self) self.action_color.triggered.connect(self.foreground_color) self.action_background = QAction(QIcon(I('format-fill-color')), _('Background color'), self) self.action_background.triggered.connect(self.background_color) self.action_block_style = QAction(QIcon(I('format-text-heading')), _('Style text block'), self) self.action_block_style.setToolTip( _('Style the selected text block')) self.block_style_menu = QMenu(self) self.action_block_style.setMenu(self.block_style_menu) self.block_style_actions = [] for text, name in [ (_('Normal'), 'p'), (_('Heading') +' 1', 'h1'), (_('Heading') +' 2', 'h2'), (_('Heading') +' 3', 'h3'), (_('Heading') +' 4', 'h4'), (_('Heading') +' 5', 'h5'), (_('Heading') +' 6', 'h6'), (_('Pre-formatted'), 'pre'), (_('Blockquote'), 'blockquote'), (_('Address'), 'address'), ]: ac = BlockStyleAction(text, name, self) self.block_style_menu.addAction(ac) self.block_style_actions.append(ac) self.action_insert_link = QAction(QIcon(I('insert-link.png')), _('Insert link or image'), self) self.action_insert_link.triggered.connect(self.insert_link) self.pageAction(QWebPage.ToggleBold).changed.connect(self.update_link_action) self.action_insert_link.setEnabled(False) self.action_clear = QAction(QIcon(I('edit-clear')), _('Clear'), self) self.action_clear.triggered.connect(self.clear_text) self.page().setLinkDelegationPolicy(QWebPage.DelegateAllLinks) self.page().linkClicked.connect(self.link_clicked) self.setHtml('') self.set_readonly(False) def update_link_action(self): wac = self.pageAction(QWebPage.ToggleBold) self.action_insert_link.setEnabled(wac.isEnabled()) def set_readonly(self, what): self.readonly = what self.page().setContentEditable(not self.readonly) def clear_text(self, *args): us = self.page().undoStack() us.beginMacro('clear all text') self.action_select_all.trigger() self.action_remove_format.trigger() self.exec_command('delete') us.endMacro() self.set_font_style() self.setFocus(Qt.OtherFocusReason) def link_clicked(self, url): open_url(url) def foreground_color(self): col = QColorDialog.getColor(Qt.black, self, _('Choose foreground color'), QColorDialog.ShowAlphaChannel) if col.isValid(): self.exec_command('foreColor', unicode(col.name())) def background_color(self): col = QColorDialog.getColor(Qt.white, self, _('Choose background color'), QColorDialog.ShowAlphaChannel) if col.isValid(): self.exec_command('hiliteColor', unicode(col.name())) def insert_link(self, *args): link, name, is_image = self.ask_link() if not link: return url = self.parse_link(link) if url.isValid(): url = unicode(url.toString()) self.setFocus(Qt.OtherFocusReason) if is_image: self.exec_command('insertHTML', '<img src="%s" alt="%s"></img>'%(prepare_string_for_xml(url, True), prepare_string_for_xml(name or _('Image'), True))) elif name: self.exec_command('insertHTML', '<a href="%s">%s</a>'%(prepare_string_for_xml(url, True), prepare_string_for_xml(name))) else: self.exec_command('createLink', url) else: error_dialog(self, _('Invalid URL'), _('The url %r is invalid') % link, show=True) def ask_link(self): d = QDialog(self) d.setWindowTitle(_('Create link')) l = QFormLayout() d.setLayout(l) d.url = QLineEdit(d) d.name = QLineEdit(d) d.treat_as_image = QCheckBox(d) d.setMinimumWidth(600) d.bb = QDialogButtonBox(QDialogButtonBox.Ok|QDialogButtonBox.Cancel) d.br = b = QPushButton(_('&Browse')) b.setIcon(QIcon(I('document_open.png'))) def cf(): files = choose_files(d, 'select link file', _('Choose file'), select_only_single_file=True) if files: path = files[0] d.url.setText(path) if path and os.path.exists(path): with lopen(path, 'rb') as f: q = what(f) is_image = q in {'jpeg', 'png', 'gif'} d.treat_as_image.setChecked(is_image) b.clicked.connect(cf) d.la = la = QLabel(_( 'Enter a URL. If you check the "Treat the URL as an image" box ' 'then the URL will be added as an image reference instead of as ' 'a link. You can also choose to create a link to a file on ' 'your computer. ' 'Note that if you create a link to a file on your computer, it ' 'will stop working if the file is moved.')) la.setWordWrap(True) la.setStyleSheet('QLabel { margin-bottom: 1.5ex }') l.setWidget(0, l.SpanningRole, la) l.addRow(_('Enter &URL:'), d.url) l.addRow(_('Treat the URL as an &image'), d.treat_as_image) l.addRow(_('Enter &name (optional):'), d.name) l.addRow(_('Choose a file on your computer:'), d.br) l.addRow(d.bb) d.bb.accepted.connect(d.accept) d.bb.rejected.connect(d.reject) d.resize(d.sizeHint()) link, name, is_image = None, None, False if d.exec_() == d.Accepted: link, name = unicode(d.url.text()).strip(), unicode(d.name.text()).strip() is_image = d.treat_as_image.isChecked() return link, name, is_image def parse_link(self, link): link = link.strip() if link and os.path.exists(link): return QUrl.fromLocalFile(link) has_schema = re.match(r'^[a-zA-Z]+:', link) if has_schema is not None: url = QUrl(link, QUrl.TolerantMode) if url.isValid(): return url if os.path.exists(link): return QUrl.fromLocalFile(link) if has_schema is None: first, _, rest = link.partition('.') prefix = 'http' if first == 'ftp': prefix = 'ftp' url = QUrl(prefix +'://'+link, QUrl.TolerantMode) if url.isValid(): return url return QUrl(link, QUrl.TolerantMode) def sizeHint(self): return QSize(150, 150) def exec_command(self, cmd, arg=None): frame = self.page().mainFrame() if arg is not None: js = 'document.execCommand("%s", false, %s);' % (cmd, json.dumps(unicode(arg))) else: js = 'document.execCommand("%s", false, null);' % cmd frame.evaluateJavaScript(js) def remove_format_cleanup(self): self.html = self.html @dynamic_property def html(self): def fget(self): ans = u'' try: check = unicode(self.page().mainFrame().toPlainText()).strip() raw = unicode(self.page().mainFrame().toHtml()) raw = xml_to_unicode(raw, strip_encoding_pats=True, resolve_entities=True)[0] raw = self.comments_pat.sub('', raw) if not check and '<img' not in raw.lower(): return ans try: root = html.fromstring(raw) except: root = fromstring(raw) elems = [] for body in root.xpath('//body'): if body.text: elems.append(body.text) elems += [html.tostring(x, encoding=unicode) for x in body if x.tag not in ('script', 'style')] if len(elems) > 1: ans = u'<div>%s</div>'%(u''.join(elems)) else: ans = u''.join(elems) if not ans.startswith('<'): ans = '<p>%s</p>'%ans ans = xml_replace_entities(ans) except: import traceback traceback.print_exc() return ans def fset(self, val): self.setHtml(val) self.set_font_style() return property(fget=fget, fset=fset) def set_font_style(self): fi = QFontInfo(QApplication.font(self)) f = fi.pixelSize() + 1 + int(tweaks['change_book_details_font_size_by']) fam = unicode(fi.family()).strip().replace('"', '') if not fam: fam = 'sans-serif' style = 'font-size: %fpx; font-family:"%s",sans-serif;' % (f, fam) # toList() is needed because PyQt on Debian is old/broken for body in self.page().mainFrame().documentElement().findAll('body').toList(): body.setAttribute('style', style) self.page().setContentEditable(not self.readonly) def keyPressEvent(self, ev): if ev.key() in (Qt.Key_Tab, Qt.Key_Escape, Qt.Key_Backtab): ev.ignore() else: return QWebView.keyPressEvent(self, ev) def keyReleaseEvent(self, ev): if ev.key() in (Qt.Key_Tab, Qt.Key_Escape, Qt.Key_Backtab): ev.ignore() else: return QWebView.keyReleaseEvent(self, ev) def contextMenuEvent(self, ev): menu = self.page().createStandardContextMenu() paste = self.pageAction(QWebPage.Paste) for action in menu.actions(): if action == paste: menu.insertAction(action, self.pageAction(QWebPage.PasteAndMatchStyle)) menu.exec_(ev.globalPos())
class EditorWidget(QWebView): # {{{ def __init__(self, parent=None): QWebView.__init__(self, parent) self.comments_pat = re.compile(r'<!--.*?-->', re.DOTALL) extra_shortcuts = { 'ToggleBold': 'Bold', 'ToggleItalic': 'Italic', 'ToggleUnderline': 'Underline', } for wac, name, icon, text, checkable in [ ('ToggleBold', 'bold', 'format-text-bold', _('Bold'), True), ('ToggleItalic', 'italic', 'format-text-italic', _('Italic'), True), ('ToggleUnderline', 'underline', 'format-text-underline', _('Underline'), True), ('ToggleStrikethrough', 'strikethrough', 'format-text-strikethrough', _('Strikethrough'), True), ('ToggleSuperscript', 'superscript', 'format-text-superscript', _('Superscript'), True), ('ToggleSubscript', 'subscript', 'format-text-subscript', _('Subscript'), True), ('InsertOrderedList', 'ordered_list', 'format-list-ordered', _('Ordered list'), True), ('InsertUnorderedList', 'unordered_list', 'format-list-unordered', _('Unordered list'), True), ('AlignLeft', 'align_left', 'format-justify-left', _('Align left'), False), ('AlignCenter', 'align_center', 'format-justify-center', _('Align center'), False), ('AlignRight', 'align_right', 'format-justify-right', _('Align right'), False), ('AlignJustified', 'align_justified', 'format-justify-fill', _('Align justified'), False), ('Undo', 'undo', 'edit-undo', _('Undo'), False), ('Redo', 'redo', 'edit-redo', _('Redo'), False), ('RemoveFormat', 'remove_format', 'trash', _('Remove formatting'), False), ('Copy', 'copy', 'edit-copy', _('Copy'), False), ('Paste', 'paste', 'edit-paste', _('Paste'), False), ('Cut', 'cut', 'edit-cut', _('Cut'), False), ('Indent', 'indent', 'format-indent-more', _('Increase Indentation'), False), ('Outdent', 'outdent', 'format-indent-less', _('Decrease Indentation'), False), ('SelectAll', 'select_all', 'edit-select-all', _('Select all'), False), ]: ac = PageAction(wac, icon, text, checkable, self) setattr(self, 'action_'+name, ac) ss = extra_shortcuts.get(wac, None) if ss: ac.setShortcut(QKeySequence(getattr(QKeySequence, ss))) if wac == 'RemoveFormat': ac.triggered.connect(self.remove_format_cleanup, type=Qt.QueuedConnection) self.action_color = QAction(QIcon(I('format-text-color')), _('Foreground color'), self) self.action_color.triggered.connect(self.foreground_color) self.action_background = QAction(QIcon(I('format-fill-color')), _('Background color'), self) self.action_background.triggered.connect(self.background_color) self.action_block_style = QAction(QIcon(I('format-text-heading')), _('Style text block'), self) self.action_block_style.setToolTip( _('Style the selected text block')) self.block_style_menu = QMenu(self) self.action_block_style.setMenu(self.block_style_menu) self.block_style_actions = [] for text, name in [ (_('Normal'), 'p'), (_('Heading') +' 1', 'h1'), (_('Heading') +' 2', 'h2'), (_('Heading') +' 3', 'h3'), (_('Heading') +' 4', 'h4'), (_('Heading') +' 5', 'h5'), (_('Heading') +' 6', 'h6'), (_('Pre-formatted'), 'pre'), (_('Blockquote'), 'blockquote'), (_('Address'), 'address'), ]: ac = BlockStyleAction(text, name, self) self.block_style_menu.addAction(ac) self.block_style_actions.append(ac) self.action_insert_link = QAction(QIcon(I('insert-link.png')), _('Insert link'), self) self.action_insert_link.triggered.connect(self.insert_link) self.action_clear = QAction(QIcon(I('edit-clear')), _('Clear'), self) self.action_clear.triggered.connect(self.clear_text) self.page().setLinkDelegationPolicy(QWebPage.DelegateAllLinks) self.page().linkClicked.connect(self.link_clicked) self.setHtml('') self.page().setContentEditable(True) def clear_text(self, *args): us = self.page().undoStack() us.beginMacro('clear all text') self.action_select_all.trigger() self.action_remove_format.trigger() self.exec_command('delete') us.endMacro() self.set_font_style() self.setFocus(Qt.OtherFocusReason) def link_clicked(self, url): open_url(url) def foreground_color(self): col = QColorDialog.getColor(Qt.black, self, _('Choose foreground color'), QColorDialog.ShowAlphaChannel) if col.isValid(): self.exec_command('foreColor', unicode(col.name())) def background_color(self): col = QColorDialog.getColor(Qt.white, self, _('Choose background color'), QColorDialog.ShowAlphaChannel) if col.isValid(): self.exec_command('hiliteColor', unicode(col.name())) def insert_link(self, *args): link, ok = QInputDialog.getText(self, _('Create link'), _('Enter URL')) if not ok: return url = self.parse_link(unicode(link)) if url.isValid(): url = unicode(url.toString()) self.exec_command('createLink', url) def parse_link(self, link): link = link.strip() has_schema = re.match(r'^[a-zA-Z]+:', link) if has_schema is not None: url = QUrl(link, QUrl.TolerantMode) if url.isValid(): return url if os.path.exists(link): return QUrl.fromLocalFile(link) if has_schema is None: first, _, rest = link.partition('.') prefix = 'http' if first == 'ftp': prefix = 'ftp' url = QUrl(prefix +'://'+link, QUrl.TolerantMode) if url.isValid(): return url return QUrl(link, QUrl.TolerantMode) def sizeHint(self): return QSize(150, 150) def exec_command(self, cmd, arg=None): frame = self.page().mainFrame() if arg is not None: js = 'document.execCommand("%s", false, "%s");' % (cmd, arg) else: js = 'document.execCommand("%s", false, null);' % cmd frame.evaluateJavaScript(js) def remove_format_cleanup(self): self.html = self.html @dynamic_property def html(self): def fget(self): ans = u'' check = unicode(self.page().mainFrame().toPlainText()).strip() if not check: return ans try: raw = unicode(self.page().mainFrame().toHtml()) raw = xml_to_unicode(raw, strip_encoding_pats=True, resolve_entities=True)[0] raw = self.comments_pat.sub('', raw) try: root = html.fromstring(raw) except: root = fromstring(raw) elems = [] for body in root.xpath('//body'): if body.text: elems.append(body.text) elems += [html.tostring(x, encoding=unicode) for x in body if x.tag not in ('script', 'style')] if len(elems) > 1: ans = u'<div>%s</div>'%(u''.join(elems)) else: ans = u''.join(elems) if not ans.startswith('<'): ans = '<p>%s</p>'%ans ans = xml_replace_entities(ans) except: import traceback traceback.print_exc() return ans def fset(self, val): self.setHtml(val) self.set_font_style() return property(fget=fget, fset=fset) def set_font_style(self): fi = QFontInfo(QApplication.font(self)) f = fi.pixelSize() + 1 + int(tweaks['change_book_details_font_size_by']) fam = unicode(fi.family()).strip().replace('"', '') if not fam: fam = 'sans-serif' style = 'font-size: %fpx; font-family:"%s",sans-serif;' % (f, fam) # toList() is needed because PyQt on Debian is old/broken for body in self.page().mainFrame().documentElement().findAll('body').toList(): body.setAttribute('style', style) self.page().setContentEditable(True) def keyPressEvent(self, ev): if ev.key() in (Qt.Key_Tab, Qt.Key_Escape, Qt.Key_Backtab): ev.ignore() else: return QWebView.keyPressEvent(self, ev) def keyReleaseEvent(self, ev): if ev.key() in (Qt.Key_Tab, Qt.Key_Escape, Qt.Key_Backtab): ev.ignore() else: return QWebView.keyReleaseEvent(self, ev)