1 /* ============================================================
2 *
3 * This file is a part of digiKam project
4 * https://www.digikam.org
5 *
6 * Date : 2004-07-01
7 * Description : dialog to edit and create digiKam Tags
8 *
9 * Copyright (C) 2004-2005 by Renchi Raju <renchi dot raju at gmail dot com>
10 * Copyright (C) 2006-2021 by Gilles Caulier <caulier dot gilles at gmail dot com>
11 *
12 * This program is free software; you can redistribute it
13 * and/or modify it under the terms of the GNU General
14 * Public License as published by the Free Software Foundation;
15 * either version 2, or (at your option)
16 * any later version.
17 *
18 * This program is distributed in the hope that it will be useful,
19 * but WITHOUT ANY WARRANTY; without even the implied warranty of
20 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 * GNU General Public License for more details.
22 *
23 * ============================================================ */
24
25 #include "tageditdlg.h"
26
27 // Qt includes
28
29 #include <QGridLayout>
30 #include <QLabel>
31 #include <QPointer>
32 #include <QTreeWidget>
33 #include <QApplication>
34 #include <QStyle>
35 #include <QStandardPaths>
36 #include <QDialogButtonBox>
37 #include <QVBoxLayout>
38 #include <QPushButton>
39
40 // KDE includes
41
42 #include <klocalizedstring.h>
43 #include <kkeysequencewidget.h>
44
45 #ifdef HAVE_KICONTHEMES
46 # include <kicondialog.h>
47 #endif
48
49 // Local includes
50
51 #include "album.h"
52 #include "syncjob.h"
53 #include "searchtextbar.h"
54 #include "tagsactionmngr.h"
55 #include "coredbconstants.h"
56 #include "digikam_debug.h"
57 #include "dxmlguiwindow.h"
58 #include "dexpanderbox.h"
59 #include "dlayoutbox.h"
60
61 namespace Digikam
62 {
63
64 class Q_DECL_HIDDEN TagsListCreationErrorDialog : public QDialog
65 {
66 Q_OBJECT
67
68 public:
69
70 TagsListCreationErrorDialog(QWidget* const parent, const QMap<QString, QString>& errMap);
~TagsListCreationErrorDialog()71 ~TagsListCreationErrorDialog() override {};
72 };
73
74 // ------------------------------------------------------------------------------
75
76 class Q_DECL_HIDDEN TagEditDlg::Private
77 {
78 public:
79
Private()80 explicit Private()
81 : create (false),
82 topLabel (nullptr),
83 iconButton (nullptr),
84 resetIconButton(nullptr),
85 buttons (nullptr),
86 keySeqWidget (nullptr),
87 mainRootAlbum (nullptr),
88 titleEdit (nullptr)
89 {
90 }
91
92 bool create;
93
94 QLabel* topLabel;
95
96 QString icon;
97
98 QPushButton* iconButton;
99 QPushButton* resetIconButton;
100
101 QDialogButtonBox* buttons;
102
103 KKeySequenceWidget* keySeqWidget;
104
105 TAlbum* mainRootAlbum;
106 SearchTextBar* titleEdit;
107 };
108
TagEditDlg(QWidget * const parent,TAlbum * const album,bool create)109 TagEditDlg::TagEditDlg(QWidget* const parent, TAlbum* const album, bool create)
110 : QDialog(parent),
111 d (new Private)
112 {
113 setModal(true);
114
115 d->buttons = new QDialogButtonBox(QDialogButtonBox::Help | QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
116 d->buttons->button(QDialogButtonBox::Ok)->setDefault(true);
117
118 if (create)
119 {
120 setWindowTitle(i18n("New Tag"));
121 }
122 else
123 {
124 setWindowTitle(i18n("Edit Tag"));
125 }
126
127 d->mainRootAlbum = album;
128 d->create = create;
129 QWidget* const page = new QWidget(this);
130
131 // --------------------------------------------------------
132
133 QGridLayout* const grid = new QGridLayout(page);
134 QLabel* const logo = new QLabel(page);
135 logo->setPixmap(QIcon::fromTheme(QLatin1String("digikam")).pixmap(QSize(48,48)));
136
137 d->topLabel = new QLabel(page);
138 d->topLabel->setAlignment(Qt::AlignLeft | Qt::AlignVCenter);
139 d->topLabel->setWordWrap(false);
140
141 DLineWidget* const line = new DLineWidget(Qt::Horizontal, page);
142
143 // --------------------------------------------------------
144
145 QLabel* const titleLabel = new QLabel(page);
146 titleLabel->setText(i18nc("@label: tag properties", "&Title:"));
147
148 d->titleEdit = new SearchTextBar(page, QLatin1String("TagEditDlgTitleEdit"), i18n("Enter tag name here..."));
149 d->titleEdit->setCaseSensitive(false);
150 titleLabel->setBuddy(d->titleEdit);
151
152 QLabel* const tipLabel = new QLabel(page);
153 tipLabel->setTextFormat(Qt::RichText);
154 tipLabel->setWordWrap(true);
155 tipLabel->setText(i18n("<p>To create new tags, you can use the following rules:</p>"
156 "<p><ul><li>'/' can be used to create a tags hierarchy.<br/>"
157 "Ex.: <i>\"Country/City/Paris\"</i></li>"
158 "<li>',' can be used to create more than one tags hierarchy at the same time.<br/>"
159 "Ex.: <i>\"City/Paris, Monument/Notre-Dame\"</i></li>"
160 "<li>If a tag hierarchy starts with '/', root tag album is used as parent.</li></ul></p>"
161 ));
162
163 if (d->create)
164 {
165 QStringList tagPaths;
166 AlbumList tList = AlbumManager::instance()->allTAlbums();
167
168 for (AlbumList::const_iterator it = tList.constBegin() ; it != tList.constEnd() ; ++it)
169 {
170 TAlbum* const tag = static_cast<TAlbum*>(*it);
171
172 if (tag && !tag->isInternalTag())
173 {
174 tagPaths << tag->tagPath();
175 }
176 }
177
178 d->titleEdit->completerModel()->setList(tagPaths);
179 }
180 else
181 {
182 d->titleEdit->setText(d->mainRootAlbum->title());
183 tipLabel->hide();
184 }
185
186 // --------------------------------------------------------
187
188 QLabel* const iconTextLabel = new QLabel(page);
189 iconTextLabel->setText(i18n("&Icon:"));
190
191 d->iconButton = new QPushButton(page);
192 d->iconButton->setFixedSize(40, 40);
193 d->iconButton->setIcon(SyncJob::getTagThumbnail(album));
194 iconTextLabel->setBuddy(d->iconButton);
195
196 // In create mode, by default assign the icon of the parent (if not root) to this new tag.
197
198 d->icon = album->icon();
199
200 d->resetIconButton = new QPushButton(QIcon::fromTheme(QLatin1String("view-refresh")), i18n("Reset"), page);
201
202 #ifndef HAVE_KICONTHEMES
203
204 iconTextLabel->hide();
205 d->iconButton->hide();
206 d->resetIconButton->hide();
207
208 #endif
209
210 // --------------------------------------------------------
211
212 QLabel* const kscTextLabel = new QLabel(page);
213 kscTextLabel->setText(i18n("&Shortcut:"));
214
215 d->keySeqWidget = new KKeySequenceWidget(page);
216 kscTextLabel->setBuddy(d->keySeqWidget);
217
218 if (!create)
219 {
220 QString Seq = album->property(TagPropertyName::tagKeyboardShortcut());
221 d->keySeqWidget->setKeySequence(Seq);
222 }
223 else
224 {
225 // Do not inherit tag shortcut, it creates a conflict shortcut, see bug 309558.
226
227 d->keySeqWidget->setCheckActionCollections(TagsActionMngr::defaultManager()->actionCollections());
228 }
229
230 QLabel* const tipLabel2 = new QLabel(page);
231 tipLabel2->setTextFormat(Qt::RichText);
232 tipLabel2->setWordWrap(true);
233 tipLabel2->setText(i18n("<p><b>Note</b>: this shortcut can be used to assign or unassign tag to items.</p>"));
234
235 // --------------------------------------------------------
236
237 const int spacing = QApplication::style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing);
238 const int cmargin = QApplication::style()->pixelMetric(QStyle::PM_DefaultChildMargin);
239
240 grid->addWidget(logo, 0, 0, 1, 1);
241 grid->addWidget(d->topLabel, 0, 1, 1, 4);
242 grid->addWidget(line, 1, 0, 1, 4);
243 grid->addWidget(tipLabel, 2, 0, 1, 4);
244 grid->addWidget(titleLabel, 3, 0, 1, 1);
245 grid->addWidget(d->titleEdit, 3, 1, 1, 3);
246 grid->addWidget(iconTextLabel, 4, 0, 1, 1);
247 grid->addWidget(d->iconButton, 4, 1, 1, 1);
248 grid->addWidget(d->resetIconButton, 4, 2, 1, 1);
249 grid->addWidget(kscTextLabel, 5, 0, 1, 1);
250 grid->addWidget(d->keySeqWidget, 5, 1, 1, 3);
251 grid->addWidget(tipLabel2, 6, 0, 1, 4);
252 grid->setRowStretch(7, 10);
253 grid->setColumnStretch(3, 10);
254 grid->setContentsMargins(cmargin, cmargin, cmargin, cmargin);
255 grid->setSpacing(spacing);
256
257 QVBoxLayout* const vbx = new QVBoxLayout(this);
258 vbx->addWidget(page);
259 vbx->addWidget(d->buttons);
260 setLayout(vbx);
261
262 // --------------------------------------------------------
263
264 connect(d->iconButton, SIGNAL(clicked()),
265 this, SLOT(slotIconChanged()));
266
267 connect(d->resetIconButton, SIGNAL(clicked()),
268 this, SLOT(slotIconResetClicked()));
269
270 connect(d->titleEdit, SIGNAL(textChanged(QString)),
271 this, SLOT(slotTitleChanged(QString)));
272
273 connect(d->buttons->button(QDialogButtonBox::Ok), SIGNAL(clicked()),
274 this, SLOT(accept()));
275
276 connect(d->buttons->button(QDialogButtonBox::Cancel), SIGNAL(clicked()),
277 this, SLOT(reject()));
278
279 connect(d->buttons->button(QDialogButtonBox::Help), SIGNAL(clicked()),
280 this, SLOT(slotHelp()));
281
282 // --------------------------------------------------------
283
284 slotTitleChanged(d->titleEdit->text());
285 d->titleEdit->setFocus();
286 adjustSize();
287 }
288
~TagEditDlg()289 TagEditDlg::~TagEditDlg()
290 {
291 delete d;
292 }
293
title() const294 QString TagEditDlg::title() const
295 {
296 return d->titleEdit->text();
297 }
298
icon() const299 QString TagEditDlg::icon() const
300 {
301 return d->icon;
302 }
303
shortcut() const304 QKeySequence TagEditDlg::shortcut() const
305 {
306 return d->keySeqWidget->keySequence();
307 }
308
slotIconResetClicked()309 void TagEditDlg::slotIconResetClicked()
310 {
311 d->icon = d->mainRootAlbum->standardIconName();
312 d->iconButton->setIcon(QIcon::fromTheme(d->icon));
313 }
314
slotIconChanged()315 void TagEditDlg::slotIconChanged()
316 {
317
318 #ifdef HAVE_KICONTHEMES
319
320 QPointer<KIconDialog> dlg = new KIconDialog(this);
321 dlg->setup(KIconLoader::NoGroup, KIconLoader::Application, false, 20, false, false, false);
322 QString icon = dlg->openDialog();
323 delete dlg;
324
325 if (icon.isEmpty() || (icon == d->icon))
326 {
327 return;
328 }
329
330 d->icon = icon;
331 d->iconButton->setIcon(QIcon::fromTheme(d->icon));
332
333 #endif
334
335 }
336
slotTitleChanged(const QString & newtitle)337 void TagEditDlg::slotTitleChanged(const QString& newtitle)
338 {
339 QString tagName = d->mainRootAlbum->tagPath();
340
341 if (tagName.endsWith(QLatin1Char('/')) && !d->mainRootAlbum->isRoot())
342 {
343 tagName.truncate(tagName.length()-1);
344 }
345
346 if (d->create)
347 {
348 if (d->titleEdit->text().startsWith(QLatin1Char('/')))
349 {
350 d->topLabel->setText(i18n("<b>Create New Tag</b>"));
351 }
352 else
353 {
354 d->topLabel->setText(i18n("<b>Create New Tag in<br/>"
355 "\"%1\"</b>", tagName));
356 }
357 }
358 else
359 {
360 d->topLabel->setText(i18n("<b>Properties of Tag<br/>"
361 "\"%1\"</b>", tagName));
362 }
363
364 QRegExp emptyTitle = QRegExp(QLatin1String("^\\s*$"));
365 bool enable = (!emptyTitle.exactMatch(newtitle) && !newtitle.isEmpty());
366 d->buttons->button(QDialogButtonBox::Ok)->setEnabled(enable);
367 }
368
tagEdit(QWidget * const parent,TAlbum * const album,QString & title,QString & icon,QKeySequence & ks)369 bool TagEditDlg::tagEdit(QWidget* const parent, TAlbum* const album, QString& title, QString& icon, QKeySequence& ks)
370 {
371 QPointer<TagEditDlg> dlg = new TagEditDlg(parent, album);
372 bool valRet = dlg->exec();
373
374 if (valRet == QDialog::Accepted)
375 {
376 title = dlg->title();
377 icon = dlg->icon();
378 ks = dlg->shortcut();
379 }
380
381 delete dlg;
382
383 return valRet;
384 }
385
tagCreate(QWidget * const parent,TAlbum * const album,QString & title,QString & icon,QKeySequence & ks)386 bool TagEditDlg::tagCreate(QWidget* const parent, TAlbum* const album, QString& title, QString& icon, QKeySequence& ks)
387 {
388 QPointer<TagEditDlg> dlg = new TagEditDlg(parent, album, true);
389
390 bool valRet = dlg->exec();
391
392 if (valRet == QDialog::Accepted)
393 {
394 title = dlg->title();
395 icon = dlg->icon();
396 ks = dlg->shortcut();
397 }
398
399 delete dlg;
400
401 return valRet;
402 }
403
createTAlbum(TAlbum * const mainRootAlbum,const QString & tagStr,const QString & icon,const QKeySequence & ks,QMap<QString,QString> & errMap)404 AlbumList TagEditDlg::createTAlbum(TAlbum* const mainRootAlbum, const QString& tagStr, const QString& icon,
405 const QKeySequence& ks, QMap<QString, QString>& errMap)
406 {
407 errMap.clear();
408 AlbumList createdTagsList;
409 TAlbum* root = nullptr;
410
411 // Check if new tags are include in a list of tags hierarchy separated by ','.
412 // Ex: /Country/France/people,/City/France/Paris
413
414 const QStringList tagsHierarchies = tagStr.split(QLatin1Char(','), QString::SkipEmptyParts);
415
416 if (tagsHierarchies.isEmpty())
417 {
418 return createdTagsList;
419 }
420
421 for (QStringList::const_iterator it = tagsHierarchies.constBegin() ;
422 it != tagsHierarchies.constEnd() ; ++it)
423 {
424 QString hierarchy = (*it).trimmed();
425
426 if (!hierarchy.isEmpty())
427 {
428 // Check if new tags is a hierarchy of tags separated by '/'.
429
430 root = nullptr;
431
432 if (hierarchy.startsWith(QLatin1Char('/')) || !mainRootAlbum)
433 {
434 root = AlbumManager::instance()->findTAlbum(0);
435 }
436 else
437 {
438 root = mainRootAlbum;
439 }
440
441 QStringList tagsList = hierarchy.split(QLatin1Char('/'), QString::SkipEmptyParts);
442 qCDebug(DIGIKAM_GENERAL_LOG) << tagsList;
443
444 if (!tagsList.isEmpty())
445 {
446 for (QStringList::const_iterator it2 = tagsList.constBegin() ;
447 it2 != tagsList.constEnd() ; ++it2)
448 {
449 QString tagPath, errMsg;
450 QString tag = (*it2).trimmed();
451
452 if (root->isRoot())
453 {
454 tagPath = QString::fromUtf8("/%1").arg(tag);
455 }
456 else
457 {
458 tagPath = QString::fromUtf8("%1/%2").arg(root->tagPath()).arg(tag);
459 }
460
461 qCDebug(DIGIKAM_GENERAL_LOG) << tag << " :: " << tagPath;
462
463 if (!tag.isEmpty())
464 {
465 // Tag already exist ?
466
467 TAlbum* const album = AlbumManager::instance()->findTAlbum(tagPath);
468
469 if (!album)
470 {
471 root = AlbumManager::instance()->createTAlbum(root, tag, icon, errMsg);
472 }
473 else
474 {
475 root = album;
476
477 if (*it2 == tagsList.last())
478 {
479 errMap.insert(tagPath, i18n("Tag name already exists"));
480 }
481 }
482
483 if (root)
484 {
485 createdTagsList.append(root);
486 }
487 }
488
489 // Sanity check if tag creation failed.
490
491 if (!root)
492 {
493 errMap.insert(tagPath, errMsg);
494 break;
495 }
496 }
497 }
498 }
499 }
500
501 // Assign the keyboard shortcut to the last tag created from the hierarchy.
502
503 if (root && !ks.isEmpty())
504 {
505 TagsActionMngr::defaultManager()->updateTagShortcut(root->id(), ks);
506 }
507
508 return createdTagsList;
509 }
510
showtagsListCreationError(QWidget * const parent,const QMap<QString,QString> & errMap)511 void TagEditDlg::showtagsListCreationError(QWidget* const parent, const QMap<QString, QString>& errMap)
512 {
513 if (!errMap.isEmpty())
514 {
515 QPointer<TagsListCreationErrorDialog> dlg = new TagsListCreationErrorDialog(parent, errMap);
516 dlg->exec();
517 delete dlg;
518 }
519 }
520
slotHelp()521 void TagEditDlg::slotHelp()
522 {
523 DXmlGuiWindow::openHandbook();
524 }
525
526 // ------------------------------------------------------------------------------
527
TagsListCreationErrorDialog(QWidget * const parent,const QMap<QString,QString> & errMap)528 TagsListCreationErrorDialog::TagsListCreationErrorDialog(QWidget* const parent, const QMap<QString, QString>& errMap)
529 : QDialog(parent)
530 {
531 setModal(true);
532 setWindowTitle(i18n("Tag creation Error"));
533
534 const int spacing = QApplication::style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing);
535 const int cmargin = QApplication::style()->pixelMetric(QStyle::PM_DefaultChildMargin);
536
537 QDialogButtonBox* const buttons = new QDialogButtonBox(QDialogButtonBox::Ok, this);
538 buttons->button(QDialogButtonBox::Ok)->setDefault(true);
539
540 QWidget* const page = new QWidget(this);
541 QVBoxLayout* const vLay = new QVBoxLayout(page);
542
543 QLabel* const label = new QLabel(i18n("An error occurred during tag creation:"), page);
544 QTreeWidget* const listView = new QTreeWidget(page);
545 listView->setHeaderLabels(QStringList() << i18n("Tag Path") << i18n("Error"));
546 listView->setRootIsDecorated(false);
547 listView->setSelectionMode(QAbstractItemView::SingleSelection);
548 listView->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
549
550 vLay->addWidget(label);
551 vLay->addWidget(listView);
552 vLay->setContentsMargins(cmargin, cmargin, cmargin, cmargin);
553 vLay->setSpacing(spacing);
554
555 for (QMap<QString, QString>::const_iterator it = errMap.constBegin() ;
556 it != errMap.constEnd() ; ++it)
557 {
558 new QTreeWidgetItem(listView, QStringList() << it.key() << it.value());
559 }
560
561 QVBoxLayout* const vbx = new QVBoxLayout(this);
562 vbx->addWidget(page);
563 vbx->addWidget(buttons);
564 setLayout(vbx);
565
566 connect(buttons->button(QDialogButtonBox::Ok), SIGNAL(clicked()),
567 this, SLOT(accept()));
568
569 adjustSize();
570 }
571
572 } // namespace Digikam
573
574 #include "tageditdlg.moc"
575