File indexing completed on 2024-04-28 15:39:47

0001 // SPDX-FileCopyrightText: 2003 David Faure <faure@kde.org>
0002 // SPDX-FileCopyrightText: 2003-2005 Stephan Binner <binner@kde.org>
0003 // SPDX-FileCopyrightText: 2003-2022 Jesper K. Pedersen <jesper.pedersen@kdab.com>
0004 // SPDX-FileCopyrightText: 2004 Andrew Coles <andrew.i.coles@googlemail.com>
0005 // SPDX-FileCopyrightText: 2004-2007 Dirk Mueller <mueller@kde.org>
0006 // SPDX-FileCopyrightText: 2006-2007 Tuomas Suutari <tuomas@nepnep.net>
0007 // SPDX-FileCopyrightText: 2007-2008 Laurent Montel <montel@kde.org>
0008 // SPDX-FileCopyrightText: 2008 Jakob Petsovits <jpetso@gmx.at>
0009 // SPDX-FileCopyrightText: 2009 Hassan Ibraheem <hasan.ibraheem@gmail.com>
0010 // SPDX-FileCopyrightText: 2010 Jan Kundrát <jkt@flaska.net>
0011 // SPDX-FileCopyrightText: 2010-2013 Miika Turkia <miika.turkia@gmail.com>
0012 // SPDX-FileCopyrightText: 2012-2024 Johannes Zarl-Zierl <johannes@zarl-zierl.at>
0013 // SPDX-FileCopyrightText: 2013 Dominik Broj <broj.dominik@gmail.com>
0014 // SPDX-FileCopyrightText: 2013 Reimar Imhof <Reimar.Imhof@netcologne.de>
0015 // SPDX-FileCopyrightText: 2014-2022 Tobias Leupold <tl@stonemx.de>
0016 // SPDX-FileCopyrightText: 2015 Yuri Chornoivan <yurchor@ukr.net>
0017 // SPDX-FileCopyrightText: 2017 Raymond Wooninck <tittiatcoke@gmail.com>
0018 //
0019 // SPDX-License-Identifier: GPL-2.0-or-later
0020 
0021 #include "ListSelect.h"
0022 
0023 #include "CompletableLineEdit.h"
0024 #include "ListViewItemHider.h"
0025 #include "ShowSelectionOnlyManager.h"
0026 
0027 #include <CategoryListView/CheckDropItem.h>
0028 #include <CategoryListView/DragableTreeWidget.h>
0029 #include <DB/CategoryItem.h>
0030 #include <DB/ImageDB.h>
0031 #include <DB/MemberMap.h>
0032 #include <kpabase/StringSet.h>
0033 
0034 #include <KIO/CopyJob>
0035 #include <KLocalizedString>
0036 #include <KMessageBox>
0037 #include <QButtonGroup>
0038 #include <QHeaderView>
0039 #include <QInputDialog>
0040 #include <QLabel>
0041 #include <QLayout>
0042 #include <QList>
0043 #include <QMenu>
0044 #include <QMouseEvent>
0045 #include <QRadioButton>
0046 #include <QToolButton>
0047 #include <QWidgetAction>
0048 #include <kcompletion_version.h>
0049 #include <kwidgetsaddons_version.h>
0050 
0051 using namespace AnnotationDialog;
0052 using CategoryListView::CheckDropItem;
0053 
0054 namespace
0055 {
0056 inline QPixmap smallIcon(const QString &iconName)
0057 {
0058     return QIcon::fromTheme(iconName).pixmap(KIconLoader::StdSizes::SizeSmall);
0059 }
0060 }
0061 
0062 AnnotationDialog::ListSelect::ListSelect(const DB::CategoryPtr &category, QWidget *parent)
0063     : QWidget(parent)
0064     , m_category(category)
0065     , m_baseTitle()
0066 {
0067     QVBoxLayout *layout = new QVBoxLayout(this);
0068 
0069     m_lineEdit = new CompletableLineEdit(this);
0070     m_lineEdit->setProperty("FocusCandidate", true);
0071     m_lineEdit->setProperty("WantsFocus", true);
0072     m_lineEdit->setPlaceholderText(i18nc("@label:textbox", "Enter a tag ..."));
0073     layout->addWidget(m_lineEdit);
0074 
0075     m_treeWidget = new CategoryListView::DragableTreeWidget(m_category, this);
0076     m_treeWidget->setHeaderLabel(QString::fromLatin1("items"));
0077     m_treeWidget->header()->hide();
0078     connect(m_treeWidget, &CategoryListView::DragableTreeWidget::itemClicked, this, &ListSelect::itemSelected);
0079     m_treeWidget->setContextMenuPolicy(Qt::CustomContextMenu);
0080     connect(m_treeWidget, &CategoryListView::DragableTreeWidget::customContextMenuRequested, this, &ListSelect::showContextMenu);
0081     connect(m_treeWidget, &CategoryListView::DragableTreeWidget::itemsChanged, this, &ListSelect::rePopulate);
0082     connect(m_treeWidget, &CategoryListView::DragableTreeWidget::itemClicked, this, &ListSelect::updateSelectionCount);
0083 
0084     layout->addWidget(m_treeWidget);
0085 
0086     // Merge CheckBox
0087     QHBoxLayout *lay2 = new QHBoxLayout;
0088     layout->addLayout(lay2);
0089 
0090     m_roIndicator = new QLabel;
0091     m_roIndicator->setPixmap(smallIcon(QString::fromLatin1("emblem-readonly")));
0092     lay2->addWidget(m_roIndicator);
0093     m_selectableIndicator = new QLabel;
0094     m_selectableIndicator->setPixmap(smallIcon(QString::fromLatin1("emblem-checked")));
0095     lay2->addWidget(m_selectableIndicator);
0096 
0097     m_or = new QRadioButton(i18n("or"), this);
0098     m_and = new QRadioButton(i18n("and"), this);
0099     lay2->addWidget(m_or);
0100     lay2->addWidget(m_and);
0101     lay2->addStretch(1);
0102 
0103     // Sorting tool button
0104     QButtonGroup *grp = new QButtonGroup(this);
0105     grp->setExclusive(true);
0106 
0107     m_alphaTreeSort = new QToolButton;
0108     m_alphaTreeSort->setIcon(smallIcon(QString::fromLatin1("view-list-tree")));
0109     m_alphaTreeSort->setCheckable(true);
0110     m_alphaTreeSort->setToolTip(i18n("Sort Alphabetically (Tree)"));
0111     grp->addButton(m_alphaTreeSort);
0112 
0113     m_alphaFlatSort = new QToolButton;
0114     m_alphaFlatSort->setIcon(smallIcon(QString::fromLatin1("draw-text")));
0115     m_alphaFlatSort->setCheckable(true);
0116     m_alphaFlatSort->setToolTip(i18n("Sort Alphabetically (Flat)"));
0117     grp->addButton(m_alphaFlatSort);
0118 
0119     m_dateSort = new QToolButton;
0120     m_dateSort->setIcon(smallIcon(QString::fromLatin1("x-office-calendar")));
0121     m_dateSort->setCheckable(true);
0122     m_dateSort->setToolTip(i18n("Sort by date"));
0123     grp->addButton(m_dateSort);
0124 
0125     m_showSelectedOnly = new QToolButton;
0126     m_showSelectedOnly->setIcon(smallIcon(QString::fromLatin1("view-filter")));
0127     m_showSelectedOnly->setCheckable(true);
0128     m_showSelectedOnly->setToolTip(i18n("Show only selected Ctrl+S"));
0129     m_showSelectedOnly->setChecked(ShowSelectionOnlyManager::instance().selectionIsLimited());
0130 
0131     m_alphaTreeSort->setChecked(Settings::SettingsData::instance()->viewSortType() == Settings::SortAlphaTree);
0132     m_alphaFlatSort->setChecked(Settings::SettingsData::instance()->viewSortType() == Settings::SortAlphaFlat);
0133     m_dateSort->setChecked(Settings::SettingsData::instance()->viewSortType() == Settings::SortLastUse);
0134     connect(m_dateSort, &QToolButton::clicked, this, &ListSelect::slotSortDate);
0135     connect(m_alphaTreeSort, &QToolButton::clicked, this, &ListSelect::slotSortAlphaTree);
0136     connect(m_alphaFlatSort, &QToolButton::clicked, this, &ListSelect::slotSortAlphaFlat);
0137     connect(m_showSelectedOnly, &QToolButton::clicked, &ShowSelectionOnlyManager::instance(), &ShowSelectionOnlyManager::toggle);
0138 
0139     lay2->addWidget(m_alphaTreeSort);
0140     lay2->addWidget(m_alphaFlatSort);
0141     lay2->addWidget(m_dateSort);
0142     lay2->addWidget(m_showSelectedOnly);
0143 
0144     connectLineEdit(m_lineEdit);
0145 
0146     populate();
0147 
0148     connect(Settings::SettingsData::instance(), &Settings::SettingsData::viewSortTypeChanged,
0149             this, &ListSelect::setViewSortType);
0150     connect(Settings::SettingsData::instance(), &Settings::SettingsData::matchTypeChanged,
0151             this, &ListSelect::updateListview);
0152 
0153     connect(&ShowSelectionOnlyManager::instance(), &ShowSelectionOnlyManager::limitToSelected, this, &ListSelect::limitToSelection);
0154     connect(&ShowSelectionOnlyManager::instance(), &ShowSelectionOnlyManager::broaden, this, &ListSelect::showAllChildren);
0155 
0156     if (category->isSpecialCategory()) {
0157         if (category->type() == DB::Category::TokensCategory)
0158             setEditMode(ListSelectEditMode::Selectable);
0159         else
0160             setEditMode(ListSelectEditMode::ReadOnly);
0161     }
0162     updateLineEditMode();
0163 }
0164 
0165 void AnnotationDialog::ListSelect::slotReturn()
0166 {
0167     if (computedEditMode() == ListSelectEditMode::Editable) {
0168         QString enteredText = m_lineEdit->text().trimmed();
0169         if (enteredText.isEmpty()) {
0170             return;
0171         }
0172 
0173         if (searchForUntaggedImagesTagNeeded()) {
0174             if (enteredText == Settings::SettingsData::instance()->untaggedTag()) {
0175                 KMessageBox::information(
0176                     this,
0177                     i18n("The tag you entered is the tag that is set automatically for newly "
0178                          "found, untagged images (cf. <interface>Settings|Configure KPhotoAlbum..."
0179                          "|Categories|Untagged Images</interface>). It will not show up here as "
0180                          "long as it is selected for this purpose."));
0181                 m_lineEdit->setText(QString());
0182                 return;
0183             }
0184         }
0185 
0186         m_category->addItem(enteredText);
0187         rePopulate();
0188 
0189         QList<QTreeWidgetItem *> items = m_treeWidget->findItems(enteredText, Qt::MatchExactly | Qt::MatchRecursive, 0);
0190         if (!items.isEmpty()) {
0191             items.at(0)->setCheckState(0, Qt::Checked);
0192             if (m_positionable) {
0193                 Q_EMIT positionableTagSelected(m_category->name(), items.at(0)->text(0));
0194             }
0195         } else {
0196             Q_ASSERT(false);
0197         }
0198 
0199         m_lineEdit->clear();
0200     }
0201     updateSelectionCount();
0202 }
0203 
0204 void ListSelect::slotExternalReturn(const QString &text)
0205 {
0206     m_lineEdit->setText(text);
0207     slotReturn();
0208 }
0209 
0210 QString AnnotationDialog::ListSelect::category() const
0211 {
0212     return m_category->name();
0213 }
0214 
0215 void AnnotationDialog::ListSelect::setSelection(const StringSet &on, const StringSet &partiallyOn)
0216 {
0217     for (QTreeWidgetItemIterator itemIt(m_treeWidget); *itemIt; ++itemIt) {
0218         if (partiallyOn.contains((*itemIt)->text(0)))
0219             (*itemIt)->setCheckState(0, Qt::PartiallyChecked);
0220         else
0221             (*itemIt)->setCheckState(0, on.contains((*itemIt)->text(0)) ? Qt::Checked : Qt::Unchecked);
0222     }
0223 
0224     m_lineEdit->clear();
0225     updateSelectionCount();
0226 }
0227 
0228 bool AnnotationDialog::ListSelect::isAND() const
0229 {
0230     return m_and->isChecked();
0231 }
0232 
0233 void AnnotationDialog::ListSelect::setMode(UsageMode mode)
0234 {
0235     m_mode = mode;
0236     updateLineEditMode();
0237     if (mode == SearchMode) {
0238         // "0" below is sorting key which ensures that None is always at top.
0239         CheckDropItem *item = new CheckDropItem(m_treeWidget, DB::ImageDB::NONE(), QString::fromLatin1("0"));
0240         configureItem(item);
0241         m_and->show();
0242         m_or->show();
0243         m_or->setChecked(true);
0244         m_showSelectedOnly->hide();
0245     } else {
0246         m_and->hide();
0247         m_or->hide();
0248         m_showSelectedOnly->show();
0249     }
0250     for (QTreeWidgetItemIterator itemIt(m_treeWidget); *itemIt; ++itemIt)
0251         configureItem(dynamic_cast<CategoryListView::CheckDropItem *>(*itemIt));
0252 
0253     // ensure that the selection count indicator matches the current mode:
0254     updateSelectionCount();
0255 }
0256 
0257 void ListSelect::setEditMode(ListSelectEditMode mode)
0258 {
0259     m_editMode = mode;
0260     updateLineEditMode();
0261 }
0262 
0263 void AnnotationDialog::ListSelect::setViewSortType(Settings::ViewSortType tp)
0264 {
0265     showAllChildren();
0266 
0267     // set sortType and redisplay with new sortType
0268     QString text = m_lineEdit->text();
0269     rePopulate();
0270     m_lineEdit->setText(text);
0271     setMode(m_mode); // generate the ***NONE*** entry if in search mode
0272 
0273     m_alphaTreeSort->setChecked(tp == Settings::SortAlphaTree);
0274     m_alphaFlatSort->setChecked(tp == Settings::SortAlphaFlat);
0275     m_dateSort->setChecked(tp == Settings::SortLastUse);
0276 }
0277 
0278 QString AnnotationDialog::ListSelect::text() const
0279 {
0280     return m_lineEdit->text();
0281 }
0282 
0283 void AnnotationDialog::ListSelect::setText(const QString &text)
0284 {
0285     m_lineEdit->setText(text);
0286     m_treeWidget->clearSelection();
0287 }
0288 
0289 void AnnotationDialog::ListSelect::itemSelected(QTreeWidgetItem *item)
0290 {
0291     if (!item) {
0292         // click outside any item
0293         return;
0294     }
0295 
0296     if (m_mode == SearchMode) {
0297         QString txt = item->text(0);
0298         QString res;
0299         QRegExp regEnd(QString::fromLatin1("\\s*[&|!]\\s*$"));
0300         QRegExp regStart(QString::fromLatin1("^\\s*[&|!]\\s*"));
0301 
0302         if (item->checkState(0) == Qt::Checked) {
0303             int matchPos = m_lineEdit->text().indexOf(txt);
0304             if (matchPos != -1) {
0305                 return;
0306             }
0307 
0308             int index = m_lineEdit->cursorPosition();
0309             QString start = m_lineEdit->text().left(index);
0310             QString end = m_lineEdit->text().mid(index);
0311 
0312             res = start;
0313             if (!start.isEmpty() && !start.contains(regEnd)) {
0314                 res += isAND() ? QString::fromLatin1(" & ") : QString::fromLatin1(" | ");
0315             }
0316             res += txt;
0317             if (!end.isEmpty() && !end.contains(regStart)) {
0318                 res += isAND() ? QString::fromLatin1(" & ") : QString::fromLatin1(" | ");
0319             }
0320             res += end;
0321         } else {
0322             int index = m_lineEdit->text().indexOf(txt);
0323             if (index == -1)
0324                 return;
0325 
0326             QString start = m_lineEdit->text().left(index);
0327             QString end = m_lineEdit->text().mid(index + txt.length());
0328             if (start.contains(regEnd))
0329                 start.replace(regEnd, QString::fromLatin1(""));
0330             else
0331                 end.replace(regStart, QString::fromLatin1(""));
0332 
0333             res = start + end;
0334         }
0335         m_lineEdit->setText(res);
0336     }
0337 
0338     else {
0339         if (m_positionable) {
0340             if (item->checkState(0) == Qt::Checked) {
0341                 Q_EMIT positionableTagSelected(m_category->name(), item->text(0));
0342             } else {
0343                 Q_EMIT positionableTagDeselected(m_category->name(), item->text(0));
0344             }
0345         }
0346 
0347         m_lineEdit->clear();
0348         showAllChildren();
0349         ensureAllInstancesAreStateChanged(item);
0350     }
0351 }
0352 
0353 void AnnotationDialog::ListSelect::showContextMenu(const QPoint &pos)
0354 {
0355     QMenu *menu = new QMenu(this);
0356 
0357     QTreeWidgetItem *item = m_treeWidget->itemAt(pos);
0358     // click on any item
0359     QString title = i18n("No Item Selected");
0360     if (item)
0361         title = item->text(0);
0362 
0363     QLabel *label = new QLabel(i18n("<b>%1</b>", title), menu);
0364     label->setAlignment(Qt::AlignCenter);
0365     QWidgetAction *action = new QWidgetAction(menu);
0366     action->setDefaultWidget(label);
0367     menu->addAction(action);
0368 
0369     QAction *deleteAction = menu->addAction(smallIcon(QString::fromLatin1("edit-delete")), i18n("Delete"));
0370     QAction *renameAction = menu->addAction(i18n("Rename..."));
0371 
0372     QLabel *categoryTitle = new QLabel(i18n("<b>Tag Groups</b>"), menu);
0373     categoryTitle->setAlignment(Qt::AlignCenter);
0374     action = new QWidgetAction(menu);
0375     action->setDefaultWidget(categoryTitle);
0376     menu->addAction(action);
0377 
0378     // -------------------------------------------------- Add/Remove member group
0379     DB::MemberMap &memberMap = DB::ImageDB::instance()->memberMap();
0380     QMenu *members = new QMenu(i18n("Tag groups"));
0381     menu->addMenu(members);
0382     QAction *newCategoryAction = nullptr;
0383     if (item) {
0384         QStringList grps = memberMap.groups(m_category->name());
0385 
0386         for (QStringList::ConstIterator it = grps.constBegin(); it != grps.constEnd(); ++it) {
0387             if (!memberMap.canAddMemberToGroup(m_category->name(), *it, item->text(0)))
0388                 continue;
0389             QAction *action = members->addAction(*it);
0390             action->setCheckable(true);
0391             action->setChecked((bool)memberMap.members(m_category->name(), *it, false).contains(item->text(0)));
0392             action->setData(*it);
0393         }
0394 
0395         if (!grps.isEmpty())
0396             members->addSeparator();
0397         newCategoryAction = members->addAction(i18n("Add this tag to a new tag group..."));
0398     }
0399 
0400     QAction *newSubcategoryAction = menu->addAction(i18n("Make this tag a tag group and add a tag..."));
0401 
0402     // -------------------------------------------------- Take item out of category
0403     QTreeWidgetItem *parent = item ? item->parent() : nullptr;
0404     QAction *takeAction = nullptr;
0405     if (parent)
0406         takeAction = menu->addAction(i18n("Remove from tag group %1", parent->text(0)));
0407 
0408     // -------------------------------------------------- sort
0409     QLabel *sortTitle = new QLabel(i18n("<b>Sorting</b>"));
0410     sortTitle->setAlignment(Qt::AlignCenter);
0411     action = new QWidgetAction(menu);
0412     action->setDefaultWidget(sortTitle);
0413     menu->addAction(action);
0414 
0415     QAction *usageAction = menu->addAction(i18n("Usage"));
0416     QAction *alphaFlatAction = menu->addAction(i18n("Alphabetical (Flat)"));
0417     QAction *alphaTreeAction = menu->addAction(i18n("Alphabetical (Tree)"));
0418     usageAction->setCheckable(true);
0419     usageAction->setChecked(Settings::SettingsData::instance()->viewSortType() == Settings::SortLastUse);
0420     alphaFlatAction->setCheckable(true);
0421     alphaFlatAction->setChecked(Settings::SettingsData::instance()->viewSortType() == Settings::SortAlphaFlat);
0422     alphaTreeAction->setCheckable(true);
0423     alphaTreeAction->setChecked(Settings::SettingsData::instance()->viewSortType() == Settings::SortAlphaTree);
0424 
0425     if (!item || computedEditMode() != Editable) {
0426         deleteAction->setEnabled(false);
0427         renameAction->setEnabled(false);
0428         members->setEnabled(false);
0429         if (newCategoryAction)
0430             newCategoryAction->setEnabled(false);
0431         newSubcategoryAction->setEnabled(false);
0432         if (takeAction)
0433             takeAction->setEnabled(false);
0434     }
0435     // -------------------------------------------------- exec
0436     QAction *which = menu->exec(m_treeWidget->mapToGlobal(pos));
0437     if (which == nullptr)
0438         return;
0439     else if (which == deleteAction) {
0440         Q_ASSERT(item);
0441         Q_ASSERT(computedEditMode() == Editable);
0442         int code = KMessageBox::warningContinueCancel(this, i18n("<p>Do you really want to delete \"%1\"?<br/>"
0443                                                                  "Deleting the item will remove any information "
0444                                                                  "about it from any image containing the item.</p>",
0445                                                                  title),
0446                                                       i18n("Really Delete %1?", item->text(0)),
0447                                                       KStandardGuiItem::del());
0448         if (code == KMessageBox::Continue) {
0449             if (item->checkState(0) == Qt::Checked && m_positionable) {
0450                 // An area could be linked against this. We can use positionableTagDeselected
0451                 // here, as the procedure is the same as if the tag had been deselected.
0452                 Q_EMIT positionableTagDeselected(m_category->name(), item->text(0));
0453             }
0454 
0455             m_category->removeItem(item->text(0));
0456             rePopulate();
0457         }
0458     } else if (which == renameAction) {
0459         Q_ASSERT(item);
0460         Q_ASSERT(computedEditMode() == Editable);
0461         bool ok;
0462         QString newStr = QInputDialog::getText(this,
0463                                                i18nc("@title", "Rename Item"), i18n("Enter new name:"),
0464                                                QLineEdit::Normal,
0465                                                item->text(0), &ok)
0466                              .trimmed();
0467 
0468         if (ok && !newStr.isEmpty() && newStr != item->text(0)) {
0469             const QString question = i18n("<p>Do you really want to rename \"%1\" to \"%2\"?<br/>"
0470                                           "Doing so will rename \"%3\" "
0471                                           "on any image containing it.</p>",
0472                                           item->text(0), newStr, item->text(0));
0473             const QString questionTitle = i18nc("@title", "Rename %1?", item->text(0));
0474 #if KWIDGETSADDONS_VERSION >= QT_VERSION_CHECK(5, 100, 0)
0475             const auto answer = KMessageBox::questionTwoActions(this,
0476                                                                 question,
0477                                                                 questionTitle,
0478                                                                 KGuiItem(i18nc("@action:button", "Rename")),
0479                                                                 KStandardGuiItem::cancel());
0480             if (answer == KMessageBox::ButtonCode::PrimaryAction) {
0481 #else
0482             const auto answer = KMessageBox::questionYesNo(this, question, questionTitle);
0483             if (answer == KMessageBox::Yes) {
0484 #endif
0485                 QString oldStr = item->text(0);
0486                 m_category->renameItem(oldStr, newStr);
0487                 bool checked = item->checkState(0) == Qt::Checked;
0488                 rePopulate();
0489                 // rePopuldate doesn't ask the backend if the item should be checked, so we need to do that.
0490                 checkItem(newStr, checked);
0491 
0492                 // rename the category image too
0493                 QString oldFile = m_category->fileForCategoryImage(category(), oldStr);
0494                 QString newFile = m_category->fileForCategoryImage(category(), newStr);
0495                 KIO::move(QUrl::fromLocalFile(oldFile), QUrl::fromLocalFile(newFile));
0496 
0497                 if (m_positionable) {
0498                     // Also take care of areas that could be linked against this
0499                     Q_EMIT positionableTagRenamed(m_category->name(), oldStr, newStr);
0500                 }
0501             }
0502         }
0503     } else if (which == usageAction) {
0504         Settings::SettingsData::instance()->setViewSortType(Settings::SortLastUse);
0505     } else if (which == alphaTreeAction) {
0506         Settings::SettingsData::instance()->setViewSortType(Settings::SortAlphaTree);
0507     } else if (which == alphaFlatAction) {
0508         Settings::SettingsData::instance()->setViewSortType(Settings::SortAlphaFlat);
0509     } else if (which == newCategoryAction) {
0510         Q_ASSERT(item);
0511         Q_ASSERT(computedEditMode() == Editable);
0512         QString superCategory = QInputDialog::getText(this,
0513                                                       i18n("New tag group"),
0514                                                       i18n("Name for the new tag group the tag will be added to:"));
0515         if (superCategory.isEmpty())
0516             return;
0517         m_category->addItem(superCategory);
0518         memberMap.addGroup(m_category->name(), superCategory);
0519         memberMap.addMemberToGroup(m_category->name(), superCategory, item->text(0));
0520         // DB::ImageDB::instance()->setMemberMap( memberMap );
0521         rePopulate();
0522     } else if (which == newSubcategoryAction) {
0523         Q_ASSERT(item);
0524         Q_ASSERT(computedEditMode() == Editable);
0525         QString subCategory = QInputDialog::getText(this,
0526                                                     i18n("Add a tag"),
0527                                                     i18n("Name for the tag to be added to this tag group:"));
0528         if (subCategory.isEmpty())
0529             return;
0530 
0531         m_category->addItem(subCategory);
0532         memberMap.addGroup(m_category->name(), item->text(0));
0533         memberMap.addMemberToGroup(m_category->name(), item->text(0), subCategory);
0534         // DB::ImageDB::instance()->setMemberMap( memberMap );
0535         m_category->addItem(subCategory);
0536 
0537         rePopulate();
0538         checkItem(subCategory, true);
0539     } else if (which == takeAction) {
0540         Q_ASSERT(item);
0541         Q_ASSERT(computedEditMode() == Editable);
0542         memberMap.removeMemberFromGroup(m_category->name(), parent->text(0), item->text(0));
0543         rePopulate();
0544     } else {
0545         Q_ASSERT(item);
0546         Q_ASSERT(computedEditMode() == Editable);
0547         QString checkedItem = which->data().value<QString>();
0548         if (which->isChecked()) // choosing the item doesn't check it, so this is the value before.
0549             memberMap.addMemberToGroup(m_category->name(), checkedItem, item->text(0));
0550         else
0551             memberMap.removeMemberFromGroup(m_category->name(), checkedItem, item->text(0));
0552         rePopulate();
0553     }
0554 
0555     delete menu;
0556 }
0557 
0558 void AnnotationDialog::ListSelect::addItems(DB::CategoryItem *item, QTreeWidgetItem *parent)
0559 {
0560     const bool isReadOnly = computedEditMode() == ListSelectEditMode::ReadOnly;
0561     for (QList<DB::CategoryItem *>::ConstIterator subcategoryIt = item->mp_subcategories.constBegin(); subcategoryIt != item->mp_subcategories.constEnd(); ++subcategoryIt) {
0562         CheckDropItem *newItem = nullptr;
0563 
0564         if (parent == nullptr)
0565             newItem = new CheckDropItem(m_treeWidget, (*subcategoryIt)->mp_name, QString());
0566         else
0567             newItem = new CheckDropItem(m_treeWidget, parent, (*subcategoryIt)->mp_name, QString());
0568 
0569         newItem->setExpanded(true);
0570         configureItem(newItem);
0571         if (isReadOnly) {
0572             newItem->setFlags(newItem->flags() ^ Qt::ItemIsUserCheckable);
0573         }
0574 
0575         addItems(*subcategoryIt, newItem);
0576     }
0577 }
0578 
0579 void AnnotationDialog::ListSelect::populate()
0580 {
0581     m_treeWidget->clear();
0582 
0583     if (Settings::SettingsData::instance()->viewSortType() == Settings::SortAlphaTree)
0584         populateAlphaTree();
0585     else if (Settings::SettingsData::instance()->viewSortType() == Settings::SortAlphaFlat)
0586         populateAlphaFlat();
0587     else
0588         populateMRU();
0589 
0590     hideUntaggedImagesTag();
0591 }
0592 
0593 bool AnnotationDialog::ListSelect::searchForUntaggedImagesTagNeeded()
0594 {
0595     if (!DB::ImageDB::instance()->untaggedCategoryFeatureConfigured()
0596         || Settings::SettingsData::instance()->untaggedImagesTagVisible()) {
0597         return false;
0598     }
0599 
0600     if (Settings::SettingsData::instance()->untaggedCategory() != category()) {
0601         return false;
0602     }
0603 
0604     return true;
0605 }
0606 
0607 void AnnotationDialog::ListSelect::hideUntaggedImagesTag()
0608 {
0609     if (!searchForUntaggedImagesTagNeeded()) {
0610         return;
0611     }
0612 
0613     QTreeWidgetItem *untaggedImagesTag = getUntaggedImagesTag();
0614     if (untaggedImagesTag) {
0615         untaggedImagesTag->setHidden(true);
0616     }
0617 }
0618 
0619 void AnnotationDialog::ListSelect::slotSortDate()
0620 {
0621     Settings::SettingsData::instance()->setViewSortType(Settings::SortLastUse);
0622 }
0623 
0624 void AnnotationDialog::ListSelect::slotSortAlphaTree()
0625 {
0626     Settings::SettingsData::instance()->setViewSortType(Settings::SortAlphaTree);
0627 }
0628 
0629 void AnnotationDialog::ListSelect::slotSortAlphaFlat()
0630 {
0631     Settings::SettingsData::instance()->setViewSortType(Settings::SortAlphaFlat);
0632 }
0633 
0634 void AnnotationDialog::ListSelect::rePopulate()
0635 {
0636     const StringSet on = itemsOn();
0637     const StringSet noChange = itemsUnchanged();
0638     populate();
0639     setSelection(on, noChange);
0640 
0641     if (ShowSelectionOnlyManager::instance().selectionIsLimited())
0642         limitToSelection();
0643 }
0644 
0645 void AnnotationDialog::ListSelect::showOnlyItemsMatching(const QString &text)
0646 {
0647     ListViewTextMatchHider dummy(text, Settings::SettingsData::instance()->matchType(), m_treeWidget);
0648     ShowSelectionOnlyManager::instance().unlimitFromSelection();
0649 }
0650 
0651 void AnnotationDialog::ListSelect::populateAlphaTree()
0652 {
0653     DB::CategoryItemPtr item = m_category->itemsCategories();
0654 
0655     m_treeWidget->setRootIsDecorated(true);
0656     addItems(item.data(), nullptr);
0657     m_treeWidget->sortByColumn(0, Qt::AscendingOrder);
0658     m_treeWidget->setSortingEnabled(true);
0659 }
0660 
0661 void AnnotationDialog::ListSelect::populateAlphaFlat()
0662 {
0663     QStringList items = m_category->itemsInclCategories();
0664     items.sort();
0665 
0666     m_treeWidget->setRootIsDecorated(false);
0667     for (QStringList::ConstIterator itemIt = items.constBegin(); itemIt != items.constEnd(); ++itemIt) {
0668         CheckDropItem *item = new CheckDropItem(m_treeWidget, *itemIt, *itemIt);
0669         configureItem(item);
0670     }
0671     m_treeWidget->sortByColumn(1, Qt::AscendingOrder);
0672     m_treeWidget->setSortingEnabled(true);
0673 }
0674 
0675 void AnnotationDialog::ListSelect::populateMRU()
0676 {
0677     QStringList items = m_category->itemsInclCategories();
0678 
0679     m_treeWidget->setRootIsDecorated(false);
0680     int index = 100000; // This counter will be converted to a string, and compared, and we don't want "1111" to be less than "2"
0681     for (QStringList::ConstIterator itemIt = items.constBegin(); itemIt != items.constEnd(); ++itemIt) {
0682         ++index;
0683         CheckDropItem *item = new CheckDropItem(m_treeWidget, *itemIt, QString::number(index));
0684         configureItem(item);
0685     }
0686     m_treeWidget->sortByColumn(1, Qt::AscendingOrder);
0687     m_treeWidget->setSortingEnabled(true);
0688 }
0689 
0690 void AnnotationDialog::ListSelect::toggleSortType()
0691 {
0692     Settings::SettingsData *data = Settings::SettingsData::instance();
0693     if (data->viewSortType() == Settings::SortLastUse)
0694         data->setViewSortType(Settings::SortAlphaTree);
0695     else if (data->viewSortType() == Settings::SortAlphaTree)
0696         data->setViewSortType(Settings::SortAlphaFlat);
0697     else
0698         data->setViewSortType(Settings::SortLastUse);
0699 }
0700 
0701 void AnnotationDialog::ListSelect::updateListview()
0702 {
0703     // update item list (e.g. when MatchType changes):
0704     showOnlyItemsMatching(text());
0705 }
0706 
0707 void AnnotationDialog::ListSelect::limitToSelection()
0708 {
0709     if (computedEditMode() != Editable)
0710         return;
0711 
0712     m_showSelectedOnly->setChecked(true);
0713     ListViewCheckedHider dummy(m_treeWidget);
0714 
0715     hideUntaggedImagesTag();
0716 }
0717 
0718 void AnnotationDialog::ListSelect::showAllChildren()
0719 {
0720     m_showSelectedOnly->setChecked(false);
0721     showOnlyItemsMatching(QString());
0722     hideUntaggedImagesTag();
0723 }
0724 
0725 QTreeWidgetItem *AnnotationDialog::ListSelect::getUntaggedImagesTag()
0726 {
0727     QList<QTreeWidgetItem *> matchingTags = m_treeWidget->findItems(
0728         Settings::SettingsData::instance()->untaggedTag(),
0729         Qt::MatchExactly | Qt::MatchRecursive, 0);
0730 
0731     // Be sure not to crash here in case the config points to a non-existent tag
0732     if (matchingTags.at(0) == nullptr) {
0733         return nullptr;
0734     } else {
0735         return matchingTags.at(0);
0736     }
0737 }
0738 
0739 void ListSelect::updateLineEditMode()
0740 {
0741     if (m_editMode == Selectable)
0742         m_lineEdit->setMode(SearchMode);
0743     else
0744         m_lineEdit->setMode(m_mode);
0745 
0746     const bool isReadOnly = computedEditMode() == ListSelectEditMode::ReadOnly;
0747     m_roIndicator->setVisible(isReadOnly);
0748     // deactivate read-only fields when editing:
0749     m_lineEdit->setEnabled((m_mode == UsageMode::SearchMode) || !isReadOnly);
0750     const bool isSelectable = computedEditMode() == ListSelectEditMode::Selectable;
0751     m_selectableIndicator->setVisible(isSelectable);
0752 }
0753 
0754 void AnnotationDialog::ListSelect::updateSelectionCount()
0755 {
0756     if (m_baseTitle.isEmpty() /* --> first time */
0757         || !parentWidget()->windowTitle().startsWith(m_baseTitle) /* --> title has changed */) {
0758 
0759         // save the original parentWidget title
0760         m_baseTitle = parentWidget()->windowTitle();
0761     }
0762 
0763     int itemsOnCount = itemsOn().size();
0764     // Don't count the untagged images tag:
0765     if (searchForUntaggedImagesTagNeeded()) {
0766         QTreeWidgetItem *untaggedImagesTag = getUntaggedImagesTag();
0767         if (untaggedImagesTag) {
0768             if (untaggedImagesTag->checkState(0) != Qt::Unchecked) {
0769                 itemsOnCount--;
0770             }
0771         }
0772     }
0773 
0774     switch (m_mode) {
0775     case InputMultiImageConfigMode:
0776         if (itemsUnchanged().size() > 0) {
0777             // if min != max
0778             // tri-state selection -> show min-max (selected items vs. partially selected items):
0779             parentWidget()->setWindowTitle(i18nc(
0780                 "Category name, then min-max of selected tags across several images. E.g. 'People (1-2)'",
0781                 "%1 (%2-%3)",
0782                 m_baseTitle,
0783                 itemsOnCount,
0784                 itemsOnCount + itemsUnchanged().size()));
0785             break;
0786         } // else fall through and only show one number:
0787         Q_FALLTHROUGH();
0788     case InputSingleImageConfigMode:
0789         if (itemsOnCount > 0) {
0790             // if any tags have been selected
0791             // "normal" on/off states -> show selected items
0792             parentWidget()->setWindowTitle(
0793                 i18nc("Category name, then number of selected tags. E.g. 'People (1)'",
0794                       "%1 (%2)",
0795                       m_baseTitle, itemsOnCount));
0796             break;
0797         } // else fall through and only show category
0798         Q_FALLTHROUGH();
0799     case SearchMode:
0800         // no indicator while searching
0801         parentWidget()->setWindowTitle(m_baseTitle);
0802         break;
0803     }
0804 }
0805 
0806 void AnnotationDialog::ListSelect::configureItem(CategoryListView::CheckDropItem *item)
0807 {
0808     bool isDNDAllowed = Settings::SettingsData::instance()->viewSortType() == Settings::SortAlphaTree;
0809     item->setDNDEnabled(isDNDAllowed && !m_category->isSpecialCategory());
0810 }
0811 
0812 ListSelectEditMode ListSelect::computedEditMode() const
0813 {
0814     if (m_mode == SearchMode)
0815         return ListSelectEditMode::Selectable;
0816     return m_editMode;
0817 }
0818 
0819 StringSet AnnotationDialog::ListSelect::itemsOn() const
0820 {
0821     return itemsOfState(Qt::Checked);
0822 }
0823 
0824 StringSet AnnotationDialog::ListSelect::itemsOff() const
0825 {
0826     return itemsOfState(Qt::Unchecked);
0827 }
0828 
0829 StringSet AnnotationDialog::ListSelect::itemsOfState(Qt::CheckState state) const
0830 {
0831     StringSet res;
0832     for (QTreeWidgetItemIterator itemIt(m_treeWidget); *itemIt; ++itemIt) {
0833         if ((*itemIt)->checkState(0) == state)
0834             res.insert((*itemIt)->text(0));
0835     }
0836     return res;
0837 }
0838 
0839 StringSet AnnotationDialog::ListSelect::itemsUnchanged() const
0840 {
0841     return itemsOfState(Qt::PartiallyChecked);
0842 }
0843 
0844 void AnnotationDialog::ListSelect::checkItem(const QString itemText, bool b)
0845 {
0846     QList<QTreeWidgetItem *> items = m_treeWidget->findItems(itemText, Qt::MatchExactly | Qt::MatchRecursive);
0847     if (!items.isEmpty())
0848         items.at(0)->setCheckState(0, b ? Qt::Checked : Qt::Unchecked);
0849     else
0850         Q_ASSERT(false);
0851 }
0852 
0853 /**
0854  * An item may be member of a number of categories. Mike may be a member of coworkers and friends.
0855  * Selecting the item in one subcategory, should select him in all.
0856  */
0857 void AnnotationDialog::ListSelect::ensureAllInstancesAreStateChanged(QTreeWidgetItem *item)
0858 {
0859     const bool on = item->checkState(0) == Qt::Checked;
0860     for (QTreeWidgetItemIterator itemIt(m_treeWidget); *itemIt; ++itemIt) {
0861         if ((*itemIt) != item && (*itemIt)->text(0) == item->text(0))
0862             (*itemIt)->setCheckState(0, on ? Qt::Checked : Qt::Unchecked);
0863     }
0864 }
0865 
0866 QWidget *AnnotationDialog::ListSelect::lineEdit() const
0867 {
0868     return m_lineEdit;
0869 }
0870 
0871 void AnnotationDialog::ListSelect::setPositionable(bool positionableState)
0872 {
0873     m_positionable = positionableState;
0874 }
0875 
0876 bool AnnotationDialog::ListSelect::positionable() const
0877 {
0878     return m_positionable;
0879 }
0880 
0881 bool AnnotationDialog::ListSelect::tagIsChecked(QString tag) const
0882 {
0883     QList<QTreeWidgetItem *> matchingTags = m_treeWidget->findItems(tag, Qt::MatchExactly | Qt::MatchRecursive, 0);
0884 
0885     if (matchingTags.isEmpty()) {
0886         return false;
0887     }
0888 
0889     return (bool)matchingTags.first()->checkState(0);
0890 }
0891 
0892 /**
0893  * @brief ListSelect::connectLineEdit associates a CompletableLineEdit with this ListSelect
0894  * This method also allows to connect an external CompletableLineEdit to work with this ListSelect.
0895  * @param le
0896  */
0897 void ListSelect::connectLineEdit(CompletableLineEdit *le)
0898 {
0899     le->setObjectName(m_category->name());
0900     le->setListView(m_treeWidget);
0901 #if KCOMPLETION_VERSION >= QT_VERSION_CHECK(5, 81, 0)
0902     connect(le, &KLineEdit::returnKeyPressed, this, &ListSelect::slotExternalReturn);
0903 #else
0904     connect(le, &KLineEdit::returnPressed, this, &ListSelect::slotExternalReturn);
0905 #endif
0906 }
0907 
0908 void AnnotationDialog::ListSelect::ensureTagIsSelected(QString category, QString tag)
0909 {
0910     if (category != m_lineEdit->objectName()) {
0911         // The selected tag's category does not belong to this ListSelect
0912         return;
0913     }
0914 
0915     // Be sure that tag is actually checked
0916     QList<QTreeWidgetItem *> matchingTags = m_treeWidget->findItems(tag, Qt::MatchExactly | Qt::MatchRecursive, 0);
0917 
0918     // If we have the requested category, but not this tag, add it.
0919     // This should only happen if the recognition database is copied from another database
0920     // or has been changed outside of KPA. But this _can_ happen and simply adding a
0921     // missing tag does not hurt ;-)
0922     if (matchingTags.isEmpty()) {
0923         m_category->addItem(tag);
0924         rePopulate();
0925         // Now, we find it
0926         matchingTags = m_treeWidget->findItems(tag, Qt::MatchExactly | Qt::MatchRecursive, 0);
0927     }
0928 
0929     matchingTags.first()->setCheckState(0, Qt::Checked);
0930 }
0931 
0932 void AnnotationDialog::ListSelect::deselectTag(QString tag)
0933 {
0934     QList<QTreeWidgetItem *> matchingTags = m_treeWidget->findItems(tag, Qt::MatchExactly | Qt::MatchRecursive, 0);
0935     matchingTags.first()->setCheckState(0, Qt::Unchecked);
0936 }
0937 
0938 // vi:expandtab:tabstop=4 shiftwidth=4:
0939 
0940 #include "moc_ListSelect.cpp"