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