// Copyright (C) 2016 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only #include #include "qprintdialog.h" #include "qabstractprintdialog_p.h" #include #include #include #include #include #include #include QT_BEGIN_NAMESPACE using namespace Qt::StringLiterals; extern qreal qt_pointMultiplier(QPageLayout::Unit unit); class QPrintDialogPrivate : public QAbstractPrintDialogPrivate { Q_DECLARE_PUBLIC(QPrintDialog) public: QPrintDialogPrivate() : printInfo(0), printPanel(0) {} void openCocoaPrintPanel(Qt::WindowModality modality); void closeCocoaPrintPanel(); inline QPrintDialog *printDialog() { return q_func(); } NSPrintInfo *printInfo; NSPrintPanel *printPanel; }; QT_END_NAMESPACE QT_USE_NAMESPACE @class QT_MANGLE_NAMESPACE(QCocoaPrintPanelDelegate); @interface QT_MANGLE_NAMESPACE(QCocoaPrintPanelDelegate) : NSObject @end @implementation QT_MANGLE_NAMESPACE(QCocoaPrintPanelDelegate) { NSPrintInfo *printInfo; } - (instancetype)initWithNSPrintInfo:(NSPrintInfo *)nsPrintInfo { if ((self = [self init])) { printInfo = nsPrintInfo; } return self; } - (void)printPanelDidEnd:(NSPrintPanel *)printPanel returnCode:(int)returnCode contextInfo:(void *)contextInfo { Q_UNUSED(printPanel); QPrintDialog *dialog = static_cast(contextInfo); QPrinter *printer = dialog->printer(); if (returnCode == NSModalResponseOK) { PMPrintSession session = static_cast(printInfo.PMPrintSession); PMPrintSettings settings = static_cast(printInfo.PMPrintSettings); UInt32 frompage, topage; PMGetFirstPage(settings, &frompage); PMGetLastPage(settings, &topage); topage = qMin(UInt32(INT_MAX), topage); dialog->setFromTo(frompage, topage); // OK, I need to map these values back let's see // If from is 1 and to is INT_MAX, then print it all // (Apologies to the folks with more than INT_MAX pages) if (dialog->fromPage() == 1 && dialog->toPage() == INT_MAX) { dialog->setPrintRange(QPrintDialog::AllPages); printer->setPageRanges(QPageRanges()); } else { dialog->setPrintRange(QPrintDialog::PageRange); // In a way a lie, but it shouldn't hurt. // Carbon hands us back a very large number here even for ALL, set it to max // in that case to follow the behavior of the other print dialogs. if (dialog->maxPage() < dialog->toPage()) dialog->setFromTo(dialog->fromPage(), dialog->maxPage()); } // Keep us in sync with chosen destination PMDestinationType dest; PMSessionGetDestinationType(session, settings, &dest); if (dest == kPMDestinationFile) { QCFType file; PMSessionCopyDestinationLocation(session, settings, &file); UInt8 localFile[2048]; // Assuming there's a POSIX file system here. CFURLGetFileSystemRepresentation(file, true, localFile, sizeof(localFile)); auto outputFile = QFileInfo(QString::fromUtf8(reinterpret_cast(localFile))); if (outputFile.suffix() == "pdf"_L1) printer->setOutputFileName(outputFile.absoluteFilePath()); else qWarning() << "Can not print to file type" << outputFile.suffix(); } else if (dest == kPMDestinationPreview) { static QTemporaryDir printPreviews; auto documentName = printer->docName(); if (documentName.isEmpty()) documentName = QGuiApplication::applicationDisplayName(); auto fileName = printPreviews.filePath(QString("%1.pdf"_L1).arg(documentName)); printer->setOutputFileName(fileName); // Ideally we would have a callback when the PDF engine is done writing // to the file, and open Preview in response to that. Lacking that, we // use the quick and dirty assumption that the the print operation will // happen synchronously after the dialog is accepted, so we can defer // the opening of the file to the next runloop pass. dispatch_async(dispatch_get_main_queue(), ^{ [NSWorkspace.sharedWorkspace openURL:[NSURL fileURLWithPath:fileName.toNSString()]]; }); } else if (dest == kPMDestinationProcessPDF) { qWarning("Printing workflows are not supported"); } else if (dest == kPMDestinationPrinter) { PMPrinter macPrinter; PMSessionGetCurrentPrinter(session, &macPrinter); QString printerId = QString::fromCFString(PMPrinterGetID(macPrinter)).trimmed(); if (printer->printerName() != printerId) printer->setPrinterName(printerId); } } // Note this code should be in QCocoaPrintDevice, but that implementation is in the plugin, // we need to move the dialog implementation into the plugin first to be able to access it. // Need to tell QPrinter/QPageLayout if the page size or orientation has been changed PMPageFormat pageFormat = static_cast([printInfo PMPageFormat]); PMPaper paper; PMGetPageFormatPaper(pageFormat, &paper); PMOrientation orientation; PMGetOrientation(pageFormat, &orientation); QPageSize pageSize; CFStringRef key; double width = 0; double height = 0; // If the PPD name is empty then is custom, for some reason PMPaperIsCustom doesn't work here PMPaperGetPPDPaperName(paper, &key); if (PMPaperGetWidth(paper, &width) == noErr && PMPaperGetHeight(paper, &height) == noErr) { QString ppdKey = QString::fromCFString(key); if (ppdKey.isEmpty()) { // Is using a custom page size as defined in the Print Dialog custom settings using mm or inches. // We can't ask PMPaper what those units actually are, we can only get the point size which may return // slightly wrong results due to rounding. // Testing shows if using metric/mm then is rounded mm, if imperial/inch is rounded to 2 decimal places // Even if we pass in our own custom size in mm with decimal places, the dialog will still round it! // Suspect internal storage is in rounded mm? if (QLocale().measurementSystem() == QLocale::MetricSystem) { QSizeF sizef = QSizeF(width, height) / qt_pointMultiplier(QPageLayout::Millimeter); // Round to 0 decimal places pageSize = QPageSize(sizef.toSize(), QPageSize::Millimeter); } else { qreal multiplier = qt_pointMultiplier(QPageLayout::Inch); const int w = qRound(width * 100 / multiplier); const int h = qRound(height * 100 / multiplier); pageSize = QPageSize(QSizeF(w / 100.0, h / 100.0), QPageSize::Inch); } } else { pageSize = QPlatformPrintDevice::createPageSize(ppdKey, QSize(width, height), QString()); } } if (pageSize.isValid() && !pageSize.isEquivalentTo(printer->pageLayout().pageSize())) printer->setPageSize(pageSize); printer->setPageOrientation(orientation == kPMLandscape ? QPageLayout::Landscape : QPageLayout::Portrait); dialog->done((returnCode == NSModalResponseOK) ? QDialog::Accepted : QDialog::Rejected); } @end QT_BEGIN_NAMESPACE void QPrintDialogPrivate::openCocoaPrintPanel(Qt::WindowModality modality) { Q_Q(QPrintDialog); if (printer->outputFormat() == QPrinter::NativeFormat) { printInfo = static_cast(printer->printEngine())->printInfo(); [printInfo retain]; } else { const QPageLayout pageLayout = printer->pageLayout(); // initialize the printInfo using the dictionary from the application-wide print info const auto dictionary = [NSPrintInfo.sharedPrintInfo dictionary]; printInfo = [[NSPrintInfo alloc] initWithDictionary:dictionary]; printInfo.orientation = pageLayout.orientation() == QPageLayout::Landscape ? NSPaperOrientationLandscape : NSPaperOrientationPortrait; printInfo.paperSize = pageLayout.pageSize().size(QPageSize::Point).toCGSize(); } // It seems the only way that PM lets you use all is if the minimum // for the page range is 1. This _kind of_ makes sense if you think about // it. However, calling PMSetFirstPage() or PMSetLastPage() always enforces // the range. // get print settings from the platform plugin PMPrintSettings settings = static_cast([printInfo PMPrintSettings]); PMSetPageRange(settings, q->minPage(), q->maxPage()); if (q->printRange() == QAbstractPrintDialog::PageRange) { PMSetFirstPage(settings, q->fromPage(), false); PMSetLastPage(settings, q->toPage(), false); } [printInfo updateFromPMPrintSettings]; QPrintDialog::PrintDialogOptions qtOptions = q->options(); NSPrintPanelOptions macOptions = NSPrintPanelShowsCopies; if (qtOptions & QPrintDialog::PrintPageRange) macOptions |= NSPrintPanelShowsPageRange; if (qtOptions & QPrintDialog::PrintShowPageSize) macOptions |= NSPrintPanelShowsPaperSize | NSPrintPanelShowsPageSetupAccessory | NSPrintPanelShowsOrientation; printPanel = [NSPrintPanel printPanel]; [printPanel retain]; [printPanel setOptions:macOptions]; // Call processEvents in case the event dispatcher has been interrupted, and needs to do // cleanup of modal sessions. Do this before showing the native dialog, otherwise it will // close down during the cleanup (QTBUG-17913): qApp->processEvents(QEventLoop::ExcludeUserInputEvents | QEventLoop::ExcludeSocketNotifiers); QT_MANGLE_NAMESPACE(QCocoaPrintPanelDelegate) *delegate = [[QT_MANGLE_NAMESPACE(QCocoaPrintPanelDelegate) alloc] initWithNSPrintInfo:printInfo]; if (modality == Qt::ApplicationModal || !q->parentWidget()) { if (modality == Qt::NonModal) qWarning("QPrintDialog is required to be modal on OS X"); // Make sure we don't interrupt the runModalWithPrintInfo call. (void) QMetaObject::invokeMethod(qApp->platformNativeInterface(), "clearCurrentThreadCocoaEventDispatcherInterruptFlag"); int rval = [printPanel runModalWithPrintInfo:printInfo]; [delegate printPanelDidEnd:printPanel returnCode:rval contextInfo:q]; } else { Q_ASSERT(q->window()); QWindow *parentWindow = q->window()->windowHandle(); NSWindow *window = static_cast(qApp->platformNativeInterface()->nativeResourceForWindow("nswindow", parentWindow)); [printPanel beginSheetWithPrintInfo:printInfo modalForWindow:window delegate:delegate didEndSelector:@selector(printPanelDidEnd:returnCode:contextInfo:) contextInfo:q]; } } void QPrintDialogPrivate::closeCocoaPrintPanel() { [printInfo release]; printInfo = 0; [printPanel release]; printPanel = 0; } QPrintDialog::QPrintDialog(QPrinter *printer, QWidget *parent) : QAbstractPrintDialog(*(new QPrintDialogPrivate), printer, parent) { setAttribute(Qt::WA_DontShowOnScreen); } QPrintDialog::QPrintDialog(QWidget *parent) : QAbstractPrintDialog(*(new QPrintDialogPrivate), 0, parent) { setAttribute(Qt::WA_DontShowOnScreen); } QPrintDialog::~QPrintDialog() { hide(); } int QPrintDialog::exec() { Q_D(QPrintDialog); QDialog::setVisible(true); QMacAutoReleasePool pool; d->openCocoaPrintPanel(Qt::ApplicationModal); d->closeCocoaPrintPanel(); QDialog::setVisible(false); return result(); } /*! \reimp */ void QPrintDialog::setVisible(bool visible) { Q_D(QPrintDialog); bool isCurrentlyVisible = (d->printPanel != 0); if (!visible == !isCurrentlyVisible) return; if (d->printer->outputFormat() != QPrinter::NativeFormat) return; QDialog::setVisible(visible); if (visible) { Qt::WindowModality modality = windowModality(); if (modality == Qt::NonModal) { // NSPrintPanels can only be modal, so we must pick a type modality = parentWidget() ? Qt::WindowModal : Qt::ApplicationModal; } d->openCocoaPrintPanel(modality); return; } else { if (d->printPanel) { d->closeCocoaPrintPanel(); return; } } } QT_END_NAMESPACE #include "moc_qprintdialog.cpp"