File indexing completed on 2024-04-14 14:35:58

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 <Klaas.Freitag@gmx.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 "scangallery.h"
0033 
0034 #include <qfileinfo.h>
0035 #include <qdir.h>
0036 #include <qevent.h>
0037 #include <qapplication.h>
0038 #include <qheaderview.h>
0039 #include <qmenu.h>
0040 #include <qinputdialog.h>
0041 #include <qfiledialog.h>
0042 #include <qmimedata.h>
0043 
0044 #include <kmessagebox.h>
0045 #include <kpropertiesdialog.h>
0046 #include <klocalizedstring.h>
0047 #include <kstandardguiitem.h>
0048 #include <kconfigskeleton.h>
0049 
0050 #include <kio/global.h>
0051 #include <kio/copyjob.h>
0052 #include <kio/deletejob.h>
0053 #include <kio/mkdirjob.h>
0054 #include <kio/pixmaploader.h>
0055 #include <kio/jobuidelegate.h>
0056 
0057 #include "imgsaver.h"
0058 #include "galleryroot.h"
0059 #include "kookasettings.h"
0060 
0061 #include "scanimage.h"
0062 #include "imagefilter.h"
0063 #include "scanicons.h"
0064 #include "recentsaver.h"
0065 #include "kooka_logging.h"
0066 
0067 
0068 #undef DEBUG_LOADING
0069 
0070 
0071 // FileTreeViewItem is not the same as KDE3's KFileTreeViewItem in that
0072 // fileItem() used to return a KFileItem *, allowing the item to be modified
0073 // through the pointer.  Now it returns a KFileItem which is a value copy of the
0074 // internal one, not a pointer to it - so the internal KFileItem cannot
0075 // be modified.  This means that we can't store information in the extra data
0076 // of the KFileItem of a FileTreeViewItem.  Including, unfortunately, our
0077 // ScanImage pointer :-(
0078 //
0079 // This is a consequence of commit 719513, "Making KFileItemList value based".
0080 //
0081 // The image pointer is stored as the Qt::UserRole data of the item
0082 // (of the ported FileTreeView) instead.
0083 
0084 ScanGallery::ScanGallery(QWidget *parent)
0085     : FileTreeView(parent)
0086 {
0087     setObjectName("ScanGallery");
0088 
0089     //header()->setStretchEnabled(true,0);      // do we like this effect?
0090 
0091     setColumnCount(3);
0092     setRootIsDecorated(false);
0093     //setSortingEnabled(true);
0094     //sortByColumn(0, Qt::AscendingOrder);
0095     setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn);
0096 
0097     QStringList labels;
0098     labels << i18n("Name");
0099     labels << i18n("Size");
0100     labels << i18n("Format");
0101     setHeaderLabels(labels);
0102 
0103     headerItem()->setTextAlignment(0, Qt::AlignLeft);
0104     headerItem()->setTextAlignment(1, Qt::AlignLeft);
0105     headerItem()->setTextAlignment(2, Qt::AlignLeft);
0106 
0107     // Drag and Drop
0108     setDragEnabled(true);               // allow drags out
0109     setAcceptDrops(true);               // allow drops in
0110     connect(this, &FileTreeView::dropped, this, &ScanGallery::slotUrlsDropped);
0111     connect(this, &QTreeWidget::itemSelectionChanged, this, [this]() { slotItemHighlighted(); });
0112     connect(this, &QTreeWidget::itemActivated, this, &ScanGallery::slotItemActivated);
0113     connect(this, &FileTreeView::fileRenamed, this, &ScanGallery::slotFileRenamed);
0114     connect(this, &QTreeWidget::itemExpanded, this, &ScanGallery::slotItemExpanded);
0115 
0116     m_startup = true;
0117     m_currSelectedDir = QUrl();
0118     mSaver = nullptr;
0119     mSavedTo = nullptr;
0120 
0121     /* create a context menu and set the title */
0122     m_contextMenu = new QMenu(this);
0123     m_contextMenu->addSection(i18n("Gallery"));
0124 }
0125 
0126 ScanGallery::~ScanGallery()
0127 {
0128     delete mSaver;
0129 }
0130 
0131 static QString columnStatesKey(int forIndex)
0132 {
0133     return (QString("GalleryState%1").arg(forIndex));
0134 }
0135 
0136 void ScanGallery::saveHeaderState(int forIndex) const
0137 {
0138     QString key = columnStatesKey(forIndex);
0139     qCDebug(KOOKA_LOG) << "to" << key;
0140     const KConfigSkeletonItem *ski = KookaSettings::self()->columnStatesItem();
0141     Q_ASSERT(ski!=nullptr);
0142     KConfigGroup grp = KookaSettings::self()->config()->group(ski->group());
0143     grp.writeEntry(key, header()->saveState().toBase64());
0144     grp.sync();
0145 }
0146 
0147 void ScanGallery::restoreHeaderState(int forIndex)
0148 {
0149     QString key = columnStatesKey(forIndex);
0150     qCDebug(KOOKA_LOG) << "from" << key;
0151     const KConfigSkeletonItem *ski = KookaSettings::self()->columnStatesItem();
0152     Q_ASSERT(ski!=nullptr);
0153     const KConfigGroup grp = KookaSettings::self()->config()->group(ski->group());
0154 
0155     QString state = grp.readEntry(key, "");
0156     if (state.isEmpty()) return;
0157 
0158     QHeaderView *hdr = header();
0159     // same workaround as needed in Akregator (even with Qt 4.6),
0160     // see r918196 and r1001242 to kdepim/akregator/src/articlelistview.cpp
0161     hdr->resizeSection(hdr->logicalIndex(hdr->count() - 1), 1);
0162     hdr->restoreState(QByteArray::fromBase64(state.toLocal8Bit()));
0163 }
0164 
0165 void ScanGallery::openRoots()
0166 {
0167     /* standard root always exists, ImgRoot creates it */
0168     const QUrl rootUrl = GalleryRoot::root();
0169     qCDebug(KOOKA_LOG) << "Standard root" << rootUrl;
0170 
0171     m_defaultBranch = openRoot(rootUrl, i18n("Kooka Gallery"));
0172     m_defaultBranch->setOpen(true);
0173 
0174     /* open more configurable image repositories, configuration TODO */
0175     //openRoot(KUrl(getenv("HOME")), i18n("Home Directory"));
0176 }
0177 
0178 FileTreeBranch *ScanGallery::openRoot(const QUrl &root, const QString &title)
0179 {
0180     FileTreeBranch *branch = addBranch(root, title);
0181 
0182     branch->setOpenPixmap(KIconLoader::global()->loadIcon("folder-image", KIconLoader::Small));
0183     branch->setShowExtensions(true);
0184 
0185     setDirOnlyMode(branch, false);
0186 
0187     connect(branch, &FileTreeBranch::newTreeViewItems, this, QOverload<FileTreeBranch *, const FileTreeViewItemList &>::of(&ScanGallery::slotDecorate));
0188     connect(branch, &FileTreeBranch::changedTreeViewItems, this, QOverload<FileTreeBranch *, const FileTreeViewItemList &>::of(&ScanGallery::slotDecorate));
0189     connect(branch, &FileTreeBranch::directoryChildCount, this, &ScanGallery::slotDirCount);
0190     connect(branch, &FileTreeBranch::populateFinished, this, &ScanGallery::slotStartupFinished);
0191 
0192     return (branch);
0193 }
0194 
0195 void ScanGallery::slotStartupFinished(FileTreeViewItem *item)
0196 {
0197     if (!m_startup) {
0198         return;    // already done
0199     }
0200     if (item != m_defaultBranch->root()) {
0201         return;    // not the 1st branch root
0202     }
0203 
0204     qCDebug(KOOKA_LOG);
0205 
0206     if (highlightedFileTreeViewItem() == nullptr) {    // nothing currently selected,
0207         // select the branch root
0208         item->setSelected(true);
0209         emit galleryPathChanged(m_defaultBranch, "/");  // tell the history combo
0210     }
0211 
0212     m_startup = false;                  // don't do this again
0213 }
0214 
0215 void ScanGallery::contextMenuEvent(QContextMenuEvent *ev)
0216 {
0217     ev->accept();
0218     if (m_contextMenu != nullptr) {
0219         m_contextMenu->exec(ev->globalPos());
0220     }
0221 }
0222 
0223 static ImageFormat getImgFormat(const FileTreeViewItem *item)
0224 {
0225     if (item == nullptr) {
0226         return (ImageFormat(""));
0227     }
0228 
0229     const KFileItem *kfi = item->fileItem();
0230     if (kfi->isNull()) {
0231         return (ImageFormat(""));
0232     }
0233 
0234     // Check that this is a plausible image format (MIME type = "image/anything")
0235     // before trying to get the image type.
0236     QString mimetype = kfi->mimetype();
0237     if (!mimetype.startsWith("image/")) {
0238         return (ImageFormat(""));
0239     }
0240 
0241     return (ImageFormat::formatForUrl(kfi->url()));
0242 }
0243 
0244 
0245 static ScanImage::Ptr imageForItem(const FileTreeViewItem *item)
0246 {
0247     if (item==nullptr) return (nullptr);        // get loaded image if any
0248     return (item->data(0, Qt::UserRole).value<ScanImage::Ptr>());
0249 }
0250 
0251 
0252 void ScanGallery::slotItemHighlighted(QTreeWidgetItem *curr)
0253 {
0254     if (curr==nullptr)
0255     {
0256         QList<QTreeWidgetItem *> selItems = selectedItems();
0257         if (!selItems.isEmpty()) curr = selItems.first();
0258     }
0259     FileTreeViewItem *item = static_cast<FileTreeViewItem *>(curr);
0260     if (item==nullptr) return;
0261 
0262     if (item->isDir()) emit showImage(nullptr, true);   // clear displayed image
0263     else
0264     {
0265         ScanImage::Ptr img = imageForItem(item);
0266         emit showImage(img, false);         // clear or redisplay image
0267     }
0268 
0269     emit itemHighlighted(item->url(), item->isDir());
0270 }
0271 
0272 
0273 void ScanGallery::slotItemActivated(QTreeWidgetItem *curr)
0274 {
0275     FileTreeViewItem *item = static_cast<FileTreeViewItem *>(curr);
0276     qCDebug(KOOKA_LOG) << item->url();
0277 
0278     //  Check if directory, hide image for now, later show a thumb view?
0279     if (item->isDir()) {                // is it a directory?
0280         emit showImage(nullptr, true);            // unload current image
0281     } else {                    // not a directory
0282         //  Load the image if necessary. This is done by loadImageForItem,
0283         //  which is async (TODO). The image finally arrives in slotImageArrived.
0284         QApplication::setOverrideCursor(Qt::WaitCursor);
0285         emit aboutToShowImage(item->url());
0286         loadImageForItem(item);
0287         QApplication::restoreOverrideCursor();
0288     }
0289 
0290     //  Notify the new directory, if it has changed
0291     QUrl newDir = itemDirectory(item);
0292     if (m_currSelectedDir != newDir) {
0293         m_currSelectedDir = newDir;
0294         emit galleryPathChanged(item->branch(), itemDirectoryRelative(item));
0295     }
0296 }
0297 
0298 // These 2 slots are called when an item is clicked/activated in the thumbnail view.
0299 
0300 void ScanGallery::slotHighlightItem(const QUrl &url)
0301 {
0302     qCDebug(KOOKA_LOG) << url;
0303 
0304     FileTreeViewItem *found = findItemByUrl(url);
0305     if (found == nullptr) {
0306         return;
0307     }
0308 
0309     bool b = blockSignals(true);
0310     scrollToItem(found);
0311     setCurrentItem(found);
0312     blockSignals(b);
0313 
0314     // Need to do this to update/clear the displayed image.  Causes a signal
0315     // to be sent back to the thumbnail view, but this is benign and fortunately
0316     // does not cause not a loop.
0317     slotItemHighlighted(found);
0318 }
0319 
0320 void ScanGallery::slotActivateItem(const QUrl &url)
0321 {
0322     qCDebug(KOOKA_LOG) << url;
0323 
0324     FileTreeViewItem *found = findItemByUrl(url);
0325     if (found == nullptr) return;
0326     slotItemActivated(found);
0327 }
0328 
0329 // This slot is called when an image has been changed by some external means
0330 // (e.g. an image transformation).  The item image is reloaded only if it
0331 // is still currently selected.
0332 
0333 void ScanGallery::slotUpdatedItem(const QUrl &url)
0334 {
0335     FileTreeViewItem *found = findItemByUrl(url);
0336     if (found == nullptr) {
0337         return;
0338     }
0339 
0340     if (found->isSelected()) {              // only if still selected
0341         slotUnloadItem(found);              // ensure unloaded for updating
0342         slotItemActivated(found);           // load the new image
0343     }
0344 }
0345 
0346 void ScanGallery::slotDirCount(FileTreeViewItem *item, int cnt)
0347 {
0348     if (item == nullptr) {
0349         return;
0350     }
0351     if (!item->isDir()) {
0352         return;
0353     }
0354 
0355     int imgCount = 0;                   // total these separately,
0356     int dirCount = 0;                   // we want individual counts
0357     int fileCount = 0;                  // for files and subfolders
0358 
0359     for (int i = 0; i < item->childCount(); ++i) {
0360         FileTreeViewItem *ci = static_cast<FileTreeViewItem *>(item->child(i));
0361         if (ci->isDir()) {
0362             ++dirCount;
0363         } else {
0364             if (ImageFormat::formatForMime(ci->fileItem()->determineMimeType()).isValid()) {
0365                 ++imgCount;
0366             } else {
0367                 ++fileCount;
0368             }
0369         }
0370     }
0371 
0372     QString cc = "";
0373     if (dirCount == 0) {
0374         if ((imgCount + fileCount) == 0) {
0375             cc = i18n("empty");
0376         } else {
0377             if (fileCount == 0) {
0378                 cc = i18np("one image", "%1 images", imgCount);
0379             } else {
0380                 cc = i18np("one file", "%1 files", (imgCount + fileCount));
0381             }
0382 
0383         }
0384     } else {
0385         if (fileCount > 0) {
0386             cc = i18np("one file, ", "%1 files, ", (imgCount + fileCount));
0387         } else if (imgCount > 0) {
0388             cc = i18np("one image, ", "%1 images, ", imgCount);
0389         }
0390 
0391         cc += i18np("1 folder", "%1 folders", dirCount);
0392     }
0393 
0394     item->setText(1, (" " + cc));
0395 }
0396 
0397 void ScanGallery::slotItemExpanded(QTreeWidgetItem *item)
0398 {
0399     if (item == nullptr) {
0400         return;
0401     }
0402     if (!(static_cast<FileTreeViewItem *>(item))->isDir()) {
0403         return;
0404     }
0405 
0406     for (int i = 0; i < item->childCount(); ++i) {
0407         FileTreeViewItem *ci = static_cast<FileTreeViewItem *>(item->child(i));
0408         if (ci->isDir() && !ci->alreadyListed()) {
0409             ci->branch()->populate(ci->url(), ci);
0410         }
0411     }
0412 }
0413 
0414 void ScanGallery::slotDecorate(FileTreeViewItem *item)
0415 {
0416     if (item == nullptr) return;
0417 
0418 #ifdef DEBUG_LOADING
0419     qCDebug(KOOKA_LOG) << item->url();
0420 #endif // DEBUG_LOADING
0421     const bool isSubImage = item->url().hasFragment();  // is this a sub-image?
0422 
0423     if (!item->isDir())                 // directories are done elsewhere
0424     {
0425         ImageFormat format = getImgFormat(item);    // this is safe for any file
0426         if (!isSubImage)                // no format for subimages
0427         {
0428             item->setText(2, (QString(" %1 ").arg(format.name())));
0429         }
0430 
0431         ScanImage::Ptr img = imageForItem(item);
0432         if (!img.isNull())              // image is loaded
0433         {
0434             // set image depth pixmap as appropriate
0435             QIcon icon;
0436             if (img->depth() == 1) icon = ScanIcons::self()->icon(ScanIcons::BlackWhite);
0437             else
0438             {
0439                 if (img->isGrayscale()) icon = ScanIcons::self()->icon(ScanIcons::Greyscale);
0440                 else icon = ScanIcons::self()->icon(ScanIcons::Colour);
0441             }
0442             item->setIcon(0, icon);
0443 
0444             if (img->subImagesCount() == 0)     // size except for containers
0445             {
0446                 QString t = i18n(" %1 x %2", img->width(), img->height());
0447                 item->setText(1, t);
0448             }
0449         }
0450         else                        // image not loaded, show file info
0451         {
0452             if (format.isValid())           // if a valid image file
0453             {
0454                 if (isSubImage)             // subimages don't show size
0455                 {
0456                     item->setIcon(0, QIcon::fromTheme("edit-copy"));
0457                     item->setText(1, QString());
0458                 }
0459                 else
0460                 {
0461                     item->setIcon(0, QIcon::fromTheme("media-floppy"));
0462                     const KFileItem *kfi = item->fileItem();
0463                     if (!kfi->isNull()) item->setText(1, (" " + KIO::convertSize(kfi->size())));
0464                 }
0465             }
0466             else                    // not an image file
0467             {                       // show its standard MIME type
0468                 item->setIcon(0, QIcon::fromTheme(KIO::iconNameForUrl(item->url()),
0469                                                   QIcon::fromTheme("application-octet-stream")));
0470             }
0471         }
0472     }
0473 
0474     // This code is quite similar to m_nextUrlToSelect in FileTreeView::slotNewTreeViewItems
0475     // When scanning a new image, we wait for the KDirLister to notice the new file,
0476     // and then we have the FileTreeViewItem that we need to display the image.
0477     if (!m_nextUrlToShow.isEmpty()) {
0478         if (m_nextUrlToShow.adjusted(QUrl::StripTrailingSlash) ==
0479             item->url().adjusted(QUrl::StripTrailingSlash)) {
0480             m_nextUrlToShow = QUrl();           // do this first to prevent recursion
0481             slotItemActivated(item);
0482             setCurrentItem(item);           // necessary in case of new file from D&D
0483         }
0484     }
0485 }
0486 
0487 void ScanGallery::slotDecorate(FileTreeBranch *branch, const FileTreeViewItemList &list)
0488 {
0489 #ifdef DEBUG_LOADING
0490     qCDebug(KOOKA_LOG) << "count" << list.count();
0491 #endif // DEBUG_LOADING
0492     for (FileTreeViewItemList::const_iterator it = list.constBegin();
0493             it != list.constEnd(); ++it) {
0494         FileTreeViewItem *ftvi = (*it);
0495         slotDecorate(ftvi);
0496         emit fileChanged(ftvi->fileItem());
0497     }
0498 }
0499 
0500 void ScanGallery::updateParent(const FileTreeViewItem *curr)
0501 {
0502     QUrl dir = itemDirectory(curr);
0503     if (curr->isDir())
0504     {
0505         // Get the actual parent of the directory.  itemDirectory() above
0506         // will, for a directory, ensure that its path ends with a slash.
0507         // So do not combine the two calls below, it is first necessary to
0508         // remove that trailing slash again and then remove the file name,
0509         // going up to the parent directory.  Combining the two flags will
0510         // do those adjustments in the wrong order, first removing the file
0511         // name (which will be empty, therefore doing nothing) and then the
0512         // trailing slash.  This effectively leaves the original directory
0513         // URL unchanged.
0514         dir = dir.adjusted(QUrl::StripTrailingSlash);
0515         dir = dir.adjusted(QUrl::RemoveFilename);
0516     }
0517 
0518 #ifdef DEBUG_LOADING
0519     qCDebug(KOOKA_LOG) << "Updating directory" << dir;
0520 #endif // DEBUG_LOADING
0521 
0522     FileTreeBranch *branch = curr->branch();        // it should have one
0523     branch->updateDirectory(dir);
0524 
0525     FileTreeViewItem *parent = branch->findItemByUrl(dir);
0526     if (parent != nullptr) parent->setExpanded(true);   // ensure parent is expanded
0527 }
0528 
0529 // "Rename" action triggered in the GUI
0530 
0531 void ScanGallery::slotRenameItems()
0532 {
0533     FileTreeViewItem *curr = highlightedFileTreeViewItem();
0534     if (curr != nullptr) {
0535         editItem(curr, 0);
0536     }
0537 }
0538 
0539 // Renaming has finished
0540 
0541 bool ScanGallery::slotFileRenamed(FileTreeViewItem *item, const QString &newName)
0542 {
0543     if (item->isRoot()) return (false);         // cannot rename root here
0544 
0545     QUrl urlFrom = item->url();
0546     QString oldName = urlFrom.fileName();
0547 
0548     QUrl urlTo(urlFrom.resolved(QUrl(newName)));
0549 
0550     /* clear selection, because the renamed image comes in through
0551      * kdirlister again
0552      */
0553     // slotUnloadItem(item);                // unnecessary, bug 68532
0554     // because of "note new URL" below
0555     qCDebug(KOOKA_LOG) << "Renaming " << urlFrom << "->" << urlTo;
0556 
0557     //setSelected(item,false);
0558 
0559     bool success = ImgSaver::renameImage(urlFrom, urlTo, true, this);
0560     if (success) {                  // rename the file
0561         item->setUrl(urlTo);                // note new URL
0562         emit fileRenamed(item->fileItem(), newName);
0563     } else {
0564         qCWarning(KOOKA_LOG) << "renaming failed";
0565         item->setText(0, oldName);          // restore original name
0566     }
0567 
0568 //    setSelected(item,true);               // restore the selection
0569     return (success);
0570 }
0571 
0572 // TODO: this function does not appear to be used
0573 /* ----------------------------------------------------------------------- */
0574 /*
0575  * Method that checks if the new filename a user enters while renaming an image is valid.
0576  * It checks for a proper extension.
0577  */
0578 
0579 static QString buildNewFilename(const QString &cmplFilename, const ImageFormat &currFormat)
0580 {
0581     /* cmplFilename = new name the user wishes.
0582      * currFormat   = the current format of the image.
0583      * if the new filename has a valid extension, which is the same as the
0584      * format of the current, fine. A ''-String has to be returned.
0585      */
0586     QFileInfo fiNew(cmplFilename);
0587     QString base = fiNew.baseName();
0588     QString newExt = fiNew.suffix().toLower();
0589     QString nowExt = currFormat.extension();
0590     QString ext = "";
0591 
0592     qCDebug(KOOKA_LOG) << "Filename wanted:" << cmplFilename << "ext" << nowExt << "->" << newExt;
0593 
0594     if (newExt.isEmpty()) {
0595         /* ok, fine -> return the currFormat-Extension */
0596         ext = base + "." + nowExt;
0597     } else if (newExt == nowExt) {
0598         /* also good, no reason to put another extension */
0599         ext = cmplFilename;
0600     } else {
0601         /* new Ext. differs from the current extension. Later. */
0602         KMessageBox::information(nullptr, i18n("You entered a file extension that differs from the existing one. That is not yet possible. Converting 'on the fly' is planned for a future release.\n"
0603                                                "Kooka corrects the extension."),
0604                                  i18n("On the Fly Conversion"));
0605         ext = base + "." + nowExt;
0606     }
0607     return (ext);
0608 }
0609 
0610 /* ----------------------------------------------------------------------- */
0611 /* The absolute URL of the item (if it is a directory), or its parent (if
0612    it is a file).
0613 */
0614 QUrl ScanGallery::itemDirectory(const FileTreeViewItem *item) const
0615 {
0616     if (item == nullptr) return (QUrl());
0617 
0618     QUrl u = item->url();
0619     if (!item->isDir()) {
0620         u = u.adjusted(QUrl::RemoveFilename);       // not a directory, remove file name
0621     } else {
0622         u = u.adjusted(QUrl::StripTrailingSlash);   // is a directory, ensure ends with "/"
0623         u.setPath(u.path()+'/');
0624     }
0625     return (u);
0626 }
0627 
0628 /* ----------------------------------------------------------------------- */
0629 /* As above, but relative to the root of its branch.  The result does not
0630    begin with a leading slash, except that a single "/" means the root.
0631    If there is some problem (no branch, or the root/item URLs do not match),
0632    the full path is returned.
0633 */
0634 QString ScanGallery::itemDirectoryRelative(const FileTreeViewItem *item) const
0635 {
0636     const QUrl u = itemDirectory(item);
0637     const FileTreeBranch *branch = item->branch();
0638     if (branch == nullptr) {
0639         return (u.path());    // no branch, can this ever happen?
0640     }
0641 
0642     QString rootUrl = branch->rootUrl().url(QUrl::StripTrailingSlash)+'/';
0643     QString itemUrl = u.url();
0644     //qCDebug(KOOKA_LOG) << "itemurl" << itemUrl << "rooturl" << rootUrl;
0645     if (itemUrl.startsWith(rootUrl)) {
0646         itemUrl.remove(0, rootUrl.length());        // remove root URL prefix
0647         //qCDebug(KOOKA_LOG) << "->" << itemUrl;
0648         if (itemUrl.isEmpty()) {
0649             itemUrl = "/";    // it is the root
0650         }
0651         //qCDebug(KOOKA_LOG) << "->" << itemUrl;
0652     } else {
0653         qCWarning(KOOKA_LOG) << "item URL" << itemUrl << "does not start with root URL" << rootUrl;
0654     }
0655 
0656     return (itemUrl);
0657 }
0658 
0659 /* ----------------------------------------------------------------------- */
0660 /* This slot receives a string from the gallery-path combobox shown under the
0661  * image gallery, the relative directory under the branch.  Now it is to assemble
0662  * a complete path from the data, find out the FileTreeViewItem associated
0663  * with it and call slotClicked with it.
0664  */
0665 
0666 void ScanGallery::slotSelectDirectory(const QString &branchName, const QString &relPath)
0667 {
0668     qCDebug(KOOKA_LOG) << "branch" << branchName << "path" << relPath;
0669 
0670     FileTreeViewItem *item;
0671     if (!branchName.isEmpty())              // find in specified branch
0672     {
0673         item = findItemInBranch(branchName, relPath);
0674     }
0675     else                        // assume the 1st/only branch
0676     {
0677         item = findItemInBranch(branches().at(0), relPath);
0678     }
0679     if (item == nullptr) return;            // not found in branch
0680 
0681     scrollToItem(item);
0682     setCurrentItem(item);
0683     slotItemActivated(item);                // load thumbnails, etc.
0684 }
0685 
0686 void ScanGallery::loadImageForItem(FileTreeViewItem *item)
0687 {
0688     if (item == nullptr) return;
0689     const KFileItem *kfi = item->fileItem();
0690     if (kfi->isNull()) return;
0691 
0692 #ifdef DEBUG_LOADING
0693     qCDebug(KOOKA_LOG) << "loading" << item->url();
0694 #endif // DEBUG_LOADING
0695     QString ret;                    // no error so far
0696 
0697     ImageFormat format = getImgFormat(item);        // check for valid image format
0698     if (!format.isValid())
0699     {
0700         ret = i18n("Not a supported image format");
0701     }
0702     else                        // valid image
0703     {
0704         ScanImage::Ptr img = imageForItem(item);
0705         if (img.isNull())               // image not already loaded
0706         {
0707 #ifdef DEBUG_LOADING
0708             qCDebug(KOOKA_LOG) << "need to load image";
0709 #endif // DEBUG_LOADING
0710 
0711             // The image needs to be loaded. Possibly it is a multi-page image.
0712             // If it is, the ScanImage has a subImageCount larger than one. We
0713             // create an subimage item for every subimage, but do not yet load
0714             // them.
0715 
0716             img.reset(new ScanImage(item->url()));
0717             if (img->errorString().isEmpty())       // image loaded OK
0718             {
0719                 if (img->subImagesCount()>1)        // see if it has subimages
0720                 {
0721 #ifdef DEBUG_LOADING
0722                     qCDebug(KOOKA_LOG) << "subimage count" << img->subImagesCount();
0723 #endif // DEBUG_LOADING
0724                     if (item->childCount()==0)      // check not already created
0725                     {
0726 #ifdef DEBUG_LOADING
0727                         qCDebug(KOOKA_LOG) << "need to create subimages";
0728 #endif // DEBUG_LOADING
0729                         // Create items for each subimage
0730                         QIcon subImgIcon = QIcon::fromTheme("edit-copy");
0731 
0732                         // Sub-images start counting from 1, ScanImage adjusts
0733                         // that back to the 0-based TIFF directory index.
0734                         for (int i = 1; i<=img->subImagesCount(); i++)
0735                         {
0736                             KFileItem newKfi(*kfi);
0737 
0738                             // Set the URL to mark this as a subimage.  The subimage
0739                             // number is set as the URL fragment;  this is detected by
0740                             // ScanImage::loadFromUrl() and used to extract the
0741                             // submimage.
0742                             QUrl u = newKfi.url();
0743                             u.setFragment(QString::number(i));
0744                             newKfi.setUrl(u);
0745 
0746                             // Create the item without a parent and then
0747                             // add it to the parent item later, so that
0748                             // the setText() below does not trigger a rename.
0749                             FileTreeViewItem *subImgItem = new FileTreeViewItem(
0750                                 static_cast<FileTreeViewItem *>(nullptr), newKfi, item->branch());
0751 
0752                             subImgItem->setText(0, i18n("Sub-image %1", i));
0753                             subImgItem->setIcon(0, subImgIcon);
0754                             item->addChild(subImgItem);
0755                         }
0756                     }
0757                 }
0758             }
0759             else
0760             {                       // image loading failed
0761                 qCDebug(KOOKA_LOG) << "Failed to load image," << img->errorString();
0762                 img.clear();                // don't try to use it below
0763             }
0764         }
0765 #ifdef DEBUG_LOADING
0766         else qCDebug(KOOKA_LOG) << "have an image already";
0767 #endif // DEBUG_LOADING
0768 
0769         if (!img.isNull())              // already loaded, or loaded above
0770         {
0771             slotImageArrived(item, img);        // display the image
0772         }
0773     }
0774 
0775     if (!ret.isEmpty())                 // image loading failed
0776     {
0777         KMessageBox::error(this,
0778                            xi18nc("@info", "Unable to load the image <filename>%2</filename><nl/>%1",
0779                                   ret, item->url().url(QUrl::PreferLocalFile)),
0780                            i18n("Image Load Error"));
0781     }
0782 }
0783 
0784 
0785 /* Hit this slot with a file for a kfiletreeviewitem. */
0786 void ScanGallery::slotImageArrived(FileTreeViewItem *item, ScanImage::Ptr img)
0787 {
0788     if (item==nullptr || img.isNull()) return;
0789                             // note image for item
0790     item->setData(0, Qt::UserRole, QVariant::fromValue(img));
0791     slotDecorate(item);
0792     emit showImage(img, false);
0793 }
0794 
0795 
0796 ScanImage::Ptr ScanGallery::getCurrImage(bool loadOnDemand)
0797 {
0798     FileTreeViewItem *curr = highlightedFileTreeViewItem();
0799     if (curr==nullptr) return (nullptr);        // no current item
0800     if (curr->isDir()) return (nullptr);        // is a directory
0801 
0802     ScanImage::Ptr img = imageForItem(curr);        // see if already loaded
0803     if (img.isNull())                   // no, try to do that
0804     {
0805         if (!loadOnDemand) return (nullptr);        // not loaded, and don't want to
0806         slotItemActivated(curr);            // select/load this image
0807         img = imageForItem(curr);           // and get image for it
0808     }
0809 
0810     return (img);
0811 }
0812 
0813 
0814 QString ScanGallery::currentImageFileName() const
0815 {
0816     QString result = "";
0817 
0818     const FileTreeViewItem *curr = highlightedFileTreeViewItem();
0819     if (curr==nullptr) return (QString());
0820 
0821     bool isLocal = false;
0822     const QUrl u = curr->fileItem()->mostLocalUrl(&isLocal);
0823     if (!isLocal) return (QString());
0824     return (u.toLocalFile());
0825 }
0826 
0827 
0828 bool ScanGallery::prepareToSave(ScanImage::ImageType type)
0829 {
0830     qCDebug(KOOKA_LOG) << "type" << type;
0831 
0832     delete mSaver; mSaver = nullptr;            // recreate a clean instance
0833 
0834     // Resolve where to save the new image when it arrives
0835     FileTreeViewItem *curr = highlightedFileTreeViewItem();
0836     if (curr==nullptr)                  // into root if nothing is selected
0837     {
0838         FileTreeBranch *branch = branches().at(0);  // there should be at least one
0839         if (branch!=nullptr)
0840         {
0841             // if user has created this????
0842             curr = findItemInBranch(branch, i18n("Incoming/"));
0843             if (curr==nullptr) curr = branch->root();
0844         }
0845 
0846         if (curr==nullptr) return (false);      // should never happen
0847         curr->setSelected(true);
0848     }
0849 
0850     mSavedTo = curr;                    // note for selecting later
0851 
0852     // Create the saver to use after the scan is complete
0853     QUrl dir(itemDirectory(curr));          // where new image will go
0854     mSaver = new ImgSaver(dir);             // create saver to use later
0855     // Pass the initial image information to the saver
0856     ImgSaver::ImageSaveStatus stat = mSaver->setImageInfo(type);
0857     if (stat==ImgSaver::SaveStatusCanceled) return (false);
0858 
0859     return (true);                  // all ready to save
0860 }
0861 
0862 QUrl ScanGallery::saveURL() const
0863 {
0864     if (mSaver == nullptr) {
0865         return (QUrl());
0866     }
0867     // TODO: relative to root
0868     return (mSaver->saveURL());
0869 }
0870 
0871 /* ----------------------------------------------------------------------- */
0872 /* This slot takes a new scanned Picture and saves it.  */
0873 
0874 void ScanGallery::addImage(ScanImage::Ptr img)
0875 {
0876     if (img.isNull()) return;               // no image to add
0877                             // if not done already
0878     if (mSaver==nullptr) prepareToSave(ScanImage::None);
0879     if (mSaver==nullptr) return;            // should never happen
0880 
0881     ImgSaver::ImageSaveStatus isstat = mSaver->saveImage(img);
0882                             // try to save the image
0883     const QUrl lurl = mSaver->lastURL();        // find out where it ended up
0884 
0885     if (isstat!=ImgSaver::SaveStatusOk &&       // image saving failed
0886         isstat!=ImgSaver::SaveStatusCanceled)       // user cancelled, just ignore
0887     {
0888         KMessageBox::error(this, xi18nc("@info", "Could not save the image<nl/><filename>%2</filename><nl/>%1",
0889                                         mSaver->errorString(isstat),
0890                                         lurl.toDisplayString(QUrl::PreferLocalFile)),
0891                            i18n("Image Save Error"));
0892     }
0893 
0894     delete mSaver; mSaver = nullptr;            // now finished with this
0895 
0896     if (isstat==ImgSaver::SaveStatusOk)         // image was saved,
0897     {                           // select the new image
0898         slotSetNextUrlToSelect(lurl);
0899         m_nextUrlToShow = lurl;
0900         if (mSavedTo!=nullptr) updateParent(mSavedTo);
0901     }
0902 }
0903 
0904 // Selects and loads the image with the given URL. This is used to restore the
0905 // last displayed image on startup.
0906 
0907 void ScanGallery::slotSelectImage(const QUrl &url)
0908 {
0909     FileTreeViewItem *found = findItemByUrl(url);
0910     if (found == nullptr) {
0911         found = m_defaultBranch->root();
0912     }
0913 
0914     scrollToItem(found);
0915     setCurrentItem(found);
0916     slotItemActivated(found);
0917 }
0918 
0919 FileTreeViewItem *ScanGallery::findItemByUrl(const QUrl &url, FileTreeBranch *branch)
0920 {
0921     QUrl u(url);
0922     if (u.scheme() == "file") {             // for local files,
0923         QDir d(url.path());             // ensure path is canonical
0924         u.setPath(d.canonicalPath());
0925     }
0926     //qCDebug(KOOKA_LOG) << "URL search for" << u;
0927 
0928     // Prepare a list of branches to search.  If the parameter 'branch'
0929     // is set, search only in the specified branch. If it is nullptr, search
0930     // all branches.
0931     FileTreeBranchList branchList;
0932     if (branch != nullptr) {
0933         branchList.append(branch);
0934     } else {
0935         branchList = branches();
0936     }
0937 
0938     FileTreeViewItem *foundItem = nullptr;
0939     for (FileTreeBranchList::const_iterator it = branchList.constBegin();
0940             it != branchList.constEnd(); ++it) {
0941         FileTreeBranch *branchloop = (*it);
0942         FileTreeViewItem *ftvi = branchloop->findItemByUrl(u);
0943         if (ftvi != nullptr) {
0944             foundItem = ftvi;
0945             //qCDebug(KOOKA_LOG) << "found item for" << ftvi->url();
0946             break;
0947         }
0948     }
0949 
0950     return (foundItem);
0951 }
0952 
0953 void ScanGallery::slotExportFile()
0954 {
0955     FileTreeViewItem *curr = highlightedFileTreeViewItem();
0956     if (curr == nullptr) {
0957         return;
0958     }
0959 
0960     if (curr->isDir()) {
0961         qCDebug(KOOKA_LOG) << "Not yet implemented!";
0962         return;
0963     }
0964 
0965     QUrl fromUrl(curr->url());
0966 
0967     QString filter;
0968     ImageFormat format = getImgFormat(curr);
0969     if (format.isValid()) filter = format.mime().filterString();
0970     else filter = i18n("All Files (*)");
0971 
0972     RecentSaver saver("exportImage");
0973     QUrl fileName = QFileDialog::getSaveFileUrl(this, i18nc("@title:window", "Export Image"),
0974                                                 saver.recentUrl(fromUrl.fileName()), filter);
0975     if (!fileName.isValid()) return;            // didn't get a file name
0976     if (fileName==fromUrl) return;          // can't save over myself
0977     saver.save(fileName);
0978 
0979     // Since the copy operation is asynchronous,
0980     // we will never know if it succeeds.
0981     ImgSaver::copyImage(fromUrl, fileName);
0982 }
0983 
0984 void ScanGallery::slotImportFile()
0985 {
0986     FileTreeViewItem *curr = highlightedFileTreeViewItem();
0987     if (curr==nullptr) return;
0988 
0989     QUrl impTarget = curr->url();
0990     if (!curr->isDir()) {
0991         FileTreeViewItem *pa = static_cast<FileTreeViewItem *>(curr->parent());
0992         impTarget = pa->url();
0993     }
0994 
0995     QString filter = ImageFilter::qtFilterString(ImageFilter::Reading, ImageFilter::AllImages|ImageFilter::AllFiles);
0996 
0997     RecentSaver saver("importImage");
0998     QUrl impUrl = QFileDialog::getOpenFileUrl(this, i18n("Import Image File to Gallery"),
0999                                               saver.recentUrl(), filter);
1000     if (!impUrl.isValid()) return;
1001     saver.save(impUrl);
1002                             // use the name of the source file
1003     impTarget = impTarget.resolved(QUrl(impUrl.fileName()));
1004     m_nextUrlToShow = impTarget;
1005     qCDebug(KOOKA_LOG) << "Importing" << impUrl << "->" << impTarget;
1006     ImgSaver::copyImage(impUrl, impTarget);
1007 }
1008 
1009 void ScanGallery::slotUrlsDropped(QDropEvent *ev, FileTreeViewItem *item)
1010 {
1011     QList<QUrl> urls = ev->mimeData()->urls();
1012     if (urls.isEmpty()) {
1013         return;
1014     }
1015 
1016     qCDebug(KOOKA_LOG) << "onto" << (item == nullptr ? "(null)" : item->url().toDisplayString())
1017                        << "srcs" << urls.count() << "first" << urls.first();
1018 
1019     if (item == nullptr) return;
1020     QUrl dest = item->url();
1021 
1022     // Check whether the drop is on top of a directory (in which case we
1023     // want to move/copy into it) or a file (move/copy into its containing
1024     // directory).
1025     if (!item->isDir()) dest = dest.adjusted(QUrl::RemoveFilename);
1026     qCDebug(KOOKA_LOG) << "resolved destination" << dest;
1027 
1028     // Make the last URL to copy the one to select next
1029     QUrl nextSel = dest.resolved(QUrl(urls.back().fileName()));
1030     m_nextUrlToShow = nextSel;
1031 
1032     KIO::Job *job;
1033     // TODO: top level window as 3rd parameter?
1034     if (ev->dropAction() == Qt::MoveAction) {
1035         job = KIO::move(urls, dest);
1036     } else {
1037         job = KIO::copy(urls, dest);
1038     }
1039     connect(job, &KJob::result, this, &ScanGallery::slotJobResult);
1040 }
1041 
1042 void ScanGallery::slotJobResult(KJob *job)
1043 {
1044     if (job->error()) job->uiDelegate()->showErrorMessage();
1045 }
1046 
1047 /* ----------------------------------------------------------------------- */
1048 void ScanGallery::slotUnloadItems()
1049 {
1050     FileTreeViewItem *curr = highlightedFileTreeViewItem();
1051     emit showImage(nullptr, false);
1052     slotUnloadItem(curr);
1053 }
1054 
1055 
1056 void ScanGallery::slotUnloadItem(FileTreeViewItem *curr)
1057 {
1058     if (curr==nullptr) return;
1059 
1060     if (curr->isDir())                  // is a directory
1061     {
1062         for (int i = 0; i<curr->childCount(); ++i)
1063         {
1064             FileTreeViewItem *child = static_cast<FileTreeViewItem *>(curr->child(i));
1065             slotUnloadItem(child);          // recursively unload contents
1066         }
1067     }
1068     else                        // is a file/image
1069     {
1070         ScanImage::Ptr img = imageForItem(curr);
1071         if (img.isNull()) return;           // nothing to unload
1072 
1073         if (img->subImagesCount()>0)            // image with subimages
1074         {
1075             while (curr->childCount()>0)        // recursively unload subimages
1076             {
1077                 FileTreeViewItem *child = static_cast<FileTreeViewItem *>(curr->takeChild(0));
1078                 slotUnloadItem(child);
1079                 delete child;
1080             }
1081         }
1082 
1083         emit unloadImage(img);
1084                             // unreference image from item
1085         curr->setData(0, Qt::UserRole, QVariant::fromValue(nullptr));
1086         slotDecorate(curr);
1087     }
1088 }
1089 
1090 void ScanGallery::slotItemProperties()
1091 {
1092     FileTreeViewItem *curr = highlightedFileTreeViewItem();
1093     if (curr == nullptr) {
1094         return;
1095     }
1096     KPropertiesDialog::showDialog(curr->url(), this);
1097 }
1098 
1099 /* ----------------------------------------------------------------------- */
1100 
1101 void ScanGallery::slotDeleteItems()
1102 {
1103     FileTreeViewItem *curr = highlightedFileTreeViewItem();
1104     if (curr == nullptr) return;
1105 
1106     QUrl urlToDel = curr->url();            // item to be deleted
1107     bool isDir = curr->isDir();             // deleting a folder?
1108     QTreeWidgetItem *nextToSelect = curr->treeWidget()->itemBelow(curr);
1109                             // select this afterwards
1110     QString s;
1111     QString dontAskKey;
1112     if (isDir) {
1113         s = xi18nc("@info", "Do you really want to permanently delete the folder<nl/>"
1114                    "<filename>%1</filename><nl/>"
1115                    "and all of its contents? It cannot be restored.", urlToDel.url(QUrl::PreferLocalFile));
1116         dontAskKey = "AskForDeleteDirs";
1117     } else {
1118         s = xi18nc("@info", "Do you really want to permanently delete the image<nl/>"
1119                    "<filename>%1</filename>?<nl/>"
1120                    "It cannot be restored.", urlToDel.url(QUrl::PreferLocalFile));
1121         dontAskKey = "AskForDeleteFiles";
1122     }
1123 
1124     if (KMessageBox::warningContinueCancel(this, s,
1125                                            i18n("Delete Gallery Item"),
1126                                            KStandardGuiItem::del(),
1127                                            KStandardGuiItem::cancel(),
1128                                            dontAskKey) != KMessageBox::Continue) {
1129         return;
1130     }
1131 
1132     slotUnloadItem(curr);               // unload item, possibly recursively
1133 
1134     qCDebug(KOOKA_LOG) << "Deleting" << urlToDel;
1135     KIO::DeleteJob *job = KIO::del(urlToDel);
1136     if (!job->exec())                   // do the deletion
1137     {
1138         KMessageBox::error(this, xi18nc("@info", "Could not delete the image or folder<nl/><filename>%2</filename><nl/>%1",
1139                                         job->errorString(),
1140                                         urlToDel.toDisplayString(QUrl::PreferLocalFile)),
1141                            i18n("File Delete Error"));
1142         return;
1143     }
1144 
1145     updateParent(curr);                 // update parent folder count
1146     if (isDir)                      // remove from the name combo
1147     {
1148         emit galleryDirectoryRemoved(curr->branch(), itemDirectoryRelative(curr));
1149     }
1150 
1151 #if 0
1152     if (nextToSelect != nullptr) {
1153         setSelected(nextToSelect, true);
1154     }
1155     //  TODO: if doing the above, also need to signal to update thumbnail
1156     //  as below.
1157     //
1158     //  But doing that leads to inconsistency between deleting the last item
1159     //  in a folder (nothing is selected afterwards) and deleting anything
1160     //  else (the next image is selected and loaded).  So leaving this
1161     //  commented out for now.
1162     curr = highlightedFileTreeViewItem();
1163     //qCDebug(KOOKA_LOG) << "new selection after delete" << (curr == nullptr ? "nullptr" : curr->url().prettyURL());
1164     if (curr != nullptr) {
1165         emit showItem(curr->fileItem());
1166     }
1167 #endif
1168 }
1169 
1170 /* ----------------------------------------------------------------------- */
1171 void ScanGallery::slotCreateFolder()
1172 {
1173     QString folder = QInputDialog::getText(this, i18n("New Folder"),
1174                                            i18n("Name for the new folder:"));
1175     if (folder.isEmpty()) return;
1176 
1177     FileTreeViewItem *item = highlightedFileTreeViewItem();
1178     if (item==nullptr) return;
1179 
1180     // The GUI ensures that the action is only enabled if the current
1181     // item is a directory.  Hence, we can assume that it is and ensure
1182     // that its path ends with a slash before setting the file name.
1183     QUrl url = item->url().adjusted(QUrl::StripTrailingSlash);
1184     url.setPath(url.path()+'/');
1185     url = url.resolved(QUrl(folder));
1186     qCDebug(KOOKA_LOG) << "Creating folder" << url;
1187 
1188     /* Since the new directory arrives in the packager in the newItems-slot, we set a
1189      * variable urlToSelectOnArrive here. The newItems-slot will honor it and select
1190      * the treeviewitem with that url.
1191      */
1192     slotSetNextUrlToSelect(url);
1193 
1194     KIO::MkdirJob *job = KIO::mkdir(url);
1195     if (!job->exec())
1196     {
1197         KMessageBox::error(this, xi18nc("@info", "Could not create the folder<nl/><filename>%2</filename><nl/>%1",
1198                                         job->errorString(), url.url(QUrl::PreferLocalFile)),
1199                            i18n("Folder Create Error"));
1200     }
1201 }
1202 
1203 void ScanGallery::setAllowRename(bool on)
1204 {
1205     qCDebug(KOOKA_LOG) << "to" << on;
1206     setEditTriggers(on ? QAbstractItemView::DoubleClicked : QAbstractItemView::NoEditTriggers);
1207 }