File indexing completed on 2024-04-21 15:12:05

0001 /************************************************************************
0002  *                                  *
0003  *  This file is part of Kooka, a scanning/OCR application using    *
0004  *  Qt <http://www.qt.io> and KDE Frameworks <http://www.kde.org>.  *
0005  *                                  *
0006  *  Copyright (C) 1999-2016 Klaas Freitag <freitag@suse.de>     *
0007  *                          Jonathan Marten <jjm@keelhaul.me.uk>    *
0008  *                                  *
0009  *  Kooka is free software; you can redistribute it and/or modify it    *
0010  *  under the terms of the GNU Library General Public License as    *
0011  *  published by the Free Software Foundation and appearing in the  *
0012  *  file COPYING included in the packaging of this file;  either    *
0013  *  version 2 of the License, or (at your option) any later version.    *
0014  *                                  *
0015  *  As a special exception, permission is given to link this program    *
0016  *  with any version of the KADMOS OCR/ICR engine (a product of     *
0017  *  reRecognition GmbH, Kreuzlingen), and distribute the resulting  *
0018  *  executable without including the source code for KADMOS in the  *
0019  *  source distribution.                        *
0020  *                                  *
0021  *  This program is distributed in the hope that it will be useful, *
0022  *  but WITHOUT ANY WARRANTY; without even the implied warranty of  *
0023  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the   *
0024  *  GNU General Public License for more details.            *
0025  *                                  *
0026  *  You should have received a copy of the GNU General Public       *
0027  *  License along with this program;  see the file COPYING.  If     *
0028  *  not, see <http://www.gnu.org/licenses/>.                *
0029  *                                  *
0030  ************************************************************************/
0031 
0032 #include "kookaview.h"
0033 
0034 #include <kio_version.h>
0035 #if KIO_VERSION >= QT_VERSION_CHECK(5, 69, 0)
0036 #define HAVE_KIO_APPLICATIONLAUNCHERJOB
0037 #endif
0038 
0039 #include <qlabel.h>
0040 #include <qlayout.h>
0041 #include <qsplitter.h>
0042 #include <qimage.h>
0043 #include <qapplication.h>
0044 #include <qicon.h>
0045 #include <qaction.h>
0046 #include <qmenu.h>
0047 #include <qprintdialog.h>
0048 #include <qprinter.h>
0049 #include <qfiledialog.h>
0050 
0051 #include <kapplicationtrader.h>
0052 #include <klocalizedstring.h>
0053 #include <kmessagebox.h>
0054 #include <kled.h>
0055 #include <kactioncollection.h>
0056 #include <kactionmenu.h>
0057 #include <kfileitem.h>
0058 #include <kmainwindow.h>
0059 
0060 #ifdef HAVE_KIO_APPLICATIONLAUNCHERJOB
0061 #include <kio/applicationlauncherjob.h>
0062 #include <kio/jobuidelegatefactory.h>
0063 #else
0064 #include <krun.h>
0065 #endif
0066 
0067 #include "scandevices.h"
0068 #include "kscandevice.h"
0069 #include "deviceselector.h"
0070 #include "adddevicedialog.h"
0071 #include "previewer.h"
0072 #include "imagecanvas.h"
0073 #include "scanimage.h"
0074 
0075 #include "kookapref.h"
0076 #include "galleryhistory.h"
0077 #include "thumbview.h"
0078 #include "abstractocrengine.h"
0079 #include "abstractdestination.h"
0080 #include "pluginmanager.h"
0081 #include "ocrresedit.h"
0082 #include "scanpresetsdialog.h"
0083 #include "kookagallery.h"
0084 #include "kookascanparams.h"
0085 #include "scangallery.h"
0086 #include "imagetransform.h"
0087 #include "statusbarmanager.h"
0088 #include "kookasettings.h"
0089 #include "imagefilter.h"
0090 #include "recentsaver.h"
0091 #include "kooka_logging.h"
0092 
0093 #include "imgprintdialog.h"
0094 #include "kookaprint.h"
0095 
0096 
0097 // ---------------------------------------------------------------------------
0098 
0099 // Some of the UI panels (the gallery and the image viewer) are common
0100 // to more that one of the main task tabs.  This means that they can't simply
0101 // be added to the tabs/splitters in the normal way, as a widget can only be
0102 // a child of one parent at a time.
0103 //
0104 // This WidgetSite acts as a layout placeholder for such reassignable widgets.
0105 // It is assigned a new child widget when tabs are switched.
0106 //
0107 // It also defines the frame style for the panels.  So, in order to maintain a
0108 // consistent look, all of those panels should derive from QWidget (or, if
0109 // from QFrame, do not set a frame style) and should set the margin of any
0110 // internal layout to 0.  (KHBox/KVBox do this automatically, but any other
0111 // layout needs the margin explicitly set).
0112 
0113 class WidgetSite : public QFrame
0114 {
0115 public:
0116     WidgetSite(QWidget *parent, QWidget *widget = nullptr);
0117     void setWidget(QWidget *widget);
0118 
0119 private:
0120     static int sCount;
0121 };
0122 
0123 int WidgetSite::sCount = 0;
0124 
0125 WidgetSite::WidgetSite(QWidget *parent, QWidget *widget)
0126     : QFrame(parent)
0127 {
0128     QString name = QString("WidgetSite-#%1").arg(++sCount);
0129     setObjectName(name.toLocal8Bit());
0130 
0131     setFrameStyle(QFrame::Panel | QFrame::Raised);  // from "scanparams.cpp"
0132     setLineWidth(1);
0133 
0134     QGridLayout *lay = new QGridLayout(this);
0135     lay->setRowStretch(0, 1);
0136     lay->setColumnStretch(0, 1);
0137 
0138     if (widget == nullptr) {
0139         QLabel *l = new QLabel(name, this);
0140         l->setAlignment(Qt::AlignVCenter | Qt::AlignHCenter);
0141         widget = l;
0142     }
0143 
0144     //qCDebug(KOOKA_LOG) << name
0145     //         << "widget is a" << widget->metaObject()->className()
0146     //         << "parent is a" << widget->parent()->metaObject()->className();
0147     lay->addWidget(widget, 0, 0);
0148     widget->show();
0149 }
0150 
0151 void WidgetSite::setWidget(QWidget *widget)
0152 {
0153     QGridLayout *lay = static_cast<QGridLayout *>(layout());
0154 
0155     QObjectList childs = children();
0156     for (QObjectList::iterator it = childs.begin(); it != childs.end(); ++it) {
0157         QObject *ch = (*it);
0158         if (ch->isWidgetType()) {
0159             QWidget *w = static_cast<QWidget *>(ch);
0160             w->hide();
0161             lay->removeWidget(w);
0162         }
0163     }
0164 
0165     //qCDebug(KOOKA_LOG) << objectName()
0166     //         << "widget is a" << widget->metaObject()->className()
0167     //         << "parent is a" << widget->parent()->metaObject()->className();
0168     lay->addWidget(widget, 0, 0);
0169     widget->show();
0170 }
0171 
0172 // ---------------------------------------------------------------------------
0173 
0174 // Convenience class for layout splitter.
0175 
0176 class WidgetSplitter : public QSplitter
0177 {
0178 public:
0179     WidgetSplitter(Qt::Orientation orientation, QWidget *parent = nullptr);
0180 };
0181 
0182 WidgetSplitter::WidgetSplitter(Qt::Orientation orientation, QWidget *parent)
0183     : QSplitter(orientation, parent)
0184 {
0185     setChildrenCollapsible(false);
0186     setContentsMargins(0, 0, 0, 0);
0187     setStretchFactor(1, 1);
0188 }
0189 
0190 // ---------------------------------------------------------------------------
0191 
0192 KookaView::KookaView(KMainWindow *parent, const QByteArray &deviceToUse)
0193     : QTabWidget(parent)
0194 {
0195     setObjectName("KookaView");
0196 
0197     mMainWindow = parent;
0198     mScanParams = nullptr;
0199     mCurrentTab = KookaView::TabNone;
0200 
0201     /** Image Viewer **/
0202     mImageCanvas = new ImageCanvas(this);
0203     mImageCanvas->setMinimumSize(100, 200);
0204     QMenu *ctxtmenu = mImageCanvas->contextMenu();
0205     if (ctxtmenu != nullptr) {
0206         ctxtmenu->addSection(i18n("Image View"));
0207     }
0208 
0209     // Connections ImageCanvas --> myself
0210     connect(mImageCanvas, QOverload<const QRect &>::of(&ImageCanvas::newRect), this, &KookaView::slotSelectionChanged);
0211 
0212     /** Thumbnail View **/
0213     mThumbView = new ThumbView(this);
0214 
0215     /** Scan Gallery **/
0216     mGallery = new KookaGallery(this);
0217     ScanGallery *packager = mGallery->galleryTree();
0218 
0219     // Connections ScanGallery --> myself
0220     connect(packager, &ScanGallery::itemHighlighted, this, &KookaView::slotGallerySelectionChanged);
0221     connect(packager, &ScanGallery::showImage, this, &KookaView::slotShowImage);
0222     connect(packager, &ScanGallery::aboutToShowImage, this, &KookaView::slotStartLoading);
0223     connect(packager, &ScanGallery::unloadImage, this, &KookaView::slotUnloadImage);
0224 
0225     // Connections ScanGallery --> ThumbView
0226     connect(packager, &ScanGallery::itemHighlighted, mThumbView, &ThumbView::slotHighlightItem);
0227     connect(packager, &ScanGallery::imageChanged, mThumbView, &ThumbView::slotImageChanged);
0228     connect(packager, &ScanGallery::fileRenamed, mThumbView, &ThumbView::slotImageRenamed);
0229 
0230     // Connections ThumbView --> ScanGallery
0231     connect(mThumbView, &ThumbView::itemHighlighted, packager, &ScanGallery::slotHighlightItem);
0232     connect(mThumbView, &ThumbView::itemActivated, packager, &ScanGallery::slotActivateItem);
0233 
0234     GalleryHistory *recentFolder = mGallery->galleryRecent();
0235 
0236     // Connections ScanGallery <--> recent folder history
0237     connect(packager, &ScanGallery::galleryPathChanged, recentFolder, &GalleryHistory::slotPathChanged);
0238     connect(packager, &ScanGallery::galleryDirectoryRemoved, recentFolder, &GalleryHistory::slotPathRemoved);
0239     connect(recentFolder, &GalleryHistory::pathSelected, packager, &ScanGallery::slotSelectDirectory);
0240 
0241     /** Scanner Settings **/
0242 
0243     if (!deviceToUse.isEmpty() && deviceToUse != "gallery") {
0244         ScanDevices::self()->addUserSpecifiedDevice(deviceToUse,
0245                 "on command line",
0246                 "",
0247                 true);
0248     }
0249 
0250     /** Scanner Device **/
0251     mScanDevice = new KScanDevice(this);
0252 
0253     // Connections KScanDevice --> myself
0254     connect(mScanDevice, &KScanDevice::sigNewImage, this, &KookaView::slotNewImageScanned);
0255     connect(mScanDevice, &KScanDevice::sigNewPreview, this, &KookaView::slotNewPreview);
0256     connect(mScanDevice, &KScanDevice::sigScanStart, this, &KookaView::slotScanStart);
0257     connect(mScanDevice, &KScanDevice::sigAcquireStart, this, &KookaView::slotAcquireStart);
0258     connect(mScanDevice, &KScanDevice::sigScanFinished, this, &KookaView::slotScanFinished);
0259 
0260     /** Scan Preview **/
0261     mPreviewCanvas = new Previewer(this);
0262     mPreviewCanvas->setMinimumSize(100, 100);
0263     ctxtmenu = mPreviewCanvas->getImageCanvas()->contextMenu();
0264     if (ctxtmenu != nullptr) {
0265         ctxtmenu->addSection(i18n("Scan Preview"));
0266     }
0267 
0268     // Connections Previewer --> myself
0269     connect(mPreviewCanvas, &Previewer::previewDimsChanged, this, &KookaView::slotPreviewDimsChanged);
0270 
0271     /** Ocr Result Text **/
0272     mOcrResEdit  = new OcrResEdit(this);
0273     mOcrResEdit->setAcceptRichText(false);
0274     mOcrResEdit->setWordWrapMode(QTextOption::NoWrap);
0275     mOcrResEdit->setPlaceholderText(i18n("OCR results will appear here"));
0276     connect(mOcrResEdit, &OcrResEdit::spellCheckStatus, this, [this](const QString &msg){ changeStatus(msg); });
0277 
0278     /** Tabs **/
0279     // TODO: not sure which tab position is best, make this configurable
0280     setTabPosition(QTabWidget::West);
0281     setTabsClosable(false);
0282 
0283     mScanPage = new WidgetSplitter(Qt::Horizontal, this);
0284     addTab(mScanPage, QIcon::fromTheme("scan"), i18n("Scan"));
0285 
0286     mGalleryPage = new WidgetSplitter(Qt::Horizontal, this);
0287     addTab(mGalleryPage, QIcon::fromTheme("image-x-generic"), i18n("Gallery"));
0288 
0289     mOcrPage = new WidgetSplitter(Qt::Vertical, this);
0290     addTab(mOcrPage, QIcon::fromTheme("ocr"), i18n("OCR"));
0291 
0292     connect(this, &QTabWidget::currentChanged, this, &KookaView::slotTabChanged);
0293 
0294     // TODO: make the splitter layouts and sizes configurable
0295 
0296     mParamsSite = new WidgetSite(this);
0297 
0298     mScanGallerySite = new WidgetSite(this);
0299     mGalleryGallerySite = new WidgetSite(this);
0300     mOcrGallerySite = new WidgetSite(this);
0301 
0302     mGalleryImgviewSite = new WidgetSite(this);
0303     mOcrImgviewSite = new WidgetSite(this);
0304 
0305     // Even widgets that are not shared between tabs are placed in a WidgetSite,
0306     // to keep a consistent frame appearance.
0307 
0308     // "Scan" page: gallery top left, scan parameters bottom left, preview right
0309     mScanSubSplitter = new WidgetSplitter(Qt::Vertical, mScanPage);
0310     mScanSubSplitter->addWidget(mScanGallerySite);          // TL
0311     mScanSubSplitter->addWidget(mParamsSite);               // BL
0312     mScanPage->addWidget(new WidgetSite(this, mPreviewCanvas));     // R
0313 
0314     // "Gallery" page: gallery left, viewer top right, thumbnails bottom right
0315     mGalleryPage->addWidget(mGalleryGallerySite);           // L
0316     mGallerySubSplitter = new WidgetSplitter(Qt::Vertical, mGalleryPage);
0317     mGallerySubSplitter->addWidget(mGalleryImgviewSite);        // TR
0318     mGallerySubSplitter->addWidget(new WidgetSite(this, mThumbView));   // BR
0319 
0320     // "OCR" page: gallery top left, viewer top right, results bottom
0321     mOcrSubSplitter = new WidgetSplitter(Qt::Horizontal, mOcrPage);
0322     mOcrSubSplitter->addWidget(mOcrGallerySite);            // TL
0323     mOcrSubSplitter->addWidget(mOcrImgviewSite);            // TR
0324     mOcrPage->addWidget(new WidgetSite(this, mOcrResEdit));     // B
0325 
0326     if (slotSelectDevice(deviceToUse, false)) {
0327         slotTabChanged(KookaView::TabScan);
0328     } else {
0329         setCurrentIndex(KookaView::TabGallery);
0330     }
0331 }
0332 
0333 KookaView::~KookaView()
0334 {
0335     delete mScanDevice;
0336 }
0337 
0338 // this gets called via Kooka::closeEvent() at shutdown
0339 void KookaView::saveWindowSettings(KConfigGroup &grp)
0340 {
0341     qCDebug(KOOKA_LOG) << "to group" << grp.name();
0342     KookaSettings::setLayoutTabIndex(currentIndex());
0343     KookaSettings::setLayoutScan1(mScanPage->saveState().toBase64());
0344     KookaSettings::setLayoutScan2(mScanSubSplitter->saveState().toBase64());
0345     KookaSettings::setLayoutGallery1(mGalleryPage->saveState().toBase64());
0346     KookaSettings::setLayoutGallery2(mGallerySubSplitter->saveState().toBase64());
0347     KookaSettings::setLayoutOcr1(mOcrPage->saveState().toBase64());
0348     KookaSettings::setLayoutOcr2(mOcrSubSplitter->saveState().toBase64());
0349 
0350     saveGalleryState();                 // for the current tab
0351     KookaSettings::self()->save();
0352 }
0353 
0354 // this gets called by Kooka::applyMainWindowSettings() at startup
0355 void KookaView::applyWindowSettings(const KConfigGroup &grp)
0356 {
0357     qCDebug(KOOKA_LOG) << "from group" << grp.name();
0358 
0359     QString set = KookaSettings::layoutScan1();
0360     if (!set.isEmpty()) mScanPage->restoreState(QByteArray::fromBase64(set.toLocal8Bit()));
0361     set = KookaSettings::layoutScan2();
0362     if (!set.isEmpty()) mScanSubSplitter->restoreState(QByteArray::fromBase64(set.toLocal8Bit()));
0363 
0364     set = KookaSettings::layoutGallery1();
0365     if (!set.isEmpty()) mGalleryPage->restoreState(QByteArray::fromBase64(set.toLocal8Bit()));
0366     set = KookaSettings::layoutGallery2();
0367     if (!set.isEmpty()) mGallerySubSplitter->restoreState(QByteArray::fromBase64(set.toLocal8Bit()));
0368 
0369     set = KookaSettings::layoutOcr1();
0370     if (!set.isEmpty()) mOcrPage->restoreState(QByteArray::fromBase64(set.toLocal8Bit()));
0371     set = KookaSettings::layoutOcr2();
0372     if (!set.isEmpty()) mOcrSubSplitter->restoreState(QByteArray::fromBase64(set.toLocal8Bit()));
0373 }
0374 
0375 void KookaView::saveGalleryState(int index) const
0376 {
0377     if (index == -1) index = currentIndex();
0378     gallery()->saveHeaderState(index);
0379 }
0380 
0381 void KookaView::restoreGalleryState(int index)
0382 {
0383     if (index == -1) index = currentIndex();
0384     gallery()->restoreHeaderState(index);
0385 }
0386 
0387 void KookaView::slotTabChanged(int index)
0388 {
0389     //qCDebug(KOOKA_LOG) << index;
0390     if (mCurrentTab != KookaView::TabNone) {
0391         saveGalleryState(mCurrentTab);
0392     }
0393     // save state of previous tab
0394     switch (index) {
0395     case KookaView::TabScan:                // Scan
0396         mScanGallerySite->setWidget(mGallery);
0397         mGalleryImgviewSite->setWidget(mImageCanvas);   // somewhere to park it
0398         emit clearStatus(StatusBarManager::ImageDims);
0399         break;
0400 
0401     case KookaView::TabGallery:             // Gallery
0402         mGalleryGallerySite->setWidget(mGallery);
0403         mGalleryImgviewSite->setWidget(mImageCanvas);
0404         emit clearStatus(StatusBarManager::PreviewDims);
0405         break;
0406 
0407     case KookaView::TabOcr:                 // OCR
0408         mOcrGallerySite->setWidget(mGallery);
0409         mOcrImgviewSite->setWidget(mImageCanvas);
0410         emit clearStatus(StatusBarManager::PreviewDims);
0411         break;
0412     }
0413 
0414     restoreGalleryState(index);             // restore state of new tab
0415     mCurrentTab = static_cast<KookaView::TabPage>(index);
0416     // note for next tab change
0417     updateSelectionState();             // update image action states
0418 }
0419 
0420 bool KookaView::slotSelectDevice(const QByteArray &useDevice, bool alwaysAsk)
0421 {
0422     qCDebug(KOOKA_LOG) << "device" << useDevice << "ask" << alwaysAsk;
0423 
0424     bool haveConnection = false;
0425     bool gallery_mode = (useDevice == "gallery");
0426     QByteArray selDevice;
0427 
0428     /* in case useDevice is the term 'gallery', the user does not want to
0429      * connect to a scanner, but only work in gallery mode. Otherwise, try
0430      * to read the device to use from config or from a user dialog */
0431     if (!gallery_mode) {
0432         selDevice =  useDevice;
0433         if (selDevice.isEmpty()) {
0434             selDevice = userDeviceSelection(alwaysAsk);
0435         }
0436 
0437         if (selDevice.isEmpty()) {          // dialogue cancelled
0438             if (mScanParams != nullptr) {
0439                 return (false);    // have setup, do nothing
0440             }
0441             gallery_mode = true;
0442         }
0443     }
0444 
0445     if (mScanParams != nullptr) {
0446         closeScanDevice();    // remove existing GUI object
0447     }
0448 
0449     mScanParams = new KookaScanParams(this);        // and create a new one
0450     connect(mScanParams, &KookaScanParams::actionSelectScanner, this, [this](){ slotSelectDevice(); });
0451     connect(mScanParams, &KookaScanParams::actionAddScanner, this, &KookaView::slotAddDevice);
0452 
0453     mParamsSite->setWidget(mScanParams);
0454 
0455     if (!selDevice.isEmpty()) {             // connect to the selected scanner
0456         while (!haveConnection) {
0457             qCDebug(KOOKA_LOG) << "Opening device" << selDevice;
0458             KScanDevice::Status stat = mScanDevice->openDevice(selDevice);
0459             if (stat == KScanDevice::Ok) {
0460                 haveConnection = true;
0461             } else {
0462                 QString msg = xi18nc("@info",
0463                                      "There was a problem opening the scanner device."
0464                                      "<nl/><nl/>"
0465                                      "Check that the scanner is connected and switched on, "
0466                                      "and that SANE support for it is correctly configured."
0467                                      "<nl/><nl/>"
0468                                      "Trying to use scanner device: <icode>%3</icode><nl/>"
0469                                      "libkookascan reported error: <message>%2</message><nl/>"
0470                                      "SANE reported error: <message>%1</message>",
0471                                      mScanDevice->lastSaneErrorMessage(),
0472                                      KScanDevice::statusMessage(stat),
0473                                      selDevice.constData());
0474 
0475                 int tryAgain = KMessageBox::warningContinueCancel(mMainWindow, msg, QString(),
0476                                KGuiItem("Retry"));
0477                 if (tryAgain == KMessageBox::Cancel) {
0478                     break;
0479                 }
0480             }
0481         }
0482 
0483         if (haveConnection) {           // scanner connected OK
0484             QSize s = mScanDevice->getMaxScanSize();    // fix for 160148
0485             mPreviewCanvas->setScannerBedSize(s.width(), s.height());
0486 
0487             // Connections ScanParams --> Previewer
0488             connect(mScanParams, &ScanParams::scanResolutionChanged, mPreviewCanvas, &Previewer::slotNewScanResolutions);
0489             connect(mScanParams, &ScanParams::scanModeChanged, mPreviewCanvas, &Previewer::slotNewScanMode);
0490             connect(mScanParams, &ScanParams::newCustomScanSize, mPreviewCanvas, &Previewer::slotNewCustomScanSize);
0491 
0492             // Connections Previewer --> ScanParams
0493             connect(mPreviewCanvas, &Previewer::newPreviewRect, mScanParams, &ScanParams::slotNewPreviewRect);
0494 
0495             mScanParams->connectDevice(mScanDevice);    // create scanning options
0496 
0497             // Load the saved preview image, if there is one
0498             ScanImage *previewImage = new ScanImage(mScanDevice->loadPreviewImage());
0499             mPreviewCanvas->setPreviewImage(ScanImage::Ptr(previewImage));
0500             // Load auto-selection options for the selected scanner
0501             mPreviewCanvas->connectScanner(mScanDevice);
0502         }
0503     }
0504 
0505     if (!haveConnection) {              // no scanner device available,
0506         // or starting in gallery mode
0507         if (mScanParams != nullptr) {
0508             mScanParams->connectDevice(nullptr, gallery_mode);
0509         }
0510     }
0511 
0512     emit signalScannerChanged(haveConnection);
0513     return (haveConnection);
0514 }
0515 
0516 void KookaView::slotAddDevice()
0517 {
0518     AddDeviceDialog d(mMainWindow, i18n("Add Scan Device"));
0519     if (d.exec()) {
0520         QByteArray dev = d.getDevice();
0521         QString dsc = d.getDescription();
0522         QByteArray type = d.getType();
0523         qCDebug(KOOKA_LOG) << "dev" << dev << "type" << type << "desc" << dsc;
0524 
0525         ScanDevices::self()->addUserSpecifiedDevice(dev, dsc, type);
0526     }
0527 }
0528 
0529 QByteArray KookaView::userDeviceSelection(bool alwaysAsk)
0530 {
0531     /* a list of backends the scan backend knows */
0532     QList<QByteArray> backends = ScanDevices::self()->allDevices();
0533     if (backends.count() == 0) {
0534         if (KMessageBox::warningContinueCancel(mMainWindow,
0535                                                xi18nc("@info",
0536                                                       "No scanner devices are available."
0537                                                       "<nl/><nl/>"
0538                                                       "If your scanner is a type that can be auto-detected by SANE, "
0539                                                       "check that it is connected, switched on and configured correctly."
0540                                                       "<nl/><nl/>"
0541                                                       "If the scanner cannot be auto-detected by SANE (this includes some network scanners), "
0542                                                       "you need to specify the device to use. "
0543                                                       "Use the <interface>Add Scan Device</interface> option to enter the backend name and parameters, "
0544                                                       "or see that dialog for more information."),
0545                                                QString(),
0546                                                KGuiItem(i18n("Add Scan Device..."))) != KMessageBox::Continue)
0547         {
0548             return ("");
0549         }
0550 
0551         slotAddDevice();
0552         backends = ScanDevices::self()->allDevices();    // refresh the list
0553         if (backends.count() == 0) {
0554             return ("");    // give up this time
0555         }
0556     }
0557 
0558     QByteArray selDevice;
0559     DeviceSelector ds(mMainWindow, backends,
0560                       (alwaysAsk ? KGuiItem() : KGuiItem(i18n("Gallery"), QIcon::fromTheme("image-x-generic"))));
0561     if (!alwaysAsk) {
0562         selDevice = ds.getDeviceFromConfig();
0563     }
0564 
0565     if (selDevice.isEmpty()) {
0566         qCDebug(KOOKA_LOG) << "no selDevice, starting selector";
0567         if (ds.exec() == QDialog::Accepted) {
0568             selDevice = ds.getSelectedDevice();
0569         }
0570         qCDebug(KOOKA_LOG) << "selector returned device" << selDevice;
0571     }
0572 
0573     return (selDevice);
0574 }
0575 
0576 QString KookaView::scannerName() const
0577 {
0578     if (mScanDevice->scannerBackendName().isEmpty()) {
0579         return (i18n("Gallery"));
0580     }
0581     if (!mScanDevice->isScannerConnected()) {
0582         return (i18n("No scanner connected"));
0583     }
0584     return (mScanDevice->scannerDescription());
0585 }
0586 
0587 bool KookaView::isScannerConnected() const
0588 {
0589     return (mScanDevice->isScannerConnected());
0590 }
0591 
0592 void KookaView::slotSelectionChanged(const QRect &newSelection)
0593 {
0594     emit signalRectangleChanged(newSelection.isValid());
0595 }
0596 
0597 //  The 'state' generated here is used by Kooka::slotUpdateViewActions().
0598 
0599 void KookaView::updateSelectionState()
0600 {
0601     KookaView::StateFlags state = KookaView::StateFlags();
0602     const FileTreeViewItem *ftvi = mGallery->galleryTree()->highlightedFileTreeViewItem();
0603     const KFileItem *fi = (ftvi != nullptr ? ftvi->fileItem() : nullptr);
0604     const bool scanmode = (mCurrentTab == KookaView::TabScan);
0605 
0606     if (scanmode)                   // Scan mode
0607     {
0608         if (mPreviewCanvas->getImageCanvas()->hasImage()) state |= KookaView::PreviewValid;
0609     }
0610     else                        // Gallery/OCR mode
0611     {
0612         state |= KookaView::GalleryShown;
0613     }
0614 
0615     if (fi != nullptr && !fi->isNull())         // have a gallery selection
0616     {
0617         if (fi->isDir()) state |= KookaView::IsDirectory;
0618         else
0619         {
0620             state |= KookaView::FileSelected;
0621             if (fi->url().hasFragment()) state |= KookaView::IsSubImage;
0622             if (ftvi->childCount()>0) state |= KookaView::HasSubImages;
0623         }
0624     }
0625 
0626     if (ftvi != nullptr && ftvi->isRoot()) state |= KookaView::RootSelected;
0627     if (imageViewer()->hasImage()) state |= KookaView::ImageValid;
0628 
0629     //qCDebug(KOOKA_LOG) << "state" << state;
0630     emit signalViewSelectionState(state);
0631 }
0632 
0633 void KookaView::slotGallerySelectionChanged()
0634 {
0635     const FileTreeViewItem *ftvi = mGallery->galleryTree()->highlightedFileTreeViewItem();
0636     const KFileItem *fi = (ftvi != nullptr ? ftvi->fileItem() : nullptr);
0637     const bool scanmode = (mCurrentTab == KookaView::TabScan);
0638 
0639     if (fi == nullptr || fi->isNull()) {       // no gallery selection
0640         if (!scanmode) {
0641             emit changeStatus(i18n("No selection"));
0642         }
0643     } else {                    // have a gallery selection
0644         if (!scanmode) {
0645             KLocalizedString str = fi->isDir() ? ki18n("Gallery folder %1") : ki18n("Gallery image %1");
0646             emit changeStatus(str.subs(fi->url().url(QUrl::PreferLocalFile)).toString());
0647         }
0648     }
0649 
0650     updateSelectionState();
0651 }
0652 
0653 void KookaView::loadStartupImage()
0654 {
0655     const bool wantReadOnStart = KookaSettings::startupReadImage();
0656     if (wantReadOnStart) {
0657         QString startup = KookaSettings::startupSelectedImage();
0658         qCDebug(KOOKA_LOG) << "load startup image" << startup;
0659         if (!startup.isEmpty()) gallery()->slotSelectImage(QUrl::fromLocalFile(startup));
0660     }
0661 }
0662 
0663 void KookaView::print()
0664 {
0665     ScanImage::Ptr img = gallery()->getCurrImage(true);
0666     if (img.isNull()) return;               // load image if necessary
0667 
0668     // create a KookaPrint (subclass of a QPrinter)
0669     KookaPrint printer;
0670     printer.setImage(img.data());
0671 
0672     QPrintDialog d(&printer, this);
0673     d.setWindowTitle(i18nc("@title:window", "Print Image"));
0674     d.setOptions(QAbstractPrintDialog::PrintToFile|QAbstractPrintDialog::PrintShowPageSize);
0675 
0676     // TODO (investigate): even with the options set as above, the options below still
0677     // appear in the print dialogue.  Is this as intended by Qt?
0678     //     d.setOption(QAbstractPrintDialog::PrintSelection, false);
0679     //     d.setOption(QAbstractPrintDialog::PrintPageRange, false);
0680 
0681     ImgPrintDialog imgTab(img, &printer);       // create tab for our options
0682     d.setOptionTabs(QList<QWidget *>() << &imgTab); // add tab to print dialogue
0683 
0684     if (!d.exec()) return;              // open the dialogue
0685     QString msg = imgTab.checkValid();          // check that settings are valid
0686     if (!msg.isEmpty())                 // if not, display error message
0687     {
0688         KMessageBox::error(this,
0689                            i18nc("@info", "Invalid print options were specified:\n\n%1", msg),
0690                            i18nc("@title:window", "Cannot Print"));
0691         return;
0692     }
0693 
0694     imgTab.updatePrintParameters();         // set final printer options
0695     printer.printImage();               // print the image
0696 }
0697 
0698 
0699 void KookaView::slotNewPreview(ScanImage::Ptr newimg)
0700 {
0701     if (newimg.isNull()) return;
0702 
0703     mPreviewCanvas->newImage(newimg);           // set new image and size
0704     updateSelectionState();
0705 }
0706 
0707 
0708 void KookaView::slotStartOcrSelection()
0709 {
0710     emit changeStatus(i18n("Starting OCR on selection"));
0711     startOCR(mImageCanvas->selectedImage());
0712 }
0713 
0714 void KookaView::slotStartOcr()
0715 {
0716     emit changeStatus(i18n("Starting OCR on the image"));
0717     startOCR(gallery()->getCurrImage(true));
0718 }
0719 
0720 void KookaView::slotStartOcrFile()
0721 {
0722     RecentSaver saver("ocrFile");
0723     QUrl url = QFileDialog::getOpenFileUrl(this, i18n("OCR File"),
0724                                            saver.recentUrl(),
0725                                            ImageFilter::qtFilterString(ImageFilter::Reading, ImageFilter::AllImages|ImageFilter::AllFiles));
0726     if (!url.isValid()) return;
0727     saver.save(url);
0728 
0729     ScanImage::Ptr img(new ScanImage(url));
0730     if (!img->isFileBound())
0731     {
0732         KMessageBox::error(mMainWindow,
0733                            xi18nc("@info", "Cannot load <filename>%1</filename> for OCR:<nl/>%2",
0734                                   url.toDisplayString(), img->errorString()),
0735                            i18n("Cannot Read OCR File"));
0736         return;
0737     }
0738 
0739     emit changeStatus(xi18nc("@info", "Starting OCR on file <filename>%1</filename>", url.url(QUrl::PreferLocalFile)));
0740     startOCR(img);
0741 }
0742 
0743 void KookaView::slotSetOcrSpellConfig(const QString &configFile)
0744 {
0745 #ifndef KF5
0746     qCDebug(KOOKA_LOG) << configFile;
0747     if (mOcrResEdit!=nullptr) mOcrResEdit->setSpellCheckingConfigFileName(configFile);
0748 #endif
0749 }
0750 
0751 void KookaView::slotOcrSpellCheck(bool interactive, bool background)
0752 {
0753     if (!interactive && !background)            // not doing anything
0754     {
0755         emit changeStatus(i18n("OCR finished"));
0756         return;
0757     }
0758 
0759     if (mOcrResEdit->document()->isEmpty())
0760     {
0761         KMessageBox::error(mMainWindow,
0762                            i18n("There is no OCR result text to spell check."),
0763                            i18n("OCR Spell Check not possible"));
0764         return;
0765     }
0766 
0767     setCurrentIndex(KookaView::TabOcr);
0768     emit changeStatus(i18n("OCR Spell Check"));
0769     if (background) mOcrResEdit->setCheckSpellingEnabled(true);
0770     if (interactive) mOcrResEdit->checkSpelling();
0771 }
0772 
0773 
0774 void KookaView::startOCR(ScanImage::Ptr img)
0775 {
0776     if (img.isNull()) return;               // no image to OCR
0777 
0778     setCurrentIndex(KookaView::TabOcr);
0779 
0780     const QString engineName = KookaSettings::ocrEngineName();
0781     if (engineName.isEmpty())
0782     {
0783         int result = KMessageBox::warningContinueCancel(mMainWindow,
0784                                                         i18n("No OCR engine is configured.\n"
0785                                                              "Please select and configure one in order to perform OCR."),
0786                                                         i18n("OCR Not Configured"),
0787                                                         KGuiItem(i18n("Configure OCR...")));
0788         if (result==KMessageBox::Continue) emit signalOcrPrefs();
0789         return;
0790     }
0791 
0792     AbstractOcrEngine *engine = qobject_cast<AbstractOcrEngine *>(PluginManager::self()->loadPlugin(PluginManager::OcrPlugin, engineName));
0793     if (engine==nullptr)
0794     {
0795         KMessageBox::error(mMainWindow, i18n("Cannot load OCR plugin '%1'", engineName));
0796         return;
0797     }
0798 
0799     // We don't know whether the plugin object has been used before.
0800     // So disconnect all of its existing signals so that they do not
0801     // get double connected.
0802     engine->disconnect();
0803 
0804     // Connections OCR Engine --> myself
0805     connect(engine, &AbstractOcrEngine::newOCRResultText, this, &KookaView::slotOcrResultAvailable);
0806     connect(engine, &AbstractOcrEngine::setSpellCheckConfig, this, &KookaView::slotSetOcrSpellConfig);
0807     connect(engine, &AbstractOcrEngine::startSpellCheck, this, &KookaView::slotOcrSpellCheck);
0808     connect(engine, &AbstractOcrEngine::openOcrPrefs, this, &KookaView::signalOcrPrefs);
0809 
0810     // Connections OCR Results --> OCR Engine
0811     connect(mOcrResEdit, &OcrResEdit::highlightWord, engine, &AbstractOcrEngine::slotHighlightWord);
0812     connect(mOcrResEdit, &OcrResEdit::scrollToWord, engine, &AbstractOcrEngine::slotScrollToWord);
0813 
0814     // Connections OCR Engine --> OCR Results
0815     connect(engine, &AbstractOcrEngine::readOnlyEditor, mOcrResEdit, &OcrResEdit::slotSetReadOnly);
0816     connect(engine, &AbstractOcrEngine::selectWord, mOcrResEdit, &OcrResEdit::slotSelectWord);
0817 
0818     engine->setImageCanvas(mImageCanvas);
0819     engine->setTextDocument(mOcrResEdit->document());
0820     engine->setImage(img);
0821     engine->openOcrDialogue(this);
0822 }
0823 
0824 
0825 void KookaView::slotOcrResultAvailable()
0826 {
0827     emit signalOcrResultAvailable(mOcrResEdit->document()->characterCount()>0);
0828     updateSelectionState();
0829 }
0830 
0831 
0832 void KookaView::slotScanStart(ScanImage::ImageType type)
0833 {
0834     // The scan parameters GUI must exist here, in order to locate the
0835     // destination plugin which is loaded and managed by KookaScanParams.
0836     AbstractDestination *dest = (mScanParams!=nullptr) ? mScanParams->destinationPlugin() : nullptr;
0837     if (dest==nullptr)
0838     {
0839         qCWarning(KOOKA_LOG) << "No destination plugin";
0840         mScanDevice->slotStopScanning();        // no point starting scan
0841         return;
0842     }
0843 
0844     // Allow the plugin to find the scan gallery, if it needs to send
0845     // the scan output to there.
0846     dest->setScanGallery(gallery());
0847 
0848     // Tell the plugin that a scan is starting.
0849     if (type!=ScanImage::Preview)
0850     {
0851         if (!dest->scanStarting(type))          // get ready to save
0852         {                       // user cancelled file prompt
0853             mScanDevice->slotStopScanning();        // abort the scan now
0854             return;
0855         }
0856     }
0857 
0858     mScanParams->setEnabled(false);         // disable GUI while scanning
0859     KLed *led = mScanParams->operationLED();        // update the LED indicator
0860     if (led!=nullptr)
0861     {
0862         led->setColor(Qt::red);             // scanner warming up
0863         led->setState(KLed::On);
0864         qApp->processEvents();              // let the change show
0865     }
0866 
0867     // Set the destination string displayed in the "Scan in Progress" dialogue
0868     if (type!=ScanImage::Preview)
0869     {
0870         mScanParams->setScanDestination(dest->scanDestinationString());
0871     }
0872 }
0873 
0874 
0875 void KookaView::slotAcquireStart()
0876 {
0877     if (mScanParams!=nullptr)
0878     {
0879         KLed *led = mScanParams->operationLED();    // update the LED indicator
0880         if (led!=nullptr)
0881         {
0882             led->setColor(Qt::green);           // scanning active
0883             qApp->processEvents();          // let the change show
0884         }
0885     }
0886 }
0887 
0888 
0889 void KookaView::slotNewImageScanned(ScanImage::Ptr img)
0890 {
0891     AbstractDestination *dest = (mScanParams!=nullptr) ? mScanParams->destinationPlugin() : nullptr;
0892     if (dest!=nullptr) dest->imageScanned(img);
0893     else qCWarning(KOOKA_LOG) << "No destination plugin";
0894 }
0895 
0896 
0897 void KookaView::slotScanFinished(KScanDevice::Status stat)
0898 {
0899     qCDebug(KOOKA_LOG) << "Scan finished with status" << stat;
0900 
0901     if (stat != KScanDevice::Ok && stat != KScanDevice::Cancelled) {
0902         QString msg = xi18nc("@info",
0903                              "There was a problem during preview or scanning."
0904                              "<nl/>"
0905                              "Check that the scanner is still connected and switched on, "
0906                              "<nl/>"
0907                              "and that media is loaded if required."
0908                              "<nl/><nl/>"
0909                              "Trying to use scanner device: <icode>%3</icode><nl/>"
0910                              "libkookascan reported error: <message>%2</message><nl/>"
0911                              "SANE reported error: <message>%1</message>",
0912                              mScanDevice->lastSaneErrorMessage(),
0913                              KScanDevice::statusMessage(stat),
0914                              mScanDevice->scannerBackendName().constData());
0915         KMessageBox::error(mMainWindow, msg);
0916     }
0917 
0918     if (mScanParams != nullptr) {
0919         mScanParams->setEnabled(true);
0920         KLed *led = mScanParams->operationLED();
0921         if (led != nullptr) {
0922             led->setColor(Qt::green);
0923             led->setState(KLed::Off);
0924         }
0925     }
0926 }
0927 
0928 
0929 void KookaView::closeScanDevice()
0930 {
0931     qCDebug(KOOKA_LOG);
0932 
0933     if (mScanParams!=nullptr)
0934     {
0935         // First save the current destination plugin settings
0936         mScanParams->saveDestinationSettings();
0937         // Then ensure that the current destination plugin is unloaded
0938         PluginManager::self()->loadPlugin(PluginManager::DestinationPlugin, QString());
0939         // Then the scan parameters GUI can be destroyed
0940         delete mScanParams;
0941         mScanParams = nullptr;
0942     }
0943 
0944     mScanDevice->closeDevice();
0945 }
0946 
0947 
0948 void KookaView::slotCreateNewImgFromSelection()
0949 {
0950     ScanImage::Ptr img = mImageCanvas->selectedImage();
0951     if (img.isNull()) return;               // no selection
0952 
0953     emit changeStatus(i18n("Create new image from selection"));
0954     gallery()->addImage(img);
0955     emit clearStatus();
0956 }
0957 
0958 
0959 void KookaView::slotTransformImage()
0960 {
0961     if (imageViewer()->isReadOnly()) {
0962         emit changeStatus(i18n("Cannot transform, image is read-only"));
0963         return;
0964     }
0965 
0966     // Get operation code from action that was triggered
0967     QAction *act = static_cast<QAction *>(sender());
0968     if (act==nullptr) return;
0969     ImageTransform::Operation op = static_cast<ImageTransform::Operation>(act->data().toInt());
0970 
0971     // This may appear to be a waste, since we are immediately unloading the image.
0972     // But we need a copy of the image anyway, so there will still be a load needed.
0973     ScanImage::Ptr loadedImage = gallery()->getCurrImage(true);
0974     if (loadedImage.isNull()) return;
0975 
0976     const QImage img = *loadedImage.data();     // get a copy of the image
0977 
0978     QString imageFile = gallery()->currentImageFileName();
0979     if (imageFile.isEmpty()) return;            // get file to save back to
0980     gallery()->slotUnloadItems();           // unload original from memory
0981 
0982     ImageTransform *transformer = new ImageTransform(img, op, imageFile, this);
0983     connect(transformer, &ImageTransform::done, gallery(), &ScanGallery::slotUpdatedItem);
0984     connect(transformer, &ImageTransform::statusMessage, this, [this](const QString &msg){ changeStatus(msg); });
0985     connect(transformer, &QThread::finished, transformer, &QObject::deleteLater);
0986     transformer->start();
0987 }
0988 
0989 
0990 void KookaView::slotSaveOcrResult()
0991 {
0992     if (mOcrResEdit != nullptr) {
0993         mOcrResEdit->slotSaveText();
0994     }
0995 }
0996 
0997 
0998 void KookaView::slotScanParams()
0999 {
1000     if (mScanDevice == nullptr) return;         // must have a scanner device
1001     ScanPresetsDialog d(mScanDevice, this);
1002     d.exec();
1003 }
1004 
1005 
1006 void KookaView::slotShowImage(ScanImage::Ptr img, bool isDir)
1007 {
1008     if (mImageCanvas!=nullptr)              // load into image viewer
1009     {
1010         mImageCanvas->newImage(img);
1011         mImageCanvas->setReadOnly(false);
1012         emit changeStatus(mImageCanvas->imageInfoString(), StatusBarManager::ImageDims);
1013     }
1014 
1015     if (!img.isNull()) emit changeStatus(i18n("Loaded image %1", img->url().url(QUrl::PreferLocalFile)));
1016     else if (!isDir) emit changeStatus(i18n("Unloaded image"));
1017     updateSelectionState();
1018 }
1019 
1020 
1021 void KookaView::slotUnloadImage()
1022 {
1023     if (mImageCanvas!=nullptr) mImageCanvas->newImage(nullptr);
1024     updateSelectionState();
1025 }
1026 
1027 
1028 /* this slot is called when the user clicks on an image in the packager
1029  * and loading of the image starts
1030  */
1031 void KookaView::slotStartLoading(const QUrl &url)
1032 {
1033     emit changeStatus(i18n("Loading %1...", url.url(QUrl::PreferLocalFile)));
1034 }
1035 
1036 void KookaView::connectViewerAction(QAction *action, bool sepBefore)
1037 {
1038     if (action == nullptr) {
1039         return;
1040     }
1041     QMenu *popup = mImageCanvas->contextMenu();
1042     if (popup == nullptr) {
1043         return;
1044     }
1045 
1046     if (sepBefore) {
1047         popup->addSeparator();
1048     }
1049     popup->addAction(action);
1050 }
1051 
1052 void KookaView::connectGalleryAction(QAction *action, bool sepBefore)
1053 {
1054     if (action == nullptr) {
1055         return;
1056     }
1057     QMenu *popup = gallery()->contextMenu();
1058     if (popup == nullptr) {
1059         return;
1060     }
1061 
1062     if (sepBefore) {
1063         popup->addSeparator();
1064     }
1065     popup->addAction(action);
1066 }
1067 
1068 void KookaView::connectThumbnailAction(QAction *action)
1069 {
1070     if (action == nullptr) {
1071         return;
1072     }
1073     QMenu *popup = mThumbView->contextMenu();
1074     if (popup != nullptr) {
1075         popup->addAction(action);
1076     }
1077 }
1078 
1079 void KookaView::connectPreviewAction(QAction *action)
1080 {
1081     if (action == nullptr) {
1082         return;
1083     }
1084     QMenu *popup = mPreviewCanvas->getImageCanvas()->contextMenu();
1085     if (popup != nullptr) {
1086         popup->addAction(action);
1087     }
1088 }
1089 
1090 void KookaView::slotApplySettings()
1091 {
1092     if (mThumbView != nullptr) {
1093         mThumbView->readSettings();    // size and background
1094     }
1095     if (mGallery != nullptr) {
1096         mGallery->readSettings();    // layout and rename
1097     }
1098 }
1099 
1100 // Starting a scan or preview, switch tab and tell the scan device
1101 
1102 void KookaView::slotStartPreview()
1103 {
1104     if (mScanParams == nullptr) {
1105         return;
1106     }
1107     setCurrentIndex(KookaView::TabScan);
1108     qApp->processEvents();              // let the tab appear
1109     mScanParams->slotAcquirePreview();
1110 }
1111 
1112 void KookaView::slotStartFinalScan()
1113 {
1114     if (mScanParams == nullptr) {
1115         return;
1116     }
1117     setCurrentIndex(KookaView::TabScan);
1118     qApp->processEvents();              // let the tab appear
1119     mScanParams->slotStartScan();
1120 }
1121 
1122 void KookaView::slotAutoSelect(bool on)
1123 {
1124     if (mPreviewCanvas == nullptr) {
1125         return;
1126     }
1127     setCurrentIndex(KookaView::TabScan);
1128     qApp->processEvents();              // let the tab appear
1129     mPreviewCanvas->slotAutoSelToggled(on);
1130 }
1131 
1132 
1133 ScanGallery *KookaView::gallery() const
1134 {
1135     return (mGallery->galleryTree());
1136 }
1137 
1138 ImageCanvas *KookaView::imageViewer() const
1139 {
1140     return (mImageCanvas);
1141 }
1142 
1143 Previewer *KookaView::previewer() const
1144 {
1145     return (mPreviewCanvas);
1146 }
1147 
1148 void KookaView::imageViewerAction(ImageCanvas::UserAction act)
1149 {
1150     if (mCurrentTab == KookaView::TabScan) {        // Scan
1151         mPreviewCanvas->getImageCanvas()->performUserAction(act);
1152     } else {                        // Gallery or OCR
1153         mImageCanvas->performUserAction(act);
1154     }
1155 }
1156 
1157 void KookaView::showOpenWithMenu(KActionMenu *menu)
1158 {
1159     FileTreeViewItem *curr = gallery()->highlightedFileTreeViewItem();
1160     QString mimeType = curr->fileItem()->mimetype();
1161     //qCDebug(KOOKA_LOG) << "Trying to open" << curr->url() << "which is" << mimeType;
1162 
1163     menu->menu()->clear();
1164 
1165     int i = 0;
1166     mOpenWithOffers = KApplicationTrader::queryByMimeType(mimeType);
1167     for (const KService::Ptr &service : qAsConst(mOpenWithOffers))
1168     {
1169         //qCDebug(KOOKA_LOG) << "> offer:" << service->name();
1170         QString actionName(service->name().replace("&", "&&"));
1171         QAction *act = new QAction(QIcon::fromTheme(service->icon()), actionName, this);
1172         connect(act, &QAction::triggered, this, [=]() { slotOpenWith(i); });
1173         menu->addAction(act);
1174     }
1175 
1176     menu->menu()->addSeparator();
1177     QAction *act = new QAction(i18n("Other..."), this);
1178     connect(act, &QAction::triggered, this, [=]() { slotOpenWith(-1); });
1179     menu->addAction(act);
1180 }
1181 
1182 void KookaView::slotOpenWith(int idx)
1183 {
1184     FileTreeViewItem *ftvi = gallery()->highlightedFileTreeViewItem();
1185     if (ftvi == nullptr) return;
1186 
1187     QList<QUrl> urllist;
1188     urllist.append(ftvi->url());
1189 
1190 #ifdef HAVE_KIO_APPLICATIONLAUNCHERJOB
1191     KIO::ApplicationLauncherJob *job;
1192 #endif
1193     if (idx!=-1)                    // application from the menu
1194     {
1195         Q_ASSERT(idx<mOpenWithOffers.count());
1196         KService::Ptr ptr = mOpenWithOffers[idx];
1197 #ifdef HAVE_KIO_APPLICATIONLAUNCHERJOB
1198         job = new KIO::ApplicationLauncherJob(ptr, mMainWindow);
1199 #else
1200         KRun::runService(*ptr, urllist, mMainWindow);
1201 #endif
1202     }
1203     else                            // last item = "Other..."
1204     {
1205 #ifdef HAVE_KIO_APPLICATIONLAUNCHERJOB
1206         job = new KIO::ApplicationLauncherJob(mMainWindow);
1207 #else
1208         KRun::displayOpenWithDialog(urllist, mMainWindow);
1209 #endif
1210     }
1211 #ifdef HAVE_KIO_APPLICATIONLAUNCHERJOB
1212     job->setUrls(urllist);
1213     job->setUiDelegate(KIO::createDefaultJobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, this));
1214     job->start();
1215 #endif
1216 }
1217 
1218 
1219 void KookaView::slotPreviewDimsChanged(const QString &dims)
1220 {
1221     emit changeStatus(dims, StatusBarManager::PreviewDims);
1222 }