class Browser(QWidget): """LilyPond documentation browser widget.""" def __init__(self, dockwidget): super(Browser, self).__init__(dockwidget) layout = QVBoxLayout(spacing=0) layout.setContentsMargins(0, 0, 0, 0) self.setLayout(layout) self.toolbar = tb = QToolBar() self.webview = QWebView(contextMenuPolicy=Qt.CustomContextMenu) self.chooser = QComboBox(sizeAdjustPolicy=QComboBox.AdjustToContents) self.search = SearchEntry(maximumWidth=200) layout.addWidget(self.toolbar) layout.addWidget(self.webview) ac = dockwidget.actionCollection ac.help_back.triggered.connect(self.webview.back) ac.help_forward.triggered.connect(self.webview.forward) ac.help_home.triggered.connect(self.showHomePage) ac.help_print.triggered.connect(self.slotPrint) self.webview.page().setNetworkAccessManager( lilydoc.network.accessmanager()) self.webview.page().setLinkDelegationPolicy(QWebPage.DelegateAllLinks) self.webview.page().linkClicked.connect(self.openUrl) self.webview.page().setForwardUnsupportedContent(True) self.webview.page().unsupportedContent.connect(self.slotUnsupported) self.webview.urlChanged.connect(self.slotUrlChanged) self.webview.customContextMenuRequested.connect( self.slotShowContextMenu) tb.addAction(ac.help_back) tb.addAction(ac.help_forward) tb.addSeparator() tb.addAction(ac.help_home) tb.addAction(ac.help_print) tb.addSeparator() tb.addWidget(self.chooser) tb.addWidget(self.search) self.chooser.activated[int].connect(self.showHomePage) self.search.textEdited.connect(self.slotSearchChanged) self.search.returnPressed.connect(self.slotSearchReturnPressed) dockwidget.mainwindow().iconSizeChanged.connect( self.updateToolBarSettings) dockwidget.mainwindow().toolButtonStyleChanged.connect( self.updateToolBarSettings) app.settingsChanged.connect(self.readSettings) self.readSettings() self.loadDocumentation() self.showInitialPage() app.settingsChanged.connect(self.loadDocumentation) app.translateUI(self) def readSettings(self): s = QSettings() s.beginGroup("documentation") ws = self.webview.page().settings() family = s.value("fontfamily", self.font().family(), type("")) size = s.value("fontsize", 16, int) ws.setFontFamily(QWebSettings.StandardFont, family) ws.setFontSize(QWebSettings.DefaultFontSize, size) fixed = textformats.formatData('editor').font ws.setFontFamily(QWebSettings.FixedFont, fixed.family()) ws.setFontSize(QWebSettings.DefaultFixedFontSize, fixed.pointSizeF() * 96 / 72) def keyPressEvent(self, ev): if ev.text() == "/": self.search.setFocus() else: super(Browser, self).keyPressEvent(ev) def translateUI(self): try: self.search.setPlaceholderText(_("Search...")) except AttributeError: pass # not in Qt 4.6 def showInitialPage(self): """Shows the preferred start page. If a local documentation instance already has a suitable version, just loads it. Otherwise connects to the allLoaded signal, that is emitted when all the documentation instances have loaded their version information and then shows the start page (if another page wasn't yet loaded). """ if self.webview.url().isEmpty(): docs = lilydoc.manager.docs() version = lilypondinfo.preferred().version() index = -1 if version: for num, doc in enumerate(docs): if doc.version() is not None and doc.version() >= version: index = num # a suitable documentation is found break if index == -1: # nothing found (or LilyPond version not available), # wait for loading or show the most recent version if not lilydoc.manager.loaded(): lilydoc.manager.allLoaded.connect(self.showInitialPage) return index = len(docs) - 1 self.chooser.setCurrentIndex(index) self.showHomePage() def loadDocumentation(self): """Puts the available documentation instances in the combobox.""" i = self.chooser.currentIndex() self.chooser.clear() for doc in lilydoc.manager.docs(): v = doc.versionString() if doc.isLocal(): t = _("(local)") else: t = _("({hostname})").format(hostname=doc.url().host()) self.chooser.addItem("{0} {1}".format(v or _("<unknown>"), t)) self.chooser.setCurrentIndex(i) if not lilydoc.manager.loaded(): lilydoc.manager.allLoaded.connect(self.loadDocumentation, -1) return def updateToolBarSettings(self): mainwin = self.parentWidget().mainwindow() self.toolbar.setIconSize(mainwin.iconSize()) self.toolbar.setToolButtonStyle(mainwin.toolButtonStyle()) def showManual(self): """Invoked when the user presses F1.""" self.slotHomeFrescobaldi() # TEMP def slotUrlChanged(self): ac = self.parentWidget().actionCollection ac.help_back.setEnabled(self.webview.history().canGoBack()) ac.help_forward.setEnabled(self.webview.history().canGoForward()) def openUrl(self, url): if url.path().endswith(('.ily', '.lyi', '.ly')): self.sourceViewer().showReply(lilydoc.network.get(url)) else: self.webview.load(url) def slotUnsupported(self, reply): helpers.openUrl(reply.url()) def slotSearchChanged(self): text = self.search.text() if not text.startswith(':'): self.webview.page().findText(text, QWebPage.FindWrapsAroundDocument) def slotSearchReturnPressed(self): text = self.search.text() if not text.startswith(':'): self.slotSearchChanged() else: pass # TODO: implement full doc search def sourceViewer(self): try: return self._sourceviewer except AttributeError: from . import sourceviewer self._sourceviewer = sourceviewer.SourceViewer(self) return self._sourceviewer def showHomePage(self): """Shows the homepage of the LilyPond documentation.""" i = self.chooser.currentIndex() if i < 0: i = 0 doc = lilydoc.manager.docs()[i] url = doc.home() if doc.isLocal(): path = url.toLocalFile() langs = lilydoc.network.langs() if langs: for lang in langs: if os.path.exists(path + '.' + lang + '.html'): path += '.' + lang break url = QUrl.fromLocalFile(path + '.html') self.webview.load(url) def slotPrint(self): printer = QPrinter() dlg = QPrintDialog(printer, self) dlg.setWindowTitle(app.caption(_("Print"))) if dlg.exec_(): self.webview.print_(printer) def slotShowContextMenu(self, pos): hit = self.webview.page().currentFrame().hitTestContent(pos) menu = QMenu() if hit.linkUrl().isValid(): a = self.webview.pageAction(QWebPage.CopyLinkToClipboard) a.setIcon(icons.get("edit-copy")) a.setText(_("Copy &Link")) menu.addAction(a) menu.addSeparator() a = menu.addAction(icons.get("window-new"), _("Open Link in &New Window")) a.triggered.connect( (lambda url: lambda: self.slotNewWindow(url))(hit.linkUrl())) else: if hit.isContentSelected(): a = self.webview.pageAction(QWebPage.Copy) a.setIcon(icons.get("edit-copy")) a.setText(_("&Copy")) menu.addAction(a) menu.addSeparator() a = menu.addAction(icons.get("window-new"), _("Open Document in &New Window")) a.triggered.connect((lambda url: lambda: self.slotNewWindow(url))( self.webview.url())) if menu.actions(): menu.exec_(self.webview.mapToGlobal(pos)) def slotNewWindow(self, url): helpers.openUrl(url)
class MainWindow(QMainWindow): subWindows = [] def __init__(self, parent=None): QMainWindow.__init__(self, parent) self.setWindowTitle("Seabiscuit2") self.menuBar = QMenuBar() self.setMenuBar(self.menuBar) self.fileMenu = QMenu("&File") self.menuBar.addMenu(self.fileMenu) self.newWindowAction = QAction("&New Window", self) self.newWindowAction.setShortcut("Ctrl+N") self.newWindowAction.triggered.connect(self.newWindow) self.fileMenu.addAction(self.newWindowAction) self.editMenu = QMenu("&Edit") self.menuBar.addMenu(self.editMenu) self.toolsMenu = QMenu("&Tools") self.menuBar.addMenu(self.toolsMenu) self.historyAction = QAction("View &History", self) self.historyAction.setShortcut("Ctrl+H") self.historyAction.triggered.connect(self.loadHistory) self.toolsMenu.addAction(self.historyAction) self.clearHistoryAction = QAction("&Clear History", self) self.clearHistoryAction.setShortcut("Ctrl+Shift+Del") self.clearHistoryAction.triggered.connect(self.clearHistory) self.toolsMenu.addAction(self.clearHistoryAction) self.helpMenu = QMenu("&Help") self.menuBar.addMenu(self.helpMenu) self.readmeAction = QAction("View &README", self) self.readmeAction.setShortcut("F1") self.readmeAction.triggered.connect(self.loadReadme) self.helpMenu.addAction(self.readmeAction) self.aboutAction = QAction("&About Seabiscuit2", self) self.aboutAction.setShortcut("F2") self.aboutAction.triggered.connect(lambda: self.render("Seabiscuit version " + version + " (running on " + platform + ")")) self.helpMenu.addAction(self.aboutAction) self.toolBar = QToolBar() self.toolBar.setMovable(False) self.toolBar.setContextMenuPolicy(Qt.CustomContextMenu) self.addToolBar(self.toolBar) self.wv = QWebView() self.backAction = self.wv.pageAction(QWebPage.Back) self.backAction.setEnabled(True) self.backAction.setShortcut("Alt+Left") self.backAction.triggered.connect(lambda: self.mdiArea.currentSubWindow().widget().back()) self.toolBar.addAction(self.backAction) self.forwardAction = self.wv.pageAction(QWebPage.Forward) self.forwardAction.setEnabled(True) self.forwardAction.setShortcut("Alt+Right") self.forwardAction.triggered.connect(lambda: self.mdiArea.currentSubWindow().widget().forward()) self.toolBar.addAction(self.forwardAction) self.reloadAction = self.wv.pageAction(QWebPage.Reload) self.reloadAction.setShortcuts(["F5", "Ctrl+R"]) self.reloadAction.triggered.connect(lambda: self.mdiArea.currentSubWindow().widget().reload()) self.toolBar.addAction(self.reloadAction) self.locationBar = QLineEdit() self.locationBar.returnPressed.connect(self.loadCommand) self.toolBar.addWidget(self.locationBar) self.focusLocationBarAction = QAction(self) self.focusLocationBarAction.setShortcuts(["Ctrl+L", "Alt+D"]) self.focusLocationBarAction.triggered.connect(self.locationBar.setFocus) self.focusLocationBarAction.triggered.connect(self.locationBar.selectAll) self.addAction(self.focusLocationBarAction) self.mdiArea = QMdiArea() self.setCentralWidget(self.mdiArea) def newWindow(self): s = SWebView(self) self.subWindows.append(s) self.mdiArea.addSubWindow(s) s.activateWindow() s.show() return s def loadHistory(self): self.newWindow() self.updateWeb(os.path.join(seabiscuit_home, "history.html")) def loadReadme(self): self.newWindow() self.updateWeb("file://" + os.path.join(sys.path[0], "README.html")) def closeEvent(self, ev): confirm = yes_no_question("Query", "Are you sure you want to quit?") if confirm: ev.accept() sys.exit() else: ev.ignore() def render(self, text): QMessageBox.question(None, "Seabiscuit2 says...", text.replace("\n", "<br>")) def updateWeb(self, url=None): if url != None: self.mdiArea.currentSubWindow().widget().load(QUrl.fromUserInput(url)) def clearHistory(self): confirm = yes_no_question("Query", "Are you sure you want to clear the browser history?") if confirm: historyfile = open(os.path.join(seabiscuit_home, "history.html"), "w") historyfile.write("<html><head><script type='text/javascript'>function breakPage(url) {top.location = url}</script></head><body style=\"color: black; background: white;\"><h2>Browsing History</h2>\n") historyfile.close() def loadCommand(self): command = unicode(self.locationBar.text()) if command.lower().startswith("browser:"): command = command.replace("browser:", "") webbrowser.open(command) self.render("The page has been loaded in your default Web browser.") elif command.lower().startswith("download:"): command = command.replace("download:", "") location = "" while not os.path.exists(location): location = unicode(QInputDialog.getText("Query", "Enter a valid folder to save your file under.")) if not os.path.exists(location): render("Invalid path!") os.chdir(location) os.system("wget '" + command + "'") os.chdir(path[0]) else: self.updateWeb(command)
class BrowserTab(QObject): """ An object for controlling a single browser tab (QWebView). It is created by splash.pool.Pool. Pool attaches to tab's deferred and waits until either a callback or an errback is called, then destroys a BrowserTab. XXX: currently cookies are not shared between "browser tabs". """ def __init__(self, network_manager, splash_proxy_factory, verbosity, render_options): """ Create a new browser tab. """ QObject.__init__(self) self.deferred = defer.Deferred() self.network_manager = network_manager self.verbosity = verbosity self._uid = render_options.get_uid() self._closing = False self._active_timers = set() self._timers_to_cancel_on_redirect = weakref.WeakKeyDictionary() # timer: callback self._timers_to_cancel_on_error = weakref.WeakKeyDictionary() # timer: callback self._js_console = None self._history = [] self._autoload_scripts = [] self._init_webpage(verbosity, network_manager, splash_proxy_factory, render_options) self._setup_logging(verbosity) self.http_client = _SplashHttpClient(self.web_page) def _init_webpage(self, verbosity, network_manager, splash_proxy_factory, render_options): """ Create and initialize QWebPage and QWebView """ self.web_page = SplashQWebPage(verbosity) self.web_page.setNetworkAccessManager(network_manager) self.web_page.splash_proxy_factory = splash_proxy_factory self.web_page.render_options = render_options self._set_default_webpage_options(self.web_page) self._setup_webpage_events() self.web_view = QWebView() self.web_view.setPage(self.web_page) self.web_view.setAttribute(Qt.WA_DeleteOnClose, True) def _set_default_webpage_options(self, web_page): """ Set QWebPage options. TODO: allow to customize them. """ settings = web_page.settings() settings.setAttribute(QWebSettings.JavascriptEnabled, True) settings.setAttribute(QWebSettings.PluginsEnabled, False) settings.setAttribute(QWebSettings.PrivateBrowsingEnabled, True) settings.setAttribute(QWebSettings.LocalStorageEnabled, True) settings.setAttribute(QWebSettings.LocalContentCanAccessRemoteUrls, True) web_page.mainFrame().setScrollBarPolicy(Qt.Vertical, Qt.ScrollBarAlwaysOff) web_page.mainFrame().setScrollBarPolicy(Qt.Horizontal, Qt.ScrollBarAlwaysOff) def _setup_logging(self, verbosity): """ Setup logging of various events """ self.logger = _BrowserTabLogger( uid=self._uid, web_page=self.web_page, verbosity=verbosity, ) self.logger.enable() def _setup_webpage_events(self): self._load_finished = WrappedSignal(self.web_page.mainFrame().loadFinished) self.web_page.mainFrame().loadFinished.connect(self._on_load_finished) self.web_page.mainFrame().urlChanged.connect(self._on_url_changed) self.web_page.mainFrame().javaScriptWindowObjectCleared.connect(self._on_javascript_window_object_cleared) def return_result(self, result): """ Return a result to the Pool. """ if self._result_already_returned(): self.logger.log("error: result is already returned", min_level=1) self.deferred.callback(result) # self.deferred = None def return_error(self, error=None): """ Return an error to the Pool. """ if self._result_already_returned(): self.logger.log("error: result is already returned", min_level=1) self.deferred.errback(error) # self.deferred = None def _result_already_returned(self): """ Return True if an error or a result is already returned to Pool """ return self.deferred.called def set_custom_headers(self, headers): """ Set custom HTTP headers to be sent with each request. Passed headers are merged with QWebKit default headers, overwriting QWebKit values in case of conflicts. """ self.web_page.custom_headers = headers def set_images_enabled(self, enabled): self.web_page.settings().setAttribute(QWebSettings.AutoLoadImages, enabled) def set_viewport(self, size): """ Set viewport size. If size is "full" viewport size is detected automatically. If can also be "<width>x<height>". """ if size == 'full': size = self.web_page.mainFrame().contentsSize() if size.isEmpty(): self.logger.log("contentsSize method doesn't work %s", min_level=1) size = defaults.VIEWPORT_FALLBACK if not isinstance(size, QSize): w, h = map(int, size.split('x')) size = QSize(w, h) self.web_page.setViewportSize(size) w, h = int(size.width()), int(size.height()) self.logger.log("viewport size is set to %sx%s" % (w, h), min_level=2) return w, h def lock_navigation(self): self.web_page.navigation_locked = True def unlock_navigation(self): self.web_page.navigation_locked = False def set_content(self, data, callback, errback, mime_type=None, baseurl=None): """ Set page contents to ``data``, then wait until page loads. Invoke a callback if load was successful or errback if it wasn't. """ if mime_type is None: mime_type = "text/html; charset=utf-8" if baseurl is None: baseurl = '' if isinstance(data, unicode): data = data.encode('utf8') callback_id = self._load_finished.connect( self._on_content_ready, callback=callback, errback=errback, ) self.logger.log("callback %s is connected to loadFinished" % callback_id, min_level=3) self.web_page.mainFrame().setContent(data, mime_type, QUrl(baseurl)) def set_user_agent(self, value): """ Set User-Agent header for future requests """ self.http_client.set_user_agent(value) def get_cookies(self): """ Return a list of all cookies in the current cookiejar """ return cookies2har(self.web_page.cookiejar.allCookies()) def init_cookies(self, cookies): """ Replace all current cookies with ``cookies`` """ self.web_page.cookiejar.init(cookies) def clear_cookies(self): """ Remove all cookies. Return a number of cookies deleted. """ return self.web_page.cookiejar.clear() def delete_cookies(self, name=None, url=None): """ Delete cookies with name == ``name``. If ``url`` is not None then only those cookies are deleted wihch are to be added when a request is sent to ``url``. Return a number of cookies deleted. """ return self.web_page.cookiejar.delete(name, url) def add_cookie(self, cookie): return self.web_page.cookiejar.add(cookie) @property def url(self): """ Current URL """ return unicode(self.web_page.mainFrame().url().toString()) def go(self, url, callback, errback, baseurl=None, http_method='GET', body=None, headers=None): """ Go to an URL. This is similar to entering an URL in address tab and pressing Enter. """ self.store_har_timing("_onStarted") if baseurl: # If baseurl is used, we download the page manually, # then set its contents to the QWebPage and let it # download related resources and render the result. cb = functools.partial( self._on_baseurl_request_finished, callback=callback, errback=errback, baseurl=baseurl, url=url, ) self.http_client.request(url, callback=cb, method=http_method, body=body, headers=headers, follow_redirects=True, ) else: # if not self._goto_callbacks.isempty(): # self.logger.log("Only a single concurrent 'go' request is supported. " # "Previous go requests will be cancelled.", min_level=1) # # When a new URL is loaded to mainFrame an errback will # # be called, so we're not cancelling this callback manually. callback_id = self._load_finished.connect( self._on_content_ready, callback=callback, errback=errback, ) self.logger.log("callback %s is connected to loadFinished" % callback_id, min_level=3) self._load_url_to_mainframe(url, http_method, body, headers=headers) def stop_loading(self): """ Stop loading of the current page and all pending page refresh/redirect requests. """ self.logger.log("stop_loading", min_level=2) self.web_view.pageAction(QWebPage.StopScheduledPageRefresh) self.web_view.stop() def close(self): """ Destroy this tab """ self._closing = True self.web_view.pageAction(QWebPage.StopScheduledPageRefresh) self.web_view.stop() self.web_view.close() self.web_page.deleteLater() self.web_view.deleteLater() self._cancel_all_timers() @skip_if_closing def _on_load_finished(self, ok): if self.web_page.maybe_redirect(ok): self.logger.log("Redirect or other non-fatal error detected", min_level=2) return if self.web_page.is_ok(ok): # or maybe_redirect: self.logger.log("loadFinished: ok", min_level=2) else: self._cancel_timers(self._timers_to_cancel_on_error) if self.web_page.error_loading(ok): self.logger.log("loadFinished: %s" % (str(self.web_page.error_info)), min_level=1) else: self.logger.log("loadFinished: unknown error", min_level=1) def _on_baseurl_request_finished(self, callback, errback, baseurl, url): """ This method is called when ``baseurl`` is used and a reply for the first request is received. """ self.logger.log("baseurl_request_finished", min_level=2) reply = self.sender() mime_type = reply.header(QNetworkRequest.ContentTypeHeader).toString() data = reply.readAll() self.set_content( data=data, callback=callback, errback=errback, mime_type=mime_type, baseurl=baseurl, ) if reply.error(): self.logger.log("Error loading %s: %s" % (url, reply.errorString()), min_level=1) def _load_url_to_mainframe(self, url, http_method, body=None, headers=None): request = self.http_client.request_obj(url, headers=headers) meth = OPERATION_QT_CONSTANTS[http_method] if body is None: # PyQT doesn't support body=None self.web_page.mainFrame().load(request, meth) else: self.web_page.mainFrame().load(request, meth, body) @skip_if_closing def _on_content_ready(self, ok, callback, errback, callback_id): """ This method is called when a QWebPage finishes loading its contents. """ if self.web_page.maybe_redirect(ok): # XXX: It assumes loadFinished will be called again because # redirect happens. If redirect is detected improperly, # loadFinished won't be called again, and Splash will return # the result only after a timeout. return self.logger.log("loadFinished: disconnecting callback %s" % callback_id, min_level=3) self._load_finished.disconnect(callback_id) if self.web_page.is_ok(ok): callback() elif self.web_page.error_loading(ok): # XXX: maybe return a meaningful error page instead of generic # error message? errback() # errback(RenderError()) else: errback() # errback(RenderError()) def wait(self, time_ms, callback, onredirect=None, onerror=None): """ Wait for time_ms, then run callback. If onredirect is True then the timer is cancelled if redirect happens. If onredirect is callable then in case of redirect the timer is cancelled and this callable is called. If onerror is True then the timer is cancelled if a render error happens. If onerror is callable then in case of a render error the timer is cancelled and this callable is called. """ timer = QTimer() timer.setSingleShot(True) timer_callback = functools.partial(self._on_wait_timeout, timer=timer, callback=callback, ) timer.timeout.connect(timer_callback) self.logger.log("waiting %sms; timer %s" % (time_ms, id(timer)), min_level=2) timer.start(time_ms) self._active_timers.add(timer) if onredirect: self._timers_to_cancel_on_redirect[timer] = onredirect if onerror: self._timers_to_cancel_on_error[timer] = onerror def _on_wait_timeout(self, timer, callback): self.logger.log("wait timeout for %s" % id(timer), min_level=2) if timer in self._active_timers: self._active_timers.remove(timer) self._timers_to_cancel_on_redirect.pop(timer, None) self._timers_to_cancel_on_error.pop(timer, None) callback() def _cancel_timer(self, timer, errback=None): self.logger.log("cancelling timer %s" % id(timer), min_level=2) if timer in self._active_timers: self._active_timers.remove(timer) timer.stop() try: if callable(errback): self.logger.log("calling timer errback", min_level=2) errback() finally: timer.deleteLater() def _cancel_timers(self, timers): for timer, oncancel in list(timers.items()): self._cancel_timer(timer, oncancel) timers.pop(timer, None) def _cancel_all_timers(self): self.logger.log("cancelling %d remaining timers" % len(self._active_timers), min_level=2) for timer in list(self._active_timers): self._cancel_timer(timer) def _on_url_changed(self, url): # log history url = unicode(url.toString()) cause_ev = self.web_page.har_log._prev_entry(url, -1) if cause_ev: self._history.append(without_private(cause_ev.data)) self._cancel_timers(self._timers_to_cancel_on_redirect) def run_js_file(self, filename): """ Load JS library from file ``filename`` to the current frame. """ with open(filename, 'rb') as f: script = f.read().decode('utf-8') return self.runjs(script) def run_js_files(self, folder): """ Load all JS libraries from ``folder`` folder to the current frame. """ for jsfile in os.listdir(folder): if jsfile.endswith('.js'): filename = os.path.join(folder, jsfile) self.run_js_file(filename) def autoload(self, js_source): """ Execute JS code before each page load """ self._autoload_scripts.append(js_source) def no_autoload(self): """ Remove all scripts scheduled for auto-loading """ self._autoload_scripts = [] def _on_javascript_window_object_cleared(self): for script in self._autoload_scripts: self.web_page.mainFrame().evaluateJavaScript(script) def http_get(self, url, callback, headers=None, follow_redirects=True): """ Send a GET request; call a callback with the reply as an argument. """ self.http_client.get(url, callback=callback, headers=headers, follow_redirects=follow_redirects ) def runjs(self, js_source): """ Run JS code in page context and return the result. Only string results are supported. """ frame = self.web_page.mainFrame() res = frame.evaluateJavaScript(js_source) return qt2py(res) def store_har_timing(self, name): self.web_page.har_log.store_timing(name) def _jsconsole_enable(self): # TODO: add public interface or make console available by default if self._js_console is not None: return self._js_console = _JavascriptConsole() frame = self.web_page.mainFrame() frame.addToJavaScriptWindowObject('console', self._js_console) def _jsconsole_messages(self): # TODO: add public interface or make console available by default if self._js_console is None: return [] return self._js_console.messages[:] def html(self): """ Return HTML of the current main frame """ self.logger.log("getting HTML", min_level=2) frame = self.web_page.mainFrame() result = bytes(frame.toHtml().toUtf8()) self.store_har_timing("_onHtmlRendered") return result def png(self, width=None, height=None, b64=False): """ Return screenshot in PNG format """ self.logger.log("getting PNG", min_level=2) image = QImage(self.web_page.viewportSize(), QImage.Format_ARGB32) painter = QPainter(image) self.web_page.mainFrame().render(painter) painter.end() self.store_har_timing("_onScreenshotPrepared") if width: image = image.scaledToWidth(width, Qt.SmoothTransformation) if height: image = image.copy(0, 0, width, height) b = QBuffer() image.save(b, "png") result = bytes(b.data()) if b64: result = base64.b64encode(result) self.store_har_timing("_onPngRendered") return result def iframes_info(self, children=True, html=True): """ Return information about all iframes """ self.logger.log("getting iframes", min_level=3) frame = self.web_page.mainFrame() result = self._frame_to_dict(frame, children, html) self.store_har_timing("_onIframesRendered") return result def har(self): """ Return HAR information """ self.logger.log("getting HAR", min_level=3) return self.web_page.har_log.todict() def history(self): """ Return history of 'main' HTTP requests """ self.logger.log("getting history", min_level=3) return copy.deepcopy(self._history) def last_http_status(self): """ Return HTTP status code of the currently loaded webpage or None if it is not available. """ if not self._history: return try: return self._history[-1]["response"]["status"] except KeyError: return def _frame_to_dict(self, frame, children=True, html=True): g = frame.geometry() res = { "url": unicode(frame.url().toString()), "requestedUrl": unicode(frame.requestedUrl().toString()), "geometry": (g.x(), g.y(), g.width(), g.height()), "title": unicode(frame.title()) } if html: res["html"] = unicode(frame.toHtml()) if children: res["childFrames"] = [ self._frame_to_dict(f, True, html) for f in frame.childFrames() ] res["frameName"] = unicode(frame.frameName()) return res
class MainWindow(QtGui.QMainWindow): def __init__(self): QtGui.QMainWindow.__init__(self) self.initUI() def initUI(self): self.centralWidget = QtGui.QWidget(self) ''' Address Bar ''' self.addbar = QtGui.QLineEdit(self) self.addbar.setGeometry(200, 16, 840, 35) self.addbar.setStyleSheet("font-size:21px") self.addbar.setFont(QtGui.QFont("Times New Roman")) self.addbar.returnPressed.connect(self.goClicked) self.addbar.cursorPositionAt(QtCore.QPoint(0, 0)) self.addbar.setFocus() self.addbar.setSizePolicy(QtGui.QSizePolicy.MinimumExpanding, True) ''' Progress Bar ''' self.progressbar = QtGui.QProgressBar(self) self.progressbar.setMaximumWidth(100) ''' Web Page Action setting ''' self.web = QWebView(loadProgress=self.progressbar.setValue, loadFinished=self.progressbar.hide, loadStarted= self.progressbar.show, titleChanged=self. setWindowTitle) self.web.setGeometry(300, 100, 1099, 768) self.web.urlChanged.connect(self.goUrlChanged) self.web.page().linkHovered.connect(self.LinkHovered) self.web.loadStarted.connect(self.pageLoadStarted) self.web.loadFinished.connect(self.pageLoadFinished) ''' Back, Forward, Reload and Stop Button ''' self.toolbar = QtGui.QToolBar(self) self.toolbar.addAction(self.web.pageAction(QWebPage.Back)) self.toolbar.addAction(self.web.pageAction(QWebPage.Forward)) self.toolbar.addAction(self.web.pageAction(QWebPage.Reload)) self.toolbar.addAction(self.web.pageAction(QWebPage.Stop)) ''' Global Web Page Setting ''' self.setting = QWebSettings.globalSettings() self.setting.setAttribute(QWebSettings.PluginsEnabled, True) self.setting.setAttribute(QWebSettings.AutoLoadImages, True) self.setting.setAttribute(QWebSettings.JavaEnabled, True) self.setting.setAttribute(QWebSettings.DnsPrefetchEnabled, True) self.setting.setAttribute(QWebSettings.WebGLEnabled, True) self.setting.setAttribute(QWebSettings.LocalStorageEnabled, True) self.setting.setAttribute(QWebSettings.JavascriptEnabled, True) self.setting.setAttribute(QWebSettings.JavascriptCanOpenWindows, True) self.setting.setAttribute(QWebSettings. OfflineWebApplicationCacheEnabled, True) self.setting.setAttribute(QWebSettings.ZoomTextOnly, True) self.setting.setAttribute(QWebSettings.AcceleratedCompositingEnabled, True) self.setting.setAttribute(QWebSettings.SpatialNavigationEnabled, True) self.setting.setAttribute(QWebSettings.LinksIncludedInFocusChain, True) self.setting.setAttribute(QWebSettings.LocalContentCanAccessFileUrls, True) ''' Main Window Setting ''' self.setGeometry(300, 100, 1099, 768) self.setWindowIcon(QtGui.QIcon("")) self.setStyleSheet("background-color:") self.status = self.statusBar() self.status.setSizeGripEnabled(False) self.status.addPermanentWidget(self.progressbar) self.status.hide() self.setCentralWidget(self.centralWidget) grid = QtGui.QGridLayout() grid.addWidget(self.toolbar, 0, 0, 1, 1) grid.addWidget(self.addbar, 0, 1, 1, 1) grid.addWidget(self.web, 2, 0, 1, 6) self.centralWidget.setLayout(grid) ''' Use system Proxy to load page ''' QNetworkProxyFactory.setUseSystemConfiguration(True) def goClicked(self): ''' When URL is entered in addressbar and enter is pressed ''' global url url = self.addbar.text() http = "http://" www = "www." https = "https://" if www in url and http not in url: url = http + url elif "." not in url: url = "http://www.google.com/search?q=" + url elif http in url and www not in url: url = url[:7] + www + url[7:] elif https in url: url = url elif http and www not in url: url = http + www + url self.addbar.setText(url) self.web.load(QtCore.QUrl(url)) self.status.show() self.web.setFocus(True) def goUrlChanged(self): ''' Update URL in Address bar if link is clicked ''' self.addbar.setText(self.web.url().toString()) def LinkHovered(self, l): self.status.showMessage(l) def pageLoadStarted(self): self.status.showMessage("Loading") self.status.show() def pageLoadFinished(self): self.addbar.setText(self.web.url().toString()) self.status.hide()
class WebpageRender(object): """ WebpageRender object renders a webpage: it downloads the page using network_manager and renders it using QWebView according to options passed to :meth:`WebpageRender.doRequest`. This class is not used directly; its subclasses are used. Subclasses choose how to return the result (as html, json, png). """ def __init__(self, network_manager, splash_proxy_factory, splash_request, verbosity): self.network_manager = network_manager self.web_view = QWebView() self.web_page = SplashQWebPage(verbosity) self.web_page.setNetworkAccessManager(self.network_manager) self.web_view.setPage(self.web_page) self.web_view.setAttribute(Qt.WA_DeleteOnClose, True) settings = self.web_page.settings() settings.setAttribute(QWebSettings.JavascriptEnabled, True) settings.setAttribute(QWebSettings.PluginsEnabled, False) settings.setAttribute(QWebSettings.PrivateBrowsingEnabled, True) settings.setAttribute(QWebSettings.LocalStorageEnabled, True) settings.setAttribute(QWebSettings.LocalContentCanAccessRemoteUrls, True) self.web_page.mainFrame().setScrollBarPolicy(Qt.Vertical, Qt.ScrollBarAlwaysOff) self.web_page.mainFrame().setScrollBarPolicy(Qt.Horizontal, Qt.ScrollBarAlwaysOff) self.splash_request = splash_request self.web_page.splash_request = splash_request self.web_page.splash_proxy_factory = splash_proxy_factory self.verbosity = verbosity self.deferred = defer.Deferred() self._finished_timer = None self._closing = False # ======= General request/response handling: def start(self, url, baseurl=None, wait=None, viewport=None, js_source=None, js_profile=None, images=None, console=False, headers=None, http_method='GET', body=None): self.web_page.har_log.store_timing("_onStarted") self.url = url self.history = [] self.web_page.settings().setAttribute(QWebSettings.AutoLoadImages, images) self.wait_time = defaults.WAIT_TIME if wait is None else wait self.js_source = js_source self.js_profile = js_profile self.console = console self.viewport = defaults.VIEWPORT if viewport is None else viewport # setup logging if self.verbosity >= 4: self.web_page.loadStarted.connect(self._loadStarted) self.web_page.mainFrame().loadFinished.connect(self._frameLoadFinished) self.web_page.mainFrame().loadStarted.connect(self._frameLoadStarted) self.web_page.mainFrame().contentsSizeChanged.connect(self._contentsSizeChanged) if self.verbosity >= 3: self.web_page.mainFrame().javaScriptWindowObjectCleared.connect(self._javaScriptWindowObjectCleared) self.web_page.mainFrame().initialLayoutCompleted.connect(self._initialLayoutCompleted) self.web_page.mainFrame().urlChanged.connect(self._urlChanged) # do the request request = QNetworkRequest() request.setUrl(QUrl(url.decode('utf8'))) self._setHeaders(request, headers) if getattr(self.splash_request, 'inspect_me', False): # Set http method and request body from the request http_method = self.splash_request.method body = self.splash_request.content.getvalue() if self.viewport != 'full': # viewport='full' can't be set if content is not loaded yet, # but in other cases it is better to set it earlier. self._setViewportSize(self.viewport) if baseurl: # If baseurl is used, we download the page manually, # then set its contents to the QWebPage and let it # download related resources and render the result. if http_method != 'GET': raise NotImplementedError() self._baseUrl = QUrl(baseurl.decode('utf8')) request.setOriginatingObject(self.web_page.mainFrame()) self._reply = self.network_manager.get(request) self._reply.finished.connect(self._requestFinished) else: self.web_page.loadFinished.connect(self._loadFinished) meth = OPERATION_QT_CONSTANTS[http_method] if body is None: # PyQT doesn't support body=None self.web_page.mainFrame().load(request, meth) else: self.web_page.mainFrame().load(request, meth, body) def render(self): """ This method is called to get the result after the requested page is downloaded and rendered. Subcalles should implement it to customize which data to return. """ raise NotImplementedError() def close(self): """ This method is called by a Pool after the rendering is done and the WebpageRender object is no longer needed. """ self._closing = True self.web_view.pageAction(QWebPage.StopScheduledPageRefresh) self.web_view.stop() self.web_view.close() self.web_page.deleteLater() self.web_view.deleteLater() def _setHeaders(self, request, headers): """ Set HTTP headers for the ``request``. """ if isinstance(headers, dict): headers = headers.items() for name, value in headers or []: request.setRawHeader(name, value) if name.lower() == 'user-agent': self.web_page.custom_user_agent = value def _requestFinished(self): """ This method is called when ``baseurl`` is used and a reply for the first request is received. """ self.log("_requestFinished %s" % id(self.splash_request)) self.web_page.loadFinished.connect(self._loadFinished) mimeType = self._reply.header(QNetworkRequest.ContentTypeHeader).toString() data = self._reply.readAll() self.web_page.mainFrame().setContent(data, mimeType, self._baseUrl) if self._reply.error(): self.log("Error loading %s: %s" % (self.url, self._reply.errorString()), min_level=1) self._reply.close() self._reply.deleteLater() def _loadFinished(self, ok): """ This method is called when a QWebPage finished loading its contents. """ if self._closing: self.log("loadFinished is ignored because WebpageRender is closing", min_level=3) return if self.deferred.called: # sometimes this callback is called multiple times self.log("loadFinished called multiple times", min_level=1) return page_ok = ok and self.web_page.errorInfo is None maybe_redirect = not ok and self.web_page.errorInfo is None error_loading = ok and self.web_page.errorInfo is not None if maybe_redirect: self.log("Redirect or other non-fatal error detected %s" % id(self.splash_request)) # XXX: It assumes loadFinished will be called again because # redirect happens. If redirect is detected improperly, # loadFinished won't be called again, and Splash will return # the result only after a timeout. # # FIXME: This can happen if server returned incorrect # Content-Type header; there is no an additional loadFinished # signal in this case. return if page_ok: # or maybe_redirect: if self.wait_time == 0: self.log("loadFinished %s; not waiting" % (id(self.splash_request))) self._loadFinishedOK() else: time_ms = int(self.wait_time * 1000) self.log("loadFinished %s; waiting %sms" % (id(self.splash_request), time_ms)) if self._finished_timer is not None: raise Exception("timer is not None!") self._finished_timer = QTimer() self._finished_timer.setSingleShot(True) self._finished_timer.timeout.connect(self._loadFinishedOK) self._finished_timer.start(time_ms) elif error_loading: self.log("loadFinished %s: %s" % (id(self.splash_request), str(self.web_page.errorInfo)), min_level=1) # XXX: maybe return a meaningful error page instead of generic # error message? self.deferred.errback(RenderError()) else: self.log("loadFinished %s: unknown error" % id(self.splash_request), min_level=1) self.deferred.errback(RenderError()) def _loadFinishedOK(self): self._finished_timer = None self.log("_loadFinishedOK %s" % id(self.splash_request)) if self._closing: self.log("loadFinishedOK is ignored because WebpageRender is closing", min_level=3) return self.web_view.pageAction(QWebPage.StopScheduledPageRefresh) self.web_view.stop() self.web_page.har_log.store_timing("_onPrepareStart") try: self._prepareRender() self.deferred.callback(self.render()) except: self.deferred.errback() def _frameLoadFinished(self, ok): self.log("mainFrame().LoadFinished %s %s" % (id(self.splash_request), ok), min_level=4) def _loadStarted(self): self.log("loadStarted %s" % id(self.splash_request), min_level=4) def _urlChanged(self, url): cause_ev = self.web_page.har_log._prev_entry(unicode(url.toString()), -1) if cause_ev: self.history.append(without_private(cause_ev.data)) msg = "mainFrame().urlChanged %s: %s" % (id(self.splash_request), qurl2ascii(url)) self.log(msg, min_level=3) if self._finished_timer is not None: self.log("Cancelling wait timer %s" % (id(self.splash_request))) self._finished_timer.stop() self._finished_timer = None def _frameLoadStarted(self): self.log("mainFrame().loadStarted %s" % id(self.splash_request), min_level=4) def _initialLayoutCompleted(self): self.log("mainFrame().initialLayoutCompleted %s" % id(self.splash_request), min_level=3) def _javaScriptWindowObjectCleared(self): self.log("mainFrame().javaScriptWindowObjectCleared %s" % id(self.splash_request), min_level=3) def _contentsSizeChanged(self): self.log("mainFrame().contentsSizeChanged %s" % id(self.splash_request), min_level=4) def _repaintRequested(self): self.log("mainFrame().repaintRequested %s" % id(self.splash_request), min_level=4) # ======= Rendering methods that subclasses can use: def _getHtml(self): self.log("getting HTML %s" % id(self.splash_request)) frame = self.web_page.mainFrame() result = bytes(frame.toHtml().toUtf8()) self.web_page.har_log.store_timing("_onHtmlRendered") return result def _getPng(self, width=None, height=None, b64=False): self.log("getting PNG %s" % id(self.splash_request)) image = QImage(self.web_page.viewportSize(), QImage.Format_ARGB32) painter = QPainter(image) self.web_page.mainFrame().render(painter) painter.end() self.web_page.har_log.store_timing("_onScreenshotPrepared") if width: image = image.scaledToWidth(width, Qt.SmoothTransformation) if height: image = image.copy(0, 0, width, height) b = QBuffer() image.save(b, "png") result = bytes(b.data()) if b64: result = base64.b64encode(result) self.web_page.har_log.store_timing("_onPngRendered") return result def _getIframes(self, children=True, html=True): self.log("getting iframes %s" % id(self.splash_request), min_level=3) frame = self.web_page.mainFrame() result = self._frameToDict(frame, children, html) self.web_page.har_log.store_timing("_onIframesRendered") return result def _getHistory(self): self.log("getting history %s" % id(self.splash_request), min_level=3) hist = copy.deepcopy(self.history) for entry in hist: if entry is not None: del entry['request']['queryString'] return hist def _getHAR(self): self.log("getting HAR %s" % id(self.splash_request), min_level=3) return self.web_page.har_log.todict() # ======= Other helper methods: def _setViewportSize(self, size): if not isinstance(size, QSize): w, h = map(int, size.split('x')) size = QSize(w, h) self.web_page.setViewportSize(size) w, h = int(size.width()), int(size.height()) self.log("viewport size for %s is set to %sx%s" % (id(self.splash_request), w, h)) def _setFullViewport(self): size = self.web_page.mainFrame().contentsSize() if size.isEmpty(): self.log("contentsSize method doesn't work %s" % id(self.splash_request), min_level=1) self._setViewportSize(defaults.VIEWPORT_FALLBACK) else: self._setViewportSize(size) self.web_page.har_log.store_timing("_onFullViewportSet") def _loadJsLibs(self, frame, js_profile): if js_profile: for jsfile in os.listdir(js_profile): if jsfile.endswith('.js'): with open(os.path.join(js_profile, jsfile)) as f: frame.evaluateJavaScript(f.read().decode('utf-8')) def _runJS(self, js_source, js_profile): js_output = None js_console_output = None if js_source: frame = self.web_page.mainFrame() if self.console: js_console = JavascriptConsole() frame.addToJavaScriptWindowObject('console', js_console) if js_profile: self._loadJsLibs(frame, js_profile) ret = frame.evaluateJavaScript(js_source) js_output = bytes(ret.toString().toUtf8()) if self.console: js_console_output = [bytes(s.toUtf8()) for s in js_console.messages] self.web_page.har_log.store_timing('_onCustomJsExecuted') return js_output, js_console_output def _frameToDict(self, frame, children=True, html=True): g = frame.geometry() res = { "url": unicode(frame.url().toString()), "requestedUrl": unicode(frame.requestedUrl().toString()), "geometry": (g.x(), g.y(), g.width(), g.height()), "title": unicode(frame.title()) } if html: res["html"] = unicode(frame.toHtml()) if children: res["childFrames"] = [self._frameToDict(f, True, html) for f in frame.childFrames()] res["frameName"] = unicode(frame.frameName()) return res def _prepareRender(self): if self.viewport == 'full': self._setFullViewport() self.js_output, self.js_console_output = self._runJS(self.js_source, self.js_profile) def log(self, text, min_level=2): if self.verbosity >= min_level: if isinstance(text, unicode): text = text.encode('unicode-escape').decode('ascii') log.msg(text, system='render')
class MainWindow(QtGui.QMainWindow): def __init__(self): QtGui.QMainWindow.__init__(self) self.initUI() def initUI(self): self.centralWidget = QtGui.QWidget(self) ''' Address Bar ''' self.addbar = QtGui.QLineEdit(self) self.addbar.setGeometry(200, 16, 840, 35) self.addbar.setStyleSheet("font-size:21px") self.addbar.setFont(QtGui.QFont("Times New Roman")) self.addbar.returnPressed.connect(self.goClicked) self.addbar.cursorPositionAt(QtCore.QPoint(0, 0)) self.addbar.setFocus() self.addbar.setSizePolicy(QtGui.QSizePolicy.MinimumExpanding, True) ''' Progress Bar ''' self.progressbar = QtGui.QProgressBar(self) self.progressbar.setMaximumWidth(100) ''' Web Page Action setting ''' self.web = QWebView(loadProgress=self.progressbar.setValue, loadFinished=self.progressbar.hide, loadStarted=self.progressbar.show, titleChanged=self.setWindowTitle) self.web.setGeometry(300, 100, 1099, 768) self.web.urlChanged.connect(self.goUrlChanged) self.web.page().linkHovered.connect(self.LinkHovered) self.web.loadStarted.connect(self.pageLoadStarted) self.web.loadFinished.connect(self.pageLoadFinished) ''' Back, Forward, Reload and Stop Button ''' self.toolbar = QtGui.QToolBar(self) self.toolbar.addAction(self.web.pageAction(QWebPage.Back)) self.toolbar.addAction(self.web.pageAction(QWebPage.Forward)) self.toolbar.addAction(self.web.pageAction(QWebPage.Reload)) self.toolbar.addAction(self.web.pageAction(QWebPage.Stop)) ''' Global Web Page Setting ''' self.setting = QWebSettings.globalSettings() self.setting.setAttribute(QWebSettings.PluginsEnabled, True) self.setting.setAttribute(QWebSettings.AutoLoadImages, True) self.setting.setAttribute(QWebSettings.JavaEnabled, True) self.setting.setAttribute(QWebSettings.DnsPrefetchEnabled, True) self.setting.setAttribute(QWebSettings.WebGLEnabled, True) self.setting.setAttribute(QWebSettings.LocalStorageEnabled, True) self.setting.setAttribute(QWebSettings.JavascriptEnabled, True) self.setting.setAttribute(QWebSettings.JavascriptCanOpenWindows, True) self.setting.setAttribute( QWebSettings.OfflineWebApplicationCacheEnabled, True) self.setting.setAttribute(QWebSettings.ZoomTextOnly, True) self.setting.setAttribute(QWebSettings.AcceleratedCompositingEnabled, True) self.setting.setAttribute(QWebSettings.SpatialNavigationEnabled, True) self.setting.setAttribute(QWebSettings.LinksIncludedInFocusChain, True) self.setting.setAttribute(QWebSettings.LocalContentCanAccessFileUrls, True) ''' Main Window Setting ''' self.setGeometry(300, 100, 1099, 768) self.setWindowIcon(QtGui.QIcon("")) self.setStyleSheet("background-color:") self.status = self.statusBar() self.status.setSizeGripEnabled(False) self.status.addPermanentWidget(self.progressbar) self.status.hide() self.setCentralWidget(self.centralWidget) grid = QtGui.QGridLayout() grid.addWidget(self.toolbar, 0, 0, 1, 1) grid.addWidget(self.addbar, 0, 1, 1, 1) grid.addWidget(self.web, 2, 0, 1, 6) self.centralWidget.setLayout(grid) ''' Use system Proxy to load page ''' QNetworkProxyFactory.setUseSystemConfiguration(True) def goClicked(self): ''' When URL is entered in addressbar and enter is pressed ''' global url url = self.addbar.text() http = "http://" www = "www." https = "https://" if www in url and http not in url: url = http + url elif "." not in url: url = "http://www.google.com/search?q=" + url elif http in url and www not in url: url = url[:7] + www + url[7:] elif https in url: url = url elif http and www not in url: url = http + www + url self.addbar.setText(url) self.web.load(QtCore.QUrl(url)) self.status.show() self.web.setFocus(True) def goUrlChanged(self): ''' Update URL in Address bar if link is clicked ''' self.addbar.setText(self.web.url().toString()) def LinkHovered(self, l): self.status.showMessage(l) def pageLoadStarted(self): self.status.showMessage("Loading") self.status.show() def pageLoadFinished(self): self.addbar.setText(self.web.url().toString()) self.status.hide()
class Browser(QWidget): """LilyPond documentation browser widget.""" def __init__(self, dockwidget): super(Browser, self).__init__(dockwidget) layout = QVBoxLayout(spacing=0) layout.setContentsMargins(0, 0, 0, 0) self.setLayout(layout) self.toolbar = tb = QToolBar() self.webview = QWebView(contextMenuPolicy=Qt.CustomContextMenu) self.chooser = QComboBox(sizeAdjustPolicy=QComboBox.AdjustToContents) self.search = SearchEntry(maximumWidth=200) layout.addWidget(self.toolbar) layout.addWidget(self.webview) ac = dockwidget.actionCollection ac.help_back.triggered.connect(self.webview.back) ac.help_forward.triggered.connect(self.webview.forward) ac.help_home.triggered.connect(self.showHomePage) ac.help_print.triggered.connect(self.slotPrint) self.webview.page().setNetworkAccessManager(lilydoc.network.accessmanager()) self.webview.page().setLinkDelegationPolicy(QWebPage.DelegateAllLinks) self.webview.page().linkClicked.connect(self.openUrl) self.webview.page().setForwardUnsupportedContent(True) self.webview.page().unsupportedContent.connect(self.slotUnsupported) self.webview.urlChanged.connect(self.slotUrlChanged) self.webview.customContextMenuRequested.connect(self.slotShowContextMenu) tb.addAction(ac.help_back) tb.addAction(ac.help_forward) tb.addSeparator() tb.addAction(ac.help_home) tb.addAction(ac.help_print) tb.addSeparator() tb.addWidget(self.chooser) tb.addWidget(self.search) self.chooser.activated[int].connect(self.showHomePage) self.search.textEdited.connect(self.slotSearchChanged) self.search.returnPressed.connect(self.slotSearchReturnPressed) dockwidget.mainwindow().iconSizeChanged.connect(self.updateToolBarSettings) dockwidget.mainwindow().toolButtonStyleChanged.connect(self.updateToolBarSettings) app.settingsChanged.connect(self.readSettings) self.readSettings() self.loadDocumentation() self.showInitialPage() app.settingsChanged.connect(self.loadDocumentation) app.translateUI(self) def readSettings(self): s = QSettings() s.beginGroup("documentation") ws = self.webview.page().settings() family = s.value("fontfamily", self.font().family(), type("")) size = s.value("fontsize", 16, int) ws.setFontFamily(QWebSettings.StandardFont, family) ws.setFontSize(QWebSettings.DefaultFontSize, size) fixed = textformats.formatData('editor').font ws.setFontFamily(QWebSettings.FixedFont, fixed.family()) ws.setFontSize(QWebSettings.DefaultFixedFontSize, fixed.pointSizeF() * 96 / 72) def keyPressEvent(self, ev): if ev.text() == "/": self.search.setFocus() else: super(Browser, self).keyPressEvent(ev) def translateUI(self): try: self.search.setPlaceholderText(_("Search...")) except AttributeError: pass # not in Qt 4.6 def showInitialPage(self): """Shows the preferred start page. If a local documentation instance already has a suitable version, just loads it. Otherwise connects to the allLoaded signal, that is emitted when all the documentation instances have loaded their version information and then shows the start page (if another page wasn't yet loaded). """ if self.webview.url().isEmpty(): docs = lilydoc.manager.docs() version = lilypondinfo.preferred().version() index = -1 if version: for num, doc in enumerate(docs): if doc.version() is not None and doc.version() >= version: index = num # a suitable documentation is found break if index == -1: # nothing found (or LilyPond version not available), # wait for loading or show the most recent version if not lilydoc.manager.loaded(): lilydoc.manager.allLoaded.connect(self.showInitialPage) return index = len(docs) - 1 self.chooser.setCurrentIndex(index) self.showHomePage() def loadDocumentation(self): """Puts the available documentation instances in the combobox.""" i = self.chooser.currentIndex() self.chooser.clear() for doc in lilydoc.manager.docs(): v = doc.versionString() if doc.isLocal(): t = _("(local)") else: t = _("({hostname})").format(hostname=doc.url().host()) self.chooser.addItem("{0} {1}".format(v or _("<unknown>"), t)) self.chooser.setCurrentIndex(i) if not lilydoc.manager.loaded(): lilydoc.manager.allLoaded.connect(self.loadDocumentation, -1) return def updateToolBarSettings(self): mainwin = self.parentWidget().mainwindow() self.toolbar.setIconSize(mainwin.iconSize()) self.toolbar.setToolButtonStyle(mainwin.toolButtonStyle()) def showManual(self): """Invoked when the user presses F1.""" self.slotHomeFrescobaldi() # TEMP def slotUrlChanged(self): ac = self.parentWidget().actionCollection ac.help_back.setEnabled(self.webview.history().canGoBack()) ac.help_forward.setEnabled(self.webview.history().canGoForward()) def openUrl(self, url): if url.path().endswith(('.ily', '.lyi', '.ly')): self.sourceViewer().showReply(lilydoc.network.get(url)) else: self.webview.load(url) def slotUnsupported(self, reply): helpers.openUrl(reply.url()) def slotSearchChanged(self): text = self.search.text() if not text.startswith(':'): self.webview.page().findText(text, QWebPage.FindWrapsAroundDocument) def slotSearchReturnPressed(self): text = self.search.text() if not text.startswith(':'): self.slotSearchChanged() else: pass # TODO: implement full doc search def sourceViewer(self): try: return self._sourceviewer except AttributeError: from . import sourceviewer self._sourceviewer = sourceviewer.SourceViewer(self) return self._sourceviewer def showHomePage(self): """Shows the homepage of the LilyPond documentation.""" i = self.chooser.currentIndex() if i < 0: i = 0 doc = lilydoc.manager.docs()[i] url = doc.home() if doc.isLocal(): path = url.toLocalFile() langs = lilydoc.network.langs() if langs: for lang in langs: if os.path.exists(path + '.' + lang + '.html'): path += '.' + lang break url = QUrl.fromLocalFile(path + '.html') self.webview.load(url) def slotPrint(self): printer = QPrinter() dlg = QPrintDialog(printer, self) dlg.setWindowTitle(app.caption(_("Print"))) if dlg.exec_(): self.webview.print_(printer) def slotShowContextMenu(self, pos): hit = self.webview.page().currentFrame().hitTestContent(pos) menu = QMenu() if hit.linkUrl().isValid(): a = self.webview.pageAction(QWebPage.CopyLinkToClipboard) a.setIcon(icons.get("edit-copy")) a.setText(_("Copy &Link")) menu.addAction(a) menu.addSeparator() a = menu.addAction(icons.get("window-new"), _("Open Link in &New Window")) a.triggered.connect((lambda url: lambda: self.slotNewWindow(url))(hit.linkUrl())) else: if hit.isContentSelected(): a = self.webview.pageAction(QWebPage.Copy) a.setIcon(icons.get("edit-copy")) a.setText(_("&Copy")) menu.addAction(a) menu.addSeparator() a = menu.addAction(icons.get("window-new"), _("Open Document in &New Window")) a.triggered.connect((lambda url: lambda: self.slotNewWindow(url))(self.webview.url())) if menu.actions(): menu.exec_(self.webview.mapToGlobal(pos)) def slotNewWindow(self, url): helpers.openUrl(url)
class BrowserTab(object): """ An object for controlling a single browser tab (QWebView). It is created by splash.pool.Pool. Pool attaches to tab's deferred and waits until either a callback or an errback is called, then destroys a BrowserTab. """ def __init__(self, network_manager, splash_proxy_factory, verbosity, render_options): """ Create a new browser tab. """ self.deferred = defer.Deferred() self.network_manager = network_manager self.verbosity = verbosity self._uid = render_options.get_uid() self._closing = False self._default_headers = None self._active_timers = set() self._timers_to_cancel_on_redirect = weakref.WeakKeyDictionary( ) # timer: callback self._timers_to_cancel_on_error = weakref.WeakKeyDictionary( ) # timer: callback self._js_console = None self._history = [] self._init_webpage(verbosity, network_manager, splash_proxy_factory, render_options) self._setup_logging(verbosity) def _init_webpage(self, verbosity, network_manager, splash_proxy_factory, render_options): """ Create and initialize QWebPage and QWebView """ self.web_page = SplashQWebPage(verbosity) self.web_page.setNetworkAccessManager(network_manager) self.web_page.splash_proxy_factory = splash_proxy_factory self.web_page.render_options = render_options self._set_default_webpage_options(self.web_page) self._setup_webpage_events() self.web_view = QWebView() self.web_view.setPage(self.web_page) self.web_view.setAttribute(Qt.WA_DeleteOnClose, True) def _set_default_webpage_options(self, web_page): """ Set QWebPage options. TODO: allow to customize them. """ settings = web_page.settings() settings.setAttribute(QWebSettings.JavascriptEnabled, True) settings.setAttribute(QWebSettings.PluginsEnabled, False) settings.setAttribute(QWebSettings.PrivateBrowsingEnabled, True) settings.setAttribute(QWebSettings.LocalStorageEnabled, True) settings.setAttribute(QWebSettings.LocalContentCanAccessRemoteUrls, True) web_page.mainFrame().setScrollBarPolicy(Qt.Vertical, Qt.ScrollBarAlwaysOff) web_page.mainFrame().setScrollBarPolicy(Qt.Horizontal, Qt.ScrollBarAlwaysOff) def _setup_logging(self, verbosity): """ Setup logging of various events """ self.logger = _BrowserTabLogger( uid=self._uid, web_page=self.web_page, verbosity=verbosity, ) self.logger.enable() def _setup_webpage_events(self): self._load_finished = WrappedSignal( self.web_page.mainFrame().loadFinished) self.web_page.mainFrame().loadFinished.connect(self._on_load_finished) self.web_page.mainFrame().urlChanged.connect(self._on_url_changed) def return_result(self, result): """ Return a result to the Pool. """ if self.result_already_returned(): self.logger.log("error: result is already returned", min_level=1) self.deferred.callback(result) # self.deferred = None def return_error(self, error=None): """ Return an error to the Pool. """ if self.result_already_returned(): self.logger.log("error: result is already returned", min_level=1) self.deferred.errback(error) # self.deferred = None def result_already_returned(self): """ Return True if an error or a result is already returned to Pool """ return self.deferred.called def set_default_headers(self, headers): """ Set default HTTP headers """ self._default_headers = headers def set_images_enabled(self, enabled): self.web_page.settings().setAttribute(QWebSettings.AutoLoadImages, enabled) def set_viewport(self, size): """ Set viewport size. If size is "full" viewport size is detected automatically. If can also be "<width>x<height>". """ if size == 'full': size = self.web_page.mainFrame().contentsSize() if size.isEmpty(): self.logger.log("contentsSize method doesn't work %s", min_level=1) size = defaults.VIEWPORT_FALLBACK if not isinstance(size, QSize): w, h = map(int, size.split('x')) size = QSize(w, h) self.web_page.setViewportSize(size) w, h = int(size.width()), int(size.height()) self.logger.log("viewport size is set to %sx%s" % (w, h), min_level=2) return w, h @property def url(self): """ Current URL """ return unicode(self.web_page.mainFrame().url().toString()) def go(self, url, callback, errback, baseurl=None, http_method='GET', body=None): """ Go to an URL. This is similar to entering an URL in address tab and pressing Enter. """ self.store_har_timing("_onStarted") if baseurl: # If baseurl is used, we download the page manually, # then set its contents to the QWebPage and let it # download related resources and render the result. if http_method != 'GET': raise NotImplementedError() request = self._create_request(url) request.setOriginatingObject(self.web_page.mainFrame()) # TODO / FIXME: add support for multiple replies # or discard/cancel previous replies self._reply = self.network_manager.get(request) cb = functools.partial( self._on_baseurl_request_finished, callback=callback, errback=errback, baseurl=baseurl, url=url, ) self._reply.finished.connect(cb) else: # if not self._goto_callbacks.isempty(): # self.logger.log("Only a single concurrent 'go' request is supported. " # "Previous go requests will be cancelled.", min_level=1) # # When a new URL is loaded to mainFrame an errback will # # be called, so we're not cancelling this callback manually. callback_id = self._load_finished.connect( self._on_goto_load_finished, callback=callback, errback=errback, ) self.logger.log("callback %s is connected to loadFinished" % callback_id, min_level=3) self._load_url_to_mainframe(url, http_method, body) def stop_loading(self): """ Stop loading of the current page and all pending page refresh/redirect requests. """ self.logger.log("stop_loading", min_level=2) self.web_view.pageAction(QWebPage.StopScheduledPageRefresh) self.web_view.stop() def _close(self): """ Destroy this tab """ self._closing = True self.web_view.pageAction(QWebPage.StopScheduledPageRefresh) self.web_view.stop() self.web_view.close() self.web_page.deleteLater() self.web_view.deleteLater() self._cancel_all_timers() @skip_if_closing def _on_load_finished(self, ok): if self.web_page.maybe_redirect(ok): self.logger.log("Redirect or other non-fatal error detected", min_level=2) return if self.web_page.is_ok(ok): # or maybe_redirect: self.logger.log("loadFinished: ok", min_level=2) else: self._cancel_timers(self._timers_to_cancel_on_error) if self.web_page.error_loading(ok): self.logger.log("loadFinished: %s" % (str(self.web_page.error_info)), min_level=1) else: self.logger.log("loadFinished: unknown error", min_level=1) def _on_baseurl_request_finished(self, callback, errback, baseurl, url): """ This method is called when ``baseurl`` is used and a reply for the first request is received. """ self.logger.log("baseurl_request_finished", min_level=2) callback_id = self._load_finished.connect( self._on_goto_load_finished, callback=callback, errback=errback, ) self.logger.log("callback %s is connected to loadFinished" % callback_id, min_level=3) baseurl = QUrl(baseurl) mimeType = self._reply.header( QNetworkRequest.ContentTypeHeader).toString() data = self._reply.readAll() self.web_page.mainFrame().setContent(data, mimeType, baseurl) if self._reply.error(): self.logger.log("Error loading %s: %s" % (url, self._reply.errorString()), min_level=1) self._reply.close() self._reply.deleteLater() def _load_url_to_mainframe(self, url, http_method, body=None): request = self._create_request(url) meth = OPERATION_QT_CONSTANTS[http_method] if body is None: # PyQT doesn't support body=None self.web_page.mainFrame().load(request, meth) else: self.web_page.mainFrame().load(request, meth, body) def _create_request(self, url): request = QNetworkRequest() request.setUrl(QUrl(url)) self._set_request_headers(request, self._default_headers) return request @skip_if_closing def _on_goto_load_finished(self, ok, callback, errback, callback_id): """ This method is called when a QWebPage finishes loading its contents. """ if self.web_page.maybe_redirect(ok): # XXX: It assumes loadFinished will be called again because # redirect happens. If redirect is detected improperly, # loadFinished won't be called again, and Splash will return # the result only after a timeout. return self.logger.log("loadFinished: disconnecting callback %s" % callback_id, min_level=3) self._load_finished.disconnect(callback_id) if self.web_page.is_ok(ok): callback() elif self.web_page.error_loading(ok): # XXX: maybe return a meaningful error page instead of generic # error message? errback() # errback(RenderError()) else: errback() # errback(RenderError()) def _set_request_headers(self, request, headers): """ Set HTTP headers for the request. """ if isinstance(headers, dict): headers = headers.items() for name, value in headers or []: request.setRawHeader(name, value) if name.lower() == 'user-agent': self.web_page.custom_user_agent = value def wait(self, time_ms, callback, onredirect=None, onerror=None): """ Wait for time_ms, then run callback. If onredirect is True then the timer is cancelled if redirect happens. If onredirect is callable then in case of redirect the timer is cancelled and this callable is called. If onerror is True then the timer is cancelled if a render error happens. If onerror is callable then in case of a render error the timer is cancelled and this callable is called. """ timer = QTimer() timer.setSingleShot(True) timer_callback = functools.partial( self._on_wait_timeout, timer=timer, callback=callback, ) timer.timeout.connect(timer_callback) self.logger.log("waiting %sms; timer %s" % (time_ms, id(timer)), min_level=2) timer.start(time_ms) self._active_timers.add(timer) if onredirect: self._timers_to_cancel_on_redirect[timer] = onredirect if onerror: self._timers_to_cancel_on_error[timer] = onerror def _on_wait_timeout(self, timer, callback): self.logger.log("wait timeout for %s" % id(timer), min_level=2) if timer in self._active_timers: self._active_timers.remove(timer) self._timers_to_cancel_on_redirect.pop(timer, None) self._timers_to_cancel_on_error.pop(timer, None) callback() def _cancel_timer(self, timer, errback=None): self.logger.log("cancelling timer %s" % id(timer), min_level=2) if timer in self._active_timers: self._active_timers.remove(timer) timer.stop() if callable(errback): self.logger.log("calling timer errback", min_level=2) errback() timer.deleteLater() def _cancel_timers(self, timers): for timer, oncancel in list(timers.items()): self._cancel_timer(timer, oncancel) timers.pop(timer, None) def _cancel_all_timers(self): self.logger.log("cancelling %d remaining timers" % len(self._active_timers), min_level=2) for timer in list(self._active_timers): self._cancel_timer(timer) def _on_url_changed(self, url): # log history url = unicode(url.toString()) cause_ev = self.web_page.har_log._prev_entry(url, -1) if cause_ev: self._history.append(without_private(cause_ev.data)) self._cancel_timers(self._timers_to_cancel_on_redirect) def inject_js(self, filename): """ Load JS library from file ``filename`` to the current frame. """ # TODO: shouldn't it keep injected scripts after redirects/reloads? with open(filename, 'rb') as f: script = f.read().decode('utf-8') return self.runjs(script) def inject_js_libs(self, folder): """ Load all JS libraries from ``folder`` folder to the current frame. """ # TODO: shouldn't it keep injected scripts after redirects/reloads? for jsfile in os.listdir(folder): if jsfile.endswith('.js'): filename = os.path.join(folder, jsfile) self.inject_js(filename) def runjs(self, js_source): """ Run JS code in page context and return the result. Only string results are supported. """ frame = self.web_page.mainFrame() res = frame.evaluateJavaScript(js_source) return qt2py(res) def store_har_timing(self, name): self.web_page.har_log.store_timing(name) def _jsconsole_enable(self): # TODO: add public interface or make console available by default if self._js_console is not None: return self._js_console = _JavascriptConsole() frame = self.web_page.mainFrame() frame.addToJavaScriptWindowObject('console', self._js_console) def _jsconsole_messages(self): # TODO: add public interface or make console available by default if self._js_console is None: return [] return self._js_console.messages[:] def html(self): """ Return HTML of the current main frame """ self.logger.log("getting HTML", min_level=2) frame = self.web_page.mainFrame() result = bytes(frame.toHtml().toUtf8()) self.store_har_timing("_onHtmlRendered") return result def png(self, width=None, height=None, b64=False): """ Return screenshot in PNG format """ self.logger.log("getting PNG", min_level=2) image = QImage(self.web_page.viewportSize(), QImage.Format_ARGB32) painter = QPainter(image) self.web_page.mainFrame().render(painter) painter.end() self.store_har_timing("_onScreenshotPrepared") if width: image = image.scaledToWidth(width, Qt.SmoothTransformation) if height: image = image.copy(0, 0, width, height) b = QBuffer() image.save(b, "png") result = bytes(b.data()) if b64: result = base64.b64encode(result) self.store_har_timing("_onPngRendered") return result def iframes_info(self, children=True, html=True): """ Return information about all iframes """ self.logger.log("getting iframes", min_level=3) frame = self.web_page.mainFrame() result = self._frame_to_dict(frame, children, html) self.store_har_timing("_onIframesRendered") return result def har(self): """ Return HAR information """ self.logger.log("getting HAR", min_level=3) return self.web_page.har_log.todict() def history(self): """ Return history of 'main' HTTP requests """ self.logger.log("getting history", min_level=3) return copy.deepcopy(self._history) def last_http_status(self): """ Return HTTP status code of the currently loaded webpage or None if it is not available. """ if not self._history: return try: return self._history[-1]["response"]["status"] except KeyError: return def _frame_to_dict(self, frame, children=True, html=True): g = frame.geometry() res = { "url": unicode(frame.url().toString()), "requestedUrl": unicode(frame.requestedUrl().toString()), "geometry": (g.x(), g.y(), g.width(), g.height()), "title": unicode(frame.title()) } if html: res["html"] = unicode(frame.toHtml()) if children: res["childFrames"] = [ self._frame_to_dict(f, True, html) for f in frame.childFrames() ] res["frameName"] = unicode(frame.frameName()) return res