1 /*
2     simplestringlisteditor.cpp
3 
4     This file is part of KMail, the KDE mail client.
5     SPDX-FileCopyrightText: 2001 Marc Mutz <mutz@kde.org>
6 
7     SPDX-FileCopyrightText: 2013-2021 Laurent Montel <montel@kde.org>
8 
9     SPDX-License-Identifier: GPL-2.0-or-later
10 */
11 
12 #include "simplestringlisteditor.h"
13 
14 #include "pimcommon_debug.h"
15 #include <KLocalizedString>
16 #include <KMessageBox>
17 #include <QHBoxLayout>
18 #include <QIcon>
19 #include <QInputDialog>
20 #include <QListWidget>
21 #include <QMenu>
22 #include <QPushButton>
23 #include <QVBoxLayout>
24 
25 //********************************************************
26 // SimpleStringListEditor
27 //********************************************************
28 using namespace PimCommon;
29 
30 class PimCommon::SimpleStringListEditorPrivate
31 {
32 public:
SimpleStringListEditorPrivate()33     SimpleStringListEditorPrivate()
34     {
35     }
36 
selectedItems() const37     QList<QListWidgetItem *> selectedItems() const
38     {
39         QList<QListWidgetItem *> listWidgetItem;
40         const int numberOfFilters = mListBox->count();
41         for (int i = 0; i < numberOfFilters; ++i) {
42             if (mListBox->item(i)->isSelected()) {
43                 listWidgetItem << mListBox->item(i);
44             }
45         }
46         return listWidgetItem;
47     }
48 
49     QListWidget *mListBox = nullptr;
50     QPushButton *mAddButton = nullptr;
51     QPushButton *mRemoveButton = nullptr;
52     QPushButton *mModifyButton = nullptr;
53     QPushButton *mUpButton = nullptr;
54     QPushButton *mDownButton = nullptr;
55     QPushButton *mCustomButton = nullptr;
56     QVBoxLayout *mButtonLayout = nullptr;
57     QString mAddDialogLabel = i18n("New entry:");
58     QString mAddDialogTitle = i18n("New Value");
59     QString mModifyDialogTitle = i18n("New Value");
60     QString mModifyDialogLabel = i18n("New entry:");
61     QString mRemoveDialogLabel = i18n("Do you want to remove selected text?");
62 };
63 
SimpleStringListEditor(QWidget * parent,ButtonCode buttons,const QString & addLabel,const QString & removeLabel,const QString & modifyLabel,const QString & addDialogLabel)64 SimpleStringListEditor::SimpleStringListEditor(QWidget *parent,
65                                                ButtonCode buttons,
66                                                const QString &addLabel,
67                                                const QString &removeLabel,
68                                                const QString &modifyLabel,
69                                                const QString &addDialogLabel)
70     : QWidget(parent)
71     , d(new SimpleStringListEditorPrivate)
72 {
73     setAddDialogLabel(addDialogLabel);
74     setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding);
75     auto hlay = new QHBoxLayout(this);
76     hlay->setContentsMargins({});
77 
78     d->mListBox = new QListWidget(this);
79 
80     d->mListBox->setContextMenuPolicy(Qt::CustomContextMenu);
81     connect(d->mListBox, &QListWidget::customContextMenuRequested, this, &SimpleStringListEditor::slotContextMenu);
82 
83     d->mListBox->setSelectionMode(QAbstractItemView::ExtendedSelection);
84     hlay->addWidget(d->mListBox, 1);
85 
86     if (buttons == None) {
87         qCDebug(PIMCOMMON_LOG) << "SimpleStringListBox called with no buttons."
88                                   "Consider using a plain QListBox instead!";
89     }
90 
91     d->mButtonLayout = new QVBoxLayout(); // inherits spacing
92     hlay->addLayout(d->mButtonLayout);
93 
94     if (buttons & Add) {
95         if (addLabel.isEmpty()) {
96             d->mAddButton = new QPushButton(i18n("&Add..."), this);
97         } else {
98             d->mAddButton = new QPushButton(addLabel, this);
99         }
100         d->mAddButton->setAutoDefault(false);
101         d->mButtonLayout->addWidget(d->mAddButton);
102         connect(d->mAddButton, &QPushButton::clicked, this, &SimpleStringListEditor::slotAdd);
103     }
104 
105     if (buttons & Modify) {
106         if (modifyLabel.isEmpty()) {
107             d->mModifyButton = new QPushButton(i18n("&Modify..."), this);
108         } else {
109             d->mModifyButton = new QPushButton(modifyLabel, this);
110         }
111         d->mModifyButton->setAutoDefault(false);
112         d->mModifyButton->setEnabled(false); // no selection yet
113         d->mButtonLayout->addWidget(d->mModifyButton);
114         connect(d->mModifyButton, &QPushButton::clicked, this, &SimpleStringListEditor::slotModify);
115         connect(d->mListBox, &QListWidget::itemDoubleClicked, this, &SimpleStringListEditor::slotModify);
116     }
117 
118     if (buttons & Remove) {
119         if (removeLabel.isEmpty()) {
120             d->mRemoveButton = new QPushButton(i18n("&Remove"), this);
121         } else {
122             d->mRemoveButton = new QPushButton(removeLabel, this);
123         }
124         d->mRemoveButton->setAutoDefault(false);
125         d->mRemoveButton->setEnabled(false); // no selection yet
126         d->mButtonLayout->addWidget(d->mRemoveButton);
127         connect(d->mRemoveButton, &QPushButton::clicked, this, &SimpleStringListEditor::slotRemove);
128     }
129 
130     if (buttons & Up) {
131         if (!(buttons & Down)) {
132             qCDebug(PIMCOMMON_LOG) << "Are you sure you want to use an Up button"
133                                       "without a Down button??";
134         }
135         d->mUpButton = new QPushButton(QString(), this);
136         d->mUpButton->setIcon(QIcon::fromTheme(QStringLiteral("go-up")));
137         d->mUpButton->setAutoDefault(false);
138         d->mUpButton->setEnabled(false); // no selection yet
139         d->mButtonLayout->addWidget(d->mUpButton);
140         connect(d->mUpButton, &QPushButton::clicked, this, &SimpleStringListEditor::slotUp);
141     }
142 
143     if (buttons & Down) {
144         if (!(buttons & Up)) {
145             qCDebug(PIMCOMMON_LOG) << "Are you sure you want to use a Down button"
146                                       "without an Up button??";
147         }
148         d->mDownButton = new QPushButton(QString(), this);
149         d->mDownButton->setIcon(QIcon::fromTheme(QStringLiteral("go-down")));
150         d->mDownButton->setAutoDefault(false);
151         d->mDownButton->setEnabled(false); // no selection yet
152         d->mButtonLayout->addWidget(d->mDownButton);
153         connect(d->mDownButton, &QPushButton::clicked, this, &SimpleStringListEditor::slotDown);
154     }
155 
156     if (buttons & Custom) {
157         d->mCustomButton = new QPushButton(i18n("&Customize..."), this);
158         d->mCustomButton->setAutoDefault(false);
159         d->mCustomButton->setEnabled(false); // no selection yet
160         d->mButtonLayout->addWidget(d->mCustomButton);
161         connect(d->mCustomButton, &QPushButton::clicked, this, &SimpleStringListEditor::slotCustomize);
162     }
163 
164     d->mButtonLayout->addStretch(1); // spacer
165 
166     connect(d->mListBox, &QListWidget::currentItemChanged, this, &SimpleStringListEditor::slotSelectionChanged);
167     connect(d->mListBox, &QListWidget::itemSelectionChanged, this, &SimpleStringListEditor::slotSelectionChanged);
168 }
169 
170 SimpleStringListEditor::~SimpleStringListEditor() = default;
171 
setUpDownAutoRepeat(bool b)172 void SimpleStringListEditor::setUpDownAutoRepeat(bool b)
173 {
174     if (d->mUpButton) {
175         d->mUpButton->setAutoRepeat(b);
176     }
177     if (d->mDownButton) {
178         d->mDownButton->setAutoRepeat(b);
179     }
180 }
181 
setStringList(const QStringList & strings)182 void SimpleStringListEditor::setStringList(const QStringList &strings)
183 {
184     d->mListBox->clear();
185     d->mListBox->addItems(strings);
186 }
187 
appendStringList(const QStringList & strings)188 void SimpleStringListEditor::appendStringList(const QStringList &strings)
189 {
190     d->mListBox->addItems(strings);
191 }
192 
stringList() const193 QStringList SimpleStringListEditor::stringList() const
194 {
195     QStringList result;
196     const int numberOfItem(d->mListBox->count());
197     result.reserve(numberOfItem);
198     for (int i = 0; i < numberOfItem; ++i) {
199         result << (d->mListBox->item(i)->text());
200     }
201     return result;
202 }
203 
containsString(const QString & str)204 bool SimpleStringListEditor::containsString(const QString &str)
205 {
206     const int numberOfItem(d->mListBox->count());
207     for (int i = 0; i < numberOfItem; ++i) {
208         if (d->mListBox->item(i)->text() == str) {
209             return true;
210         }
211     }
212     return false;
213 }
214 
setButtonText(ButtonCode button,const QString & text)215 void SimpleStringListEditor::setButtonText(ButtonCode button, const QString &text)
216 {
217     switch (button) {
218     case Add:
219         if (!d->mAddButton) {
220             break;
221         }
222         d->mAddButton->setText(text);
223         return;
224     case Remove:
225         if (!d->mRemoveButton) {
226             break;
227         }
228         d->mRemoveButton->setText(text);
229         return;
230     case Modify:
231         if (!d->mModifyButton) {
232             break;
233         }
234         d->mModifyButton->setText(text);
235         return;
236     case Custom:
237         if (!d->mCustomButton) {
238             break;
239         }
240         d->mCustomButton->setText(text);
241         return;
242     case Up:
243     case Down:
244         qCDebug(PIMCOMMON_LOG) << "SimpleStringListEditor: Cannot change text of"
245                                   "Up and Down buttons: they don't contains text!";
246         return;
247     default:
248         if (button & All) {
249             qCDebug(PIMCOMMON_LOG) << "No such button!";
250         } else {
251             qCDebug(PIMCOMMON_LOG) << "Can only set text for one button at a time!";
252         }
253         return;
254     }
255 
256     qCDebug(PIMCOMMON_LOG) << "The requested button has not been created!";
257 }
258 
addNewEntry()259 void SimpleStringListEditor::addNewEntry()
260 {
261     bool ok = false;
262     const QString newEntry = QInputDialog::getText(this, d->mAddDialogTitle, d->mAddDialogLabel, QLineEdit::Normal, QString(), &ok);
263     if (ok && !newEntry.trimmed().isEmpty()) {
264         insertNewEntry(newEntry);
265     }
266 }
267 
insertNewEntry(const QString & entry)268 void SimpleStringListEditor::insertNewEntry(const QString &entry)
269 {
270     QString newEntry = entry;
271     // let the user verify the string before adding
272     Q_EMIT aboutToAdd(newEntry);
273     if (!newEntry.isEmpty() && !containsString(newEntry)) {
274         d->mListBox->addItem(newEntry);
275         slotSelectionChanged();
276         Q_EMIT changed();
277     }
278 }
279 
slotAdd()280 void SimpleStringListEditor::slotAdd()
281 {
282     addNewEntry();
283 }
284 
slotCustomize()285 void SimpleStringListEditor::slotCustomize()
286 {
287     QListWidgetItem *item = d->mListBox->currentItem();
288     if (!item) {
289         return;
290     }
291     const QString newText = customEntry(item->text());
292     if (!newText.isEmpty()) {
293         item->setText(newText);
294         Q_EMIT changed();
295     }
296 }
297 
customEntry(const QString & text)298 QString SimpleStringListEditor::customEntry(const QString &text)
299 {
300     Q_UNUSED(text)
301     return {};
302 }
303 
slotRemove()304 void SimpleStringListEditor::slotRemove()
305 {
306     const QList<QListWidgetItem *> selectedItems = d->mListBox->selectedItems();
307     if (selectedItems.isEmpty()) {
308         return;
309     }
310     const int answer = KMessageBox::warningYesNo(this, d->mRemoveDialogLabel, i18n("Remove"), KStandardGuiItem::remove(), KStandardGuiItem::cancel());
311     if (answer == KMessageBox::Yes) {
312         for (QListWidgetItem *item : selectedItems) {
313             delete d->mListBox->takeItem(d->mListBox->row(item));
314         }
315         slotSelectionChanged();
316         Q_EMIT changed();
317     }
318 }
319 
modifyEntry(const QString & text)320 QString SimpleStringListEditor::modifyEntry(const QString &text)
321 {
322     bool ok = false;
323     QString newText = QInputDialog::getText(this, d->mModifyDialogTitle, d->mModifyDialogLabel, QLineEdit::Normal, text, &ok);
324     Q_EMIT aboutToAdd(newText);
325 
326     if (!ok || newText.trimmed().isEmpty() || newText == text) {
327         return QString();
328     }
329 
330     return newText;
331 }
332 
slotModify()333 void SimpleStringListEditor::slotModify()
334 {
335     QListWidgetItem *item = d->mListBox->currentItem();
336     if (!item) {
337         return;
338     }
339     const QString newText = modifyEntry(item->text());
340     if (!newText.isEmpty()) {
341         item->setText(newText);
342         Q_EMIT changed();
343     }
344 }
345 
setRemoveDialogLabel(const QString & removeDialogLabel)346 void SimpleStringListEditor::setRemoveDialogLabel(const QString &removeDialogLabel)
347 {
348     d->mRemoveDialogLabel = removeDialogLabel;
349 }
350 
setAddDialogLabel(const QString & addDialogLabel)351 void SimpleStringListEditor::setAddDialogLabel(const QString &addDialogLabel)
352 {
353     d->mAddDialogLabel = addDialogLabel;
354 }
355 
setAddDialogTitle(const QString & str)356 void SimpleStringListEditor::setAddDialogTitle(const QString &str)
357 {
358     d->mAddDialogTitle = str;
359 }
360 
setModifyDialogTitle(const QString & str)361 void SimpleStringListEditor::setModifyDialogTitle(const QString &str)
362 {
363     d->mModifyDialogTitle = str;
364 }
365 
setModifyDialogLabel(const QString & str)366 void SimpleStringListEditor::setModifyDialogLabel(const QString &str)
367 {
368     d->mModifyDialogLabel = str;
369 }
370 
slotUp()371 void SimpleStringListEditor::slotUp()
372 {
373     QList<QListWidgetItem *> listWidgetItem = d->selectedItems();
374     if (listWidgetItem.isEmpty()) {
375         return;
376     }
377 
378     const int numberOfItem(listWidgetItem.count());
379     const int currentRow = d->mListBox->currentRow();
380     if ((numberOfItem == 1) && (currentRow == 0)) {
381         qCDebug(PIMCOMMON_LOG) << "Called while the _topmost_ filter is selected, ignoring.";
382         return;
383     }
384     bool wasMoved = false;
385 
386     for (int i = 0; i < numberOfItem; ++i) {
387         const int posItem = d->mListBox->row(listWidgetItem.at(i));
388         if (posItem == i) {
389             continue;
390         }
391         QListWidgetItem *item = d->mListBox->takeItem(posItem);
392         d->mListBox->insertItem(posItem - 1, item);
393 
394         wasMoved = true;
395     }
396     if (wasMoved) {
397         Q_EMIT changed();
398         d->mListBox->setCurrentRow(currentRow - 1);
399     }
400 }
401 
slotDown()402 void SimpleStringListEditor::slotDown()
403 {
404     QList<QListWidgetItem *> listWidgetItem = d->selectedItems();
405     if (listWidgetItem.isEmpty()) {
406         return;
407     }
408 
409     const int numberOfElement(d->mListBox->count());
410     const int numberOfItem(listWidgetItem.count());
411     const int currentRow = d->mListBox->currentRow();
412     if ((numberOfItem == 1) && (currentRow == numberOfElement - 1)) {
413         qCDebug(PIMCOMMON_LOG) << "Called while the _last_ filter is selected, ignoring.";
414         return;
415     }
416 
417     int j = 0;
418     bool wasMoved = false;
419     for (int i = numberOfItem - 1; i >= 0; --i, j++) {
420         const int posItem = d->mListBox->row(listWidgetItem.at(i));
421         if (posItem == (numberOfElement - 1 - j)) {
422             continue;
423         }
424         QListWidgetItem *item = d->mListBox->takeItem(posItem);
425         d->mListBox->insertItem(posItem + 1, item);
426         wasMoved = true;
427     }
428     if (wasMoved) {
429         Q_EMIT changed();
430         d->mListBox->setCurrentRow(currentRow + 1);
431     }
432 }
433 
slotSelectionChanged()434 void SimpleStringListEditor::slotSelectionChanged()
435 {
436     const QList<QListWidgetItem *> lstSelectedItems = d->mListBox->selectedItems();
437     const int numberOfItemSelected(lstSelectedItems.count());
438     const bool uniqItemSelected = (numberOfItemSelected == 1);
439     const bool aItemIsSelected = !lstSelectedItems.isEmpty();
440     // if there is one, item will be non-null (ie. true), else 0
441     // (ie. false):
442     if (d->mRemoveButton) {
443         d->mRemoveButton->setEnabled(aItemIsSelected);
444     }
445 
446     if (d->mModifyButton) {
447         d->mModifyButton->setEnabled(uniqItemSelected);
448     }
449 
450     const int currentIndex = d->mListBox->currentRow();
451 
452     const bool allItemSelected = (d->mListBox->count() == numberOfItemSelected);
453     const bool theLast = (currentIndex >= d->mListBox->count() - 1);
454     const bool theFirst = (currentIndex == 0);
455 
456     if (d->mCustomButton) {
457         d->mCustomButton->setEnabled(uniqItemSelected);
458     }
459 
460     if (d->mUpButton) {
461         d->mUpButton->setEnabled(aItemIsSelected && ((uniqItemSelected && !theFirst) || (!uniqItemSelected)) && !allItemSelected);
462     }
463     if (d->mDownButton) {
464         d->mDownButton->setEnabled(aItemIsSelected && ((uniqItemSelected && !theLast) || (!uniqItemSelected)) && !allItemSelected);
465     }
466 }
467 
slotContextMenu(const QPoint & pos)468 void SimpleStringListEditor::slotContextMenu(const QPoint &pos)
469 {
470     QList<QListWidgetItem *> lstSelectedItems = d->mListBox->selectedItems();
471     const bool hasItemsSelected = !lstSelectedItems.isEmpty();
472     QMenu menu(this);
473     if (d->mAddButton) {
474         QAction *act = menu.addAction(d->mAddButton->text(), this, &SimpleStringListEditor::slotAdd);
475         act->setIcon(QIcon::fromTheme(QStringLiteral("list-add")));
476     }
477     if (d->mModifyButton && (lstSelectedItems.count() == 1)) {
478         QAction *act = menu.addAction(d->mModifyButton->text(), this, &SimpleStringListEditor::slotModify);
479         act->setIcon(QIcon::fromTheme(QStringLiteral("document-edit")));
480     }
481     if (d->mRemoveButton && hasItemsSelected) {
482         menu.addSeparator();
483         QAction *act = menu.addAction(d->mRemoveButton->text(), this, &SimpleStringListEditor::slotRemove);
484         act->setIcon(QIcon::fromTheme(QStringLiteral("list-remove")));
485     }
486     if (!menu.isEmpty()) {
487         menu.exec(d->mListBox->mapToGlobal(pos));
488     }
489 }
490 
sizeHint() const491 QSize SimpleStringListEditor::sizeHint() const
492 {
493     // Override height because we want the widget to be tall enough to fit the
494     // button columns, but we want to allow it to be made smaller than list
495     // sizeHint().height()
496     QSize sh = QWidget::sizeHint();
497     sh.setHeight(d->mButtonLayout->minimumSize().height());
498     return sh;
499 }
500