/*
   SPDX-FileCopyrightText: 2003-2007 Clarence Dang <dang@kde.org>
   SPDX-FileCopyrightText: 2007 John Layt <john@layt.net>
   SPDX-FileCopyrightText: 2007, 2011, 2015 Martin Koller <kollix@aon.at>

   SPDX-License-Identifier: BSD-2-Clause
*/

#include "kpMainWindow.h"
#include "kpMainWindowPrivate.h"

#include <QAction>
#include <QApplication>
#include <QCheckBox>
#include <QDialog>
#include <QDialogButtonBox>
#include <QFileDialog>
#include <QImageReader>
#include <QImageWriter>
#include <QLabel>
#include <QMimeDatabase>
#include <QPainter>
#include <QPixmap>
#include <QPrintDialog>
#include <QPrintPreviewDialog>
#include <QPrinter>
#include <QScreen>
#include <QSize>
#include <QSpinBox>
#include <QTimer>
#include <QVBoxLayout>

#include <KActionCollection>
#include <KConfigGroup>
#include <KEMailClientLauncherJob>
#include <KFileCustomDialog>
#include <KLocalization>
#include <KLocalizedString>
#include <KMessageBox>
#include <KRecentFilesAction>
#include <KSharedConfig>
#include <KStandardAction>
#include <KStandardShortcut>
#include <kwidgetsaddons_version.h>

#include "commands/imagelib/kpDocumentMetaInfoCommand.h"
#include "commands/kpCommandHistory.h"
#include "dialogs/imagelib/kpDocumentMetaInfoDialog.h"
#include "document/kpDocument.h"
#include "kpDefs.h"
#include "kpLogCategories.h"
#include "pixmapfx/kpPixmapFX.h"
#include "views/kpView.h"
#include "views/manager/kpViewManager.h"
#include "widgets/kpDocumentSaveOptionsWidget.h"
#include "widgets/kpPrintDialogPage.h"

#if HAVE_KSANE
#include "../scan/sanedialog.h"
#endif // HAVE_KSANE

// private
void kpMainWindow::setupFileMenuActions()
{
#if DEBUG_KP_MAIN_WINDOW
    qCDebug(kpLogMainWindow) << "kpMainWindow::setupFileMenuActions()";
#endif
    KActionCollection *ac = actionCollection();

    d->actionNew = KStandardAction::openNew(this, SLOT(slotNew()), ac);
    d->actionOpen = KStandardAction::open(this, SLOT(slotOpen()), ac);

    d->actionOpenRecent = KStandardAction::openRecent(this, &kpMainWindow::slotOpenRecent, ac);
    connect(d->actionOpenRecent, &KRecentFilesAction::recentListCleared, this, &kpMainWindow::slotRecentListCleared);
    d->actionOpenRecent->loadEntries(KSharedConfig::openConfig()->group(QStringLiteral(kpSettingsGroupRecentFiles)));
#if DEBUG_KP_MAIN_WINDOW
    qCDebug(kpLogMainWindow) << "\trecent URLs=" << d->actionOpenRecent->items();
#endif

    d->actionSave = KStandardAction::save(this, SLOT(slotSave()), ac);
    d->actionSaveAs = KStandardAction::saveAs(this, SLOT(slotSaveAs()), ac);

    d->actionExport = ac->addAction(QStringLiteral("file_export"));
    d->actionExport->setText(i18n("E&xport..."));
    d->actionExport->setIcon(QIcon::fromTheme(QStringLiteral("document-export")));
    d->actionExport->setToolTip(i18nc("@info:tooltip", "Export to another file"));
    d->actionExport->setWhatsThis(xi18nc("@info:whatsthis",
                                         "This saves the document with a different name, similar to <interface>Save As</interface>, "
                                         "but the current document is not changed.<nl/>"
                                         "This way you can keep the last-used save folder independent from the folder of the current document."));
    connect(d->actionExport, &QAction::triggered, this, &kpMainWindow::slotExport);

    d->actionScan = ac->addAction(QStringLiteral("file_scan"));
    d->actionScan->setText(i18n("Scan..."));
    d->actionScan->setIcon(QIcon::fromTheme(QStringLiteral("scanner")));
#if HAVE_KSANE
    connect(d->actionScan, &QAction::triggered, this, &kpMainWindow::slotScan);
#else
    d->actionScan->setEnabled(false);
#endif // HAVE_KSANE

    d->actionScreenshot = ac->addAction(QStringLiteral("file_screenshot"));
    d->actionScreenshot->setText(i18n("Acquire Screenshot"));
    connect(d->actionScreenshot, &QAction::triggered, this, &kpMainWindow::slotScreenshot);

    d->actionProperties = ac->addAction(QStringLiteral("file_properties"));
    d->actionProperties->setText(i18n("Properties"));
    d->actionProperties->setIcon(QIcon::fromTheme(QStringLiteral("document-properties")));
    connect(d->actionProperties, &QAction::triggered, this, &kpMainWindow::slotProperties);

    // d->actionRevert = KStandardAction::revert (this, SLOT (slotRevert()), ac);
    d->actionReload = ac->addAction(QStringLiteral("file_revert"));
    d->actionReload->setText(i18n("Reloa&d"));
    d->actionReload->setIcon(QIcon::fromTheme(QStringLiteral("view-refresh")));
    connect(d->actionReload, &QAction::triggered, this, &kpMainWindow::slotReload);
    ac->setDefaultShortcuts(d->actionReload, KStandardShortcut::reload());
    slotEnableReload();

    d->actionPrint = KStandardAction::print(this, SLOT(slotPrint()), ac);
    d->actionPrintPreview = KStandardAction::printPreview(this, SLOT(slotPrintPreview()), ac);

    d->actionMail = KStandardAction::mail(this, SLOT(slotMail()), ac);

    d->actionClose = KStandardAction::close(this, SLOT(slotClose()), ac);
    d->actionQuit = KStandardAction::quit(this, SLOT(slotQuit()), ac);

    d->scanDialog = nullptr;

    enableFileMenuDocumentActions(false);
}

//---------------------------------------------------------------------

// private
void kpMainWindow::enableFileMenuDocumentActions(bool enable)
{
    // d->actionNew
    // d->actionOpen

    // d->actionOpenRecent

    d->actionSave->setEnabled(enable);
    d->actionSaveAs->setEnabled(enable);

    d->actionExport->setEnabled(enable);

    // d->actionScan

    d->actionProperties->setEnabled(enable);

    // d->actionReload

    d->actionPrint->setEnabled(enable);
    d->actionPrintPreview->setEnabled(enable);

    d->actionMail->setEnabled(enable);

    d->actionClose->setEnabled(enable);
    // d->actionQuit->setEnabled (enable);
}

//---------------------------------------------------------------------

// private
void kpMainWindow::addRecentURL(const QUrl &url_)
{
    // HACK: KRecentFilesAction::loadEntries() clears the KRecentFilesAction::d->urls
    //       map.
    //
    //       So afterwards, the URL ref, our method is given, points to an
    //       element in this now-cleared map (see KRecentFilesAction::urlSelected(QAction*)).
    //       Accessing it would result in a crash.
    //
    //       To avoid the crash, make a copy of it before calling
    //       loadEntries() and use this copy, instead of the to-be-dangling
    //       ref.
    const QUrl url = url_; // DO NOT MAKE IT A REFERENCE, THE CALL BELOW TO loadEntries DESTROYS url_

#if DEBUG_KP_MAIN_WINDOW
    qCDebug(kpLogMainWindow) << "kpMainWindow::addRecentURL(" << url << ")";
#endif
    if (url.isEmpty())
        return;

    KSharedConfig::Ptr cfg = KSharedConfig::openConfig();

    // KConfig::readEntry() does not actually reread from disk, hence doesn't
    // realize what other processes have done e.g. Settings / Show Path
    cfg->reparseConfiguration();

#if DEBUG_KP_MAIN_WINDOW
    qCDebug(kpLogMainWindow) << "\trecent URLs=" << d->actionOpenRecent->items();
#endif
    // HACK: Something might have changed interprocess.
    // If we could PROPAGATE: interprocess, then this wouldn't be required.
    d->actionOpenRecent->loadEntries(cfg->group(QStringLiteral(kpSettingsGroupRecentFiles)));
#if DEBUG_KP_MAIN_WINDOW
    qCDebug(kpLogMainWindow) << "\tafter loading config=" << d->actionOpenRecent->items();
#endif

    d->actionOpenRecent->addUrl(url);

    d->actionOpenRecent->saveEntries(cfg->group(QStringLiteral(kpSettingsGroupRecentFiles)));
    cfg->sync();

#if DEBUG_KP_MAIN_WINDOW
    qCDebug(kpLogMainWindow) << "\tnew recent URLs=" << d->actionOpenRecent->items();
#endif

    // TODO: PROPAGATE: interprocess
    // TODO: Is this loop safe since a KMainWindow later along in the list,
    //       could be closed as the code in the body almost certainly re-enters
    //       the event loop?  Problem for KDE 3 as well, I think.
    for (auto *kmw : KMainWindow::memberList()) {
        Q_ASSERT(dynamic_cast<kpMainWindow *>(kmw));
        auto *mw = dynamic_cast<kpMainWindow *>(kmw);

#if DEBUG_KP_MAIN_WINDOW
        qCDebug(kpLogMainWindow) << "\t\tmw=" << mw;
#endif

        if (mw != this) {
            // WARNING: Do not use KRecentFilesAction::setItems()
            //          - it does not work since only its superclass,
            //          KSelectAction, implements setItems() and can't
            //          update KRecentFilesAction's URL list.

            // Avoid URL memory leak in KRecentFilesAction::loadEntries().
            mw->d->actionOpenRecent->clear();

            mw->d->actionOpenRecent->loadEntries(cfg->group(QStringLiteral(kpSettingsGroupRecentFiles)));
#if DEBUG_KP_MAIN_WINDOW
            qCDebug(kpLogMainWindow) << "\t\t\tcheck recent URLs=" << mw->d->actionOpenRecent->items();
#endif
        }
    }
}

//---------------------------------------------------------------------

// private slot
// TODO: Disable action if
//       (d->configOpenImagesInSameWindow && d->document && d->document->isEmpty())
//       as it does nothing if this is true.
void kpMainWindow::slotNew()
{
    toolEndShape();

    if (d->document && !d->configOpenImagesInSameWindow) {
        // A document -- empty or otherwise -- is open.
        // Force open a new window.  In contrast, open() might not open
        // a new window in this case.
        auto *win = new kpMainWindow();
        win->show();
    } else {
        open(QUrl(), true /*create an empty doc*/);
    }
}

//---------------------------------------------------------------------

// private
QSize kpMainWindow::defaultDocSize() const
{
    // KConfig::readEntry() does not actually reread from disk, hence doesn't
    // realize what other processes have done e.g. Settings / Show Path
    KSharedConfig::openConfig()->reparseConfiguration();

    KConfigGroup cfg(KSharedConfig::openConfig(), QStringLiteral(kpSettingsGroupGeneral));

    QSize docSize = cfg.readEntry(kpSettingLastDocSize, QSize());

    if (docSize.isEmpty()) {
        docSize = QSize(400, 300);
    } else {
        // Don't get too big or you'll thrash (or even lock up) the computer
        // just by opening a window
        docSize = QSize(qMin(2048, docSize.width()), qMin(2048, docSize.height()));
    }

    return docSize;
}

//---------------------------------------------------------------------

// private
void kpMainWindow::saveDefaultDocSize(const QSize &size)
{
#if DEBUG_KP_MAIN_WINDOW
    qCDebug(kpLogMainWindow) << "\tCONFIG: saving Last Doc Size = " << size;
#endif

    KConfigGroup cfg(KSharedConfig::openConfig(), QStringLiteral(kpSettingsGroupGeneral));

    cfg.writeEntry(kpSettingLastDocSize, size);
    cfg.sync();
}

//---------------------------------------------------------------------

// private
bool kpMainWindow::shouldOpen()
{
    if (d->configOpenImagesInSameWindow) {
#if DEBUG_KP_MAIN_WINDOW
        qCDebug(kpLogMainWindow) << "\topenImagesInSameWindow";
#endif
        // (this brings up a dialog and might save the current doc)
        if (!queryCloseDocument()) {
#if DEBUG_KP_MAIN_WINDOW
            qCDebug(kpLogMainWindow) << "\t\tqueryCloseDocument() aborts open";
#endif
            return false;
        }
    }

    return true;
}

//---------------------------------------------------------------------

// private
void kpMainWindow::setDocumentChoosingWindow(kpDocument *doc)
{
    // Want new window?
    if (d->document && !d->document->isEmpty() && !d->configOpenImagesInSameWindow) {
        // Send doc to new window.
        auto *win = new kpMainWindow(doc);
        win->show();
    } else {
        // (sets up views, doc signals)
        setDocument(doc);
    }
}

//---------------------------------------------------------------------

// private
kpDocument *kpMainWindow::openInternal(const QUrl &url, const QSize &fallbackDocSize, bool newDocSameNameIfNotExist)
{
    // If using OpenImagesInSameWindow mode, ask whether to close the
    // current document.
    if (!shouldOpen())
        return nullptr;

    // Create/open doc.
    auto *newDoc = new kpDocument(fallbackDocSize.width(), fallbackDocSize.height(), documentEnvironment());

    if (!newDoc->open(url, newDocSameNameIfNotExist)) {
#if DEBUG_KP_MAIN_WINDOW
        qCDebug(kpLogMainWindow) << "\topen failed";
#endif
        delete newDoc;
        return nullptr;
    }

#if DEBUG_KP_MAIN_WINDOW
    qCDebug(kpLogMainWindow) << "\topen OK";
#endif
    // Send document to current or new window.
    setDocumentChoosingWindow(newDoc);

    return newDoc;
}

//---------------------------------------------------------------------

// private
bool kpMainWindow::open(const QUrl &url, bool newDocSameNameIfNotExist)
{
#if DEBUG_KP_MAIN_WINDOW
    qCDebug(kpLogMainWindow) << "kpMainWindow::open(" << url << ",newDocSameNameIfNotExist=" << newDocSameNameIfNotExist << ")";
#endif

    kpDocument *newDoc = openInternal(url, defaultDocSize(), newDocSameNameIfNotExist);
    if (newDoc) {
        if (newDoc->isFromExistingURL())
            addRecentURL(url);
        return true;
    }

    return false;
}

//---------------------------------------------------------------------

// private
QList<QUrl> kpMainWindow::askForOpenURLs(const QString &caption, bool allowMultipleURLs)
{
    QMimeDatabase db;
    QStringList filterList;
    QString filter;
    for (const auto &type : QImageReader::supportedMimeTypes()) {
        if (!filter.isEmpty()) {
            filter += QLatin1Char(' ');
        }

        QMimeType mime(db.mimeTypeForName(QString::fromLatin1(type)));
        if (mime.isValid()) {
            QString glob = mime.globPatterns().join(QLatin1Char(' '));

            filter += glob;

            // I want to show the mime comment AND the file glob pattern,
            // but to avoid that the "All Supported Files" entry shows ALL glob patterns,
            // I must add the pattern here a second time so that QFileDialog::HideNameFilterDetails
            // can hide the first pattern and I still see the second one
            filterList << mime.comment() + QStringLiteral(" (%1)(%2)").arg(glob).arg(glob);
        }
    }

    filterList.prepend(i18n("All Supported Files (%1)", filter));

    QFileDialog fd(this);
    fd.setNameFilters(filterList);
    fd.setOption(QFileDialog::HideNameFilterDetails);
    fd.setWindowTitle(caption);

    if (allowMultipleURLs) {
        fd.setFileMode(QFileDialog::ExistingFiles);
    }

    if (fd.exec()) {
        return fd.selectedUrls();
    }

    return {};
}

//---------------------------------------------------------------------

// private slot
void kpMainWindow::slotOpen()
{
    toolEndShape();

    const QList<QUrl> urls = askForOpenURLs(i18nc("@title:window", "Open Image"));

    for (const auto &url : urls) {
        open(url);
    }
}

//---------------------------------------------------------------------

// private slot
void kpMainWindow::slotOpenRecent(const QUrl &url)
{
#if DEBUG_KP_MAIN_WINDOW
    qCDebug(kpLogMainWindow) << "kpMainWindow::slotOpenRecent(" << url << ")";
    qCDebug(kpLogMainWindow) << "\titems=" << d->actionOpenRecent->items();
#endif

    toolEndShape();

    open(url);

    // If the open is successful, addRecentURL() would have bubbled up the
    // URL in the File / Open Recent action.  As a side effect, the URL is
    // deselected.
    //
    // If the open fails, we should deselect the URL:
    //
    // 1. for consistency
    //
    // 2. because it has not been opened.
    //
    d->actionOpenRecent->setCurrentItem(-1);
}

//---------------------------------------------------------------------

void kpMainWindow::slotRecentListCleared()
{
    d->actionOpenRecent->saveEntries(KSharedConfig::openConfig()->group(QStringLiteral(kpSettingsGroupRecentFiles)));
}

//---------------------------------------------------------------------

#if HAVE_KSANE
// private slot
void kpMainWindow::slotScan()
{
#if DEBUG_KP_MAIN_WINDOW
    qCDebug(kpLogMainWindow) << "kpMainWindow::slotScan() scanDialog=" << d->scanDialog;
#endif

    toolEndShape();

    if (!d->scanDialog) {
        // Create scan dialog
        d->scanDialog = new SaneDialog(this);

        // No scanning support (kdegraphics/libkscan) installed?
        if (!d->scanDialog) {
            KMessageBox::error(this, i18n("Failed to open scanning dialog."), i18nc("@title:window", "Scanning Failed"));
            return;
        }

#if DEBUG_KP_MAIN_WINDOW
        qCDebug(kpLogMainWindow) << "\tcreated scanDialog=" << d->scanDialog;
#endif
        connect(d->scanDialog, &SaneDialog::finalImage, this, &kpMainWindow::slotScanned);
    }

    // If using OpenImagesInSameWindow mode, ask whether to close the
    // current document.
    //
    // Do this after scan support is detected.  Because if it's not, what
    // would be the point of closing the document?
    //
    // Ideally, we would do this after the user presses "Final Scan" in
    // the scan dialog and before the scan begins (if the user wants to
    // cancel the scan operation, it would be annoying to offer this choice
    // only after the slow scan is completed) but the KScanDialog API does
    // not allow this.  So we settle for doing this before any
    // scan dialogs are shown.  We don't do this between KScanDialog::setup()
    // and KScanDialog::exec() as it could be confusing alternating between
    // scanning and KolourPaint dialogs.
    if (!shouldOpen()) {
        return;
    }

#if DEBUG_KP_MAIN_WINDOW
    qCDebug(kpLogMainWindow) << "\tcalling setup";
#endif
    // Bring up dialog to select scan device.
    // If there is no scanner, we find that this does not bring up a dialog
    // but still returns true.
    if (d->scanDialog->setup()) {
#if DEBUG_KP_MAIN_WINDOW
        qCDebug(kpLogMainWindow) << "\t\tOK - showing dialog";
#endif
        // Called only if scanner configured/available.
        //
        // In reality, this seems to be called even if you press "Cancel" in
        // the KScanDialog::setup() dialog!
        //
        // We use exec() to make sure it's modal.  show() seems to work too
        // but better safe than sorry.
        d->scanDialog->exec();
    } else {
        // Have never seen this code path execute even if "Cancel" is pressed.
#if DEBUG_KP_MAIN_WINDOW
        qCDebug(kpLogMainWindow) << "\t\tFAIL";
#endif
    }
}

//---------------------------------------------------------------------

// private slot
void kpMainWindow::slotScanned(const QImage &image, int)
{
#if DEBUG_KP_MAIN_WINDOW
    qCDebug(kpLogMainWindow) << "kpMainWindow::slotScanned() image.rect=" << image.rect();
#endif

#if DEBUG_KP_MAIN_WINDOW
    qCDebug(kpLogMainWindow) << "\thiding dialog";
#endif
    // (KScanDialog does not close itself after a scan is made)
    //
    // Close the dialog, first thing:
    //
    // 1. This means that any dialogs we bring up won't be nested on top.
    //
    // 2. We don't want to return from this method but forget to close
    //    the dialog.  So do it before anything else.
    d->scanDialog->hide();

    // (just in case there's some drawing between slotScan() exiting and
    //  us being called)
    toolEndShape();

    // TODO: Maybe this code should be moved into kpdocument.cpp -
    //       since it resembles the responsibilities of kpDocument::open().

    kpDocumentSaveOptions saveOptions;
    kpDocumentMetaInfo metaInfo;

    kpDocument::getDataFromImage(image, saveOptions, metaInfo);

    // Create document from image and meta info.
    auto *doc = new kpDocument(image.width(), image.height(), documentEnvironment());
    doc->setImage(image);
    doc->setSaveOptions(saveOptions);
    doc->setMetaInfo(metaInfo);

    // Send document to current or new window.
    setDocumentChoosingWindow(doc);
}
#endif // HAVE_KSANE

//---------------------------------------------------------------------

void kpMainWindow::slotScreenshot()
{
    toolEndShape();

    auto *dialog = new QDialog(this);
    auto *buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, dialog);
    connect(buttons, &QDialogButtonBox::accepted, dialog, &QDialog::accept);
    connect(buttons, &QDialogButtonBox::rejected, dialog, &QDialog::reject);

    auto *label = new QLabel(i18n("Snapshot Delay"));
    auto *seconds = new QSpinBox;
    seconds->setRange(0, 99);
    KLocalization::setupSpinBoxFormatString(seconds, ki18np("%v second", "%v seconds"));
    seconds->setSpecialValueText(i18n("No delay"));

    auto *hideWindow = new QCheckBox(i18n("Hide Main Window"));
    hideWindow->setChecked(true);

    auto *vbox = new QVBoxLayout(dialog);
    vbox->addWidget(label);
    vbox->addWidget(seconds);
    vbox->addWidget(hideWindow);
    vbox->addWidget(buttons);

    if (dialog->exec() == QDialog::Rejected) {
        delete dialog;
        return;
    }

    if (hideWindow->isChecked()) {
        hide();
    }

    // at least 1 seconds to make sure the window is hidden and the hide effect already stopped
    QTimer::singleShot((seconds->value() + 1) * 1000, this, &kpMainWindow::slotMakeScreenshot);

    delete dialog;
}

//---------------------------------------------------------------------

void kpMainWindow::slotMakeScreenshot()
{
    QCoreApplication::processEvents();
    QPixmap pixmap = QGuiApplication::primaryScreen()->grabWindow(0 /* entire screen*/);

    auto *doc = new kpDocument(pixmap.width(), pixmap.height(), documentEnvironment());
    doc->setImage(pixmap.toImage());

    // Send document to current or new window.
    setDocumentChoosingWindow(doc);

    show(); // in case we hid the mainwindow, show it again
}

//---------------------------------------------------------------------

// private slot
void kpMainWindow::slotProperties()
{
    toolEndShape();

    kpDocumentMetaInfoDialog dialog(document()->metaInfo(), this);

    if (dialog.exec() && !dialog.isNoOp()) {
        commandHistory()->addCommand(
            new kpDocumentMetaInfoCommand(i18n("Document Properties"), dialog.metaInfo() /*new*/, *document()->metaInfo() /*old*/, commandEnvironment()));
    }
}

//---------------------------------------------------------------------

// private slot
bool kpMainWindow::save(bool localOnly)
{
    if (d->document->url().isEmpty() || !QImageWriter::supportedMimeTypes().contains(d->document->saveOptions()->mimeType().toLatin1()) ||
        // SYNC: kpDocument::getPixmapFromFile() can't determine quality
        //       from file so it has been set initially to an invalid value.
        (d->document->saveOptions()->mimeTypeHasConfigurableQuality() && d->document->saveOptions()->qualityIsInvalid())
        || (localOnly && !d->document->url().isLocalFile())) {
        return saveAs(localOnly);
    }

    if (d->document->save(!d->document->savedAtLeastOnceBefore() /*lossy prompt*/)) {
        addRecentURL(d->document->url());
        return true;
    }

    return false;
}

//---------------------------------------------------------------------

// private slot
bool kpMainWindow::slotSave()
{
    toolEndShape();

    return save();
}

//---------------------------------------------------------------------

// private
QUrl kpMainWindow::askForSaveURL(const QString &caption,
                                 const QString &startURL,
                                 const kpImage &imageToBeSaved,
                                 const kpDocumentSaveOptions &startSaveOptions,
                                 const kpDocumentMetaInfo &docMetaInfo,
                                 const QString &forcedSaveOptionsGroup,
                                 bool localOnly,
                                 kpDocumentSaveOptions *chosenSaveOptions,
                                 bool isSavingForFirstTime,
                                 bool *allowLossyPrompt)
{
#if DEBUG_KP_MAIN_WINDOW
    qCDebug(kpLogMainWindow) << "kpMainWindow::askForURL() startURL=" << startURL;
    startSaveOptions.printDebug("\tstartSaveOptions");
#endif

    bool reparsedConfiguration = false;

    // KConfig::readEntry() does not actually reread from disk, hence doesn't
    // realize what other processes have done e.g. Settings / Show Path
    // so reparseConfiguration() must be called
#define SETUP_READ_CFG()                                                                                                                                       \
    if (!reparsedConfiguration) {                                                                                                                              \
        KSharedConfig::openConfig()->reparseConfiguration();                                                                                                   \
        reparsedConfiguration = true;                                                                                                                          \
    }                                                                                                                                                          \
                                                                                                                                                               \
    KConfigGroup cfg(KSharedConfig::openConfig(), forcedSaveOptionsGroup);

    if (chosenSaveOptions) {
        *chosenSaveOptions = kpDocumentSaveOptions();
    }

    if (allowLossyPrompt) {
        *allowLossyPrompt = true; // play it safe for now
    }

    kpDocumentSaveOptions fdSaveOptions = startSaveOptions;

    QStringList mimeTypes;
    for (const auto &type : QImageWriter::supportedMimeTypes()) {
        mimeTypes << QString::fromLatin1(type);
    }
#if DEBUG_KP_MAIN_WINDOW
    QStringList sortedMimeTypes = mimeTypes;
    sortedMimeTypes.sort();
    qCDebug(kpLogMainWindow) << "\tmimeTypes=" << mimeTypes << "\tsortedMimeTypes=" << sortedMimeTypes;
#endif
    if (mimeTypes.isEmpty()) {
        qCCritical(kpLogMainWindow) << "No output mimetypes!";
        return {};
    }

#define MIME_TYPE_IS_VALID() (!fdSaveOptions.mimeTypeIsInvalid() && mimeTypes.contains(fdSaveOptions.mimeType()))
    if (!MIME_TYPE_IS_VALID()) {
#if DEBUG_KP_MAIN_WINDOW
        qCDebug(kpLogMainWindow) << "\tmimeType=" << fdSaveOptions.mimeType() << " not valid, get default";
#endif

        SETUP_READ_CFG();

        fdSaveOptions.setMimeType(kpDocumentSaveOptions::defaultMimeType(cfg));

        if (!MIME_TYPE_IS_VALID()) {
#if DEBUG_KP_MAIN_WINDOW
            qCDebug(kpLogMainWindow) << "\tmimeType=" << fdSaveOptions.mimeType() << " not valid, get hardcoded";
#endif
            if (mimeTypes.contains(QLatin1String("image/png"))) {
                fdSaveOptions.setMimeType(QStringLiteral("image/png"));
            } else if (mimeTypes.contains(QLatin1String("image/bmp"))) {
                fdSaveOptions.setMimeType(QStringLiteral("image/bmp"));
            } else {
                fdSaveOptions.setMimeType(mimeTypes.first());
            }
        }
    }
#undef MIME_TYPE_IS_VALID

    if (fdSaveOptions.colorDepthIsInvalid()) {
        SETUP_READ_CFG();

        fdSaveOptions.setColorDepth(kpDocumentSaveOptions::defaultColorDepth(cfg));
        fdSaveOptions.setDither(kpDocumentSaveOptions::defaultDither(cfg));
    }

    if (fdSaveOptions.qualityIsInvalid()) {
        SETUP_READ_CFG();

        fdSaveOptions.setQuality(kpDocumentSaveOptions::defaultQuality(cfg));
    }
#if DEBUG_KP_MAIN_WINDOW
    fdSaveOptions.printDebug("\tcorrected saveOptions passed to fileDialog");
#endif

    auto *saveOptionsWidget = new kpDocumentSaveOptionsWidget(imageToBeSaved, fdSaveOptions, docMetaInfo, this);

    KFileCustomDialog fd(QUrl(startURL), this);
    fd.setOperationMode(KFileWidget::Saving);
    fd.setWindowTitle(caption);
    fd.setCustomWidget(saveOptionsWidget);
    KFileWidget *fw = fd.fileWidget();
    fw->setConfirmOverwrite(true);
    fw->setFilters(KFileFilter::fromMimeTypes(mimeTypes), KFileFilter::fromMimeType(fdSaveOptions.mimeType()));
    if (localOnly) {
        fw->setMode(KFile::File | KFile::LocalOnly);
    }

    saveOptionsWidget->setVisualParent(&fd);

    connect(fw, &KFileWidget::filterChanged, saveOptionsWidget, [saveOptionsWidget](const KFileFilter &filter) {
        const QStringList patterns = filter.mimePatterns();
        if (!patterns.isEmpty()) {
            saveOptionsWidget->setMimeType(patterns.first());
        }
    });

    if (fd.exec() == QDialog::Accepted) {
        kpDocumentSaveOptions newSaveOptions = saveOptionsWidget->documentSaveOptions();
#if DEBUG_KP_MAIN_WINDOW
        newSaveOptions.printDebug("\tnewSaveOptions");
#endif

        KConfigGroup cfg(KSharedConfig::openConfig(), forcedSaveOptionsGroup);

        // Save options user forced - probably want to use them in future
        kpDocumentSaveOptions::saveDefaultDifferences(cfg, fdSaveOptions, newSaveOptions);
        cfg.sync();

        if (chosenSaveOptions) {
            *chosenSaveOptions = newSaveOptions;
        }

        const QList<QUrl> selectedUrls = fw->selectedUrls();
        if (selectedUrls.isEmpty()) { // shouldn't happen
            return {};
        }
        const QUrl selectedUrl = selectedUrls.at(0);

        if (allowLossyPrompt) {
            // SYNC: kpDocumentSaveOptions elements - everything except quality
            //       (one quality setting is "just as lossy" as another so no
            //        need to continually warn due to quality change)
            *allowLossyPrompt = (isSavingForFirstTime || selectedUrl != QUrl(startURL) || newSaveOptions.mimeType() != startSaveOptions.mimeType()
                                 || newSaveOptions.colorDepth() != startSaveOptions.colorDepth() || newSaveOptions.dither() != startSaveOptions.dither());
#if DEBUG_KP_MAIN_WINDOW
            qCDebug(kpLogMainWindow) << "\tallowLossyPrompt=" << *allowLossyPrompt;
#endif
        }

#if DEBUG_KP_MAIN_WINDOW
        qCDebug(kpLogMainWindow) << "\tselectedUrl=" << selectedUrl;
#endif
        return selectedUrl;
    }

    return {};
#undef SETUP_READ_CFG
}

//---------------------------------------------------------------------

// private slot
bool kpMainWindow::saveAs(bool localOnly)
{
    kpDocumentSaveOptions chosenSaveOptions;
    bool allowLossyPrompt;
    QUrl chosenURL = askForSaveURL(i18nc("@title:window", "Save Image As"),
                                   d->document->url().url(),
                                   d->document->imageWithSelection(),
                                   *d->document->saveOptions(),
                                   *d->document->metaInfo(),
                                   QLatin1String(kpSettingsGroupFileSaveAs),
                                   localOnly,
                                   &chosenSaveOptions,
                                   !d->document->savedAtLeastOnceBefore(),
                                   &allowLossyPrompt);

    if (chosenURL.isEmpty()) {
        return false;
    }

    if (!d->document->saveAs(chosenURL, chosenSaveOptions, allowLossyPrompt)) {
        return false;
    }

    addRecentURL(chosenURL);

    return true;
}

//---------------------------------------------------------------------

// private slot
bool kpMainWindow::slotSaveAs()
{
    toolEndShape();

    return saveAs();
}

//---------------------------------------------------------------------

// private slot
bool kpMainWindow::slotExport()
{
    toolEndShape();

    kpDocumentSaveOptions chosenSaveOptions;
    bool allowLossyPrompt;
    QUrl chosenURL = askForSaveURL(i18nc("@title:window", "Export"),
                                   d->lastExportURL.url(),
                                   d->document->imageWithSelection(),
                                   d->lastExportSaveOptions,
                                   *d->document->metaInfo(),
                                   QLatin1String(kpSettingsGroupFileExport),
                                   false /*allow remote files*/,
                                   &chosenSaveOptions,
                                   d->exportFirstTime,
                                   &allowLossyPrompt);

    if (chosenURL.isEmpty()) {
        return false;
    }

    if (!kpDocument::savePixmapToFile(d->document->imageWithSelection(), chosenURL, chosenSaveOptions, *d->document->metaInfo(), allowLossyPrompt, this)) {
        return false;
    }

    addRecentURL(chosenURL);

    d->lastExportURL = chosenURL;
    d->lastExportSaveOptions = chosenSaveOptions;

    d->exportFirstTime = false;

    return true;
}

//---------------------------------------------------------------------

// private slot
void kpMainWindow::slotEnableReload()
{
    d->actionReload->setEnabled(d->document);
}

//---------------------------------------------------------------------

// private slot
bool kpMainWindow::slotReload()
{
    toolEndShape();

    Q_ASSERT(d->document);

    QUrl oldURL = d->document->url();

    if (d->document->isModified()) {
        int result = KMessageBox::Cancel;

        if (d->document->isFromExistingURL() && !oldURL.isEmpty()) {
            result = KMessageBox::warningContinueCancel(this,
                                                        i18n("The document \"%1\" has been modified.\n"
                                                             "Reloading will lose all changes since you last saved it.\n"
                                                             "Are you sure?",
                                                             d->document->prettyFilename()),
                                                        QString() /*caption*/,
                                                        KGuiItem(i18n("&Reload")));
        } else {
            result = KMessageBox::warningContinueCancel(this,
                                                        i18n("The document \"%1\" has been modified.\n"
                                                             "Reloading will lose all changes.\n"
                                                             "Are you sure?",
                                                             d->document->prettyFilename()),
                                                        QString() /*caption*/,
                                                        KGuiItem(i18n("&Reload")));
        }

        if (result != KMessageBox::Continue) {
            return false;
        }
    }

    kpDocument *doc = nullptr;

    // If it's _supposed to_ come from an existing URL or it actually exists
    if (d->document->isFromExistingURL() || d->document->urlExists(oldURL)) {
#if DEBUG_KP_MAIN_WINDOW
        qCDebug(kpLogMainWindow) << "kpMainWindow::slotReload() reloading from disk!";
#endif

        doc = new kpDocument(1, 1, documentEnvironment());
        if (!doc->open(oldURL)) {
            delete doc;
            doc = nullptr;
            return false;
        }

        addRecentURL(oldURL);
    } else {
#if DEBUG_KP_MAIN_WINDOW
        qCDebug(kpLogMainWindow) << "kpMainWindow::slotReload() create doc";
#endif

        doc = new kpDocument(d->document->constructorWidth(), d->document->constructorHeight(), documentEnvironment());
        doc->setURL(oldURL, false /*not from URL*/);
    }

    setDocument(doc);

    return true;
}

// private
void kpMainWindow::sendDocumentNameToPrinter(QPrinter *printer)
{
    QUrl url = d->document->url();
    if (!url.isEmpty()) {
        int dot;

        QString fileName = url.fileName();
        dot = fileName.lastIndexOf(QLatin1Char('.'));

        // file.ext but not .hidden-file?
        if (dot > 0) {
            fileName.truncate(dot);
        }

#if DEBUG_KP_MAIN_WINDOW
        qCDebug(kpLogMainWindow) << "kpMainWindow::sendDocumentNameToPrinter() fileName=" << fileName << " dir=" << url.path();
#endif
        printer->setDocName(fileName);
    }
}

//--------------------------------------------------------------------------------

void kpMainWindow::setPrinterPageOrientation(QPrinter *printer)
{
    const bool isLandscape = d->document->width() > d->document->height();
    printer->setPageOrientation(isLandscape ? QPageLayout::Landscape : QPageLayout::Portrait);
}

//--------------------------------------------------------------------------------

void kpMainWindow::sendPreviewToPrinter(QPrinter *printer)
{
    sendImageToPrinter(printer, false);
}

//--------------------------------------------------------------------------------
// private
void kpMainWindow::sendImageToPrinter(QPrinter *printer, bool showPrinterSetupDialog)
{
    // Get image to be printed.
    kpImage image = d->document->imageWithSelection();

    // Get image DPI.
    auto imageDotsPerMeterX = double(d->document->metaInfo()->dotsPerMeterX());
    auto imageDotsPerMeterY = double(d->document->metaInfo()->dotsPerMeterY());
#if DEBUG_KP_MAIN_WINDOW
    qCDebug(kpLogMainWindow) << "kpMainWindow::sendImageToPrinter() image:"
                             << " width=" << image.width() << " height=" << image.height() << " dotsPerMeterX=" << imageDotsPerMeterX
                             << " dotsPerMeterY=" << imageDotsPerMeterY;
#endif

    // Image DPI invalid (e.g. new image, could not read from file
    // or Qt3 doesn't implement DPI for JPEG)?
    if (imageDotsPerMeterX <= 0 || imageDotsPerMeterY <= 0) {
        // Even if just one DPI dimension is invalid, mutate both DPI
        // dimensions as we have no information about the intended
        // aspect ratio anyway (and other dimension likely to be invalid).

        // When rendering text onto a document, the fonts are rasterised
        // according to the screen's DPI.
        // TODO: I think we should use the image's DPI.  Technically
        //       possible?
        //
        //       So no matter what computer you draw text on, you get
        //       the same pixels.
        //
        // So we must print at the screen's DPI to get the right text size.
        //
        // Unfortunately, this means that moving to a different screen DPI
        // affects printing.  If you edited the image at a different screen
        // DPI than when you print, you get incorrect results.  Furthermore,
        // this is bogus if you don't have text in your image.  Worse still,
        // what if you have multiple screens connected to the same computer
        // with different DPIs?
        // TODO: mysteriously, someone else is setting this to 96dpi always.
        QPixmap arbitraryScreenElement(1, 1);
        const QPaintDevice *screenDevice = &arbitraryScreenElement;
        const auto dpiX = screenDevice->logicalDpiX();
        const auto dpiY = screenDevice->logicalDpiY();
#if DEBUG_KP_MAIN_WINDOW
        qCDebug(kpLogMainWindow) << "\tusing screen dpi: x=" << dpiX << " y=" << dpiY;
#endif

        imageDotsPerMeterX = dpiX * KP_INCHES_PER_METER;
        imageDotsPerMeterY = dpiY * KP_INCHES_PER_METER;
    }

    // Get page size (excluding margins).
    // Coordinate (0,0) is the X here:
    //     mmmmm
    //     mX  m
    //     m   m       m = margin
    //     m   m
    //     mmmmm
    const auto printerWidthMM = printer->widthMM();
    const auto printerHeightMM = printer->heightMM();

    auto dpiX = imageDotsPerMeterX / KP_INCHES_PER_METER;
    auto dpiY = imageDotsPerMeterY / KP_INCHES_PER_METER;

#if DEBUG_KP_MAIN_WINDOW
    qCDebug(kpLogMainWindow) << "\tprinter: widthMM=" << printerWidthMM << " heightMM=" << printerHeightMM;

    qCDebug(kpLogMainWindow) << "\timage: dpiX=" << dpiX << " dpiY=" << dpiY;
#endif

    //
    // If image doesn't fit on page at intended DPI, change the DPI.
    //

    const auto scaleDpiX = (image.width() / (printerWidthMM / KP_MILLIMETERS_PER_INCH)) / dpiX;
    const auto scaleDpiY = (image.height() / (printerHeightMM / KP_MILLIMETERS_PER_INCH)) / dpiY;
    const auto scaleDpi = qMax(scaleDpiX, scaleDpiY);

#if DEBUG_KP_MAIN_WINDOW
    qCDebug(kpLogMainWindow) << "\t\tscaleDpi: x=" << scaleDpiX << " y=" << scaleDpiY << " --> scale at " << scaleDpi << " to fit?";
#endif

    // Need to increase resolution to fit page?
    if (scaleDpi > 1.0) {
        dpiX *= scaleDpi;
        dpiY *= scaleDpi;
#if DEBUG_KP_MAIN_WINDOW
        qCDebug(kpLogMainWindow) << "\t\t\tto fit page, scaled to:"
                                 << " dpiX=" << dpiX << " dpiY=" << dpiY;
#endif
    }

    // Make sure DPIs are equal as that's all QPrinter::setResolution()
    // supports.  We do this in such a way that we only ever stretch an
    // image, to avoid losing information.  Don't antialias as the printer
    // will do that to translate our DPI to its physical resolution and
    // double-antialiasing looks bad.
    if (dpiX > dpiY) {
#if DEBUG_KP_MAIN_WINDOW
        qCDebug(kpLogMainWindow) << "\tdpiX > dpiY; stretching image height to equalise DPIs to dpiX=" << dpiX;
#endif
        kpPixmapFX::scale(&image, image.width(), qMax(1, qRound(image.height() * dpiX / dpiY)), false /*don't antialias*/);

        dpiY = dpiX;
    } else if (dpiY > dpiX) {
#if DEBUG_KP_MAIN_WINDOW
        qCDebug(kpLogMainWindow) << "\tdpiY > dpiX; stretching image width to equalise DPIs to dpiY=" << dpiY;
#endif
        kpPixmapFX::scale(&image, qMax(1, qRound(image.width() * dpiY / dpiX)), image.height(), false /*don't antialias*/);

        dpiX = dpiY;
    }

    Q_ASSERT(dpiX == dpiY);

    // QPrinter::setResolution() has to be called before QPrinter::setup().
    printer->setResolution(qMax(1, qRound(dpiX)));

    sendDocumentNameToPrinter(printer);

    if (showPrinterSetupDialog) {
        auto *optionsPage = new kpPrintDialogPage(this);
        optionsPage->setPrintImageCenteredOnPage(d->configPrintImageCenteredOnPage);

        QPrintDialog printDialog(printer, this);
        printDialog.setOptionTabs({optionsPage});
        printDialog.setWindowTitle(i18nc("@title:window", "Print Image"));

        // Display dialog.
        const bool wantToPrint = printDialog.exec();

        if (optionsPage->printImageCenteredOnPage() != d->configPrintImageCenteredOnPage) {
            // Save config option even if the dialog was cancelled.
            d->configPrintImageCenteredOnPage = optionsPage->printImageCenteredOnPage();

            KConfigGroup cfg(KSharedConfig::openConfig(), QStringLiteral(kpSettingsGroupGeneral));
            cfg.writeEntry(kpSettingPrintImageCenteredOnPage, d->configPrintImageCenteredOnPage);
            cfg.sync();
        }

        if (!wantToPrint) {
            return;
        }
    }

    // Send image to printer.
    QPainter painter;
    painter.begin(printer);

    double originX = 0, originY = 0;

    // Center image on page?
    if (d->configPrintImageCenteredOnPage) {
        originX = (printer->width() - image.width()) / 2;
        originY = (printer->height() - image.height()) / 2;
    }

    painter.drawImage(qRound(originX), qRound(originY), image);
    painter.end();
}

//---------------------------------------------------------------------

// private slot
void kpMainWindow::slotPrint()
{
    toolEndShape();

    QPrinter printer;
    setPrinterPageOrientation(&printer);

    sendImageToPrinter(&printer, true /*showPrinterSetupDialog*/);
}

//---------------------------------------------------------------------

// private slot
void kpMainWindow::slotPrintPreview()
{
    toolEndShape();

    QPrinter printer;
    setPrinterPageOrientation(&printer);
    QPrintPreviewDialog printPreview(&printer, this);
    connect(&printPreview, &QPrintPreviewDialog::paintRequested, this, &kpMainWindow::sendPreviewToPrinter);

    printPreview.exec();
}

//---------------------------------------------------------------------

// private slot
void kpMainWindow::slotMail()
{
    toolEndShape();

    if (d->document->url().isEmpty() /*no name*/ || !(d->document->isFromExistingURL() && d->document->urlExists(d->document->url()))
        || d->document->isModified() /*needs to be saved*/) {
        int result = KMessageBox::questionTwoActions(this,
                                                     i18n("You must save this image before sending it.\n"
                                                          "Do you want to save it?"),
                                                     QString(),
                                                     KStandardGuiItem::save(),
                                                     KStandardGuiItem::cancel());

        if (result == KMessageBox::ButtonCode::PrimaryAction) {
            if (!save()) {
                // save failed or aborted - don't email
                return;
            }
        } else {
            // don't want to save - don't email
            return;
        }
    }

    auto *job = new KEMailClientLauncherJob;
    job->setSubject(d->document->prettyFilename());
    job->setAttachments({d->document->url()});
    job->start();
}

//---------------------------------------------------------------------

// private
bool kpMainWindow::queryCloseDocument()
{
    toolEndShape();

    if (!d->document || !d->document->isModified()) {
        return true; // ok to close current doc
    }

    int result = KMessageBox::warningTwoActionsCancel(this,
                                                      i18n("The document \"%1\" has been modified.\n"
                                                           "Do you want to save it?",
                                                           d->document->prettyFilename()),
                                                      QString() /*caption*/,
                                                      KStandardGuiItem::save(),
                                                      KStandardGuiItem::discard());

    switch (result) {
    case KMessageBox::ButtonCode::PrimaryAction:
        return slotSave(); // close only if save succeeds
    case KMessageBox::ButtonCode::SecondaryAction:
        return true; // close without saving
    default:
        return false; // don't close current doc
    }
}

//---------------------------------------------------------------------

// private virtual [base KMainWindow]
bool kpMainWindow::queryClose()
{
#if DEBUG_KP_MAIN_WINDOW
    qCDebug(kpLogMainWindow) << "kpMainWindow::queryClose()";
#endif
    toolEndShape();

    if (!queryCloseDocument()) {
        return false;
    }

    if (!queryCloseColors()) {
        return false;
    }

    return true;
}

//---------------------------------------------------------------------

// private slot
void kpMainWindow::slotClose()
{
    toolEndShape();

    if (!queryCloseDocument()) {
        return;
    }

    setDocument(nullptr);
}

//---------------------------------------------------------------------

// private slot
void kpMainWindow::slotQuit()
{
    toolEndShape();

    close(); // will call queryClose()
}

//---------------------------------------------------------------------
