Wed, 28 Sep 2016 20:04:44 +0200
Started to implement real printing support for the new web browser and Qt >= 5.7.0.
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/Tools/FilePrinter.py Wed Sep 28 20:04:44 2016 +0200 @@ -0,0 +1,576 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing an object for printing of files. +""" + +# +# This code is inspired by and ported from Qupzilla. +# Original Copyright (C) 2016 by Kevin Kofler <kevin.kofler@chello.at> +# + +from __future__ import unicode_literals + +from PyQt5.QtCore import qVersion, QFile, QStandardPaths, QProcess +from PyQt5.QtPrintSupport import QPrinter, QPrintEngine + +import Globals + + +_FilePrintJobs = [] + +class FilePrinter(object): + """ + Class implementing methods for printing on *nix systems. + """ + # + # Whether file(s) get deleted by the application or by the print system. + # + ApplicationDeletesFiles = 0 + SystemDeletesFiles = 1 + + # + # Whether pages to be printed are selected by the application or the print + # system. + # + # If application side, then the generated file will only contain those + # pages selected by the user, so FilePrinter will print all the pages in + # the file. + # + # If system side, then the file will contain all the pages in the + # document, and the print system will print the users selected print range + # from out of the file. + # + # Note: system side only works in CUPS, not LPR. + # + ApplicationSelectsPages = 0 + SystemSelectsPages = 1 + + def __init__(self): + """ + Constructor + """ + self.__paperSizesMap = { + QPrinter.A0: "A0", + QPrinter.A1: "A1", + QPrinter.A2: "A2", + QPrinter.A3: "A3", + QPrinter.A4: "A4", + QPrinter.A5: "A5", + QPrinter.A6: "A6", + QPrinter.A7: "A7", + QPrinter.A8: "A8", + QPrinter.A9: "A9", + QPrinter.B0: "B0", + QPrinter.B1: "B1", + QPrinter.B2: "B2", + QPrinter.B3: "B3", + QPrinter.B4: "B4", + QPrinter.B5: "B5", + QPrinter.B6: "B6", + QPrinter.B7: "B7", + QPrinter.B8: "B8", + QPrinter.B9: "B9", + QPrinter.B10: "B10", + QPrinter.C5E: "C5", + QPrinter.Comm10E: "Comm10", + QPrinter.DLE: "DL", + QPrinter.Executive: "Executive", + QPrinter.Folio: "Folio", + QPrinter.Ledger: "Ledger", + QPrinter.Legal: "Legal", + QPrinter.Letter: "Letter", + QPrinter.Tabloid: "Tabloid", + } + + self.__paperSourcesMap = { + QPrinter.Auto: "", + QPrinter.Cassette: "Cassette", + QPrinter.Envelope: "Envelope", + QPrinter.EnvelopeManual: "EnvelopeManual", + QPrinter.FormSource: "FormSource", + QPrinter.LargeCapacity: "LargeCapacity", + QPrinter.LargeFormat: "LargeFormat", + QPrinter.Lower: "Lower", + QPrinter.MaxPageSource: "MaxPageSource", + QPrinter.Middle: "Middle", + QPrinter.Manual: "Manual", + QPrinter.OnlyOne: "OnlyOne", + QPrinter.Tractor: "Tractor", + QPrinter.SmallFormat: "SmallFormat", + } + + self.__process = None + self.__doDeleteFile = FilePrinter.ApplicationDeletesFiles + self.__fileName = "" + + def _doPrintFile(self, printer, fileName, fileDeletePolicy, + pageSelectPolicy, pageRange): + """ + Protected method to print a file + + @param printer reference to the printer to print to + @type QPrinter + @param fileName name (path) of the file to be printed + @type str + @param fileDeletePolicy policy determining who deletes the file to be + printed (application or system) + @type int (0 or 1) + @param pageSelectPolicy policy determining who selects the pages to be + printed (application or system) + @type int (0 or 1) + @param pageRange string determining the page range(s) to be printed, if + SystemSelectsPages was given for pageSelectPolicy and user chose + Selection in print dialog + @type str + @return flag indicating successful print job submission + @rtype bool + """ + if not QFile.exists(fileName): + return False + + self.__fileName = fileName + self.__doDeleteFile = ( + fileDeletePolicy == FilePrinter.SystemDeletesFiles) + + if printer.printerState() in [QPrinter.Aborted, QPrinter.Error]: + if self.__doDeleteFile: + QFile.remove(fileName) + return False + + # + # Print via lpr/lp command + # + + # + # Decide what executable to use to print with, need the CUPS version + # of lpr if available. Some distros name the CUPS version of lpr as + # lpr-cups or lpr.cups so try those first before default to lpr, or + # failing that to lp. + if QStandardPaths.findExecutable("lpr-cups"): + exe = "lpr-cups" + elif QStandardPaths.findExecutable("lpr.cups"): + exe = "lpr.cups" + elif QStandardPaths.findExecutable("lpr"): + exe = "lpr" + elif QStandardPaths.findExecutable("lp"): + exe = "lp" + else: + if self.__doDeleteFile: + QFile.remove(fileName) + return False + + useCupsOptions = isCupsAvailable() + argsList = self._printArguments( + printer, fileDeletePolicy, pageSelectPolicy, useCupsOptions, + pageRange, exe) + argsList.append(fileName) + + self.__process = QProcess() + if qVersion() < "5.6.0": + self.__process.error.connect(self.__processError) + else: + self.__process.errorOccurred.connect(self.__processError) + self.__process.finished.connect(self.__processFinished) + self.__process.start(exe, argsList) + if not self.__process.waitForStarted(10000): + # it failed to start + self.__doCleanup(self.__doDeleteFile) + return False + + return True + + def __doCleanup(self, deleteFile): + """ + Private method to perform some internal cleanup actions. + + @param deleteFile flag indicating to delete the print file + @type bool + """ + if deleteFile: + QFile.remove(self.__fileName) + + self.__process.deleteLater() + self.__process = None + + if self in _FilePrintJobs: + _FilePrintJobs.remove(self) + + def __processError(self, error): + """ + Private slot handling process errors. + + @param error error value + @type QProcess.ProcessError + """ + self.__doCleanup(self.__doDeleteFile) + + def __processFinished(self, exitCode, exitStatus): + """ + Private slot handling the end of the process. + + @param exitCode exit code of the process + @type int + @param exitStatus exit status of the process + @type QProcess.ExitStatus + """ + self.__doCleanup(self.__doDeleteFile and ( + exitStatus != QProcess.NormalExit or exitCode != 0)) + + def _printArguments(self, printer, fileDeletePolicy, pageSelectPolicy, + useCupsOptions, pageRange, variant): + """ + Protected method to assemble the command line arguments for the print + command. + + @param printer reference to the printer to print to + @type QPrinter + @param fileDeletePolicy policy determining who deletes the file to be + printed (application or system) + @type int (0 or 1) + @param pageSelectPolicy policy determining who selects the pages to be + printed (application or system) + @type int (0 or 1) + @param useCupsOptions flag indicating to assemble the arguments for + CUPS + @type bool + @param pageRange string determining the page range(s) to be printed, if + SystemSelectsPages was given for pageSelectPolicy and user chose + Selection in print dialog + @type str + @param variant string identifying the print command variant + @type str + @return assembled command line arguments for the print command + @rtype list of str + """ + if variant.startswith("lpr"): + variant = "lpr" + + args = [] + args.extend(self._destination(printer, variant)) + args.extend(self._copies(printer, variant)) + args.extend(self._jobname(printer, variant)) + args.extend(self._pages(printer, pageSelectPolicy, pageRange, + useCupsOptions, variant)) + if useCupsOptions: + args.extend(self._cupsOptions(printer)) + args.extend(self._deleteFile(printer, fileDeletePolicy, variant)) + if variant == "lp": + args.append("--") + + return args + + def _destination(self, printer, variant): + """ + Protected method to assemble the printer destination arguments. + + @param printer reference to the printer to print to + @type QPrinter + @param variant string identifying the print command variant + @type str + @return assembled printer destination arguments + @rtype list of str + """ + if variant == "lp": + return ["-d", printer.printerName()] + elif variant == "lpr": + return ["-P", printer.printerName()] + else: + return [] + + def _copies(self, printer, variant): + """ + Protected method to assemble the number of copies arguments. + + @param printer reference to the printer to print to + @type QPrinter + @param variant string identifying the print command variant + @type str + @return assembled number of copies arguments + @rtype list of str + """ + copies = printer.copyCount() + if variant == "lp": + return ["-n", str(copies)] + elif variant == "lpr": + return ["-#{0}".format(copies)] + else: + return [] + + def _jobname(self, printer, variant): + """ + Protected method to assemble the jobname arguments. + + @param printer reference to the printer to print to + @type QPrinter + @param variant string identifying the print command variant + @type str + @return assembled jobname arguments + @rtype list of str + """ + if printer.docName(): + if variant == "lp": + return ["-t", printer.docName()] + elif variant == "lpr": + shortenedDocName = printer.docName()[:255] + return ["-J", shortenedDocName] + + return [] + + def _deleteFile(self, printer, fileDeletePolicy, variant): + """ + Protected method to assemble the jobname arguments. + + @param printer reference to the printer to print to + @type QPrinter + @param fileDeletePolicy policy determining who deletes the file to be + printed (application or system) + @type int (0 or 1) + @param variant string identifying the print command variant + @type str + @return assembled jobname arguments + @rtype list of str + """ + if fileDeletePolicy == FilePrinter.SystemDeletesFiles and \ + variant == "lpr": + return ["-r"] + else: + return [] + + def _pages(self, printer, pageSelectPolicy, pageRange, useCupsOptions, + variant): + """ + Protected method to assemble the page range(s) arguments. + + @param printer reference to the printer to print to + @type QPrinter + @param pageSelectPolicy policy determining who selects the pages to be + printed (application or system) + @type int (0 or 1) + @param pageRange string determining the page range(s) to be printed, if + SystemSelectsPages was given for pageSelectPolicy and user chose + Selection in print dialog + @type str + @param useCupsOptions flag indicating to assemble the arguments for + CUPS + @type bool + @param variant string identifying the print command variant + @type str + @return assembled page range(s) arguments + @rtype list of str + """ + if pageSelectPolicy == FilePrinter.SystemSelectsPages: + if printer.printRange() == QPrinter.Selection and bool(pageRange): + if variant == "lp": + return ["-P", pageRange] + elif variant == "lpr": + return ["-o", "page-ranges={0}".format(pageRange)] + + if printer.printRange() == QPrinter.PageRange: + if variant == "lp": + return ["-P", "{0}-{1}".format( + printer.fromPage(), printer.toPage())] + elif variant == "lpr": + return ["-o", "page-ranges={0}-{1}".format( + printer.fromPage(), printer.toPage())] + + return [] # all pages + + def _cupsOptions(self, printer): + """ + Protected method to assemble the CUPS specific arguments. + + @param printer reference to the printer to print to + @type QPrinter + @return assembled CUPS arguments + @rtype list of str + """ + options = [] + options.extend(self._optionMedia(printer)) + options.extend(self._optionDoubleSidedPrinting(printer)) + options.extend(self._optionPageOrder(printer)) + options.extend(self._optionCollateCopies(printer)) + options.extend(self._optionCupsProperties(printer)) + + return options + + def _optionMedia(self, printer): + """ + Protected method to assemble the print media arguments. + + @param printer reference to the printer to print to + @type QPrinter + @return assembled print media arguments + @rtype list of str + """ + pageSize = self._mediaPageSize(printer) + paperSource = self._mediaPaperSource(printer) + + if pageSize and paperSource: + return ["-o", "media={0},{1}".format(pageSize, paperSource)] + + elif pageSize: + return ["-o", "media={0}".format(pageSize)] + + elif paperSource: + return ["-o", "media={0}".format(paperSource)] + + return [] + + def _mediaPageSize(self, printer): + """ + Protected method to get the page size argument. + + @param printer reference to the printer to print to + @type QPrinter + @return page size argument + @rtype str + """ + pageSize = printer.pageSize() + if pageSize in self.__paperSizesMap: + return self.__paperSizesMap[pageSize] + else: + return "" + + def _mediaPaperSource(self, printer): + """ + Protected method to get the paper source argument. + + @param printer reference to the printer to print to + @type QPrinter + @return paper source argument + @rtype str + """ + paperSource = printer.paperSource() + if paperSource in self.__paperSourcesMap: + return self.__paperSourcesMap[paperSource] + else: + return "" + + def _optionDoubleSidedPrinting(self, printer): + """ + Protected method to assemble the double sided printing arguments. + + @param printer reference to the printer to print to + @type QPrinter + @return assembled double sided printing arguments + @rtype list of str + """ + duplex = printer.duplex() + + if duplex == QPrinter.DuplexNone: + return ["-o", "sides=one-sided"] + elif duplex == QPrinter.DuplexAuto: + if printer.orientation() == QPrinter.Landscape: + return ["-o", "sides=two-sided-short-edge"] + else: + return ["-o", "sides=two-sided-long-edge"] + elif duplex == QPrinter.DuplexLongSide: + return ["-o", "sides=two-sided-long-edge"] + elif duplex == QPrinter.DuplexShortSide: + return ["-o", "sides=two-sided-short-edge"] + else: + return [] # use printer default + + def _optionPageOrder(self, printer): + """ + Protected method to assemble the page order arguments. + + @param printer reference to the printer to print to + @type QPrinter + @return assembled page order arguments + @rtype list of str + """ + if printer.pageOrder() == QPrinter.LastPageFirst: + return ["-o", "outputorder=reverse"] + else: + return ["-o", "outputorder=normal"] + + def _optionCollateCopies(self, printer): + """ + Protected method to assemble the collate copies arguments. + + @param printer reference to the printer to print to + @type QPrinter + @return assembled collate copies arguments + @rtype list of str + """ + if printer.collateCopies(): + return ["-o", "Collate=True"] + else: + return ["-o", "Collate=False"] + + def _optionCupsProperties(self, printer): + """ + Protected method to assemble the CUPS properties arguments. + + @param printer reference to the printer to print to + @type QPrinter + @return assembled CUPS properties arguments + @rtype list of str + """ + options = Globals.toList(printer.printEngine().property( + QPrintEngine.PrintEnginePropertyKey(0xfe00))) + + cupsOptions = [] + index = 0 + while index < len(options): + if options[index + 1]: + cupsOptions.extend(["-o", "{0}={1}".format( + options[index], options[index + 1])]) + else: + cupsOptions.extend(["-o", options[index]]) + index += 2 + + return cupsOptions + + +def isCupsAvailable(): + """ + Static method to test the availability of CUPS. + + @return flag indicating the availability of CUPS + @rtype bool + """ + if Globals.isMacPlatform(): + # OS X/MacOS always have CUPS + return True + elif Globals.isLinuxPlatform(): + testPrinter = QPrinter() + return testPrinter.supportsMultipleCopies() + else: + return False + + +def printFile(printer, fileName, + fileDeletePolicy=FilePrinter.ApplicationDeletesFiles, + pageSelectPolicy=FilePrinter.ApplicationSelectsPages, + pageRange=""): + """ + Static method to print a file. + + Note: Only CUPS and LPR on *nix systems is supported. + + @param printer reference to the printer to print to + @type QPrinter + @param fileName name (path) of the file to be printed + @type str + @param fileDeletePolicy policy determining who deletes the file to be + printed (application or system) + @type int (0 or 1) + @param pageSelectPolicy policy determining who selects the pages to be + printed (application or system) + @type int (0 or 1) + @param pageRange string determining the page range(s) to be printed, if + SystemSelectsPages was given for pageSelectPolicy and user chose + Selection in print dialog + @type str + """ + fp = FilePrinter() + if fp._doPrintFile(printer, fileName, fileDeletePolicy, pageSelectPolicy, + pageRange): + _FilePrintJobs.append(fp) +
--- a/WebBrowser/WebBrowserTabWidget.py Tue Sep 27 19:41:11 2016 +0200 +++ b/WebBrowser/WebBrowserTabWidget.py Wed Sep 28 20:04:44 2016 +0200 @@ -11,10 +11,11 @@ import os -from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt, QUrl, QFile, qVersion +from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt, QUrl, QDir, QFile, \ + QFileDevice, QTemporaryFile, qVersion from PyQt5.QtGui import QIcon, QPixmap, QPainter from PyQt5.QtWidgets import QWidget, QHBoxLayout, QMenu, QToolButton, QDialog -from PyQt5.QtPrintSupport import QPrinter, QPrintDialog +from PyQt5.QtPrintSupport import QPrinter, QPrintDialog, QAbstractPrintDialog from E5Gui.E5TabWidget import E5TabWidget from E5Gui import E5MessageBox @@ -23,6 +24,7 @@ from .WebBrowserView import WebBrowserView from .WebBrowserPage import WebBrowserPage from .Tools import WebBrowserTools +from .Tools import FilePrinter from . import WebInspector import UI.PixmapCache @@ -150,6 +152,8 @@ self.__initTabContextMenu() self.__historyCompleter = None + + self.__pdfPrinter = None def __initTabContextMenu(self): """ @@ -179,11 +183,11 @@ self.__tabContextMenu.addAction( self.tr('Close All'), self.closeAllBrowsers) self.__tabContextMenu.addSeparator() - if not Globals.isWindowsPlatform(): - # TODO: implement printing based on printToPdf() + if not Globals.isWindowsPlatform() and qVersion() < "5.7.0": self.__tabContextMenu.addAction( UI.PixmapCache.getIcon("printPreview.png"), self.tr('Print Preview'), self.__tabContextMenuPrintPreview) + if not Globals.isWindowsPlatform() or qVersion() >= "5.7.0": self.__tabContextMenu.addAction( UI.PixmapCache.getIcon("print.png"), self.tr('Print'), self.__tabContextMenuPrint) @@ -624,10 +628,32 @@ if printerName: printer.setPrinterName(printerName) printer.setResolution(Preferences.getPrinter("Resolution")) + documentName = WebBrowserTools.getFileNameFromUrl(browser.url()) + printer.setDocName(documentName) printDialog = QPrintDialog(printer, self) + printDialog.setOptions(QAbstractPrintDialog.PrintToFile | + QAbstractPrintDialog.PrintShowPageSize) + if not Globals.isWindowsPlatform(): + if FilePrinter.isCupsAvailable(): + printDialog.setOption(QAbstractPrintDialog.PrintCollateCopies) + printDialog.setOption(QAbstractPrintDialog.PrintPageRange) if printDialog.exec_() == QDialog.Accepted: - browser.render(printer) + if not hasattr(browser.page(), "printToPdf"): + browser.render(printer) + else: + if printer.outputFormat() == QPrinter.PdfFormat: + # print to PDF file selected + browser.page().printToPdf( + lambda pdf: self.__pdfGeneratedForSave( + printer.outputFileName(), pdf), + printer.pageLayout()) + else: + # print to printer + self.__pdfPrinter = printer + browser.page().printToPdf( + self.__pdfGeneratedForPrinting, + printer.pageLayout()) @pyqtSlot() def printBrowserPdf(self, browser=None): @@ -652,20 +678,20 @@ filePath, pageLayout = dlg.getData() if filePath: if os.path.exists(filePath): - res = E5MessageBox.warning( - self, - self.tr("Print to PDF"), + res = E5MessageBox.warning( + self, + self.tr("Print to PDF"), self.tr("""<p>The file <b>{0}</b> exists""" """ already. Shall it be""" - """ overwritten?</p>""").format(filePath), - E5MessageBox.StandardButtons( - E5MessageBox.No | - E5MessageBox.Yes), - E5MessageBox.No) + """ overwritten?</p>""").format(filePath), + E5MessageBox.StandardButtons( + E5MessageBox.No | + E5MessageBox.Yes), + E5MessageBox.No) if res == E5MessageBox.No: return browser.page().printToPdf( - lambda p: self.__pdfGenerated(filePath, p), + lambda pdf: self.__pdfGeneratedForSave(filePath, pdf), pageLayout) elif Globals.isLinuxPlatform(): printer = QPrinter(mode=QPrinter.HighResolution) @@ -685,9 +711,9 @@ if printDialog.exec_() == QDialog.Accepted: browser.render(printer) - def __pdfGenerated(self, filePath, pdfData): + def __pdfGeneratedForSave(self, filePath, pdfData): """ - Private slot handling the generated PDF data. + Private slot to save the generated PDF data to a file. @param filePath path to save the PDF to @type str @@ -698,11 +724,50 @@ return pdfFile = QFile(filePath) - if not pdfFile.open(QFile.WriteOnly): + if pdfFile.open(QFile.WriteOnly): + pdfFile.write(pdfData) + pdfFile.close() + if pdfFile.error() != QFileDevice.NoError: + E5MessageBox.critical( + self, + self.tr("Print to PDF"), + self.tr("""<p>The PDF could not be written to file <b>{0}""" + """</b>.</p><p><b>Error:</b> {1}</p>""").format( + filePath, pdfFile.errorString()), + E5MessageBox.StandardButtons( + E5MessageBox.Ok)) + + def __pdfGeneratedForPrinting(self, pdfData): + """ + Private slot to print the generated PDF data. + + @param pdfData generated PDF document + @type QByteArray + """ + if self.__pdfPrinter is None or pdfData.isEmpty(): return - pdfFile.write(pdfData) - pdfFile.close() + tempFile = QTemporaryFile(QDir.tempPath() + "/ericBrowserXXXXXX.pdf") + tempFile.setAutoRemove(False) + if tempFile.open(): + bytesWritten = tempFile.write(pdfData) + tempFile.close() + if bytesWritten == pdfData.size(): + if Globals.isWindowsPlatform(): + printerName = self.__pdfPrinter.printerName() + import ctypes + ctypes.windll.shell32.ShellExecuteW( + self.winId(), "printto", tempFile.fileName(), + '"{0}"'.format(printerName), None, 0) + else: + FilePrinter.printFile( + self.__pdfPrinter, tempFile.fileName(), + FilePrinter.FilePrinter.SystemDeletesFiles, + FilePrinter.FilePrinter.SystemSelectsPages) + else: + tempFile.remove() + + self.__pdfPrinter = None @pyqtSlot() def printPreviewBrowser(self, browser=None):
--- a/WebBrowser/WebBrowserWindow.py Tue Sep 27 19:41:11 2016 +0200 +++ b/WebBrowser/WebBrowserWindow.py Wed Sep 28 20:04:44 2016 +0200 @@ -701,8 +701,8 @@ bookmarksManager.exportBookmarks) self.__actions.append(self.exportBookmarksAct) - if not Globals.isWindowsPlatform(): - # TODO: implement printing based on printToPdf() + if not Globals.isWindowsPlatform() or \ + qVersion() >= "5.7.0": self.printAct = E5Action( self.tr('Print'), UI.PixmapCache.getIcon("print.png"), @@ -739,7 +739,7 @@ else: self.printPdfAct = None - if not Globals.isWindowsPlatform(): + if not Globals.isWindowsPlatform() and qVersion() < "5.7.0": self.printPreviewAct = E5Action( self.tr('Print Preview'), UI.PixmapCache.getIcon("printPreview.png"), @@ -1956,7 +1956,8 @@ filetb.addAction(self.printAct) if self.printPdfAct: filetb.addAction(self.printPdfAct) - filetb.addSeparator() + if self.printPreviewAct or self.printAct or self.printPdfAct: + filetb.addSeparator() filetb.addAction(self.closeAct) filetb.addAction(self.exitAct)
--- a/eric6.e4p Tue Sep 27 19:41:11 2016 +0200 +++ b/eric6.e4p Wed Sep 28 20:04:44 2016 +0200 @@ -1424,6 +1424,7 @@ <Source>WebBrowser/TabManager/TabManagerWidget.py</Source> <Source>WebBrowser/TabManager/__init__.py</Source> <Source>WebBrowser/Tools/DelayedFileWatcher.py</Source> + <Source>WebBrowser/Tools/FilePrinter.py</Source> <Source>WebBrowser/Tools/PrintToPdfDialog.py</Source> <Source>WebBrowser/Tools/Scripts.py</Source> <Source>WebBrowser/Tools/WebBrowserTools.py</Source>