1 /************************************************************************
2  *									*
3  *  This file is part of Kooka, a scanning/OCR application using	*
4  *  Qt <http://www.qt.io> and KDE Frameworks <http://www.kde.org>.	*
5  *									*
6  *  Copyright (C) 1999-2016 Klaas Freitag <Klaas.Freitag@gmx.de>	*
7  *                          Jonathan Marten <jjm@keelhaul.me.uk>	*
8  *									*
9  *  Kooka is free software; you can redistribute it and/or modify it	*
10  *  under the terms of the GNU Library General Public License as	*
11  *  published by the Free Software Foundation and appearing in the	*
12  *  file COPYING included in the packaging of this file;  either	*
13  *  version 2 of the License, or (at your option) any later version.	*
14  *									*
15  *  As a special exception, permission is given to link this program	*
16  *  with any version of the KADMOS OCR/ICR engine (a product of		*
17  *  reRecognition GmbH, Kreuzlingen), and distribute the resulting	*
18  *  executable without including the source code for KADMOS in the	*
19  *  source distribution.						*
20  *									*
21  *  This program is distributed in the hope that it will be useful,	*
22  *  but WITHOUT ANY WARRANTY; without even the implied warranty of	*
23  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the	*
24  *  GNU General Public License for more details.			*
25  *									*
26  *  You should have received a copy of the GNU General Public		*
27  *  License along with this program;  see the file COPYING.  If		*
28  *  not, see <http://www.gnu.org/licenses/>.				*
29  *									*
30  ************************************************************************/
31 
32 #include "scangallery.h"
33 
34 #include <qfileinfo.h>
35 #include <qdir.h>
36 #include <qevent.h>
37 #include <qapplication.h>
38 #include <qheaderview.h>
39 #include <qmenu.h>
40 #include <qdebug.h>
41 #include <qinputdialog.h>
42 #include <qfiledialog.h>
43 #include <qmimedata.h>
44 
45 #include <kmessagebox.h>
46 #include <kpropertiesdialog.h>
47 #include <klocalizedstring.h>
48 #include <kstandardguiitem.h>
49 #include <kconfigskeleton.h>
50 
51 #include <kio/global.h>
52 #include <kio/copyjob.h>
53 #include <kio/deletejob.h>
54 #include <kio/mkdirjob.h>
55 #include <kio/pixmaploader.h>
56 #include <kio/jobuidelegate.h>
57 
58 #include "imgsaver.h"
59 #include "kookaimage.h"
60 #include "kookapref.h"
61 #include "kookasettings.h"
62 
63 #include "imagemetainfo.h"
64 #include "imagefilter.h"
65 #include "scanicons.h"
66 #include "recentsaver.h"
67 
68 
69 #undef DEBUG_LOADING
70 
71 
72 // FileTreeViewItem is not the same as KDE3's KFileTreeViewItem in that
73 // fileItem() used to return a KFileItem *, allowing the item to be modified
74 // through the pointer.  Now it returns a KFileItem which is a value copy of the
75 // internal one, not a pointer to it - so the internal KFileItem cannot
76 // be modified.  This means that we can't store information in the extra data
77 // of the KFileItem of a FileTreeViewItem.  Including, unfortunately, our
78 // KookaImage pointer :-(
79 //
80 // This is a consequence of commit 719513, "Making KFileItemList value based".
81 //
82 // We store the image pointer in the 'clientData' of the item (of the ported
83 // FileTreeView) instead.
84 // TODO: use FileTreeViewItem->data(Qt::UserRole)
85 
ScanGallery(QWidget * parent)86 ScanGallery::ScanGallery(QWidget *parent)
87     : FileTreeView(parent)
88 {
89     setObjectName("ScanGallery");
90 
91     //header()->setStretchEnabled(true,0);      // do we like this effect?
92 
93     setColumnCount(3);
94     setRootIsDecorated(false);
95     //setSortingEnabled(true);
96     //sortByColumn(0, Qt::AscendingOrder);
97     setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn);
98 
99     QStringList labels;
100     labels << i18n("Name");
101     labels << i18n("Size");
102     labels << i18n("Format");
103     setHeaderLabels(labels);
104 
105     headerItem()->setTextAlignment(0, Qt::AlignLeft);
106     headerItem()->setTextAlignment(1, Qt::AlignLeft);
107     headerItem()->setTextAlignment(2, Qt::AlignLeft);
108 
109     // Drag and Drop
110     setDragEnabled(true);               // allow drags out
111     setAcceptDrops(true);               // allow drops in
112     connect(this, SIGNAL(dropped(QDropEvent*,FileTreeViewItem*)),
113             SLOT(slotUrlsDropped(QDropEvent*,FileTreeViewItem*)));
114 
115     connect(this, SIGNAL(itemSelectionChanged()),
116             SLOT(slotItemHighlighted()));
117     connect(this, SIGNAL(itemActivated(QTreeWidgetItem*,int)),
118             SLOT(slotItemActivated(QTreeWidgetItem*)));
119     connect(this, SIGNAL(fileRenamed(FileTreeViewItem*,QString)),
120             SLOT(slotFileRenamed(FileTreeViewItem*,QString)));
121     connect(this, SIGNAL(itemExpanded(QTreeWidgetItem*)),
122             SLOT(slotItemExpanded(QTreeWidgetItem*)));
123 
124     m_startup = true;
125     m_currSelectedDir = QUrl();
126     mSaver = nullptr;
127     mSavedTo = nullptr;
128 
129     /* create a context menu and set the title */
130     m_contextMenu = new QMenu(this);
131     m_contextMenu->addSection(i18n("Gallery"));
132 }
133 
~ScanGallery()134 ScanGallery::~ScanGallery()
135 {
136     delete mSaver;
137     //qDebug();
138 }
139 
columnStatesKey(int forIndex)140 static QString columnStatesKey(int forIndex)
141 {
142     return (QString("GalleryState%1").arg(forIndex));
143 }
144 
saveHeaderState(int forIndex) const145 void ScanGallery::saveHeaderState(int forIndex) const
146 {
147     QString key = columnStatesKey(forIndex);
148     //qDebug() << "to" << key;
149     const KConfigSkeletonItem *ski = KookaSettings::self()->columnStatesItem();
150     Q_ASSERT(ski!=nullptr);
151     KConfigGroup grp = KookaSettings::self()->config()->group(ski->group());
152     grp.writeEntry(key, header()->saveState().toBase64());
153     grp.sync();
154 }
155 
restoreHeaderState(int forIndex)156 void ScanGallery::restoreHeaderState(int forIndex)
157 {
158     QString key = columnStatesKey(forIndex);
159     //qDebug() << "from" << key;
160     const KConfigSkeletonItem *ski = KookaSettings::self()->columnStatesItem();
161     Q_ASSERT(ski!=nullptr);
162     const KConfigGroup grp = KookaSettings::self()->config()->group(ski->group());
163 
164     QString state = grp.readEntry(key, "");
165     if (state.isEmpty()) return;
166 
167     QHeaderView *hdr = header();
168     // same workaround as needed in Akregator (even with Qt 4.6),
169     // see r918196 and r1001242 to kdepim/akregator/src/articlelistview.cpp
170     hdr->resizeSection(hdr->logicalIndex(hdr->count() - 1), 1);
171     hdr->restoreState(QByteArray::fromBase64(state.toLocal8Bit()));
172 }
173 
openRoots()174 void ScanGallery::openRoots()
175 {
176     /* standard root always exists, ImgRoot creates it */
177     QUrl rootUrl = QUrl::fromLocalFile(KookaPref::galleryRoot());
178     //qDebug() << "Standard root" << rootUrl.url();
179 
180     m_defaultBranch = openRoot(rootUrl, i18n("Kooka Gallery"));
181     m_defaultBranch->setOpen(true);
182 
183     /* open more configurable image repositories, configuration TODO */
184     //openRoot(KUrl(getenv("HOME")), i18n("Home Directory"));
185 }
186 
openRoot(const QUrl & root,const QString & title)187 FileTreeBranch *ScanGallery::openRoot(const QUrl &root, const QString &title)
188 {
189     FileTreeBranch *branch = addBranch(root, title);
190 
191     branch->setOpenPixmap(KIconLoader::global()->loadIcon("folder-image", KIconLoader::Small));
192     branch->setShowExtensions(true);
193 
194     setDirOnlyMode(branch, false);
195 
196     connect(branch, SIGNAL(newTreeViewItems(FileTreeBranch*,FileTreeViewItemList)),
197             SLOT(slotDecorate(FileTreeBranch*,FileTreeViewItemList)));
198 
199     connect(branch, SIGNAL(changedTreeViewItems(FileTreeBranch*,FileTreeViewItemList)),
200             SLOT(slotDecorate(FileTreeBranch*,FileTreeViewItemList)));
201 
202     connect(branch, SIGNAL(directoryChildCount(FileTreeViewItem*,int)),
203             SLOT(slotDirCount(FileTreeViewItem*,int)));
204 
205     connect(branch, SIGNAL(populateFinished(FileTreeViewItem*)),
206             SLOT(slotStartupFinished(FileTreeViewItem*)));
207 
208     return (branch);
209 }
210 
slotStartupFinished(FileTreeViewItem * item)211 void ScanGallery::slotStartupFinished(FileTreeViewItem *item)
212 {
213     if (!m_startup) {
214         return;    // already done
215     }
216     if (item != m_defaultBranch->root()) {
217         return;    // not the 1st branch root
218     }
219 
220     //qDebug();
221 
222     if (highlightedFileTreeViewItem() == nullptr) {    // nothing currently selected,
223         // select the branch root
224         item->setSelected(true);
225         emit galleryPathChanged(m_defaultBranch, "/");  // tell the history combo
226     }
227 
228     m_startup = false;                  // don't do this again
229 }
230 
contextMenuEvent(QContextMenuEvent * ev)231 void ScanGallery::contextMenuEvent(QContextMenuEvent *ev)
232 {
233     ev->accept();
234     if (m_contextMenu != nullptr) {
235         m_contextMenu->exec(ev->globalPos());
236     }
237 }
238 
getImgFormat(const FileTreeViewItem * item)239 static ImageFormat getImgFormat(const FileTreeViewItem *item)
240 {
241     if (item == nullptr) {
242         return (ImageFormat(""));
243     }
244 
245     const KFileItem *kfi = item->fileItem();
246     if (kfi->isNull()) {
247         return (ImageFormat(""));
248     }
249 
250     // Check that this is a plausible image format (MIME type = "image/anything")
251     // before trying to get the image type.
252     QString mimetype = kfi->mimetype();
253     if (!mimetype.startsWith("image/")) {
254         return (ImageFormat(""));
255     }
256 
257     return (ImageFormat::formatForUrl(kfi->url()));
258 }
259 
imageForItem(const FileTreeViewItem * item)260 static KookaImage *imageForItem(const FileTreeViewItem *item)
261 {
262     if (item == nullptr) return (nullptr);			// get loaded image if any
263     return (static_cast<KookaImage *>(item->clientData()));
264 }
265 
slotItemHighlighted(QTreeWidgetItem * curr)266 void ScanGallery::slotItemHighlighted(QTreeWidgetItem *curr)
267 {
268     if (curr==nullptr)
269     {
270         QList<QTreeWidgetItem *> selItems = selectedItems();
271         if (!selItems.isEmpty()) curr = selItems.first();
272     }
273     FileTreeViewItem *item = static_cast<FileTreeViewItem *>(curr);
274     if (item==nullptr) return;
275 
276     //qDebug() << item->url();
277 
278     if (item->isDir()) {
279         emit showImage(nullptr, true);    // clear displayed image
280     } else {
281         KookaImage *img = imageForItem(item);
282         emit showImage(img, false);         // clear or redisplay image
283     }
284 
285     emit itemHighlighted(item->url(), item->isDir());
286 }
287 
slotItemActivated(QTreeWidgetItem * curr)288 void ScanGallery::slotItemActivated(QTreeWidgetItem *curr)
289 {
290     FileTreeViewItem *item = static_cast<FileTreeViewItem *>(curr);
291     //qDebug() << item->url();
292 
293     //  Check if directory, hide image for now, later show a thumb view?
294     if (item->isDir()) {                // is it a directory?
295         emit showImage(nullptr, true);            // unload current image
296     } else {                    // not a directory
297         //  Load the image if necessary. This is done by loadImageForItem,
298         //  which is async (TODO). The image finally arrives in slotImageArrived.
299         QApplication::setOverrideCursor(Qt::WaitCursor);
300         emit aboutToShowImage(item->url());
301         loadImageForItem(item);
302         QApplication::restoreOverrideCursor();
303     }
304 
305     //  Notify the new directory, if it has changed
306     QUrl newDir = itemDirectory(item);
307     if (m_currSelectedDir != newDir) {
308         m_currSelectedDir = newDir;
309         emit galleryPathChanged(item->branch(), itemDirectoryRelative(item));
310     }
311 }
312 
313 // These 2 slots are called when an item is clicked/activated in the thumbnail view.
314 
slotHighlightItem(const QUrl & url)315 void ScanGallery::slotHighlightItem(const QUrl &url)
316 {
317     //qDebug() << url;
318 
319     FileTreeViewItem *found = findItemByUrl(url);
320     if (found == nullptr) {
321         return;
322     }
323 
324     bool b = blockSignals(true);
325     scrollToItem(found);
326     setCurrentItem(found);
327     blockSignals(b);
328 
329     // Need to do this to update/clear the displayed image.  Causes a signal
330     // to be sent back to the thumbnail view, but this is benign and fortunately
331     // does not cause not a loop.
332     slotItemHighlighted(found);
333 }
334 
slotActivateItem(const QUrl & url)335 void ScanGallery::slotActivateItem(const QUrl &url)
336 {
337     //qDebug() << url;
338 
339     FileTreeViewItem *found = findItemByUrl(url);
340     if (found == nullptr) {
341         return;
342     }
343 
344     slotItemActivated(found);
345 }
346 
347 // This slot is called when an image has been changed by some external means
348 // (e.g. an image transformation).  The item image is reloaded only if it
349 // is still currently selected.
350 
slotUpdatedItem(const QUrl & url)351 void ScanGallery::slotUpdatedItem(const QUrl &url)
352 {
353     FileTreeViewItem *found = findItemByUrl(url);
354     if (found == nullptr) {
355         return;
356     }
357 
358     if (found->isSelected()) {              // only if still selected
359         slotUnloadItem(found);              // ensure unloaded for updating
360         slotItemActivated(found);           // load the new image
361     }
362 }
363 
slotDirCount(FileTreeViewItem * item,int cnt)364 void ScanGallery::slotDirCount(FileTreeViewItem *item, int cnt)
365 {
366     if (item == nullptr) {
367         return;
368     }
369     if (!item->isDir()) {
370         return;
371     }
372 
373     int imgCount = 0;                   // total these separately,
374     int dirCount = 0;                   // we want individual counts
375     int fileCount = 0;                  // for files and subfolders
376 
377     for (int i = 0; i < item->childCount(); ++i) {
378         FileTreeViewItem *ci = static_cast<FileTreeViewItem *>(item->child(i));
379         if (ci->isDir()) {
380             ++dirCount;
381         } else {
382             if (ImageFormat::formatForMime(ci->fileItem()->determineMimeType()).isValid()) {
383                 ++imgCount;
384             } else {
385                 ++fileCount;
386             }
387         }
388     }
389 
390     QString cc = "";
391     if (dirCount == 0) {
392         if ((imgCount + fileCount) == 0) {
393             cc = i18n("empty");
394         } else {
395             if (fileCount == 0) {
396                 cc = i18np("one image", "%1 images", imgCount);
397             } else {
398                 cc = i18np("one file", "%1 files", (imgCount + fileCount));
399             }
400 
401         }
402     } else {
403         if (fileCount > 0) {
404             cc = i18np("one file, ", "%1 files, ", (imgCount + fileCount));
405         } else if (imgCount > 0) {
406             cc = i18np("one image, ", "%1 images, ", imgCount);
407         }
408 
409         cc += i18np("1 folder", "%1 folders", dirCount);
410     }
411 
412     item->setText(1, (" " + cc));
413 }
414 
slotItemExpanded(QTreeWidgetItem * item)415 void ScanGallery::slotItemExpanded(QTreeWidgetItem *item)
416 {
417     if (item == nullptr) {
418         return;
419     }
420     if (!(static_cast<FileTreeViewItem *>(item))->isDir()) {
421         return;
422     }
423 
424     for (int i = 0; i < item->childCount(); ++i) {
425         FileTreeViewItem *ci = static_cast<FileTreeViewItem *>(item->child(i));
426         if (ci->isDir() && !ci->alreadyListed()) {
427             ci->branch()->populate(ci->url(), ci);
428         }
429     }
430 }
431 
slotDecorate(FileTreeViewItem * item)432 void ScanGallery::slotDecorate(FileTreeViewItem *item)
433 {
434     if (item == nullptr) return;
435 
436 #ifdef DEBUG_LOADING
437     qDebug() << item->url();
438 #endif // DEBUG_LOADING
439     const bool isSubImage = item->url().hasFragment();	// is this a sub-image?
440 
441     if (!item->isDir())					// directories are done elsewhere
442     {
443         ImageFormat format = getImgFormat(item);	// this is safe for any file
444         if (!isSubImage)				// no format for subimages
445         {
446             item->setText(2, (QString(" %1 ").arg(format.name())));
447         }
448 
449         const KookaImage *img = imageForItem(item);
450         if (img != nullptr)				// image is loaded
451         {
452             // set image depth pixmap as appropriate
453             QIcon icon;
454             if (img->depth() == 1) icon = ScanIcons::self()->icon(ScanIcons::BlackWhite);
455             else
456             {
457                 if (img->isGrayscale()) icon = ScanIcons::self()->icon(ScanIcons::Greyscale);
458                 else icon = ScanIcons::self()->icon(ScanIcons::Colour);
459             }
460             item->setIcon(0, icon);
461 
462             if (img->subImagesCount() == 0)		// size except for containers
463             {
464                 QString t = i18n(" %1 x %2", img->width(), img->height());
465                 item->setText(1, t);
466             }
467         }
468         else						// image not loaded, show file info
469         {
470             if (format.isValid())			// if a valid image file
471             {
472                 if (isSubImage)				// subimages don't show size
473                 {
474                     item->setIcon(0, QIcon::fromTheme("edit-copy"));
475                     item->setText(1, QString());
476                 }
477                 else
478                 {
479                     item->setIcon(0, QIcon::fromTheme("media-floppy"));
480                     const KFileItem *kfi = item->fileItem();
481                     if (!kfi->isNull()) item->setText(1, (" " + KIO::convertSize(kfi->size())));
482                 }
483             }
484             else					// not an image file
485             {						// show its standard MIME type
486                 item->setIcon(0, KIO::pixmapForUrl(item->url(), 0, KIconLoader::Small));
487             }
488         }
489     }
490 
491     // This code is quite similar to m_nextUrlToSelect in FileTreeView::slotNewTreeViewItems
492     // When scanning a new image, we wait for the KDirLister to notice the new file,
493     // and then we have the FileTreeViewItem that we need to display the image.
494     if (!m_nextUrlToShow.isEmpty()) {
495         if (m_nextUrlToShow.adjusted(QUrl::StripTrailingSlash) ==
496             item->url().adjusted(QUrl::StripTrailingSlash)) {
497             m_nextUrlToShow = QUrl();           // do this first to prevent recursion
498             slotItemActivated(item);
499             setCurrentItem(item);           // necessary in case of new file from D&D
500         }
501     }
502 }
503 
slotDecorate(FileTreeBranch * branch,const FileTreeViewItemList & list)504 void ScanGallery::slotDecorate(FileTreeBranch *branch, const FileTreeViewItemList &list)
505 {
506     //qDebug() << "count" << list.count();
507     for (FileTreeViewItemList::const_iterator it = list.constBegin();
508             it != list.constEnd(); ++it) {
509         FileTreeViewItem *ftvi = (*it);
510         slotDecorate(ftvi);
511         emit fileChanged(ftvi->fileItem());
512     }
513 }
514 
updateParent(const FileTreeViewItem * curr)515 void ScanGallery::updateParent(const FileTreeViewItem *curr)
516 {
517     FileTreeBranch *branch = branches().at(0);      /* There should be at least one */
518     if (branch == nullptr) {
519         return;
520     }
521 
522     QUrl dir = itemDirectory(curr);
523     //qDebug() << "Updating directory" << dir;
524     branch->updateDirectory(dir);
525 
526     FileTreeViewItem *parent = branch->findItemByUrl(dir);
527     if (parent != nullptr) {
528         parent->setExpanded(true);    /* Ensure parent is expanded */
529     }
530 }
531 
532 // "Rename" action triggered in the GUI
533 
slotRenameItems()534 void ScanGallery::slotRenameItems()
535 {
536     FileTreeViewItem *curr = highlightedFileTreeViewItem();
537     if (curr != nullptr) {
538         editItem(curr, 0);
539     }
540 }
541 
542 // Renaming has finished
543 
slotFileRenamed(FileTreeViewItem * item,const QString & newName)544 bool ScanGallery::slotFileRenamed(FileTreeViewItem *item, const QString &newName)
545 {
546     if (item->isRoot()) return (false);			// cannot rename root here
547 
548     QUrl urlFrom = item->url();
549     //qDebug() << "url" << urlFrom << "->" << newName;
550     QString oldName = urlFrom.fileName();
551 
552     QUrl urlTo(urlFrom.resolved(QUrl(newName)));
553 
554     /* clear selection, because the renamed image comes in through
555      * kdirlister again
556      */
557     // slotUnloadItem(item);                // unnecessary, bug 68532
558     // because of "note new URL" below
559     qDebug() << "Renaming " << urlFrom << "->" << urlTo;
560 
561     //setSelected(item,false);
562 
563     bool success = ImgSaver::renameImage(urlFrom, urlTo, true, this);
564     if (success) {                  // rename the file
565         item->setUrl(urlTo);                // note new URL
566         emit fileRenamed(item->fileItem(), newName);
567     } else {
568         //qDebug() << "renaming failed";
569         item->setText(0, oldName);          // restore original name
570     }
571 
572 //    setSelected(item,true);               // restore the selection
573     return (success);
574 }
575 
576 // TODO: this function does not appear to be used
577 /* ----------------------------------------------------------------------- */
578 /*
579  * Method that checks if the new filename a user enters while renaming an image is valid.
580  * It checks for a proper extension.
581  */
582 
buildNewFilename(const QString & cmplFilename,const ImageFormat & currFormat)583 static QString buildNewFilename(const QString &cmplFilename, const ImageFormat &currFormat)
584 {
585     /* cmplFilename = new name the user wishes.
586      * currFormat   = the current format of the image.
587      * if the new filename has a valid extension, which is the same as the
588      * format of the current, fine. A ''-String has to be returned.
589      */
590     QFileInfo fiNew(cmplFilename);
591     QString base = fiNew.baseName();
592     QString newExt = fiNew.suffix().toLower();
593     QString nowExt = currFormat.extension();
594     QString ext = "";
595 
596     //qDebug() << "Filename wanted:" << cmplFilename << "ext" << nowExt << "->" << newExt;
597 
598     if (newExt.isEmpty()) {
599         /* ok, fine -> return the currFormat-Extension */
600         ext = base + "." + nowExt;
601     } else if (newExt == nowExt) {
602         /* also good, no reason to put another extension */
603         ext = cmplFilename;
604     } else {
605         /* new Ext. differs from the current extension. Later. */
606         KMessageBox::sorry(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"
607                                       "Kooka corrects the extension."),
608                            i18n("On the Fly Conversion"));
609         ext = base + "." + nowExt;
610     }
611     return (ext);
612 }
613 
614 /* ----------------------------------------------------------------------- */
615 /* The absolute URL of the item (if it is a directory), or its parent (if
616    it is a file).
617 */
itemDirectory(const FileTreeViewItem * item) const618 QUrl ScanGallery::itemDirectory(const FileTreeViewItem *item) const
619 {
620     if (item == nullptr) {
621         //qDebug() << "no item";
622         return (QUrl());
623     }
624 
625     QUrl u = item->url();
626     if (!item->isDir()) {
627         u = u.adjusted(QUrl::RemoveFilename);		// not a directory, remove file name
628     } else {
629         u = u.adjusted(QUrl::StripTrailingSlash);	// is a directory, ensure ends with "/"
630         u.setPath(u.path()+'/');
631     }
632     return (u);
633 }
634 
635 /* ----------------------------------------------------------------------- */
636 /* As above, but relative to the root of its branch.  The result does not
637    begin with a leading slash, except that a single "/" means the root.
638    If there is some problem (no branch, or the root/item URLs do not match),
639    the full path is returned.
640 */
itemDirectoryRelative(const FileTreeViewItem * item) const641 QString ScanGallery::itemDirectoryRelative(const FileTreeViewItem *item) const
642 {
643     const QUrl u = itemDirectory(item);
644     const FileTreeBranch *branch = item->branch();
645     if (branch == nullptr) {
646         return (u.path());    // no branch, can this ever happen?
647     }
648 
649     QString rootUrl = branch->rootUrl().url(QUrl::StripTrailingSlash)+'/';
650     QString itemUrl = u.url();
651     //qDebug() << "itemurl" << itemUrl << "rooturl" << rootUrl;
652     if (itemUrl.startsWith(rootUrl)) {
653         itemUrl.remove(0, rootUrl.length());        // remove root URL prefix
654         //qDebug() << "->" << itemUrl;
655         if (itemUrl.isEmpty()) {
656             itemUrl = "/";    // it is the root
657         }
658         //qDebug() << "->" << itemUrl;
659     } else {
660         //qDebug() << "item URL" << itemUrl << "does not start with root URL" << rootUrl;
661     }
662 
663     return (itemUrl);
664 }
665 
666 /* ----------------------------------------------------------------------- */
667 /* This slot receives a string from the gallery-path combobox shown under the
668  * image gallery, the relative directory under the branch.  Now it is to assemble
669  * a complete path from the data, find out the FileTreeViewItem associated
670  * with it and call slotClicked with it.
671  */
672 
slotSelectDirectory(const QString & branchName,const QString & relPath)673 void ScanGallery::slotSelectDirectory(const QString &branchName, const QString &relPath)
674 {
675     //qDebug() << "branch" << branchName << "path" << relPath;
676 
677     FileTreeViewItem *item;
678     if (!branchName.isEmpty())				// find in specified branch
679     {
680         item = findItemInBranch(branchName, relPath);
681     }
682     else						// assume the 1st/only branch
683     {
684         item = findItemInBranch(branches().at(0), relPath);
685     }
686     if (item == nullptr) return;			// not found in branch
687 
688     scrollToItem(item);
689     setCurrentItem(item);
690     slotItemActivated(item);				// load thumbnails, etc.
691 }
692 
loadImageForItem(FileTreeViewItem * item)693 void ScanGallery::loadImageForItem(FileTreeViewItem *item)
694 {
695     if (item == nullptr) return;
696     const KFileItem *kfi = item->fileItem();
697     if (kfi->isNull()) return;
698 
699 #ifdef DEBUG_LOADING
700     qDebug() << "loading" << item->url();
701 #endif // DEBUG_LOADING
702     QString ret;					// no error so far
703 
704     ImageFormat format = getImgFormat(item);		// check for valid image format
705     if (!format.isValid())
706     {
707         ret = i18n("Not a supported image format");
708     }
709     else						// valid image
710     {
711         KookaImage *img = imageForItem(item);
712         if (img==nullptr)				// image not already loaded
713         {
714 #ifdef DEBUG_LOADING
715             qDebug() << "need to load image";
716 #endif // DEBUG_LOADING
717 
718             // The image needs to be loaded. Possibly it is a multi-page image.
719             // If it is, the KookaImage has a subImageCount larger than one. We
720             // create an subimage item for every subimage, but do not yet load
721             // them.
722 
723             img = new KookaImage();
724             ret = img->loadFromUrl(item->url());
725             if (ret.isEmpty())				// image loaded OK
726             {
727                 img->setFileItem(kfi);			// store the KFileItem
728 
729                 if (img->subImagesCount()>1)		// see if it has subimages
730                 {
731 #ifdef DEBUG_LOADING
732                     qDebug() << "subimage count" << img->subImagesCount();
733 #endif // DEBUG_LOADING
734                     if (item->childCount()==0)		// check not already created
735                     {
736 #ifdef DEBUG_LOADING
737                         qDebug() << "need to create subimages";
738 #endif // DEBUG_LOADING
739                         // Create items for each subimage
740                         QIcon subImgIcon = QIcon::fromTheme("edit-copy");
741 
742                         // Sub-images start counting from 1, KookaImage adjusts
743                         // that back to the 0-based TIFF directory index.
744                         for (int i = 1; i<=img->subImagesCount(); i++)
745                         {
746                             KFileItem newKfi(*kfi);
747 
748                             // Set the URL to mark this as a subimage.  The subimage
749                             // number is set as the URL fragment;  this is detected by
750                             // KookaImage::loadFromUrl() and used to extract the
751                             // submimage.
752                             QUrl u = newKfi.url();
753                             u.setFragment(QString::number(i));
754                             newKfi.setUrl(u);
755 
756                             // Create the item without a parent and then
757                             // add it to the parent item later, so that
758                             // the setText() below does not trigger a rename.
759                             FileTreeViewItem *subImgItem = new FileTreeViewItem(
760                                 static_cast<FileTreeViewItem *>(nullptr), newKfi, item->branch());
761 
762                             subImgItem->setText(0, i18n("Sub-image %1", i));
763                             subImgItem->setIcon(0, subImgIcon);
764                             item->addChild(subImgItem);
765                         }
766                     }
767                 }
768             }
769             else
770             {
771                 delete img;				// image loading failed
772                 img = nullptr;				// don't try to use it below
773             }
774         }
775 #ifdef DEBUG_LOADING
776         else qDebug() << "have an image already";
777 #endif // DEBUG_LOADING
778 
779         if (img!=nullptr)				// already loaded, or loaded above
780         {
781             slotImageArrived(item, img);		// display the image
782         }
783     }
784 
785     if (!ret.isEmpty())					// image loading failed
786     {
787         KMessageBox::error(this,
788                            xi18nc("@info", "Unable to load the image <filename>%2</filename><nl/>%1",
789                                   ret, item->url().url(QUrl::PreferLocalFile)),
790                            i18n("Image Load Error"));
791     }
792 }
793 
794 /* Hit this slot with a file for a kfiletreeviewitem. */
slotImageArrived(FileTreeViewItem * item,KookaImage * image)795 void ScanGallery::slotImageArrived(FileTreeViewItem *item, KookaImage *image)
796 {
797     if (item == nullptr || image == nullptr) {
798         return;
799     }
800 
801     //qDebug() << item->text(0);
802 
803     item->setClientData(image);             // note image for item
804     slotDecorate(item);
805     emit showImage(image, false);
806 }
807 
getCurrImage(bool loadOnDemand)808 const KookaImage *ScanGallery::getCurrImage(bool loadOnDemand)
809 {
810     FileTreeViewItem *curr = highlightedFileTreeViewItem();
811     if (curr == nullptr) {
812         return (nullptr);    // no current item
813     }
814     if (curr->isDir()) {
815         return (nullptr);    // is a directory
816     }
817 
818     KookaImage *img = imageForItem(curr);		// see if already loaded
819     if (img == nullptr) {				// no, try to do that
820         if (!loadOnDemand) {
821             return (nullptr);				// not loaded, and don't want to
822         }
823         slotItemActivated(curr);			// select/load this image
824         img = imageForItem(curr);			// and get image for it
825     }
826 
827     return (img);
828 }
829 
830 
currentImageFileName() const831 QString ScanGallery::currentImageFileName() const
832 {
833     QString result = "";
834 
835     const FileTreeViewItem *curr = highlightedFileTreeViewItem();
836     if (curr==nullptr) return (QString());
837 
838     bool isLocal = false;
839     const QUrl u = curr->fileItem()->mostLocalUrl(isLocal);
840     if (!isLocal) return (QString());
841     return (u.toLocalFile());
842 }
843 
844 
prepareToSave(const ImageMetaInfo * info)845 bool ScanGallery::prepareToSave(const ImageMetaInfo *info)
846 {
847     if (info == nullptr) {
848         //qDebug() << "no image info";
849     } else {
850         //qDebug() << "type" << info->getImageType();
851     }
852 
853     delete mSaver; mSaver = nullptr;			// recreate with clean info
854 
855     // Resolve where to save the new image when it arrives
856     FileTreeViewItem *curr = highlightedFileTreeViewItem();
857     if (curr == nullptr) {				// into root if nothing is selected
858         FileTreeBranch *branch = branches().at(0);	// there should be at least one
859         if (branch != nullptr) {
860             // if user has created this????
861             curr = findItemInBranch(branch, i18n("Incoming/"));
862             if (curr == nullptr) {
863                 curr = branch->root();
864             }
865         }
866 
867         if (curr == nullptr) {
868             return (false);    // should never happen
869         }
870         curr->setSelected(true);
871     }
872 
873     mSavedTo = curr;					// note for selecting later
874 
875     // Create the ImgSaver to use later
876     QUrl dir(itemDirectory(curr));			// where new image will go
877     mSaver = new ImgSaver(dir);				// create saver to use later
878 
879     if (info != nullptr) {				// have image information,
880 							// tell saver about it
881         ImgSaver::ImageSaveStatus stat = mSaver->setImageInfo(info);
882         if (stat == ImgSaver::SaveStatusCanceled) {
883             return (false);
884         }
885     }
886 
887     return (true);					// all ready to save
888 }
889 
saveURL() const890 QUrl ScanGallery::saveURL() const
891 {
892     if (mSaver == nullptr) {
893         return (QUrl());
894     }
895     // TODO: relative to root
896     return (mSaver->saveURL());
897 }
898 
899 /* ----------------------------------------------------------------------- */
900 /* This slot takes a new scanned Picture and saves it.  */
901 
addImage(const QImage * img,const ImageMetaInfo * info)902 void ScanGallery::addImage(const QImage *img, const ImageMetaInfo *info)
903 {
904     if (img == nullptr) {
905         return;    // nothing to save!
906     }
907     //qDebug() << "size" << img->size() << "depth" << img->depth();
908 
909     if (mSaver == nullptr) {
910         prepareToSave(nullptr);				// if not done already
911     }
912     if (mSaver == nullptr) {
913         return;						// should never happen
914     }
915 
916     ImgSaver::ImageSaveStatus isstat = mSaver->saveImage(img);
917     // try to save the image
918     QUrl lurl = mSaver->lastURL();			// record where it ended up
919 
920     if (isstat != ImgSaver::SaveStatusOk &&		// image saving failed
921             isstat != ImgSaver::SaveStatusCanceled) {   // user cancelled, just ignore
922         KMessageBox::error(this, xi18nc("@info", "Could not save the image<nl/><filename>%2</filename><nl/>%1",
923                                         mSaver->errorString(isstat),
924                                         lurl.url(QUrl::PreferLocalFile)),
925                            i18n("Image Save Error"));
926     }
927 
928     delete mSaver; mSaver = nullptr;			// now finished with this
929 
930     if (isstat == ImgSaver::SaveStatusOk) {		// image was saved OK,
931         // select the new image
932         slotSetNextUrlToSelect(lurl);
933         m_nextUrlToShow = lurl;
934         if (mSavedTo != nullptr) {
935             updateParent(mSavedTo);
936         }
937     }
938 }
939 
940 // Selects and loads the image with the given URL. This is used to restore the
941 // last displayed image on startup.
942 
slotSelectImage(const QUrl & url)943 void ScanGallery::slotSelectImage(const QUrl &url)
944 {
945     FileTreeViewItem *found = findItemByUrl(url);
946     if (found == nullptr) {
947         found = m_defaultBranch->root();
948     }
949 
950     scrollToItem(found);
951     setCurrentItem(found);
952     slotItemActivated(found);
953 }
954 
findItemByUrl(const QUrl & url,FileTreeBranch * branch)955 FileTreeViewItem *ScanGallery::findItemByUrl(const QUrl &url, FileTreeBranch *branch)
956 {
957     QUrl u(url);
958     if (u.scheme() == "file") {				// for local files,
959         QDir d(url.path());				// ensure path is canonical
960         u.setPath(d.canonicalPath());
961     }
962     //qDebug() << "URL search for" << u;
963 
964     // Prepare a list of branches to search.  If the parameter 'branch'
965     // is set, search only in the specified branch. If it is nullptr, search
966     // all branches.
967     FileTreeBranchList branchList;
968     if (branch != nullptr) {
969         branchList.append(branch);
970     } else {
971         branchList = branches();
972     }
973 
974     FileTreeViewItem *foundItem = nullptr;
975     for (FileTreeBranchList::const_iterator it = branchList.constBegin();
976             it != branchList.constEnd(); ++it) {
977         FileTreeBranch *branchloop = (*it);
978         FileTreeViewItem *ftvi = branchloop->findItemByUrl(u);
979         if (ftvi != nullptr) {
980             foundItem = ftvi;
981             //qDebug() << "found item for" << ftvi->url();
982             break;
983         }
984     }
985 
986     return (foundItem);
987 }
988 
slotExportFile()989 void ScanGallery::slotExportFile()
990 {
991     FileTreeViewItem *curr = highlightedFileTreeViewItem();
992     if (curr == nullptr) {
993         return;
994     }
995 
996     if (curr->isDir()) {
997         //qDebug() << "Not yet implemented!";
998         return;
999     }
1000 
1001     QUrl fromUrl(curr->url());
1002 
1003     QString filter;
1004     ImageFormat format = getImgFormat(curr);
1005     if (format.isValid()) filter = format.mime().filterString();
1006     else filter = i18n("All Files (*)");
1007 
1008     RecentSaver saver("exportImage");
1009     QUrl fileName = QFileDialog::getSaveFileUrl(this, i18nc("@title:window", "Export Image"),
1010                                                 saver.recentUrl(fromUrl.fileName()), filter);
1011     if (!fileName.isValid()) return;			// didn't get a file name
1012     if (fileName==fromUrl) return;			// can't save over myself
1013     saver.save(fileName);
1014 
1015     // Since the copy operation is asynchronous,
1016     // we will never know if it succeeds.
1017     ImgSaver::copyImage(fromUrl, fileName);
1018 }
1019 
slotImportFile()1020 void ScanGallery::slotImportFile()
1021 {
1022     FileTreeViewItem *curr = highlightedFileTreeViewItem();
1023     if (curr==nullptr) return;
1024 
1025     QUrl impTarget = curr->url();
1026     if (!curr->isDir()) {
1027         FileTreeViewItem *pa = static_cast<FileTreeViewItem *>(curr->parent());
1028         impTarget = pa->url();
1029     }
1030 
1031     QString filter = ImageFilter::qtFilterString(ImageFilter::Reading, ImageFilter::AllImages|ImageFilter::AllFiles);
1032 
1033     RecentSaver saver("importImage");
1034     QUrl impUrl = QFileDialog::getOpenFileUrl(this, i18n("Import Image File to Gallery"),
1035                                               saver.recentUrl(), filter);
1036     if (!impUrl.isValid()) return;
1037     saver.save(impUrl);
1038 							// use the name of the source file
1039     impTarget = impTarget.resolved(QUrl(impUrl.fileName()));
1040     m_nextUrlToShow = impTarget;
1041     qDebug() << "Importing" << impUrl << "->" << impTarget;
1042     ImgSaver::copyImage(impUrl, impTarget);
1043 }
1044 
slotUrlsDropped(QDropEvent * ev,FileTreeViewItem * item)1045 void ScanGallery::slotUrlsDropped(QDropEvent *ev, FileTreeViewItem *item)
1046 {
1047     QList<QUrl> urls = ev->mimeData()->urls();
1048     if (urls.isEmpty()) {
1049         return;
1050     }
1051 
1052     //qDebug() << "onto" << (item == nullptr ? "nullptr" : item->url().prettyUrl())
1053     //<< "srcs" << urls.count() << "first" << urls.first();
1054 
1055     if (item == nullptr) return;
1056     QUrl dest = item->url();
1057 
1058     // Check whether the drop is on top of a directory (in which case we
1059     // want to move/copy into it) or a file (move/copy into its containing
1060     // directory).
1061     if (!item->isDir()) dest = dest.adjusted(QUrl::RemoveFilename);
1062     qDebug() << "resolved destination" << dest;
1063 
1064     // Make the last URL to copy the one to select next
1065     QUrl nextSel = dest.resolved(QUrl(urls.back().fileName()));
1066     m_nextUrlToShow = nextSel;
1067 
1068     KIO::Job *job;
1069     // TODO: top level window as 3rd parameter?
1070     if (ev->dropAction() == Qt::MoveAction) {
1071         job = KIO::move(urls, dest);
1072     } else {
1073         job = KIO::copy(urls, dest);
1074     }
1075     connect(job, SIGNAL(result(KJob*)), SLOT(slotJobResult(KJob*)));
1076 }
1077 
slotJobResult(KJob * job)1078 void ScanGallery::slotJobResult(KJob *job)
1079 {
1080     //qDebug() << "error" << job->error();
1081     if (job->error()) {
1082         job->uiDelegate()->showErrorMessage();
1083     }
1084 }
1085 
1086 /* ----------------------------------------------------------------------- */
slotUnloadItems()1087 void ScanGallery::slotUnloadItems()
1088 {
1089     FileTreeViewItem *curr = highlightedFileTreeViewItem();
1090     emit showImage(nullptr, false);
1091     slotUnloadItem(curr);
1092 }
1093 
slotUnloadItem(FileTreeViewItem * curr)1094 void ScanGallery::slotUnloadItem(FileTreeViewItem *curr)
1095 {
1096     if (curr == nullptr) {
1097         return;
1098     }
1099 
1100     if (curr->isDir()) {				// is a directory
1101         for (int i = 0; i < curr->childCount(); ++i) {
1102             FileTreeViewItem *child = static_cast<FileTreeViewItem *>(curr->child(i));
1103             slotUnloadItem(child);			// recursively unload contents
1104         }
1105     } else {						// is a file/image
1106         const KookaImage *image = imageForItem(curr);
1107         if (image == nullptr) {
1108             return;					// ok, nothing to unload
1109         }
1110 
1111         if (image->subImagesCount() > 0) {		// image with subimages
1112             while (curr->childCount() > 0) {		// recursively unload subimages
1113                 FileTreeViewItem *child = static_cast<FileTreeViewItem *>(curr->takeChild(0));
1114                 slotUnloadItem(child);
1115                 delete child;
1116             }
1117         }
1118 
1119         emit unloadImage(image);
1120         delete image;
1121 
1122         curr->setClientData(nullptr);			// clear image from item
1123         slotDecorate(curr);
1124     }
1125 }
1126 
slotItemProperties()1127 void ScanGallery::slotItemProperties()
1128 {
1129     FileTreeViewItem *curr = highlightedFileTreeViewItem();
1130     if (curr == nullptr) {
1131         return;
1132     }
1133     KPropertiesDialog::showDialog(curr->url(), this);
1134 }
1135 
1136 /* ----------------------------------------------------------------------- */
1137 
slotDeleteItems()1138 void ScanGallery::slotDeleteItems()
1139 {
1140     FileTreeViewItem *curr = highlightedFileTreeViewItem();
1141     if (curr == nullptr) {
1142         return;
1143     }
1144 
1145     QUrl urlToDel = curr->url();			// item to be deleted
1146     bool isDir = curr->isDir();				// deleting a folder?
1147     QTreeWidgetItem *nextToSelect = curr->treeWidget()->itemBelow(curr);
1148     // select this afterwards
1149     QString s;
1150     QString dontAskKey;
1151     if (isDir) {
1152         s = xi18nc("@info", "Do you really want to permanently delete the folder<nl/>"
1153                    "<filename>%1</filename><nl/>"
1154                    "and all of its contents? It cannot be restored.", urlToDel.url(QUrl::PreferLocalFile));
1155         dontAskKey = "AskForDeleteDirs";
1156     } else {
1157         s = xi18nc("@info", "Do you really want to permanently delete the image<nl/>"
1158                    "<filename>%1</filename>?<nl/>"
1159                    "It cannot be restored.", urlToDel.url(QUrl::PreferLocalFile));
1160         dontAskKey = "AskForDeleteFiles";
1161     }
1162 
1163     if (KMessageBox::warningContinueCancel(this, s,
1164                                            i18n("Delete Gallery Item"),
1165                                            KStandardGuiItem::del(),
1166                                            KStandardGuiItem::cancel(),
1167                                            dontAskKey) != KMessageBox::Continue) {
1168         return;
1169     }
1170 
1171     slotUnloadItem(curr);
1172     qDebug() << "Deleting" << urlToDel;
1173     KIO::DeleteJob *job = KIO::del(urlToDel);
1174     if (!job->exec())
1175     {
1176         KMessageBox::error(this, xi18nc("@info", "Could not delete the image or folder<nl/><filename>%2</filename><nl/>%1",
1177                                         job->errorString(),
1178                                         urlToDel.url(QUrl::PreferLocalFile)),
1179                            i18n("File Delete Error"));
1180         return;
1181     }
1182 
1183     updateParent(curr);					// update parent folder count
1184     if (isDir) {					// remove from the name combo
1185         emit galleryDirectoryRemoved(curr->branch(), itemDirectoryRelative(curr));
1186     }
1187 
1188 #if 0
1189     if (nextToSelect != nullptr) {
1190         setSelected(nextToSelect, true);
1191     }
1192     //  TODO: if doing the above, also need to signal to update thumbnail
1193     //  as below.
1194     //
1195     //  But doing that leads to inconsistency between deleting the last item
1196     //  in a folder (nothing is selected afterwards) and deleting anything
1197     //  else (the next image is selected and loaded).  So leaving this
1198     //  commented out for now.
1199     curr = highlightedFileTreeViewItem();
1200     //qDebug() << "new selection after delete" << (curr == nullptr ? "nullptr" : curr->url().prettyURL());
1201     if (curr != nullptr) {
1202         emit showItem(curr->fileItem());
1203     }
1204 #endif
1205 }
1206 
1207 /* ----------------------------------------------------------------------- */
slotCreateFolder()1208 void ScanGallery::slotCreateFolder()
1209 {
1210     QString folder = QInputDialog::getText(this, i18n("New Folder"),
1211                                            i18n("Name for the new folder:"));
1212     if (folder.isEmpty()) return;
1213 
1214     FileTreeViewItem *item = highlightedFileTreeViewItem();
1215     if (item==nullptr) return;
1216 
1217     // The GUI ensures that the action is only enabled if the current
1218     // item is a directory.  Hence, we can assume that it is and ensure
1219     // that its path ends with a slash before setting the file name.
1220     QUrl url = item->url().adjusted(QUrl::StripTrailingSlash);
1221     url.setPath(url.path()+'/');
1222     url = url.resolved(QUrl(folder));
1223     qDebug() << "Creating folder" << url;
1224 
1225     /* Since the new directory arrives in the packager in the newItems-slot, we set a
1226      * variable urlToSelectOnArrive here. The newItems-slot will honor it and select
1227      * the treeviewitem with that url.
1228      */
1229     slotSetNextUrlToSelect(url);
1230 
1231     KIO::MkdirJob *job = KIO::mkdir(url);
1232     if (!job->exec())
1233     {
1234         KMessageBox::error(this, xi18nc("@info", "Could not create the folder<nl/><filename>%2</filename><nl/>%1",
1235                                         job->errorString(), url.url(QUrl::PreferLocalFile)),
1236                            i18n("Folder Create Error"));
1237     }
1238 }
1239 
setAllowRename(bool on)1240 void ScanGallery::setAllowRename(bool on)
1241 {
1242     //qDebug() << "to" << on;
1243     setEditTriggers(on ? QAbstractItemView::DoubleClicked : QAbstractItemView::NoEditTriggers);
1244 }
1245