File indexing completed on 2024-05-05 04:22:02

0001 // SPDX-FileCopyrightText: 2003-2020 The KPhotoAlbum Development Team
0002 // SPDX-FileCopyrightText: 2022-2023 Johannes Zarl-Zierl <johannes@zarl-zierl.at>
0003 //
0004 // SPDX-License-Identifier: GPL-2.0-or-later
0005 
0006 #include "ImportDialog.h"
0007 
0008 #include "ImageRow.h"
0009 #include "ImportMatcher.h"
0010 #include "KimFileReader.h"
0011 #include "MD5CheckPage.h"
0012 
0013 #include <DB/ImageDB.h>
0014 #include <DB/ImageInfo.h>
0015 #include <kpabase/SettingsData.h>
0016 
0017 #include <KHelpClient>
0018 #include <KLocalizedString>
0019 #include <KMessageBox>
0020 #include <QCheckBox>
0021 #include <QComboBox>
0022 #include <QDir>
0023 #include <QFileDialog>
0024 #include <QGridLayout>
0025 #include <QHBoxLayout>
0026 #include <QLabel>
0027 #include <QLineEdit>
0028 #include <QPixmap>
0029 #include <QPushButton>
0030 #include <QScrollArea>
0031 #include <kwidgetsaddons_version.h>
0032 
0033 using Utilities::StringSet;
0034 
0035 class QPushButton;
0036 using namespace ImportExport;
0037 
0038 ImportDialog::ImportDialog(QWidget *parent)
0039     : KAssistantDialog(parent)
0040     , m_hasFilled(false)
0041     , m_md5CheckPage(nullptr)
0042 {
0043 }
0044 
0045 bool ImportDialog::exec(KimFileReader *kimFileReader, const QUrl &kimFileURL)
0046 {
0047     m_kimFileReader = kimFileReader;
0048 
0049     if (kimFileURL.isLocalFile()) {
0050         QDir cwd;
0051         // convert relative local path to absolute
0052         m_kimFile = QUrl::fromLocalFile(cwd.absoluteFilePath(kimFileURL.toLocalFile()))
0053                         .adjusted(QUrl::NormalizePathSegments);
0054     } else {
0055         m_kimFile = kimFileURL;
0056     }
0057 
0058     QByteArray indexXML = m_kimFileReader->indexXML();
0059     if (indexXML.isNull())
0060         return false;
0061 
0062     bool ok = readFile(indexXML);
0063     if (!ok)
0064         return false;
0065 
0066     setupPages();
0067 
0068     return KAssistantDialog::exec();
0069 }
0070 
0071 bool ImportDialog::readFile(const QByteArray &data)
0072 {
0073     DB::ReaderPtr reader = DB::ReaderPtr(new DB::XmlReader(DB::ImageDB::instance()->uiDelegate(),
0074                                                            m_kimFile.toDisplayString()));
0075     reader->addData(data);
0076 
0077     DB::ElementInfo info = reader->readNextStartOrStopElement(QString::fromUtf8("KimDaBa-export"));
0078     if (!info.isStartToken)
0079         reader->complainStartElementExpected(QString::fromUtf8("KimDaBa-export"));
0080 
0081     // Read source
0082     QString source = reader->attribute(QString::fromUtf8("location")).toLower();
0083     if (source != QString::fromLatin1("inline") && source != QString::fromLatin1("external")) {
0084         KMessageBox::error(this, i18n("<p>XML file did not specify the source of the images, "
0085                                       "this is a strong indication that the file is corrupted</p>"));
0086         return false;
0087     }
0088 
0089     m_externalSource = (source == QString::fromLatin1("external"));
0090 
0091     // Read base url
0092     m_baseUrl = QUrl::fromUserInput(reader->attribute(QString::fromLatin1("baseurl")));
0093 
0094     while (reader->readNextStartOrStopElement(QString::fromUtf8("image")).isStartToken) {
0095         const DB::FileName fileName = DB::FileName::fromRelativePath(reader->attribute(QString::fromUtf8("file")));
0096         DB::ImageInfoPtr info = DB::ImageDB::createImageInfo(fileName, reader);
0097         m_images.append(info);
0098     }
0099     // the while loop already read the end element, so we tell readEndElement to not read the next token:
0100     reader->readEndElement(false);
0101 
0102     return true;
0103 }
0104 
0105 void ImportDialog::setupPages()
0106 {
0107     createIntroduction();
0108     createImagesPage();
0109     createDestination();
0110     createCategoryPages();
0111     connect(this, &ImportDialog::currentPageChanged, this, &ImportDialog::updateNextButtonState);
0112     QPushButton *helpButton = buttonBox()->button(QDialogButtonBox::Help);
0113     connect(helpButton, &QPushButton::clicked, this, &ImportDialog::slotHelp);
0114 }
0115 
0116 void ImportDialog::createIntroduction()
0117 {
0118     QString txt = i18n("<h1><font size=\"+2\">Welcome to KPhotoAlbum Import</font></h1>"
0119                        "This wizard will take you through the steps of an import operation. The steps are: "
0120                        "<ol><li>First you must select which images you want to import from the export file. "
0121                        "You do so by selecting the checkbox next to the image.</li>"
0122                        "<li>Next you must tell KPhotoAlbum in which folder to put the images. This folder must "
0123                        "of course be contained by the base folder KPhotoAlbum uses for images. "
0124                        "KPhotoAlbum will take care to avoid name clashes</li>"
0125                        "<li>The next step is to specify which categories you want to import (People, Places, ... ) "
0126                        "and also tell KPhotoAlbum how to match the categories from the file to your categories. "
0127                        "Imagine you load from a file, where a category is called <b>Blomst</b> (which is the "
0128                        "Danish word for flower), then you would likely want to match this with your category, which might be "
0129                        "called <b>Blume</b> (which is the German word for flower) - of course given you are German.</li>"
0130                        "<li>The final steps, is matching the individual tokens from the categories. I may call myself <b>Jesper</b> "
0131                        "in my image database, while you want to call me by my full name, namely <b>Jesper K. Pedersen</b>. "
0132                        "In this step non matches will be highlighted in red, so you can see which tokens was not found in your "
0133                        "database, or which tokens was only a partial match.</li></ol>");
0134 
0135     QLabel *intro = new QLabel(txt, this);
0136     intro->setWordWrap(true);
0137     addPage(intro, i18nc("@title:tab introduction page", "Introduction"));
0138 }
0139 
0140 void ImportDialog::createImagesPage()
0141 {
0142     QScrollArea *top = new QScrollArea;
0143     top->setWidgetResizable(true);
0144 
0145     QWidget *container = new QWidget;
0146     QVBoxLayout *lay1 = new QVBoxLayout(container);
0147     top->setWidget(container);
0148 
0149     // Select all and Deselect All buttons
0150     QHBoxLayout *lay2 = new QHBoxLayout;
0151     lay1->addLayout(lay2);
0152 
0153     QPushButton *selectAll = new QPushButton(i18n("Select All"), container);
0154     lay2->addWidget(selectAll);
0155     QPushButton *selectNone = new QPushButton(i18n("Deselect All"), container);
0156     lay2->addWidget(selectNone);
0157     lay2->addStretch(1);
0158     connect(selectAll, &QPushButton::clicked, this, &ImportDialog::slotSelectAll);
0159     connect(selectNone, &QPushButton::clicked, this, &ImportDialog::slotSelectNone);
0160 
0161     QGridLayout *lay3 = new QGridLayout;
0162     lay1->addLayout(lay3);
0163 
0164     lay3->setColumnStretch(2, 1);
0165 
0166     int row = 0;
0167     for (DB::ImageInfoListConstIterator it = m_images.constBegin(); it != m_images.constEnd(); ++it, ++row) {
0168         DB::ImageInfoPtr info = *it;
0169         ImageRow *ir = new ImageRow(info, this, m_kimFileReader, container);
0170         lay3->addWidget(ir->m_checkbox, row, 0);
0171 
0172         QPixmap pixmap = m_kimFileReader->loadThumbnail(info->fileName().relative());
0173         if (!pixmap.isNull()) {
0174             QPushButton *but = new QPushButton(container);
0175             but->setIcon(pixmap);
0176             but->setIconSize(pixmap.size());
0177             lay3->addWidget(but, row, 1);
0178             connect(but, &QPushButton::clicked, ir, &ImageRow::showImage);
0179         } else {
0180             QLabel *label = new QLabel(info->label());
0181             lay3->addWidget(label, row, 1);
0182         }
0183 
0184         QLabel *label = new QLabel(QString::fromLatin1("<p>%1</p>").arg(info->description()));
0185         lay3->addWidget(label, row, 2);
0186         m_imagesSelect.append(ir);
0187     }
0188 
0189     addPage(top, i18n("Select Which Images to Import"));
0190 }
0191 
0192 void ImportDialog::createDestination()
0193 {
0194     QWidget *top = new QWidget(this);
0195     QVBoxLayout *topLay = new QVBoxLayout(top);
0196     QHBoxLayout *lay = new QHBoxLayout;
0197     topLay->addLayout(lay);
0198 
0199     topLay->addStretch(1);
0200 
0201     QLabel *label = new QLabel(i18n("Destination of images: "), top);
0202     lay->addWidget(label);
0203 
0204     m_destinationEdit = new QLineEdit(top);
0205     lay->addWidget(m_destinationEdit, 1);
0206 
0207     QPushButton *but = new QPushButton(QString::fromLatin1("..."), top);
0208     but->setFixedWidth(30);
0209     lay->addWidget(but);
0210 
0211     m_destinationEdit->setText(Settings::SettingsData::instance()->imageDirectory());
0212     connect(but, &QPushButton::clicked, this, &ImportDialog::slotEditDestination);
0213     connect(m_destinationEdit, &QLineEdit::textChanged, this, &ImportDialog::updateNextButtonState);
0214     m_destinationPage = addPage(top, i18n("Destination of Images"));
0215 }
0216 
0217 void ImportDialog::slotEditDestination()
0218 {
0219     QString file = QFileDialog::getExistingDirectory(this, QString(), m_destinationEdit->text());
0220     if (!file.isNull()) {
0221         if (!QFileInfo(file).absoluteFilePath().startsWith(QFileInfo(Settings::SettingsData::instance()->imageDirectory()).absoluteFilePath())) {
0222             KMessageBox::error(this, i18n("The folder must be a subfolder of %1", Settings::SettingsData::instance()->imageDirectory()));
0223         } else if (QFileInfo(file).absoluteFilePath().startsWith(
0224                        QFileInfo(Settings::SettingsData::instance()->imageDirectory()).absoluteFilePath() + QString::fromLatin1("CategoryImages"))) {
0225             KMessageBox::error(this, i18n("This folder is reserved for category images."));
0226         } else {
0227             m_destinationEdit->setText(file);
0228             updateNextButtonState();
0229         }
0230     }
0231 }
0232 
0233 void ImportDialog::updateNextButtonState()
0234 {
0235     bool enabled = true;
0236     if (currentPage() == m_destinationPage) {
0237         QString dest = m_destinationEdit->text();
0238         if (QFileInfo(dest).isFile())
0239             enabled = false;
0240         else if (!QFileInfo(dest).absoluteFilePath().startsWith(QFileInfo(Settings::SettingsData::instance()->imageDirectory()).absoluteFilePath()))
0241             enabled = false;
0242     }
0243 
0244     setValid(currentPage(), enabled);
0245 }
0246 
0247 void ImportDialog::createCategoryPages()
0248 {
0249     QStringList categories;
0250     const DB::ImageInfoList images = selectedImages();
0251     for (DB::ImageInfoListConstIterator it = images.constBegin(); it != images.constEnd(); ++it) {
0252         const DB::ImageInfoPtr info = *it;
0253         const QStringList categoriesForImage = info->availableCategories();
0254         for (const QString &category : categoriesForImage) {
0255             auto catPtr = DB::ImageDB::instance()->categoryCollection()->categoryForName(category);
0256             if (!categories.contains(category) && !(catPtr && catPtr->isSpecialCategory())) {
0257                 categories.append(category);
0258             }
0259         }
0260     }
0261 
0262     if (!categories.isEmpty()) {
0263         m_categoryMatcher = new ImportMatcher(QString(), QString(), categories, DB::ImageDB::instance()->categoryCollection()->categoryNames(DB::CategoryCollection::IncludeSpecialCategories::No),
0264                                               false, this);
0265         m_categoryMatcherPage = addPage(m_categoryMatcher, i18n("Match Categories"));
0266 
0267         QWidget *dummy = new QWidget;
0268         m_dummy = addPage(dummy, QString());
0269     } else {
0270         m_categoryMatcherPage = nullptr;
0271         possiblyAddMD5CheckPage();
0272     }
0273 }
0274 
0275 ImportMatcher *ImportDialog::createCategoryPage(const QString &myCategory, const QString &otherCategory)
0276 {
0277     StringSet otherItems;
0278     DB::ImageInfoList images = selectedImages();
0279     for (DB::ImageInfoListConstIterator it = images.constBegin(); it != images.constEnd(); ++it) {
0280         otherItems += (*it)->itemsOfCategory(otherCategory);
0281     }
0282 
0283     QStringList myItems = DB::ImageDB::instance()->categoryCollection()->categoryForName(myCategory)->itemsInclCategories();
0284     myItems.sort();
0285 
0286     const QStringList otherItemsList(otherItems.begin(), otherItems.end());
0287     ImportMatcher *matcher = new ImportMatcher(otherCategory, myCategory, otherItemsList, myItems, true, this);
0288     addPage(matcher, myCategory);
0289     return matcher;
0290 }
0291 
0292 void ImportDialog::next()
0293 {
0294     if (currentPage() == m_destinationPage) {
0295         QString dir = m_destinationEdit->text();
0296         if (!QFileInfo::exists(dir)) {
0297             const QString question = i18n("Folder %1 does not exist. Should it be created?", dir);
0298             const QString title = i18nc("@title", "Create folder?");
0299 #if KWIDGETSADDONS_VERSION >= QT_VERSION_CHECK(5, 100, 0)
0300             const auto answer = KMessageBox::questionTwoActions(this,
0301                                                                 question,
0302                                                                 title,
0303                                                                 KGuiItem(i18nc("@action:button", "Create")),
0304                                                                 KStandardGuiItem::cancel());
0305             if (answer != KMessageBox::ButtonCode::PrimaryAction)
0306                 return;
0307 #else
0308             const auto answer = KMessageBox::questionYesNo(this, question, title);
0309             if (answer != KMessageBox::Yes)
0310                 return;
0311 #endif
0312             bool ok = QDir().mkpath(dir);
0313             if (!ok) {
0314                 KMessageBox::error(this, i18n("Error creating folder %1", dir));
0315                 return;
0316             }
0317         }
0318     }
0319     if (!m_hasFilled && currentPage() == m_categoryMatcherPage) {
0320         m_hasFilled = true;
0321         m_categoryMatcher->setEnabled(false);
0322         removePage(m_dummy);
0323 
0324         ImportMatcher *matcher = nullptr;
0325         const auto matchers = m_categoryMatcher->m_matchers;
0326         for (const CategoryMatch *match : matchers) {
0327             if (match->m_checkbox->isChecked()) {
0328                 matcher = createCategoryPage(match->m_combobox->currentText(), match->m_text);
0329                 m_matchers.append(matcher);
0330             }
0331         }
0332         possiblyAddMD5CheckPage();
0333     }
0334 
0335     KAssistantDialog::next();
0336 }
0337 
0338 void ImportDialog::slotSelectAll()
0339 {
0340     selectImage(true);
0341 }
0342 
0343 void ImportDialog::slotSelectNone()
0344 {
0345     selectImage(false);
0346 }
0347 
0348 void ImportDialog::selectImage(bool on)
0349 {
0350     for (ImageRow *row : qAsConst(m_imagesSelect)) {
0351         row->m_checkbox->setChecked(on);
0352     }
0353 }
0354 
0355 DB::ImageInfoList ImportDialog::selectedImages() const
0356 {
0357     DB::ImageInfoList res;
0358     for (QList<ImageRow *>::ConstIterator it = m_imagesSelect.begin(); it != m_imagesSelect.end(); ++it) {
0359         if ((*it)->m_checkbox->isChecked())
0360             res.append((*it)->m_info);
0361     }
0362     return res;
0363 }
0364 
0365 void ImportDialog::slotHelp()
0366 {
0367     KHelpClient::invokeHelp(QString::fromLatin1("chp-importExport"));
0368 }
0369 
0370 ImportSettings ImportExport::ImportDialog::settings()
0371 {
0372     ImportSettings settings;
0373     settings.setSelectedImages(selectedImages());
0374     settings.setDestination(m_destinationEdit->text());
0375     settings.setExternalSource(m_externalSource);
0376     settings.setKimFile(m_kimFile);
0377     settings.setBaseURL(m_baseUrl);
0378 
0379     if (m_md5CheckPage) {
0380         settings.setImportActions(m_md5CheckPage->settings());
0381     }
0382 
0383     for (ImportMatcher *match : m_matchers)
0384         settings.addCategoryMatchSetting(match->settings());
0385 
0386     return settings;
0387 }
0388 
0389 void ImportExport::ImportDialog::possiblyAddMD5CheckPage()
0390 {
0391     if (MD5CheckPage::pageNeeded(settings())) {
0392         m_md5CheckPage = new MD5CheckPage(settings());
0393         addPage(m_md5CheckPage, i18n("How to resolve clashes"));
0394     }
0395 }
0396 
0397 // vi:expandtab:tabstop=4 shiftwidth=4:
0398 
0399 #include "moc_ImportDialog.cpp"