From a6a030e92fbccaff30eaca895b403da7dab1df29 Mon Sep 17 00:00:00 2001 From: Jeremy Kaplan Date: Sun, 10 Jul 2016 21:42:43 -0400 Subject: [PATCH 1/8] Add :print --pdf flag to skip printing dialog --- doc/help/commands.asciidoc | 3 ++- qutebrowser/browser/commands.py | 14 ++++++++++++-- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/doc/help/commands.asciidoc b/doc/help/commands.asciidoc index bd85f2eac..1abcaffd5 100644 --- a/doc/help/commands.asciidoc +++ b/doc/help/commands.asciidoc @@ -487,12 +487,13 @@ If the pasted text contains newlines, each line gets opened in its own tab. [[print]] === print -Syntax: +:print [*--preview*]+ +Syntax: +:print [*--preview*] [*--pdf* 'file']+ Print the current/[count]th tab. ==== optional arguments * +*-p*+, +*--preview*+: Show preview instead of printing. +* +*-f*+, +*--pdf*+: The file path to write the PDF to. ==== count The tab index to print. diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index 460b6ffe8..73dc84c90 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -31,7 +31,7 @@ from PyQt5.QtWebKit import QWebSettings from PyQt5.QtWidgets import QApplication, QTabBar from PyQt5.QtCore import Qt, QUrl, QEvent from PyQt5.QtGui import QKeyEvent -from PyQt5.QtPrintSupport import QPrintDialog, QPrintPreviewDialog +from PyQt5.QtPrintSupport import QPrintDialog, QPrinter, QPrintPreviewDialog from PyQt5.QtWebKitWidgets import QWebPage import pygments import pygments.lexers @@ -301,12 +301,14 @@ class CommandDispatcher: @cmdutils.register(instance='command-dispatcher', name='print', scope='window') @cmdutils.argument('count', count=True) - def printpage(self, preview=False, count=None): + @cmdutils.argument('pdf', flag='f') + def printpage(self, preview=False, count=None, *, pdf=None): """Print the current/[count]th tab. Args: preview: Show preview instead of printing. count: The tab index to print, or None. + pdf: The file path to write the PDF to. """ if not qtutils.check_print_compat(): # WORKAROUND (remove this when we bump the requirements to 5.3.0) @@ -322,6 +324,14 @@ class CommandDispatcher: Qt.WindowMinimizeButtonHint) diag.paintRequested.connect(tab.print) diag.exec_() + elif pdf: + pdf = os.path.expanduser(pdf) + directory = os.path.dirname(pdf) + if directory and not os.path.exists(directory): + os.mkdir(directory) + printer = QPrinter() + printer.setOutputFileName(pdf) + tab.print(printer) else: diag = QPrintDialog() diag.setAttribute(Qt.WA_DeleteOnClose) From 62ae793a24813db81e46cedaf49984cc8a821558 Mon Sep 17 00:00:00 2001 From: Jeremy Kaplan Date: Mon, 11 Jul 2016 20:07:54 -0400 Subject: [PATCH 2/8] Generate docs --- README.asciidoc | 1 + qutebrowser/browser/commands.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/README.asciidoc b/README.asciidoc index 6c7c3a8c7..9bad4c53b 100644 --- a/README.asciidoc +++ b/README.asciidoc @@ -227,6 +227,7 @@ Contributors, sorted by the number of commits in descending order: * Matthias Lisin * Marcel Schilling * Johannes Martinsson +* Jeremy Kaplan * Jean-Christophe Petkovich * Jay Kamat * Helen Sherwood-Taylor diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index 73dc84c90..45232282a 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -301,7 +301,7 @@ class CommandDispatcher: @cmdutils.register(instance='command-dispatcher', name='print', scope='window') @cmdutils.argument('count', count=True) - @cmdutils.argument('pdf', flag='f') + @cmdutils.argument('pdf', flag='f', metavar='file') def printpage(self, preview=False, count=None, *, pdf=None): """Print the current/[count]th tab. From 6b2b096f3cfa6cfe861fc01ae1183d1fe5190fec Mon Sep 17 00:00:00 2001 From: Jeremy Kaplan Date: Mon, 11 Jul 2016 20:08:24 -0400 Subject: [PATCH 3/8] Add test for :print --pdf --- qutebrowser/browser/commands.py | 1 + tests/end2end/features/conftest.py | 3 ++- tests/end2end/features/misc.feature | 7 +++++++ tests/end2end/features/test_misc_bdd.py | 11 +++++++++++ 4 files changed, 21 insertions(+), 1 deletion(-) diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index 45232282a..99ef1f088 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -332,6 +332,7 @@ class CommandDispatcher: printer = QPrinter() printer.setOutputFileName(pdf) tab.print(printer) + log.misc.debug("Print to file: {}".format(pdf)) else: diag = QPrintDialog() diag.setAttribute(Qt.WA_DeleteOnClose) diff --git a/tests/end2end/features/conftest.py b/tests/end2end/features/conftest.py index 9d1d5c074..8c9eea825 100644 --- a/tests/end2end/features/conftest.py +++ b/tests/end2end/features/conftest.py @@ -179,7 +179,7 @@ def set_setting(quteproc, httpbin, sect, opt, value): @bdd.when(bdd.parsers.parse("I run {command}")) -def run_command(quteproc, httpbin, command): +def run_command(quteproc, httpbin, tmpdir, command): """Run a qutebrowser command. The suffix "with count ..." can be used to pass a count to the command. @@ -199,6 +199,7 @@ def run_command(quteproc, httpbin, command): command = command.replace('(port)', str(httpbin.port)) command = command.replace('(testdata)', utils.abs_datapath()) + command = command.replace('(tmpdir)', str(tmpdir)) quteproc.send_cmd(command, count=count, invalid=invalid) diff --git a/tests/end2end/features/misc.feature b/tests/end2end/features/misc.feature index ed415958d..5842a12a5 100644 --- a/tests/end2end/features/misc.feature +++ b/tests/end2end/features/misc.feature @@ -339,6 +339,13 @@ Feature: Various utility commands. And I run :debug-pyeval QApplication.instance().activeModalWidget().close() Then no crash should happen + Scenario: print pdf + Given a new tmpdir + When I open data/hello.txt + And I run :print --pdf (tmpdir)/hello.pdf + And I wait for "Print to file: *" in the log or skip the test + Then the file hello.pdf should exist in the tmpdir + # :pyeval Scenario: Running :pyeval diff --git a/tests/end2end/features/test_misc_bdd.py b/tests/end2end/features/test_misc_bdd.py index 443583166..7a28d059c 100644 --- a/tests/end2end/features/test_misc_bdd.py +++ b/tests/end2end/features/test_misc_bdd.py @@ -73,3 +73,14 @@ def check_cookie(quteproc, name, value): data = json.loads(content) print(data) assert data['cookies'][name] == value + + +@bdd.given(bdd.parsers.parse('a new tmpdir')) +def tmpdir(tmpdir_factory): + return tmpdir_factory.mktemp('tmpdir') + + +@bdd.then(bdd.parsers.parse('the file {filename} should exist in the tmpdir')) +def file_exists(quteproc, tmpdir, filename): + path = tmpdir / filename + assert os.path.exists(str(path)) From cd4eff364a841b36c6fa1653ef8f0284ab1beef0 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 12 Jul 2016 12:54:11 +0200 Subject: [PATCH 4/8] Add printing to tab API This fixes printing for QtWebKit, and hopefully will make printing to PDF work with QtWebEngine with Qt >= 5.7 --- qutebrowser/browser/browsertab.py | 24 ++++++++ qutebrowser/browser/commands.py | 56 ++++++++++--------- qutebrowser/browser/webengine/webenginetab.py | 24 ++++++++ qutebrowser/browser/webkit/webkittab.py | 28 ++++++++++ tests/unit/browser/test_tab.py | 1 + 5 files changed, 107 insertions(+), 26 deletions(-) diff --git a/qutebrowser/browser/browsertab.py b/qutebrowser/browser/browsertab.py index 609c98204..f6579c87e 100644 --- a/qutebrowser/browser/browsertab.py +++ b/qutebrowser/browser/browsertab.py @@ -24,6 +24,7 @@ import itertools from PyQt5.QtCore import pyqtSignal, pyqtSlot, QUrl, QObject, QPoint from PyQt5.QtGui import QIcon from PyQt5.QtWidgets import QWidget, QLayout +from PyQt5.QtPrintSupport import QPrinter from qutebrowser.keyinput import modeman from qutebrowser.config import config @@ -105,6 +106,27 @@ class TabData: self.inspector = None +class AbstractPrinting: + + """Attribute of AbstractTab for printing the page.""" + + def __init__(self): + self._widget = None + + def check_pdf_support(self): + raise NotImplementedError + + def check_printer_support(self): + raise NotImplementedError + + def to_pdf(self, filename): + raise NotImplementedError + + @pyqtSlot(QPrinter) + def to_printer(self, printer): + raise NotImplementedError + + class AbstractSearch(QObject): """Attribute of AbstractTab for doing searches. @@ -468,6 +490,7 @@ class AbstractTab(QWidget): # parent=self) # self.zoom = AbstractZoom(win_id=win_id) # self.search = AbstractSearch(parent=self) + # self.printing = AbstractPrinting() self.data = TabData() self._layout = None self._widget = None @@ -485,6 +508,7 @@ class AbstractTab(QWidget): self.caret._widget = widget self.zoom._widget = widget self.search._widget = widget + self.printing._widget = widget widget.mouse_wheel_zoom.connect(self.zoom._on_mouse_wheel_zoom) widget.setParent(self) self.setFocusProxy(widget) diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index 74d857de7..761c58280 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -28,7 +28,7 @@ import functools from PyQt5.QtWidgets import QApplication, QTabBar from PyQt5.QtCore import Qt, QUrl, QEvent from PyQt5.QtGui import QKeyEvent -from PyQt5.QtPrintSupport import QPrintDialog, QPrinter, QPrintPreviewDialog +from PyQt5.QtPrintSupport import QPrintDialog, QPrintPreviewDialog from PyQt5.QtWebKitWidgets import QWebPage try: from PyQt5.QtWebEngineWidgets import QWebEnginePage @@ -308,33 +308,37 @@ class CommandDispatcher: count: The tab index to print, or None. pdf: The file path to write the PDF to. """ - if not qtutils.check_print_compat(): - # WORKAROUND (remove this when we bump the requirements to 5.3.0) - raise cmdexc.CommandError( - "Printing on Qt < 5.3.0 on Windows is broken, please upgrade!") tab = self._cntwidget(count) - if tab is not None: - if preview: - diag = QPrintPreviewDialog() - diag.setAttribute(Qt.WA_DeleteOnClose) - diag.setWindowFlags(diag.windowFlags() | - Qt.WindowMaximizeButtonHint | - Qt.WindowMinimizeButtonHint) - diag.paintRequested.connect(tab.print) - diag.exec_() - elif pdf: - pdf = os.path.expanduser(pdf) - directory = os.path.dirname(pdf) - if directory and not os.path.exists(directory): - os.mkdir(directory) - printer = QPrinter() - printer.setOutputFileName(pdf) - tab.print(printer) - log.misc.debug("Print to file: {}".format(pdf)) + if tab is None: + return + + try: + if pdf: + tab.printing.check_pdf_support() else: - diag = QPrintDialog() - diag.setAttribute(Qt.WA_DeleteOnClose) - diag.open(lambda: tab.print(diag.printer())) + tab.printing.check_printer_support() + except browsertab.WebTabError as e: + raise cmdexc.CommandError(e) + + if preview: + diag = QPrintPreviewDialog() + diag.setAttribute(Qt.WA_DeleteOnClose) + diag.setWindowFlags(diag.windowFlags() | + Qt.WindowMaximizeButtonHint | + Qt.WindowMinimizeButtonHint) + diag.paintRequested.connect(tab.printing.to_printer) + diag.exec_() + elif pdf: + pdf = os.path.expanduser(pdf) + directory = os.path.dirname(pdf) + if directory and not os.path.exists(directory): + os.mkdir(directory) + tab.printing.to_pdf(pdf) + log.misc.debug("Print to file: {}".format(pdf)) + else: + diag = QPrintDialog() + diag.setAttribute(Qt.WA_DeleteOnClose) + diag.open(lambda: tab.printing.to_printer(diag.printer())) @cmdutils.register(instance='command-dispatcher', scope='window') def tab_clone(self, bg=False, window=False): diff --git a/qutebrowser/browser/webengine/webenginetab.py b/qutebrowser/browser/webengine/webenginetab.py index 5e4ab7417..04c60c02d 100644 --- a/qutebrowser/browser/webengine/webenginetab.py +++ b/qutebrowser/browser/webengine/webenginetab.py @@ -25,6 +25,7 @@ from PyQt5.QtCore import pyqtSlot, Qt, QEvent, QPoint from PyQt5.QtGui import QKeyEvent, QIcon from PyQt5.QtWidgets import QApplication +from PyQt5.QtPrintSupport import QPrinter # pylint: disable=no-name-in-module,import-error,useless-suppression from PyQt5.QtWebEngineWidgets import QWebEnginePage # pylint: enable=no-name-in-module,import-error,useless-suppression @@ -34,6 +35,28 @@ from qutebrowser.browser.webengine import webview from qutebrowser.utils import usertypes, qtutils, log +class WebEnginePrinting(browsertab.AbstractPrinting): + + """QtWebEngine implementations related to printing.""" + + def check_pdf_support(self): + if not hasattr(self._widget.page(), 'printToPdf'): + raise browsertab.WebTabError( + "Printing to PDF is unsupported with QtWebEngine on Qt > 5.7") + + def check_printer_support(self): + raise browsertab.WebTabError( + "Printing is unsupported with QtWebEngine") + + def to_pdf(self, filename): + self._widget.page().printToPdf(filename) + + @pyqtSlot(QPrinter) + def to_printer(self, printer): + # Should never be called + assert False + + class WebEngineSearch(browsertab.AbstractSearch): """QtWebEngine implementations related to searching on the page.""" @@ -256,6 +279,7 @@ class WebEngineTab(browsertab.AbstractTab): tab=self, parent=self) self.zoom = WebEngineZoom(win_id=win_id, parent=self) self.search = WebEngineSearch(parent=self) + self.printing = WebEnginePrinting() self._set_widget(widget) self._connect_signals() self.backend = usertypes.Backend.QtWebEngine diff --git a/qutebrowser/browser/webkit/webkittab.py b/qutebrowser/browser/webkit/webkittab.py index 7ccd465a5..48016d627 100644 --- a/qutebrowser/browser/webkit/webkittab.py +++ b/qutebrowser/browser/webkit/webkittab.py @@ -27,12 +27,39 @@ from PyQt5.QtCore import pyqtSlot, Qt, QEvent, QUrl, QPoint, QTimer from PyQt5.QtGui import QKeyEvent from PyQt5.QtWebKitWidgets import QWebPage from PyQt5.QtWebKit import QWebSettings +from PyQt5.QtPrintSupport import QPrinter from qutebrowser.browser import browsertab from qutebrowser.browser.webkit import webview, tabhistory from qutebrowser.utils import qtutils, objreg, usertypes, utils +class WebKitPrinting(browsertab.AbstractPrinting): + + """QtWebKit implementations related to printing.""" + + def _do_check(self): + if not qtutils.check_print_compat(): + # WORKAROUND (remove this when we bump the requirements to 5.3.0) + raise browsertab.WebTabError( + "Printing on Qt < 5.3.0 on Windows is broken, please upgrade!") + + def check_pdf_support(self): + self._do_check() + + def check_printer_support(self): + self._do_check() + + def to_pdf(self, filename): + printer = QPrinter() + printer.setOutputFileName(filename) + self.to_printer(printer) + + @pyqtSlot(QPrinter) + def to_printer(self, printer): + self._widget.print(printer) + + class WebKitSearch(browsertab.AbstractSearch): """QtWebKit implementations related to searching on the page.""" @@ -461,6 +488,7 @@ class WebKitTab(browsertab.AbstractTab): tab=self, parent=self) self.zoom = WebKitZoom(win_id=win_id, parent=self) self.search = WebKitSearch(parent=self) + self.printing = WebKitPrinting() self._set_widget(widget) self._connect_signals() self.zoom.set_default() diff --git a/tests/unit/browser/test_tab.py b/tests/unit/browser/test_tab.py index 722fa04d0..7f1ae3073 100644 --- a/tests/unit/browser/test_tab.py +++ b/tests/unit/browser/test_tab.py @@ -77,6 +77,7 @@ def test_tab(qtbot, view, config_stub, tab_registry): tab=tab_w, parent=tab_w) tab_w.zoom = browsertab.AbstractZoom(win_id=tab_w.win_id) tab_w.search = browsertab.AbstractSearch(parent=tab_w) + tab_w.printing = browsertab.AbstractPrinting() tab_w._set_widget(w) assert tab_w._widget is w From fce825f9dfaf443c43396934dbb4b4960595569c Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 12 Jul 2016 13:22:47 +0200 Subject: [PATCH 5/8] Remove redundant "Given a new tmpdir" step With the (tmpdir) replacement we'll get a temporary directory no matter what, so this is unnecessary. --- tests/end2end/features/misc.feature | 1 - tests/end2end/features/test_misc_bdd.py | 5 ----- 2 files changed, 6 deletions(-) diff --git a/tests/end2end/features/misc.feature b/tests/end2end/features/misc.feature index 5842a12a5..a0f3eced5 100644 --- a/tests/end2end/features/misc.feature +++ b/tests/end2end/features/misc.feature @@ -340,7 +340,6 @@ Feature: Various utility commands. Then no crash should happen Scenario: print pdf - Given a new tmpdir When I open data/hello.txt And I run :print --pdf (tmpdir)/hello.pdf And I wait for "Print to file: *" in the log or skip the test diff --git a/tests/end2end/features/test_misc_bdd.py b/tests/end2end/features/test_misc_bdd.py index 7a28d059c..9ada5e484 100644 --- a/tests/end2end/features/test_misc_bdd.py +++ b/tests/end2end/features/test_misc_bdd.py @@ -75,11 +75,6 @@ def check_cookie(quteproc, name, value): assert data['cookies'][name] == value -@bdd.given(bdd.parsers.parse('a new tmpdir')) -def tmpdir(tmpdir_factory): - return tmpdir_factory.mktemp('tmpdir') - - @bdd.then(bdd.parsers.parse('the file {filename} should exist in the tmpdir')) def file_exists(quteproc, tmpdir, filename): path = tmpdir / filename From 77035851a36fe1aca60b166fb53605d18242ffd6 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 12 Jul 2016 13:28:43 +0200 Subject: [PATCH 6/8] Sanity check the PDF file for :print --pdf test --- tests/end2end/features/misc.feature | 2 +- tests/end2end/features/test_misc_bdd.py | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/tests/end2end/features/misc.feature b/tests/end2end/features/misc.feature index a0f3eced5..48f51aa43 100644 --- a/tests/end2end/features/misc.feature +++ b/tests/end2end/features/misc.feature @@ -343,7 +343,7 @@ Feature: Various utility commands. When I open data/hello.txt And I run :print --pdf (tmpdir)/hello.pdf And I wait for "Print to file: *" in the log or skip the test - Then the file hello.pdf should exist in the tmpdir + Then the PDF hello.pdf should exist in the tmpdir # :pyeval diff --git a/tests/end2end/features/test_misc_bdd.py b/tests/end2end/features/test_misc_bdd.py index 9ada5e484..f9cd50766 100644 --- a/tests/end2end/features/test_misc_bdd.py +++ b/tests/end2end/features/test_misc_bdd.py @@ -75,7 +75,8 @@ def check_cookie(quteproc, name, value): assert data['cookies'][name] == value -@bdd.then(bdd.parsers.parse('the file {filename} should exist in the tmpdir')) -def file_exists(quteproc, tmpdir, filename): +@bdd.then(bdd.parsers.parse('the PDF {filename} should exist in the tmpdir')) +def pdf_exists(quteproc, tmpdir, filename): path = tmpdir / filename - assert os.path.exists(str(path)) + data = path.read_binary() + assert data.startswith(b'%PDF') From 9f6b3973d33738deb6dc0f1c7c3713a440549617 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 12 Jul 2016 13:29:32 +0200 Subject: [PATCH 7/8] Adjust :print --pdf test title --- tests/end2end/features/misc.feature | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/end2end/features/misc.feature b/tests/end2end/features/misc.feature index 48f51aa43..08befc45a 100644 --- a/tests/end2end/features/misc.feature +++ b/tests/end2end/features/misc.feature @@ -339,7 +339,7 @@ Feature: Various utility commands. And I run :debug-pyeval QApplication.instance().activeModalWidget().close() Then no crash should happen - Scenario: print pdf + Scenario: print --pdf When I open data/hello.txt And I run :print --pdf (tmpdir)/hello.pdf And I wait for "Print to file: *" in the log or skip the test From 995b6012225dce3b04879b02e060a550eac26f4e Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 12 Jul 2016 13:31:09 +0200 Subject: [PATCH 8/8] Update docs --- CHANGELOG.asciidoc | 2 ++ README.asciidoc | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc index e2efcfe05..0b1806ac4 100644 --- a/CHANGELOG.asciidoc +++ b/CHANGELOG.asciidoc @@ -26,6 +26,8 @@ Added `$QUTE_DOWNLOAD_DIR` available for userscripts. - New option `ui` -> `status-position` to configure the position of the status bar (top/bottom). +- New `--pdf ` argument for `:print` which can be used to generate a + PDF without a dialog. Changed ~~~~~~~ diff --git a/README.asciidoc b/README.asciidoc index 3efc88905..a6c4050dd 100644 --- a/README.asciidoc +++ b/README.asciidoc @@ -201,6 +201,7 @@ Contributors, sorted by the number of commits in descending order: * Link * Larry Hynes * Johannes Altmanninger +* Jeremy Kaplan * Ismail * Edgar Hipp * Daryl Finlay @@ -228,7 +229,6 @@ Contributors, sorted by the number of commits in descending order: * Matthias Lisin * Marcel Schilling * Johannes Martinsson -* Jeremy Kaplan * Jean-Christophe Petkovich * Jay Kamat * Helen Sherwood-Taylor