File indexing completed on 2024-04-28 04:21:19

0001 // SPDX-FileCopyrightText: 2003-2022 The KPhotoAlbum Development Team
0002 // SPDX-FileCopyrightText: 2023 Johannes Zarl-Zierl <johannes@zarl-zierl.at>
0003 //
0004 // SPDX-License-Identifier: GPL-2.0-or-later
0005 
0006 #include "CategoryPage.h"
0007 
0008 // Qt includes
0009 #include <QCheckBox>
0010 #include <QComboBox>
0011 #include <QGridLayout>
0012 #include <QGroupBox>
0013 #include <QHBoxLayout>
0014 #include <QLabel>
0015 #include <QLocale>
0016 #include <QPushButton>
0017 #include <QSpinBox>
0018 #include <QVBoxLayout>
0019 
0020 // KDE includes
0021 #include <KIconButton>
0022 #include <KLocalizedString>
0023 #include <KMessageBox>
0024 #include <kwidgetsaddons_version.h>
0025 
0026 // Local includes
0027 #include "CategoryItem.h"
0028 #include "SettingsDialog.h"
0029 #include "UntaggedGroupBox.h"
0030 
0031 #include <DB/CategoryCollection.h>
0032 #include <DB/ImageDB.h>
0033 #include <DB/MemberMap.h>
0034 #include <MainWindow/DirtyIndicator.h>
0035 #include <MainWindow/Window.h>
0036 
0037 Settings::CategoryPage::CategoryPage(QWidget *parent)
0038     : QWidget(parent)
0039 {
0040     QVBoxLayout *mainLayout = new QVBoxLayout(this);
0041 
0042     // The category settings
0043 
0044     QGroupBox *categoryGroupBox = new QGroupBox;
0045     mainLayout->addWidget(categoryGroupBox);
0046     categoryGroupBox->setTitle(i18n("Category Settings"));
0047     QHBoxLayout *categoryLayout = new QHBoxLayout(categoryGroupBox);
0048 
0049     // Category list
0050 
0051     QVBoxLayout *categorySideLayout = new QVBoxLayout;
0052     categoryLayout->addLayout(categorySideLayout);
0053 
0054     m_categoriesListWidget = new QListWidget;
0055 
0056     connect(m_categoriesListWidget, &QListWidget::itemClicked, this, &CategoryPage::editCategory);
0057     connect(m_categoriesListWidget, &QListWidget::itemSelectionChanged, this, &CategoryPage::editSelectedCategory);
0058     connect(m_categoriesListWidget, &QListWidget::itemChanged, this, &CategoryPage::categoryNameChanged);
0059 
0060     categorySideLayout->addWidget(m_categoriesListWidget);
0061 
0062     // New, Delete, and buttons
0063 
0064     QHBoxLayout *newDeleteRenameLayout = new QHBoxLayout;
0065     categorySideLayout->addLayout(newDeleteRenameLayout);
0066 
0067     m_newCategoryButton = new QPushButton(i18n("New"));
0068     connect(m_newCategoryButton, &QPushButton::clicked, this, &CategoryPage::newCategory);
0069     newDeleteRenameLayout->addWidget(m_newCategoryButton);
0070 
0071     m_delItem = new QPushButton(i18n("Delete"));
0072     connect(m_delItem, &QPushButton::clicked, this, &CategoryPage::deleteCurrentCategory);
0073     newDeleteRenameLayout->addWidget(m_delItem);
0074 
0075     m_renameItem = new QPushButton(i18n("Rename"));
0076     connect(m_renameItem, &QPushButton::clicked, this, &CategoryPage::renameCurrentCategory);
0077     newDeleteRenameLayout->addWidget(m_renameItem);
0078 
0079     // Category settings
0080 
0081     QVBoxLayout *rightSideLayout = new QVBoxLayout;
0082     categoryLayout->addLayout(rightSideLayout);
0083 
0084     // Header
0085     m_categoryLabel = new QLabel;
0086     m_categoryLabel->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Maximum);
0087     rightSideLayout->addWidget(m_categoryLabel);
0088 
0089     // Pending rename label
0090     m_renameLabel = new QLabel;
0091     m_renameLabel->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Maximum);
0092     rightSideLayout->addWidget(m_renameLabel);
0093     QDialog *parentDialog = qobject_cast<QDialog *>(parent);
0094     connect(parentDialog, &QDialog::rejected, m_renameLabel, &QLabel::clear);
0095 
0096     // Some space looks better here :-)
0097     QLabel *spacer = new QLabel;
0098     rightSideLayout->addWidget(spacer);
0099 
0100     // Here we start with the actual settings
0101 
0102     QGridLayout *settingsLayout = new QGridLayout;
0103     rightSideLayout->addLayout(settingsLayout);
0104 
0105     int row = 0;
0106 
0107     // Positionable
0108     m_positionableLabel = new QLabel(i18n("Positionable tags:"));
0109     settingsLayout->addWidget(m_positionableLabel, row, 0);
0110     m_positionable = new QCheckBox(i18n("Tags in this category can be\n"
0111                                         "associated with areas within images"));
0112     settingsLayout->addWidget(m_positionable, row, 1);
0113     connect(m_positionable, &QCheckBox::clicked, this, &CategoryPage::positionableChanged);
0114     row++;
0115 
0116     // Icon
0117     m_iconLabel = new QLabel(i18n("Icon:"));
0118     settingsLayout->addWidget(m_iconLabel, row, 0);
0119     m_icon = new KIconButton;
0120     settingsLayout->addWidget(m_icon, row, 1);
0121     m_icon->setIconSize(32);
0122     m_icon->setIcon(QString::fromUtf8("personsIcon"));
0123     connect(m_icon, &KIconButton::iconChanged, this, &CategoryPage::iconChanged);
0124     row++;
0125 
0126     // Thumbnail size
0127     m_thumbnailSizeInCategoryLabel = new QLabel(i18n("Thumbnail size:"));
0128     settingsLayout->addWidget(m_thumbnailSizeInCategoryLabel, row, 0);
0129     m_thumbnailSizeInCategory = new QSpinBox;
0130     m_thumbnailSizeInCategory->setRange(32, 512);
0131     m_thumbnailSizeInCategory->setSingleStep(32);
0132     settingsLayout->addWidget(m_thumbnailSizeInCategory, row, 1);
0133     connect(m_thumbnailSizeInCategory, static_cast<void (QSpinBox::*)(int)>(&QSpinBox::valueChanged), this, &CategoryPage::thumbnailSizeChanged);
0134     row++;
0135 
0136     // Preferred View
0137     m_preferredViewLabel = new QLabel(i18n("Preferred view:"));
0138     settingsLayout->addWidget(m_preferredViewLabel, row, 0);
0139     m_preferredView = new QComboBox;
0140     settingsLayout->addWidget(m_preferredView, row, 1);
0141     m_preferredView->addItems(QStringList()
0142                               << i18n("List View")
0143                               << i18n("List View with Custom Thumbnails")
0144                               << i18n("Icon View")
0145                               << i18n("Icon View with Custom Thumbnails"));
0146     connect(m_preferredView, static_cast<void (QComboBox::*)(int)>(&QComboBox::activated), this, &CategoryPage::preferredViewChanged);
0147 
0148     rightSideLayout->addStretch();
0149 
0150     // Info about the database not being saved
0151 
0152     QHBoxLayout *dbNotSavedLayout = new QHBoxLayout;
0153     mainLayout->addLayout(dbNotSavedLayout);
0154 
0155     m_dbNotSavedLabel = new QLabel(i18n("<font color='red'>"
0156                                         "The database has unsaved changes. As long as those are "
0157                                         "not saved,<br/>the names of categories can't be changed "
0158                                         "and new ones can't be added."
0159                                         "</font>"));
0160     m_dbNotSavedLabel->setWordWrap(true);
0161     dbNotSavedLayout->addWidget(m_dbNotSavedLabel);
0162 
0163     m_saveDbNowButton = new QPushButton(i18n("Save the DB now"));
0164     m_saveDbNowButton->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Minimum);
0165     connect(m_saveDbNowButton, &QPushButton::clicked, this, &CategoryPage::saveDbNow);
0166     dbNotSavedLayout->addWidget(m_saveDbNowButton);
0167 
0168     resetInterface();
0169 
0170     // Untagged images
0171     m_untaggedBox = new UntaggedGroupBox;
0172     mainLayout->addWidget(m_untaggedBox);
0173 
0174     m_currentCategory = nullptr;
0175 
0176     m_categoryNamesChanged = false;
0177 }
0178 
0179 void Settings::CategoryPage::resetInterface()
0180 {
0181     enableDisable(false);
0182     if (m_categoriesListWidget->currentItem())
0183         m_categoriesListWidget->currentItem()->setSelected(false);
0184     resetCategoryLabel();
0185     m_renameLabel->hide();
0186 }
0187 
0188 void Settings::CategoryPage::editSelectedCategory()
0189 {
0190     editCategory(m_categoriesListWidget->currentItem());
0191 }
0192 
0193 void Settings::CategoryPage::editCategory(QListWidgetItem *i)
0194 {
0195     if (i == nullptr) {
0196         return;
0197     }
0198 
0199     m_categoryNameBeforeEdit = i->text();
0200 
0201     Settings::CategoryItem *item = static_cast<Settings::CategoryItem *>(i);
0202     m_currentCategory = item;
0203     m_categoryLabel->setText(i18n("Settings for category <b>%1</b>", item->originalName()));
0204 
0205     if (m_currentCategory->originalName() != m_categoryNameBeforeEdit) {
0206         m_renameLabel->setText(i18n("<i>Pending change: rename to \"%1\"</i>", m_categoryNameBeforeEdit));
0207         m_renameLabel->show();
0208     } else {
0209         m_renameLabel->clear();
0210         m_renameLabel->hide();
0211     }
0212 
0213     m_positionable->setChecked(item->positionable());
0214     m_icon->setIcon(item->icon());
0215     m_thumbnailSizeInCategory->setValue(item->thumbnailSize());
0216     m_preferredView->setCurrentIndex(static_cast<int>(item->viewType()));
0217 
0218     enableDisable(true);
0219 
0220     if (item->originalName()
0221         == DB::ImageDB::instance()->categoryCollection()->categoryForSpecial(DB::Category::TokensCategory)->name()) {
0222 
0223         m_delItem->setEnabled(false);
0224         m_positionableLabel->setEnabled(false);
0225         m_positionable->setEnabled(false);
0226         m_thumbnailSizeInCategoryLabel->setEnabled(false);
0227         m_thumbnailSizeInCategory->setEnabled(false);
0228         m_preferredViewLabel->setEnabled(false);
0229         m_preferredView->setEnabled(false);
0230     }
0231 }
0232 
0233 void Settings::CategoryPage::categoryNameChanged(QListWidgetItem *item)
0234 {
0235     QString newCategoryName = item->text().simplified();
0236     m_categoriesListWidget->blockSignals(true);
0237     item->setText(QString());
0238     m_categoriesListWidget->blockSignals(false);
0239 
0240     // Now let's check if the new name is valid :-)
0241 
0242     // If it's empty, we're done here. The new name can't be empty.
0243     if (newCategoryName.isEmpty()) {
0244         resetCategory(item);
0245         return;
0246     }
0247 
0248     // We don't want to have special category names.
0249     // We do have to search both for the localized version and the C locale version, because a user
0250     // could start KPA e. g. with a German locale and create a "Folder" category (which would not
0251     // be caught by i18n("Folder")), and then start KPA with the C locale, which would produce a
0252     // doubled "Folder" category.
0253     if (newCategoryName == i18n("Folder")
0254         || newCategoryName == QString::fromUtf8("Folder")
0255         || newCategoryName == i18n("Media Type")
0256         || newCategoryName == QString::fromUtf8("Media Type")) {
0257 
0258         resetCategory(item);
0259         KMessageBox::error(this,
0260                            i18n("<p>Can't change the name of category \"%1\" to \"%2\":</p>"
0261                                 "<p>\"%2\" is a special category name which is reserved and can't "
0262                                 "be used for a normal category.</p>",
0263                                 m_currentCategory->text(), newCategoryName),
0264                            i18n("Invalid category name"));
0265         return;
0266     }
0267 
0268     // Let's see if we already have a category with this name.
0269     if (m_categoriesListWidget->findItems(newCategoryName, Qt::MatchExactly).size() > 0) {
0270         resetCategory(item);
0271         KMessageBox::error(this,
0272                            i18n("<p>Can't change the name of category \"%1\" to \"%2\":</p>"
0273                                 "<p>A category with this name already exists.</p>",
0274                                 m_currentCategory->text(), newCategoryName),
0275                            i18n("Invalid category name"));
0276         return;
0277     }
0278 
0279     // Let's see if we have any pending name changes that would cause collisions.
0280     for (int i = 0; i < m_categoriesListWidget->count(); i++) {
0281         Settings::CategoryItem *cat = static_cast<Settings::CategoryItem *>(m_categoriesListWidget->item(i));
0282         if (cat == m_currentCategory) {
0283             continue;
0284         }
0285 
0286         if (newCategoryName == cat->originalName()) {
0287             resetCategory(item);
0288             KMessageBox::error(this,
0289                                i18n("<p>Can't change the name of category \"%1\" to \"%2\":</p>"
0290                                     "<p>There's a pending rename action on the category \"%2\". "
0291                                     "Please save this change first.</p>",
0292                                     m_currentCategory->text(), newCategoryName),
0293                                i18n("Unsaved pending renaming action"));
0294             return;
0295         }
0296     }
0297 
0298     m_categoriesListWidget->blockSignals(true);
0299     item->setText(newCategoryName);
0300     m_categoriesListWidget->blockSignals(false);
0301 
0302     Q_EMIT categoryChangesPending();
0303     m_untaggedBox->categoryRenamed(m_categoryNameBeforeEdit, newCategoryName);
0304     m_currentCategory->setLabel(newCategoryName);
0305     editCategory(m_currentCategory);
0306 
0307     m_categoryNamesChanged = true;
0308 }
0309 
0310 void Settings::CategoryPage::resetCategory(QListWidgetItem *item)
0311 {
0312     m_categoriesListWidget->blockSignals(true);
0313     item->setText(m_categoryNameBeforeEdit);
0314     m_categoriesListWidget->blockSignals(false);
0315 }
0316 
0317 void Settings::CategoryPage::positionableChanged(bool positionable)
0318 {
0319     if (!m_currentCategory) {
0320         return;
0321     }
0322 
0323     if (!positionable) {
0324         const QString question = i18n("<p>Do you really want to make \"%1\" "
0325                                       "non-positionable?</p>"
0326                                       "<p>All areas linked against this category "
0327                                       "will be discarded!</p>",
0328                                       m_currentCategory->text());
0329 
0330 #if KWIDGETSADDONS_VERSION >= QT_VERSION_CHECK(5, 100, 0)
0331         const auto answer = KMessageBox::questionTwoActions(this,
0332                                                             question,
0333                                                             i18nc("@title", "Confirm Action"),
0334                                                             KStandardGuiItem::discard(),
0335                                                             KStandardGuiItem::cancel());
0336         if (answer == KMessageBox::ButtonCode::SecondaryAction) {
0337             m_positionable->setCheckState(Qt::Checked);
0338             return;
0339         }
0340 #else
0341         int answer = KMessageBox::questionYesNo(this, question);
0342         if (answer == KMessageBox::No) {
0343             m_positionable->setCheckState(Qt::Checked);
0344             return;
0345         }
0346 #endif
0347     }
0348 
0349     m_currentCategory->setPositionable(positionable);
0350 }
0351 
0352 void Settings::CategoryPage::iconChanged(const QString &icon)
0353 {
0354     if (m_currentCategory) {
0355         m_currentCategory->setIcon(icon);
0356     }
0357 }
0358 
0359 void Settings::CategoryPage::thumbnailSizeChanged(int size)
0360 {
0361     if (m_currentCategory) {
0362         m_currentCategory->setThumbnailSize(size);
0363     }
0364 }
0365 
0366 void Settings::CategoryPage::preferredViewChanged(int i)
0367 {
0368     if (m_currentCategory) {
0369         m_currentCategory->setViewType(static_cast<DB::Category::ViewType>(i));
0370     }
0371 }
0372 
0373 void Settings::CategoryPage::newCategory()
0374 {
0375     // Here starts the real function
0376 
0377     QString newCategory = i18n("New category");
0378     QString checkedCategory = newCategory;
0379     int i = 1;
0380     while (m_categoriesListWidget->findItems(checkedCategory, Qt::MatchExactly).size() > 0) {
0381         i++;
0382         checkedCategory = QString::fromUtf8("%1 %2").arg(newCategory).arg(i);
0383     }
0384 
0385     m_categoriesListWidget->blockSignals(true);
0386     m_currentCategory = new Settings::CategoryItem(checkedCategory,
0387                                                    QString(),
0388                                                    DB::Category::TreeView,
0389                                                    64,
0390                                                    m_categoriesListWidget);
0391     m_currentCategory->markAsNewCategory();
0392     Q_EMIT categoryChangesPending();
0393     m_currentCategory->setLabel(checkedCategory);
0394     m_currentCategory->setSelected(true);
0395     m_categoriesListWidget->blockSignals(false);
0396 
0397     m_positionable->setChecked(false);
0398     m_icon->setIcon(QIcon());
0399     m_thumbnailSizeInCategory->setValue(64);
0400     enableDisable(true);
0401 
0402     editCategory(m_currentCategory);
0403     m_categoriesListWidget->editItem(m_currentCategory);
0404     m_untaggedBox->categoryAdded(newCategory);
0405 }
0406 
0407 void Settings::CategoryPage::deleteCurrentCategory()
0408 {
0409     const QString question = i18n("<p>Really delete category \"%1\"?</p>",
0410                                   m_currentCategory->text());
0411 #if KWIDGETSADDONS_VERSION >= QT_VERSION_CHECK(5, 100, 0)
0412     const auto answer = KMessageBox::questionTwoActions(this,
0413                                                         question,
0414                                                         i18n("Confirm Action"),
0415                                                         KStandardGuiItem::del(),
0416                                                         KStandardGuiItem::cancel());
0417     if (answer == KMessageBox::ButtonCode::SecondaryAction) {
0418         return;
0419     }
0420 #else
0421     const auto answer = KMessageBox::questionYesNo(this, question);
0422     if (answer == KMessageBox::No) {
0423         return;
0424     }
0425 #endif
0426 
0427     m_untaggedBox->categoryDeleted(m_currentCategory->text());
0428     m_deletedCategories.append(m_currentCategory);
0429     m_categoriesListWidget->takeItem(m_categoriesListWidget->row(m_currentCategory));
0430     m_currentCategory = nullptr;
0431     m_positionable->setChecked(false);
0432     m_icon->setIcon(QIcon());
0433     m_thumbnailSizeInCategory->setValue(64);
0434     enableDisable(false);
0435     resetCategoryLabel();
0436 
0437     editCategory(m_categoriesListWidget->currentItem());
0438     Q_EMIT categoryChangesPending();
0439 }
0440 
0441 void Settings::CategoryPage::renameCurrentCategory()
0442 {
0443     m_categoriesListWidget->editItem(m_currentCategory);
0444 }
0445 
0446 void Settings::CategoryPage::enableDisable(bool b)
0447 {
0448     m_delItem->setEnabled(b);
0449     m_positionableLabel->setEnabled(b);
0450     m_positionable->setEnabled(b);
0451     m_icon->setEnabled(b);
0452     m_iconLabel->setEnabled(b);
0453     m_thumbnailSizeInCategoryLabel->setEnabled(b);
0454     m_thumbnailSizeInCategory->setEnabled(b);
0455     m_preferredViewLabel->setEnabled(b);
0456     m_preferredView->setEnabled(b);
0457 
0458     auto signalBlocker = QSignalBlocker(m_categoriesListWidget);
0459 
0460     if (MainWindow::Window::theMainWindow()->dbIsDirty()) {
0461         m_dbNotSavedLabel->show();
0462         m_saveDbNowButton->show();
0463         m_renameItem->setEnabled(false);
0464         m_newCategoryButton->setEnabled(false);
0465 
0466         for (int i = 0; i < m_categoriesListWidget->count(); i++) {
0467             QListWidgetItem *currentItem = m_categoriesListWidget->item(i);
0468             currentItem->setFlags(currentItem->flags() & ~Qt::ItemIsEditable);
0469         }
0470     } else {
0471         m_dbNotSavedLabel->hide();
0472         m_saveDbNowButton->hide();
0473         m_renameItem->setEnabled(b);
0474         m_newCategoryButton->setEnabled(true);
0475 
0476         for (int i = 0; i < m_categoriesListWidget->count(); i++) {
0477             QListWidgetItem *currentItem = m_categoriesListWidget->item(i);
0478             currentItem->setFlags(currentItem->flags() | Qt::ItemIsEditable);
0479         }
0480     }
0481 }
0482 
0483 void Settings::CategoryPage::saveSettings(Settings::SettingsData *opt, DB::MemberMap *memberMap)
0484 {
0485     // Delete items
0486     for (CategoryItem *item : qAsConst(m_deletedCategories)) {
0487         item->removeFromDatabase();
0488     }
0489     m_deletedCategories.clear();
0490 
0491     // Created or Modified items
0492     for (int i = 0; i < m_categoriesListWidget->count(); ++i) {
0493         CategoryItem *item = static_cast<CategoryItem *>(m_categoriesListWidget->item(i));
0494         item->submit(memberMap);
0495     }
0496 
0497     // FIXME(jzarl): wtf? we need to fix this atrocity...
0498     DB::ImageDB::instance()->memberMap() = *memberMap;
0499     m_untaggedBox->saveSettings(opt);
0500 
0501     if (m_categoryNamesChanged) {
0502         // Probably, one or more category names have been edited. Save the database so that
0503         // all thumbnails are referenced with the correct name.
0504         MainWindow::Window::theMainWindow()->slotSave();
0505         m_categoryNamesChanged = false;
0506     }
0507 }
0508 
0509 void Settings::CategoryPage::loadSettings(Settings::SettingsData *opt)
0510 {
0511     auto signalBlocker = QSignalBlocker(m_categoriesListWidget);
0512     m_categoriesListWidget->clear();
0513 
0514     const QList<DB::CategoryPtr> categories = DB::ImageDB::instance()->categoryCollection()->categories();
0515     for (const DB::CategoryPtr &category : categories) {
0516         if (category->type() == DB::Category::PlainCategory
0517             || category->type() == DB::Category::TokensCategory) {
0518             Settings::CategoryItem *item = new CategoryItem(category->name(),
0519                                                             category->iconName(),
0520                                                             category->viewType(),
0521                                                             category->thumbnailSize(),
0522                                                             m_categoriesListWidget,
0523                                                             category->positionable());
0524             Q_UNUSED(item)
0525         }
0526     }
0527 
0528     m_untaggedBox->loadSettings(opt);
0529 }
0530 
0531 void Settings::CategoryPage::resetCategoryLabel()
0532 {
0533     m_categoryLabel->setText(i18n("<i>Choose a category to edit it</i>"));
0534 }
0535 
0536 void Settings::CategoryPage::saveDbNow()
0537 {
0538     MainWindow::Window::theMainWindow()->slotSave();
0539     resetInterface();
0540     enableDisable(false);
0541 }
0542 
0543 void Settings::CategoryPage::resetCategoryNamesChanged()
0544 {
0545     m_categoryNamesChanged = false;
0546 }
0547 
0548 // vi:expandtab:tabstop=4 shiftwidth=4:
0549 
0550 #include "moc_CategoryPage.cpp"