1 /* SPDX-FileCopyrightText: 2003-2020 The KPhotoAlbum Development Team
2 
3    SPDX-License-Identifier: GPL-2.0-or-later
4 */
5 
6 #include "ImportDialog.h"
7 
8 #include "ImageRow.h"
9 #include "ImportMatcher.h"
10 #include "KimFileReader.h"
11 #include "MD5CheckPage.h"
12 
13 #include <DB/ImageInfo.h>
14 #include <XMLDB/Database.h>
15 #include <kpabase/SettingsData.h>
16 
17 #include <KHelpClient>
18 #include <KLocalizedString>
19 #include <KMessageBox>
20 #include <QCheckBox>
21 #include <QComboBox>
22 #include <QDir>
23 #include <QFileDialog>
24 #include <QGridLayout>
25 #include <QHBoxLayout>
26 #include <QLabel>
27 #include <QLineEdit>
28 #include <QPixmap>
29 #include <QPushButton>
30 #include <QScrollArea>
31 
32 using Utilities::StringSet;
33 
34 class QPushButton;
35 using namespace ImportExport;
36 
ImportDialog(QWidget * parent)37 ImportDialog::ImportDialog(QWidget *parent)
38     : KAssistantDialog(parent)
39     , m_hasFilled(false)
40     , m_md5CheckPage(nullptr)
41 {
42 }
43 
exec(KimFileReader * kimFileReader,const QUrl & kimFileURL)44 bool ImportDialog::exec(KimFileReader *kimFileReader, const QUrl &kimFileURL)
45 {
46     m_kimFileReader = kimFileReader;
47 
48     if (kimFileURL.isLocalFile()) {
49         QDir cwd;
50         // convert relative local path to absolute
51         m_kimFile = QUrl::fromLocalFile(cwd.absoluteFilePath(kimFileURL.toLocalFile()))
52                         .adjusted(QUrl::NormalizePathSegments);
53     } else {
54         m_kimFile = kimFileURL;
55     }
56 
57     QByteArray indexXML = m_kimFileReader->indexXML();
58     if (indexXML.isNull())
59         return false;
60 
61     bool ok = readFile(indexXML);
62     if (!ok)
63         return false;
64 
65     setupPages();
66 
67     return KAssistantDialog::exec();
68 }
69 
readFile(const QByteArray & data)70 bool ImportDialog::readFile(const QByteArray &data)
71 {
72     XMLDB::ReaderPtr reader = XMLDB::ReaderPtr(new XMLDB::XmlReader(DB::ImageDB::instance()->uiDelegate(),
73                                                                     m_kimFile.toDisplayString()));
74     reader->addData(data);
75 
76     XMLDB::ElementInfo info = reader->readNextStartOrStopElement(QString::fromUtf8("KimDaBa-export"));
77     if (!info.isStartToken)
78         reader->complainStartElementExpected(QString::fromUtf8("KimDaBa-export"));
79 
80     // Read source
81     QString source = reader->attribute(QString::fromUtf8("location")).toLower();
82     if (source != QString::fromLatin1("inline") && source != QString::fromLatin1("external")) {
83         KMessageBox::error(this, i18n("<p>XML file did not specify the source of the images, "
84                                       "this is a strong indication that the file is corrupted</p>"));
85         return false;
86     }
87 
88     m_externalSource = (source == QString::fromLatin1("external"));
89 
90     // Read base url
91     m_baseUrl = QUrl::fromUserInput(reader->attribute(QString::fromLatin1("baseurl")));
92 
93     while (reader->readNextStartOrStopElement(QString::fromUtf8("image")).isStartToken) {
94         const DB::FileName fileName = DB::FileName::fromRelativePath(reader->attribute(QString::fromUtf8("file")));
95         DB::ImageInfoPtr info = XMLDB::Database::createImageInfo(fileName, reader);
96         m_images.append(info);
97     }
98     // the while loop already read the end element, so we tell readEndElement to not read the next token:
99     reader->readEndElement(false);
100 
101     return true;
102 }
103 
setupPages()104 void ImportDialog::setupPages()
105 {
106     createIntroduction();
107     createImagesPage();
108     createDestination();
109     createCategoryPages();
110     connect(this, &ImportDialog::currentPageChanged, this, &ImportDialog::updateNextButtonState);
111     QPushButton *helpButton = buttonBox()->button(QDialogButtonBox::Help);
112     connect(helpButton, &QPushButton::clicked, this, &ImportDialog::slotHelp);
113 }
114 
createIntroduction()115 void ImportDialog::createIntroduction()
116 {
117     QString txt = i18n("<h1><font size=\"+2\">Welcome to KPhotoAlbum Import</font></h1>"
118                        "This wizard will take you through the steps of an import operation. The steps are: "
119                        "<ol><li>First you must select which images you want to import from the export file. "
120                        "You do so by selecting the checkbox next to the image.</li>"
121                        "<li>Next you must tell KPhotoAlbum in which directory to put the images. This directory must "
122                        "of course be below the directory root KPhotoAlbum uses for images. "
123                        "KPhotoAlbum will take care to avoid name clashes</li>"
124                        "<li>The next step is to specify which categories you want to import (People, Places, ... ) "
125                        "and also tell KPhotoAlbum how to match the categories from the file to your categories. "
126                        "Imagine you load from a file, where a category is called <b>Blomst</b> (which is the "
127                        "Danish word for flower), then you would likely want to match this with your category, which might be "
128                        "called <b>Blume</b> (which is the German word for flower) - of course given you are German.</li>"
129                        "<li>The final steps, is matching the individual tokens from the categories. I may call myself <b>Jesper</b> "
130                        "in my image database, while you want to call me by my full name, namely <b>Jesper K. Pedersen</b>. "
131                        "In this step non matches will be highlighted in red, so you can see which tokens was not found in your "
132                        "database, or which tokens was only a partial match.</li></ol>");
133 
134     QLabel *intro = new QLabel(txt, this);
135     intro->setWordWrap(true);
136     addPage(intro, i18nc("@title:tab introduction page", "Introduction"));
137 }
138 
createImagesPage()139 void ImportDialog::createImagesPage()
140 {
141     QScrollArea *top = new QScrollArea;
142     top->setWidgetResizable(true);
143 
144     QWidget *container = new QWidget;
145     QVBoxLayout *lay1 = new QVBoxLayout(container);
146     top->setWidget(container);
147 
148     // Select all and Deselect All buttons
149     QHBoxLayout *lay2 = new QHBoxLayout;
150     lay1->addLayout(lay2);
151 
152     QPushButton *selectAll = new QPushButton(i18n("Select All"), container);
153     lay2->addWidget(selectAll);
154     QPushButton *selectNone = new QPushButton(i18n("Deselect All"), container);
155     lay2->addWidget(selectNone);
156     lay2->addStretch(1);
157     connect(selectAll, &QPushButton::clicked, this, &ImportDialog::slotSelectAll);
158     connect(selectNone, &QPushButton::clicked, this, &ImportDialog::slotSelectNone);
159 
160     QGridLayout *lay3 = new QGridLayout;
161     lay1->addLayout(lay3);
162 
163     lay3->setColumnStretch(2, 1);
164 
165     int row = 0;
166     for (DB::ImageInfoListConstIterator it = m_images.constBegin(); it != m_images.constEnd(); ++it, ++row) {
167         DB::ImageInfoPtr info = *it;
168         ImageRow *ir = new ImageRow(info, this, m_kimFileReader, container);
169         lay3->addWidget(ir->m_checkbox, row, 0);
170 
171         QPixmap pixmap = m_kimFileReader->loadThumbnail(info->fileName().relative());
172         if (!pixmap.isNull()) {
173             QPushButton *but = new QPushButton(container);
174             but->setIcon(pixmap);
175             but->setIconSize(pixmap.size());
176             lay3->addWidget(but, row, 1);
177             connect(but, &QPushButton::clicked, ir, &ImageRow::showImage);
178         } else {
179             QLabel *label = new QLabel(info->label());
180             lay3->addWidget(label, row, 1);
181         }
182 
183         QLabel *label = new QLabel(QString::fromLatin1("<p>%1</p>").arg(info->description()));
184         lay3->addWidget(label, row, 2);
185         m_imagesSelect.append(ir);
186     }
187 
188     addPage(top, i18n("Select Which Images to Import"));
189 }
190 
createDestination()191 void ImportDialog::createDestination()
192 {
193     QWidget *top = new QWidget(this);
194     QVBoxLayout *topLay = new QVBoxLayout(top);
195     QHBoxLayout *lay = new QHBoxLayout;
196     topLay->addLayout(lay);
197 
198     topLay->addStretch(1);
199 
200     QLabel *label = new QLabel(i18n("Destination of images: "), top);
201     lay->addWidget(label);
202 
203     m_destinationEdit = new QLineEdit(top);
204     lay->addWidget(m_destinationEdit, 1);
205 
206     QPushButton *but = new QPushButton(QString::fromLatin1("..."), top);
207     but->setFixedWidth(30);
208     lay->addWidget(but);
209 
210     m_destinationEdit->setText(Settings::SettingsData::instance()->imageDirectory());
211     connect(but, &QPushButton::clicked, this, &ImportDialog::slotEditDestination);
212     connect(m_destinationEdit, &QLineEdit::textChanged, this, &ImportDialog::updateNextButtonState);
213     m_destinationPage = addPage(top, i18n("Destination of Images"));
214 }
215 
slotEditDestination()216 void ImportDialog::slotEditDestination()
217 {
218     QString file = QFileDialog::getExistingDirectory(this, QString(), m_destinationEdit->text());
219     if (!file.isNull()) {
220         if (!QFileInfo(file).absoluteFilePath().startsWith(QFileInfo(Settings::SettingsData::instance()->imageDirectory()).absoluteFilePath())) {
221             KMessageBox::error(this, i18n("The directory must be a subdirectory of %1", Settings::SettingsData::instance()->imageDirectory()));
222         } else if (QFileInfo(file).absoluteFilePath().startsWith(
223                        QFileInfo(Settings::SettingsData::instance()->imageDirectory()).absoluteFilePath() + QString::fromLatin1("CategoryImages"))) {
224             KMessageBox::error(this, i18n("This directory is reserved for category images."));
225         } else {
226             m_destinationEdit->setText(file);
227             updateNextButtonState();
228         }
229     }
230 }
231 
updateNextButtonState()232 void ImportDialog::updateNextButtonState()
233 {
234     bool enabled = true;
235     if (currentPage() == m_destinationPage) {
236         QString dest = m_destinationEdit->text();
237         if (QFileInfo(dest).isFile())
238             enabled = false;
239         else if (!QFileInfo(dest).absoluteFilePath().startsWith(QFileInfo(Settings::SettingsData::instance()->imageDirectory()).absoluteFilePath()))
240             enabled = false;
241     }
242 
243     setValid(currentPage(), enabled);
244 }
245 
createCategoryPages()246 void ImportDialog::createCategoryPages()
247 {
248     QStringList categories;
249     const DB::ImageInfoList images = selectedImages();
250     for (DB::ImageInfoListConstIterator it = images.constBegin(); it != images.constEnd(); ++it) {
251         const DB::ImageInfoPtr info = *it;
252         const QStringList categoriesForImage = info->availableCategories();
253         for (const QString &category : categoriesForImage) {
254             auto catPtr = DB::ImageDB::instance()->categoryCollection()->categoryForName(category);
255             if (!categories.contains(category) && !(catPtr && catPtr->isSpecialCategory())) {
256                 categories.append(category);
257             }
258         }
259     }
260 
261     if (!categories.isEmpty()) {
262         m_categoryMatcher = new ImportMatcher(QString(), QString(), categories, DB::ImageDB::instance()->categoryCollection()->categoryNames(DB::CategoryCollection::IncludeSpecialCategories::No),
263                                               false, this);
264         m_categoryMatcherPage = addPage(m_categoryMatcher, i18n("Match Categories"));
265 
266         QWidget *dummy = new QWidget;
267         m_dummy = addPage(dummy, QString());
268     } else {
269         m_categoryMatcherPage = nullptr;
270         possiblyAddMD5CheckPage();
271     }
272 }
273 
createCategoryPage(const QString & myCategory,const QString & otherCategory)274 ImportMatcher *ImportDialog::createCategoryPage(const QString &myCategory, const QString &otherCategory)
275 {
276     StringSet otherItems;
277     DB::ImageInfoList images = selectedImages();
278     for (DB::ImageInfoListConstIterator it = images.constBegin(); it != images.constEnd(); ++it) {
279         otherItems += (*it)->itemsOfCategory(otherCategory);
280     }
281 
282     QStringList myItems = DB::ImageDB::instance()->categoryCollection()->categoryForName(myCategory)->itemsInclCategories();
283     myItems.sort();
284 
285 #if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
286     const QStringList otherItemsList(otherItems.begin(), otherItems.end());
287 #else
288     const QStringList otherItemsList = otherItems.toList();
289 #endif
290     ImportMatcher *matcher = new ImportMatcher(otherCategory, myCategory, otherItemsList, myItems, true, this);
291     addPage(matcher, myCategory);
292     return matcher;
293 }
294 
next()295 void ImportDialog::next()
296 {
297     if (currentPage() == m_destinationPage) {
298         QString dir = m_destinationEdit->text();
299         if (!QFileInfo(dir).exists()) {
300             int answer = KMessageBox::questionYesNo(this, i18n("Directory %1 does not exist. Should it be created?", dir));
301             if (answer == KMessageBox::Yes) {
302                 bool ok = QDir().mkpath(dir);
303                 if (!ok) {
304                     KMessageBox::error(this, i18n("Error creating directory %1", dir));
305                     return;
306                 }
307             } else
308                 return;
309         }
310     }
311     if (!m_hasFilled && currentPage() == m_categoryMatcherPage) {
312         m_hasFilled = true;
313         m_categoryMatcher->setEnabled(false);
314         removePage(m_dummy);
315 
316         ImportMatcher *matcher = nullptr;
317         const auto matchers = m_categoryMatcher->m_matchers;
318         for (const CategoryMatch *match : matchers) {
319             if (match->m_checkbox->isChecked()) {
320                 matcher = createCategoryPage(match->m_combobox->currentText(), match->m_text);
321                 m_matchers.append(matcher);
322             }
323         }
324         possiblyAddMD5CheckPage();
325     }
326 
327     KAssistantDialog::next();
328 }
329 
slotSelectAll()330 void ImportDialog::slotSelectAll()
331 {
332     selectImage(true);
333 }
334 
slotSelectNone()335 void ImportDialog::slotSelectNone()
336 {
337     selectImage(false);
338 }
339 
selectImage(bool on)340 void ImportDialog::selectImage(bool on)
341 {
342     for (ImageRow *row : qAsConst(m_imagesSelect)) {
343         row->m_checkbox->setChecked(on);
344     }
345 }
346 
selectedImages() const347 DB::ImageInfoList ImportDialog::selectedImages() const
348 {
349     DB::ImageInfoList res;
350     for (QList<ImageRow *>::ConstIterator it = m_imagesSelect.begin(); it != m_imagesSelect.end(); ++it) {
351         if ((*it)->m_checkbox->isChecked())
352             res.append((*it)->m_info);
353     }
354     return res;
355 }
356 
slotHelp()357 void ImportDialog::slotHelp()
358 {
359     KHelpClient::invokeHelp(QString::fromLatin1("chp-importExport"));
360 }
361 
settings()362 ImportSettings ImportExport::ImportDialog::settings()
363 {
364     ImportSettings settings;
365     settings.setSelectedImages(selectedImages());
366     settings.setDestination(m_destinationEdit->text());
367     settings.setExternalSource(m_externalSource);
368     settings.setKimFile(m_kimFile);
369     settings.setBaseURL(m_baseUrl);
370 
371     if (m_md5CheckPage) {
372         settings.setImportActions(m_md5CheckPage->settings());
373     }
374 
375     for (ImportMatcher *match : m_matchers)
376         settings.addCategoryMatchSetting(match->settings());
377 
378     return settings;
379 }
380 
possiblyAddMD5CheckPage()381 void ImportExport::ImportDialog::possiblyAddMD5CheckPage()
382 {
383     if (MD5CheckPage::pageNeeded(settings())) {
384         m_md5CheckPage = new MD5CheckPage(settings());
385         addPage(m_md5CheckPage, i18n("How to resolve clashes"));
386     }
387 }
388 
389 // vi:expandtab:tabstop=4 shiftwidth=4:
390