File indexing completed on 2024-04-28 08:11:06
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 }