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