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