[office/kmymoney] kmymoney: Allow completely deleting tags without prior reassignment

Thomas Baumgart null at kde.org
Wed Jan 5 20:48:30 GMT 2022


Git commit 5a04e6286adcee81fbe84497a0ce397b3df97f5a by Thomas Baumgart.
Committed on 05/01/2022 at 20:48.
Pushed by tbaumgart into branch 'master'.

Allow completely deleting tags without prior reassignment

FEATURE: 420114
GUI:

M  +42   -24   kmymoney/dialogs/ktagreassigndlg.cpp
M  +21   -12   kmymoney/dialogs/ktagreassigndlg.h
M  +84   -25   kmymoney/dialogs/ktagreassigndlg.ui
M  +61   -45   kmymoney/views/ktagsview.cpp

https://invent.kde.org/office/kmymoney/commit/5a04e6286adcee81fbe84497a0ce397b3df97f5a

diff --git a/kmymoney/dialogs/ktagreassigndlg.cpp b/kmymoney/dialogs/ktagreassigndlg.cpp
index 85e15ae6e..2cf61dad9 100644
--- a/kmymoney/dialogs/ktagreassigndlg.cpp
+++ b/kmymoney/dialogs/ktagreassigndlg.cpp
@@ -1,17 +1,14 @@
 /*
-    SPDX-FileCopyrightText: 2011-2012 Alessandro Russo <axela74 at yahoo.it>
-    SPDX-FileCopyrightText: 2017 Łukasz Wojniłowicz <lukasz.wojnilowicz at gmail.com>
-    SPDX-FileCopyrightText: 2020 Thomas Baumgart <tbaumgart at kde.org>
+    SPDX-FileCopyrightText: 2022 Thomas Baumgart <tbaumgart at kde.org>
     SPDX-License-Identifier: GPL-2.0-or-later
 */
 
-#include "ktagreassigndlg.h"
-#include "ui_ktagreassigndlg.h"
-
 // ----------------------------------------------------------------------------
 // QT Includes
 
+#include <QCheckBox>
 #include <QList>
+#include <QPushButton>
 
 // ----------------------------------------------------------------------------
 // KDE Includes
@@ -19,15 +16,38 @@
 // ----------------------------------------------------------------------------
 // Project Includes
 
+#include "idfilter.h"
+#include "ktagreassigndlg.h"
 #include "mymoneyfile.h"
 #include "tagsmodel.h"
-#include "idfilter.h"
 
-KTagReassignDlg::KTagReassignDlg(QWidget* parent) :
-    QDialog(parent),
-    ui(new Ui::KTagReassignDlg)
+#include "ui_ktagreassigndlg.h"
+
+KTagReassignDlg::KTagReassignDlg(QWidget* parent)
+    : QDialog(parent)
+    , ui(new Ui::KTagReassignDlg)
+    , model(new IdFilter(this))
 {
+    auto checkValidInput = [&]() {
+        const auto idx = model->index(ui->tagCombo->currentIndex(), 0);
+        const auto validInput = (!idx.data(eMyMoney::Model::IdRole).toString().isEmpty() || ui->removeCheckBox->isChecked());
+        ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(validInput);
+    };
+
     ui->setupUi(this);
+
+    model->setSourceModel(MyMoneyFile::instance()->tagsModel());
+    model->setSortLocaleAware(true);
+    ui->tagCombo->setModel(model);
+
+    connect(ui->tagCombo, QOverload<int>::of(&QComboBox::currentIndexChanged), this, [&]() {
+        checkValidInput();
+    });
+    connect(ui->removeCheckBox, &QCheckBox::toggled, this, [&]() {
+        checkValidInput();
+    });
+
+    ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false);
 }
 
 KTagReassignDlg::~KTagReassignDlg()
@@ -35,20 +55,18 @@ KTagReassignDlg::~KTagReassignDlg()
     delete ui;
 }
 
-QString KTagReassignDlg::show(const QList<QString>& tagslist)
+void KTagReassignDlg::setupFilter(const QList<QString>& tagslist)
 {
-    auto filter = new IdFilter(this);
-    filter->setFilterList(tagslist);
-    filter->setSourceModel(MyMoneyFile::instance()->tagsModel());
-    filter->setSortLocaleAware(true);
-    filter->sort(0);
-    ui->tagCombo->setModel(filter);
-
-    // execute dialog and if aborted, return empty string
-    if (this->exec() == QDialog::Rejected)
-        return QString();
-
-    // otherwise return id of selected tag
-    const auto idx = filter->index(ui->tagCombo->currentIndex(), 0);
+    qobject_cast<IdFilter*>(model)->setFilterList(tagslist);
+    model->sort(0);
+    ui->tagCombo->setCurrentIndex(-1);
+}
+
+QString KTagReassignDlg::reassignTo() const
+{
+    if (ui->removeCheckBox->isChecked())
+        return {};
+
+    const auto idx = ui->tagCombo->model()->index(ui->tagCombo->currentIndex(), 0);
     return idx.data(eMyMoney::Model::IdRole).toString();
 }
diff --git a/kmymoney/dialogs/ktagreassigndlg.h b/kmymoney/dialogs/ktagreassigndlg.h
index 0dcd85702..2ae03917e 100644
--- a/kmymoney/dialogs/ktagreassigndlg.h
+++ b/kmymoney/dialogs/ktagreassigndlg.h
@@ -1,7 +1,7 @@
 /*
     SPDX-FileCopyrightText: 2011-2012 Alessandro Russo <axela74 at yahoo.it>
     SPDX-FileCopyrightText: 2017 Łukasz Wojniłowicz <lukasz.wojnilowicz at gmail.com>
-    SPDX-FileCopyrightText: 2020 Thomas Baumgart <tbaumgart at kde.org>
+    SPDX-FileCopyrightText: 2022 Thomas Baumgart <tbaumgart at kde.org>
     SPDX-License-Identifier: GPL-2.0-or-later
 */
 
@@ -29,7 +29,7 @@ class KTagReassignDlg;
  *  Implementation of the dialog that lets the user select a tag in order
  *  to re-assign transactions (for instance, if tags are deleted).
  */
-
+class QSortFilterProxyModel;
 class KMM_BASE_DIALOGS_EXPORT KTagReassignDlg : public QDialog
 {
     Q_OBJECT
@@ -40,19 +40,28 @@ public:
     ~KTagReassignDlg();
 
     /**
-      * This function sets up the dialog, lets the user select a tag and returns
-      * the id of the selected tag in the tagslist.
-      *
-      * @param tagslist reference to QList of tag ids that are not available
-      *                 for re-assignment
-      *
-      * @return Returns the id of the selected tag in the list or QString() if
-      *         the dialog was aborted.
-      */
-    QString show(const QList<QString>& tagslist);
+     * This function sets up the dialog, lets the user select a tag and returns
+     * the id of the selected tag in the tagslist.
+     *
+     * @param tagslist reference to QList of tag ids that are not available
+     *                 for re-assignment
+     *
+     * @return Returns the id of the selected tag in the list or QString() if
+     *         the dialog was aborted.
+     */
+    void setupFilter(const QList<QString>& tagslist);
+
+    /**
+     * Returns the id that shall be used as replacement or empty
+     * if the tag is to be deleted completely from the data.
+     *
+     * @returns The id of a tag or empty string when deletion is requested.
+     */
+    QString reassignTo() const;
 
 private:
     Ui::KTagReassignDlg *ui;
+    QSortFilterProxyModel* model;
 };
 
 #endif // KTAGREASSIGNDLG_H
diff --git a/kmymoney/dialogs/ktagreassigndlg.ui b/kmymoney/dialogs/ktagreassigndlg.ui
index e4225bda8..8ddf2f0f8 100644
--- a/kmymoney/dialogs/ktagreassigndlg.ui
+++ b/kmymoney/dialogs/ktagreassigndlg.ui
@@ -6,12 +6,12 @@
    <rect>
     <x>0</x>
     <y>0</y>
-    <width>558</width>
-    <height>312</height>
+    <width>577</width>
+    <height>306</height>
    </rect>
   </property>
   <property name="windowTitle">
-   <string>Reassign tags</string>
+   <string>Delete or reassign tags</string>
   </property>
   <property name="sizeGripEnabled">
    <bool>false</bool>
@@ -21,22 +21,72 @@
   </property>
   <layout class="QVBoxLayout">
    <item>
-    <widget class="QLabel" name="textLabel1">
+    <widget class="QWidget" name="tagSelectionGroup" native="true">
      <property name="minimumSize">
       <size>
-       <width>300</width>
+       <width>0</width>
        <height>0</height>
       </size>
      </property>
-     <property name="text">
-      <string>The transactions associated with the selected tags need to be re-assigned to a different tag before the selected tags can be deleted. Please select a tag from the list below.</string>
-     </property>
-     <property name="alignment">
-      <set>Qt::AlignJustify|Qt::AlignTop</set>
-     </property>
-     <property name="wordWrap">
-      <bool>true</bool>
-     </property>
+     <layout class="QVBoxLayout" name="verticalLayout">
+      <property name="leftMargin">
+       <number>0</number>
+      </property>
+      <property name="topMargin">
+       <number>0</number>
+      </property>
+      <property name="rightMargin">
+       <number>0</number>
+      </property>
+      <property name="bottomMargin">
+       <number>0</number>
+      </property>
+      <item>
+       <widget class="QLabel" name="textLabel1">
+        <property name="minimumSize">
+         <size>
+          <width>300</width>
+          <height>0</height>
+         </size>
+        </property>
+        <property name="text">
+         <string>To complete the deletion operation, either select a tag form the available tags that will replace the selected tags before they are delete or mark the checkbox below depending on your choice. The OK button becomes accessible once you have selected a tag or marked the checkbox.</string>
+        </property>
+        <property name="alignment">
+         <set>Qt::AlignJustify|Qt::AlignTop</set>
+        </property>
+        <property name="wordWrap">
+         <bool>true</bool>
+        </property>
+       </widget>
+      </item>
+      <item>
+       <spacer name="verticalSpacer">
+        <property name="orientation">
+         <enum>Qt::Vertical</enum>
+        </property>
+        <property name="sizeHint" stdset="0">
+         <size>
+          <width>20</width>
+          <height>16</height>
+         </size>
+        </property>
+       </spacer>
+      </item>
+      <item>
+       <widget class="QLabel" name="textLabel2">
+        <property name="text">
+         <string>Available tags:</string>
+        </property>
+        <property name="wordWrap">
+         <bool>false</bool>
+        </property>
+       </widget>
+      </item>
+      <item>
+       <widget class="QComboBox" name="tagCombo"/>
+      </item>
+     </layout>
     </widget>
    </item>
    <item>
@@ -56,18 +106,12 @@
     </spacer>
    </item>
    <item>
-    <widget class="QLabel" name="textLabel2">
+    <widget class="QCheckBox" name="removeCheckBox">
      <property name="text">
-      <string>Available tags:</string>
-     </property>
-     <property name="wordWrap">
-      <bool>false</bool>
+      <string>Remove selected tags from all transactions, schedules and reports before deleting them.</string>
      </property>
     </widget>
    </item>
-   <item>
-    <widget class="QComboBox" name="tagCombo" native="true"/>
-   </item>
    <item>
     <spacer name="spacer9">
      <property name="orientation">
@@ -104,7 +148,6 @@
   </layout>
  </widget>
  <layoutdefault spacing="6" margin="11"/>
- <customwidgets/>
  <resources/>
  <connections>
   <connection>
@@ -115,7 +158,7 @@
    <hints>
     <hint type="sourcelabel">
      <x>313</x>
-     <y>286</y>
+     <y>352</y>
     </hint>
     <hint type="destinationlabel">
      <x>76</x>
@@ -131,7 +174,7 @@
    <hints>
     <hint type="sourcelabel">
      <x>462</x>
-     <y>291</y>
+     <y>357</y>
     </hint>
     <hint type="destinationlabel">
      <x>392</x>
@@ -139,5 +182,21 @@
     </hint>
    </hints>
   </connection>
+  <connection>
+   <sender>removeCheckBox</sender>
+   <signal>toggled(bool)</signal>
+   <receiver>tagSelectionGroup</receiver>
+   <slot>setDisabled(bool)</slot>
+   <hints>
+    <hint type="sourcelabel">
+     <x>278</x>
+     <y>98</y>
+    </hint>
+    <hint type="destinationlabel">
+     <x>278</x>
+     <y>143</y>
+    </hint>
+   </hints>
+  </connection>
  </connections>
 </ui>
diff --git a/kmymoney/views/ktagsview.cpp b/kmymoney/views/ktagsview.cpp
index 42569f6bf..b6ce92c3f 100644
--- a/kmymoney/views/ktagsview.cpp
+++ b/kmymoney/views/ktagsview.cpp
@@ -40,6 +40,7 @@
 #include "mymoneyexception.h"
 #include "mymoneymoney.h"
 #include "mymoneyprice.h"
+#include "mymoneyreport.h"
 #include "mymoneyschedule.h"
 #include "mymoneysecurity.h"
 #include "mymoneysplit.h"
@@ -47,6 +48,7 @@
 #include "specialdatesfilter.h"
 #include "specialdatesmodel.h"
 #include "tagsmodel.h"
+
 #include "ui_ktagsview.h"
 
 using namespace Icons;
@@ -67,7 +69,6 @@ public:
         , m_renameProxyModel(nullptr)
         , m_updateAction(nullptr)
         , m_needLoad(true)
-        , m_allowEditing(true)
     {
     }
 
@@ -133,6 +134,7 @@ public:
 
         m_renameProxyModel->setSourceModel(MyMoneyFile::instance()->tagsModel());
 
+        ui->m_tagsList->setSelectionMode(QAbstractItemView::ExtendedSelection);
         q->connect(ui->m_tagsList->selectionModel(), &QItemSelectionModel::selectionChanged, q, &KTagsView::slotTagSelectionChanged);
         q->connect(ui->m_register->selectionModel(), &QItemSelectionModel::selectionChanged, q, &KTagsView::slotTransactionSelectionChanged);
 
@@ -202,7 +204,7 @@ public:
 
         const auto tagIds = m_selections.selection(SelectedObjects::Tag);
 
-        if (tagIds.isEmpty() || !ui->m_tabWidget->isEnabled()) {
+        if (tagIds.isEmpty()) {
             ui->m_balanceLabel->setText(i18n("Balance: %1", balance.formatMoney(file->baseCurrency().smallestAccountFraction())));
             return;
         }
@@ -322,11 +324,6 @@ public:
      * This member holds the load state of page
      */
     bool m_needLoad;
-
-    /**
-     * This signals whether a tag can be edited
-     **/
-    bool m_allowEditing;
 };
 
 // *** KTagsView Implementation ***
@@ -352,12 +349,15 @@ void KTagsView::slotRenameSingleTag(const QModelIndex& idx, const QVariant& valu
 {
     Q_D(KTagsView);
     //if there is no current item selected, exit
-    if (d->m_allowEditing == false || !idx.isValid())
+    if (!idx.isValid())
         return;
 
     //qDebug() << "[KTagsView::slotRenameTag]";
     // create a copy of the new name without appended whitespaces
     const auto new_name = value.toString();
+    // reload
+    d->m_tag = MyMoneyFile::instance()->tagsModel()->itemById(idx.data(eMyMoney::Model::IdRole).toString());
+    ;
     if (d->m_tag.name() != new_name) {
         MyMoneyFileTransaction ft;
         try {
@@ -414,7 +414,6 @@ void KTagsView::aboutToHide()
 
 void KTagsView::slotTagSelectionChanged(const QItemSelection& selected, const QItemSelection& deselected)
 {
-    Q_UNUSED(selected)
     Q_UNUSED(deselected)
 
     Q_D(KTagsView);
@@ -423,7 +422,9 @@ void KTagsView::slotTagSelectionChanged(const QItemSelection& selected, const QI
     // loop over all tags and count the number of tags, also
     // obtain last selected tag
 
-    d->m_selections.clearSelections();
+    for (const auto& idx : deselected.indexes()) {
+        d->m_selections.removeSelection(SelectedObjects::Tag, idx.data(eMyMoney::Model::IdRole).toString());
+    }
     for (const auto& idx : selected.indexes()) {
         d->m_selections.addSelection(SelectedObjects::Tag, idx.data(eMyMoney::Model::IdRole).toString());
     }
@@ -434,21 +435,18 @@ void KTagsView::slotTagSelectionChanged(const QItemSelection& selected, const QI
         d->m_tag = MyMoneyFile::instance()->tagsModel()->itemById(d->m_selections.selection(SelectedObjects::Tag).at(0));
     }
 
-    emit requestSelectionChange(d->m_selections);
-
     try {
         d->m_newName = d->m_tag.name();
         d->loadDetails();
         slotTagDataChanged();
-
-        d->ui->m_tabWidget->setEnabled(true);
         d->showTransactions();
 
     } catch (const MyMoneyException &e) {
         qDebug("exception during display of tag: %s", e.what());
         d->m_tag = MyMoneyTag();
     }
-    d->m_allowEditing = true;
+
+    emit requestSelectionChange(d->m_selections);
 }
 
 void KTagsView::slotTransactionSelectionChanged(const QItemSelection& selected, const QItemSelection& deselected)
@@ -650,16 +648,6 @@ void KTagsView::slotDeleteTag()
         }
     }
 
-    // get confirmation from user
-    QString prompt;
-    if (selectedTags.size() == 1)
-        prompt = i18n("<p>Do you really want to remove the tag <b>%1</b>?</p>", selectedTags.front().name());
-    else
-        prompt = i18n("Do you really want to remove all selected tags?");
-
-    if (KMessageBox::questionYesNo(this, prompt, i18n("Remove Tag")) == KMessageBox::No)
-        return;
-
     MyMoneyFileTransaction ft;
     try {
         // create a transaction filter that contains all tags selected for removal
@@ -686,33 +674,55 @@ void KTagsView::slotDeleteTag()
                 }
             }
         }
+
+        QList<MyMoneyReport> used_reports;
+        for (const auto& report : file->reportList()) {
+            QStringList tagList(report.tags());
+            for (const auto& tag : tagList) {
+                if (d->tagInList(selectedTags, tag)) {
+                    used_reports.push_back(report);
+                    break;
+                }
+            }
+        }
+
 //     qDebug() << "[KTagsView::slotDeleteTag]  " << used_schedules.count() << " schedules use one of the selected tags";
 
         MyMoneyTag newTag;
         // if at least one tag is still referenced, we need to reassign its transactions first
-        if (!translist.isEmpty() || !used_schedules.isEmpty()) {
-            // show error message if no tags remain
-            //FIXME-ALEX Tags are optional so we can delete all of them and simply delete every tagId from every transaction
-            if (remainingTags.isEmpty()) {
-                KMessageBox::sorry(this, i18n("At least one transaction/scheduled transaction is still referenced by a tag. "
-                                              "Currently you have all tags selected. However, at least one tag must remain so "
-                                              "that the transaction/scheduled transaction can be reassigned."));
-                return;
-            }
-
+        if (!translist.isEmpty() || !used_schedules.isEmpty() || !used_reports.isEmpty()) {
             // show transaction reassignment dialog
             QPointer<KTagReassignDlg> dlg = new KTagReassignDlg(this);
             KMyMoneyMVCCombo::setSubstringSearchForChildren(dlg, !KMyMoneySettings::stringMatchFromStart());
-            auto tag_id = dlg->show(d->m_selections.selection(SelectedObjects::Tag));
-            delete dlg; // and kill the dialog
-            if (tag_id.isEmpty())  //FIXME-ALEX Let the user choose to not reassign a to-be deleted tag to another one.
+            dlg->setupFilter(d->m_selections.selection(SelectedObjects::Tag));
+            if ((dlg->exec() == QDialog::Rejected) || !dlg) {
+                delete dlg;
                 return; // the user aborted the dialog, so let's abort as well
+            }
+            auto newTagId = dlg->reassignTo();
+            delete dlg; // and kill the dialog
 
-            newTag = file->tag(tag_id);
+            if (!newTagId.isEmpty()) {
+                newTag = file->tag(newTagId);
+            }
 
-            // TODO : check if we have a report that explicitly uses one of our tags
-            //        and issue an appropriate warning
+            // check if we have a report that explicitly uses one of our tags
+            // and remove/replace it
             try {
+                // now loop over all report and reassign tag
+                for (auto report : used_reports) {
+                    QStringList tagIdList(report.tags());
+                    for (const auto& tagId : tagIdList) {
+                        if (d->tagInList(selectedTags, tagId)) {
+                            report.removeReference(tagId);
+                            if (!newTagId.isEmpty()) {
+                                report.addTag(newTagId);
+                            }
+                        }
+                    }
+                    file->modifyReport(report); // modify the transaction in the MyMoney object
+                }
+
                 // now loop over all transactions and reassign tag
                 for (auto& transaction : translist) {
                     // create a copy of the splits list in the transaction
@@ -723,8 +733,11 @@ void KTagsView::slotDeleteTag()
                             // if the split is assigned to one of the selected tags, we need to modify it
                             if (d->tagInList(selectedTags, tagIdList[i])) {
                                 tagIdList.removeAt(i);
-                                if (tagIdList.indexOf(tag_id) == -1)
-                                    tagIdList.append(tag_id);
+                                if (!newTagId.isEmpty()) {
+                                    if (tagIdList.indexOf(newTagId) == -1) {
+                                        tagIdList.append(newTagId);
+                                    }
+                                }
                                 i = -1; // restart from the first element
                             }
                         }
@@ -745,8 +758,11 @@ void KTagsView::slotDeleteTag()
                         for (auto i = 0; i < tagIdList.size(); ++i) {
                             if (d->tagInList(selectedTags, tagIdList[i])) {
                                 tagIdList.removeAt(i);
-                                if (tagIdList.indexOf(tag_id) == -1)
-                                    tagIdList.append(tag_id);
+                                if (!newTagId.isEmpty()) {
+                                    if (tagIdList.indexOf(newTagId) == -1) {
+                                        tagIdList.append(newTagId);
+                                    }
+                                }
                                 i = -1; // restart from the first element
                             }
                         }


More information about the kde-doc-english mailing list