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