1 /*
2     This file is part of the KDE libraries
3     SPDX-FileCopyrightText: 2000 David Faure <faure@kde.org>
4     SPDX-FileCopyrightText: 2000 Alexander Neundorf <neundorf@kde.org>
5     SPDX-FileCopyrightText: 2000, 2002 Carsten Pfeiffer <pfeiffer@kde.org>
6     SPDX-FileCopyrightText: 2010 Sebastian Trueg <trueg@kde.org>
7 
8     SPDX-License-Identifier: LGPL-2.0-or-later
9 */
10 
11 #include "keditlistwidget.h"
12 
13 #include <QApplication>
14 #include <QComboBox>
15 #include <QHBoxLayout>
16 #include <QKeyEvent>
17 #include <QLineEdit>
18 #include <QListView>
19 #include <QPushButton>
20 #include <QStringList>
21 #include <QStringListModel>
22 #include <QVBoxLayout>
23 
24 class KEditListWidgetPrivate
25 {
26 public:
KEditListWidgetPrivate(KEditListWidget * parent)27     KEditListWidgetPrivate(KEditListWidget *parent)
28         : q(parent)
29     {
30     }
31     QListView *listView = nullptr;
32     QPushButton *servUpButton = nullptr;
33     QPushButton *servDownButton = nullptr;
34     QPushButton *servNewButton = nullptr;
35     QPushButton *servRemoveButton = nullptr;
36     QLineEdit *lineEdit = nullptr;
37     QWidget *editingWidget = nullptr;
38     QVBoxLayout *mainLayout = nullptr;
39     QVBoxLayout *btnsLayout = nullptr;
40     QStringListModel *model = nullptr;
41 
42     bool checkAtEntering;
43     KEditListWidget::Buttons buttons;
44 
45     void init(bool check = false, KEditListWidget::Buttons buttons = KEditListWidget::All, QWidget *representationWidget = nullptr);
46     void setEditor(QLineEdit *lineEdit, QWidget *representationWidget = nullptr);
47     void updateButtonState();
48     QModelIndex selectedIndex();
49 
50 private:
51     KEditListWidget *const q;
52 };
53 
init(bool check,KEditListWidget::Buttons newButtons,QWidget * representationWidget)54 void KEditListWidgetPrivate::init(bool check, KEditListWidget::Buttons newButtons, QWidget *representationWidget)
55 {
56     checkAtEntering = check;
57 
58     servNewButton = servRemoveButton = servUpButton = servDownButton = nullptr;
59     q->setSizePolicy(QSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred));
60 
61     mainLayout = new QVBoxLayout(q);
62     mainLayout->setContentsMargins(0, 0, 0, 0);
63 
64     QHBoxLayout *subLayout = new QHBoxLayout;
65     btnsLayout = new QVBoxLayout;
66     btnsLayout->addStretch();
67 
68     model = new QStringListModel(q);
69     listView = new QListView(q);
70     listView->setModel(model);
71 
72     subLayout->addWidget(listView);
73     subLayout->addLayout(btnsLayout);
74 
75     mainLayout->addLayout(subLayout);
76 
77     setEditor(lineEdit, representationWidget);
78 
79     buttons = KEditListWidget::Buttons();
80     q->setButtons(newButtons);
81 
82     q->connect(listView->selectionModel(), &QItemSelectionModel::selectionChanged, q, &KEditListWidget::slotSelectionChanged);
83 }
84 
setEditor(QLineEdit * newLineEdit,QWidget * representationWidget)85 void KEditListWidgetPrivate::setEditor(QLineEdit *newLineEdit, QWidget *representationWidget)
86 {
87     if (editingWidget != lineEdit && editingWidget != representationWidget) {
88         delete editingWidget;
89     }
90     if (lineEdit != newLineEdit) {
91         delete lineEdit;
92     }
93     lineEdit = newLineEdit ? newLineEdit : new QLineEdit(q);
94     editingWidget = representationWidget ? representationWidget : lineEdit;
95 
96     if (representationWidget) {
97         representationWidget->setParent(q);
98     }
99 
100     mainLayout->insertWidget(0, editingWidget); // before subLayout
101 
102     lineEdit->installEventFilter(q);
103 
104     q->connect(lineEdit, &QLineEdit::textChanged, q, &KEditListWidget::typedSomething);
105     q->connect(lineEdit, &QLineEdit::returnPressed, q, &KEditListWidget::addItem);
106 
107     // maybe supplied lineedit has some text already
108     q->typedSomething(lineEdit->text());
109 
110     // fix tab ordering
111     q->setTabOrder(editingWidget, listView);
112     QWidget *w = listView;
113     if (servNewButton) {
114         q->setTabOrder(w, servNewButton);
115         w = servNewButton;
116     }
117     if (servRemoveButton) {
118         q->setTabOrder(w, servRemoveButton);
119         w = servRemoveButton;
120     }
121     if (servUpButton) {
122         q->setTabOrder(w, servUpButton);
123         w = servUpButton;
124     }
125     if (servDownButton) {
126         q->setTabOrder(w, servDownButton);
127         w = servDownButton;
128     }
129 }
130 
updateButtonState()131 void KEditListWidgetPrivate::updateButtonState()
132 {
133     const bool hasSelectedItem = selectedIndex().isValid();
134 
135     // TODO: merge with enableMoveButtons()
136     QPushButton *const buttons[3] = {servUpButton, servDownButton, servRemoveButton};
137 
138     for (QPushButton *button : buttons) {
139         if (button) {
140             // keep focus in widget
141             if (!hasSelectedItem && button->hasFocus()) {
142                 lineEdit->setFocus(Qt::OtherFocusReason);
143             }
144             button->setEnabled(hasSelectedItem);
145         }
146     }
147 }
148 
selectedIndex()149 QModelIndex KEditListWidgetPrivate::selectedIndex()
150 {
151     QItemSelectionModel *selection = listView->selectionModel();
152     const QModelIndexList selectedIndexes = selection->selectedIndexes();
153     if (!selectedIndexes.isEmpty() && selectedIndexes[0].isValid()) {
154         return selectedIndexes[0];
155     } else {
156         return QModelIndex();
157     }
158 }
159 
160 class KEditListWidgetCustomEditorPrivate
161 {
162 public:
KEditListWidgetCustomEditorPrivate(KEditListWidget::CustomEditor * qq)163     KEditListWidgetCustomEditorPrivate(KEditListWidget::CustomEditor *qq)
164         : q(qq)
165         , representationWidget(nullptr)
166         , lineEdit(nullptr)
167     {
168     }
169 
170     KEditListWidget::CustomEditor *q;
171     QWidget *representationWidget;
172     QLineEdit *lineEdit;
173 };
174 
CustomEditor()175 KEditListWidget::CustomEditor::CustomEditor()
176     : d(new KEditListWidgetCustomEditorPrivate(this))
177 {
178 }
179 
CustomEditor(QWidget * repWidget,QLineEdit * edit)180 KEditListWidget::CustomEditor::CustomEditor(QWidget *repWidget, QLineEdit *edit)
181     : d(new KEditListWidgetCustomEditorPrivate(this))
182 {
183     d->representationWidget = repWidget;
184     d->lineEdit = edit;
185 }
186 
CustomEditor(QComboBox * combo)187 KEditListWidget::CustomEditor::CustomEditor(QComboBox *combo)
188     : d(new KEditListWidgetCustomEditorPrivate(this))
189 {
190     d->representationWidget = combo;
191     d->lineEdit = qobject_cast<QLineEdit *>(combo->lineEdit());
192     Q_ASSERT(d->lineEdit);
193 }
194 
195 KEditListWidget::CustomEditor::~CustomEditor() = default;
196 
setRepresentationWidget(QWidget * repWidget)197 void KEditListWidget::CustomEditor::setRepresentationWidget(QWidget *repWidget)
198 {
199     d->representationWidget = repWidget;
200 }
201 
setLineEdit(QLineEdit * edit)202 void KEditListWidget::CustomEditor::setLineEdit(QLineEdit *edit)
203 {
204     d->lineEdit = edit;
205 }
206 
representationWidget() const207 QWidget *KEditListWidget::CustomEditor::representationWidget() const
208 {
209     return d->representationWidget;
210 }
211 
lineEdit() const212 QLineEdit *KEditListWidget::CustomEditor::lineEdit() const
213 {
214     return d->lineEdit;
215 }
216 
KEditListWidget(QWidget * parent)217 KEditListWidget::KEditListWidget(QWidget *parent)
218     : QWidget(parent)
219     , d(new KEditListWidgetPrivate(this))
220 {
221     d->init();
222 }
223 
KEditListWidget(const CustomEditor & custom,QWidget * parent,bool checkAtEntering,Buttons buttons)224 KEditListWidget::KEditListWidget(const CustomEditor &custom, QWidget *parent, bool checkAtEntering, Buttons buttons)
225     : QWidget(parent)
226     , d(new KEditListWidgetPrivate(this))
227 {
228     d->lineEdit = custom.lineEdit();
229     d->init(checkAtEntering, buttons, custom.representationWidget());
230 }
231 
232 KEditListWidget::~KEditListWidget() = default;
233 
setCustomEditor(const CustomEditor & editor)234 void KEditListWidget::setCustomEditor(const CustomEditor &editor)
235 {
236     d->setEditor(editor.lineEdit(), editor.representationWidget());
237 }
238 
listView() const239 QListView *KEditListWidget::listView() const
240 {
241     return d->listView;
242 }
243 
lineEdit() const244 QLineEdit *KEditListWidget::lineEdit() const
245 {
246     return d->lineEdit;
247 }
248 
addButton() const249 QPushButton *KEditListWidget::addButton() const
250 {
251     return d->servNewButton;
252 }
253 
removeButton() const254 QPushButton *KEditListWidget::removeButton() const
255 {
256     return d->servRemoveButton;
257 }
258 
upButton() const259 QPushButton *KEditListWidget::upButton() const
260 {
261     return d->servUpButton;
262 }
263 
downButton() const264 QPushButton *KEditListWidget::downButton() const
265 {
266     return d->servDownButton;
267 }
268 
count() const269 int KEditListWidget::count() const
270 {
271     return int(d->model->rowCount());
272 }
273 
setButtons(Buttons buttons)274 void KEditListWidget::setButtons(Buttons buttons)
275 {
276     if (d->buttons == buttons) {
277         return;
278     }
279 
280     if ((buttons & Add) && !d->servNewButton) {
281         d->servNewButton = new QPushButton(QIcon::fromTheme(QStringLiteral("list-add")), tr("&Add", "@action:button"), this);
282         d->servNewButton->setEnabled(false);
283         d->servNewButton->show();
284         connect(d->servNewButton, &QAbstractButton::clicked, this, &KEditListWidget::addItem);
285 
286         d->btnsLayout->insertWidget(0, d->servNewButton);
287     } else if ((buttons & Add) == 0 && d->servNewButton) {
288         delete d->servNewButton;
289         d->servNewButton = nullptr;
290     }
291 
292     if ((buttons & Remove) && !d->servRemoveButton) {
293         d->servRemoveButton = new QPushButton(QIcon::fromTheme(QStringLiteral("list-remove")), tr("&Remove", "@action:button"), this);
294         d->servRemoveButton->setEnabled(false);
295         d->servRemoveButton->show();
296         connect(d->servRemoveButton, &QAbstractButton::clicked, this, &KEditListWidget::removeItem);
297 
298         d->btnsLayout->insertWidget(1, d->servRemoveButton);
299     } else if ((buttons & Remove) == 0 && d->servRemoveButton) {
300         delete d->servRemoveButton;
301         d->servRemoveButton = nullptr;
302     }
303 
304     if ((buttons & UpDown) && !d->servUpButton) {
305         d->servUpButton = new QPushButton(QIcon::fromTheme(QStringLiteral("arrow-up")), tr("Move &Up", "@action:button"), this);
306         d->servUpButton->setEnabled(false);
307         d->servUpButton->show();
308         connect(d->servUpButton, &QAbstractButton::clicked, this, &KEditListWidget::moveItemUp);
309 
310         d->servDownButton = new QPushButton(QIcon::fromTheme(QStringLiteral("arrow-down")), tr("Move &Down", "@action:button"), this);
311         d->servDownButton->setEnabled(false);
312         d->servDownButton->show();
313         connect(d->servDownButton, &QAbstractButton::clicked, this, &KEditListWidget::moveItemDown);
314 
315         d->btnsLayout->insertWidget(2, d->servUpButton);
316         d->btnsLayout->insertWidget(3, d->servDownButton);
317     } else if ((buttons & UpDown) == 0 && d->servUpButton) {
318         delete d->servUpButton;
319         d->servUpButton = nullptr;
320         delete d->servDownButton;
321         d->servDownButton = nullptr;
322     }
323 
324     d->buttons = buttons;
325 }
326 
setCheckAtEntering(bool check)327 void KEditListWidget::setCheckAtEntering(bool check)
328 {
329     d->checkAtEntering = check;
330 }
331 
checkAtEntering()332 bool KEditListWidget::checkAtEntering()
333 {
334     return d->checkAtEntering;
335 }
336 
typedSomething(const QString & text)337 void KEditListWidget::typedSomething(const QString &text)
338 {
339     if (currentItem() >= 0) {
340         if (currentText() != d->lineEdit->text()) {
341             // IMHO changeItem() shouldn't do anything with the value
342             // of currentItem() ... like changing it or emitting signals ...
343             // but TT disagree with me on this one (it's been that way since ages ... grrr)
344             bool block = d->listView->signalsBlocked();
345             d->listView->blockSignals(true);
346             QModelIndex currentIndex = d->selectedIndex();
347             if (currentIndex.isValid()) {
348                 d->model->setData(currentIndex, text);
349             }
350             d->listView->blockSignals(block);
351             Q_EMIT changed();
352         }
353     }
354 
355     if (!d->servNewButton) {
356         return;
357     }
358 
359     if (!d->lineEdit->hasAcceptableInput()) {
360         d->servNewButton->setEnabled(false);
361         return;
362     }
363 
364     if (!d->checkAtEntering) {
365         d->servNewButton->setEnabled(!text.isEmpty());
366     } else {
367         if (text.isEmpty()) {
368             d->servNewButton->setEnabled(false);
369         } else {
370             QStringList list = d->model->stringList();
371             bool enable = !list.contains(text, Qt::CaseSensitive);
372             d->servNewButton->setEnabled(enable);
373         }
374     }
375 }
376 
moveItemUp()377 void KEditListWidget::moveItemUp()
378 {
379     if (!d->listView->isEnabled()) {
380         QApplication::beep();
381         return;
382     }
383 
384     QModelIndex index = d->selectedIndex();
385     if (index.isValid()) {
386         if (index.row() == 0) {
387             QApplication::beep();
388             return;
389         }
390 
391         QModelIndex aboveIndex = d->model->index(index.row() - 1, index.column());
392 
393         QString tmp = d->model->data(aboveIndex, Qt::DisplayRole).toString();
394         d->model->setData(aboveIndex, d->model->data(index, Qt::DisplayRole));
395         d->model->setData(index, tmp);
396 
397         d->listView->selectionModel()->select(index, QItemSelectionModel::Deselect);
398         d->listView->selectionModel()->select(aboveIndex, QItemSelectionModel::Select);
399     }
400 
401     Q_EMIT changed();
402 }
403 
moveItemDown()404 void KEditListWidget::moveItemDown()
405 {
406     if (!d->listView->isEnabled()) {
407         QApplication::beep();
408         return;
409     }
410 
411     QModelIndex index = d->selectedIndex();
412     if (index.isValid()) {
413         if (index.row() == d->model->rowCount() - 1) {
414             QApplication::beep();
415             return;
416         }
417 
418         QModelIndex belowIndex = d->model->index(index.row() + 1, index.column());
419 
420         QString tmp = d->model->data(belowIndex, Qt::DisplayRole).toString();
421         d->model->setData(belowIndex, d->model->data(index, Qt::DisplayRole));
422         d->model->setData(index, tmp);
423 
424         d->listView->selectionModel()->select(index, QItemSelectionModel::Deselect);
425         d->listView->selectionModel()->select(belowIndex, QItemSelectionModel::Select);
426     }
427 
428     Q_EMIT changed();
429 }
430 
addItem()431 void KEditListWidget::addItem()
432 {
433     // when checkAtEntering is true, the add-button is disabled, but this
434     // slot can still be called through Key_Return/Key_Enter. So we guard
435     // against this.
436     if (!d->servNewButton || !d->servNewButton->isEnabled()) {
437         return;
438     }
439 
440     QModelIndex currentIndex = d->selectedIndex();
441 
442     const QString &currentTextLE = d->lineEdit->text();
443     bool alreadyInList(false);
444     // if we didn't check for dupes at the inserting we have to do it now
445     if (!d->checkAtEntering) {
446         // first check current item instead of dumb iterating the entire list
447         if (currentIndex.isValid()) {
448             if (d->model->data(currentIndex, Qt::DisplayRole).toString() == currentTextLE) {
449                 alreadyInList = true;
450             }
451         } else {
452             alreadyInList = d->model->stringList().contains(currentTextLE, Qt::CaseSensitive);
453         }
454     }
455     if (d->servNewButton) {
456         // prevent losing the focus by it being moved outside of this widget
457         // as well as support the user workflow a little by moving the focus
458         // to the lineedit. chances are that users will add some items consecutively,
459         // so this will save a manual focus change, and it is also consistent
460         // to what happens on the click on the Remove button
461         if (d->servNewButton->hasFocus()) {
462             d->lineEdit->setFocus(Qt::OtherFocusReason);
463         }
464         d->servNewButton->setEnabled(false);
465     }
466 
467     bool block = d->lineEdit->signalsBlocked();
468     d->lineEdit->blockSignals(true);
469     d->lineEdit->clear();
470     d->lineEdit->blockSignals(block);
471 
472     d->listView->selectionModel()->setCurrentIndex(currentIndex, QItemSelectionModel::Deselect);
473 
474     if (!alreadyInList) {
475         block = d->listView->signalsBlocked();
476 
477         if (currentIndex.isValid()) {
478             d->model->setData(currentIndex, currentTextLE);
479         } else {
480             QStringList lst;
481             lst << currentTextLE;
482             lst << d->model->stringList();
483             d->model->setStringList(lst);
484         }
485         Q_EMIT changed();
486         Q_EMIT added(currentTextLE); // TODO: pass the index too
487     }
488 
489     d->updateButtonState();
490 }
491 
currentItem() const492 int KEditListWidget::currentItem() const
493 {
494     QModelIndex selectedIndex = d->selectedIndex();
495     if (selectedIndex.isValid()) {
496         return selectedIndex.row();
497     } else {
498         return -1;
499     }
500 }
501 
removeItem()502 void KEditListWidget::removeItem()
503 {
504     QModelIndex currentIndex = d->selectedIndex();
505     if (!currentIndex.isValid()) {
506         return;
507     }
508 
509     if (currentIndex.row() >= 0) {
510         // prevent losing the focus by it being moved outside of this widget
511         // as well as support the user workflow a little by moving the focus
512         // to the lineedit. chances are that users will add some item next,
513         // so this will save a manual focus change,
514         if (d->servRemoveButton && d->servRemoveButton->hasFocus()) {
515             d->lineEdit->setFocus(Qt::OtherFocusReason);
516         }
517 
518         QString removedText = d->model->data(currentIndex, Qt::DisplayRole).toString();
519 
520         d->model->removeRows(currentIndex.row(), 1);
521 
522         d->listView->selectionModel()->clear();
523 
524         Q_EMIT changed();
525 
526         Q_EMIT removed(removedText);
527     }
528 
529     d->updateButtonState();
530 }
531 
enableMoveButtons(const QModelIndex & newIndex,const QModelIndex &)532 void KEditListWidget::enableMoveButtons(const QModelIndex &newIndex, const QModelIndex &)
533 {
534     int index = newIndex.row();
535 
536     // Update the lineEdit when we select a different line.
537     if (currentText() != d->lineEdit->text()) {
538         d->lineEdit->setText(currentText());
539     }
540 
541     bool moveEnabled = d->servUpButton && d->servDownButton;
542 
543     if (moveEnabled) {
544         if (d->model->rowCount() <= 1) {
545             d->servUpButton->setEnabled(false);
546             d->servDownButton->setEnabled(false);
547         } else if (index == (d->model->rowCount() - 1)) {
548             d->servUpButton->setEnabled(true);
549             d->servDownButton->setEnabled(false);
550         } else if (index == 0) {
551             d->servUpButton->setEnabled(false);
552             d->servDownButton->setEnabled(true);
553         } else {
554             d->servUpButton->setEnabled(true);
555             d->servDownButton->setEnabled(true);
556         }
557     }
558 
559     if (d->servRemoveButton) {
560         d->servRemoveButton->setEnabled(true);
561     }
562 }
563 
clear()564 void KEditListWidget::clear()
565 {
566     d->lineEdit->clear();
567     d->model->setStringList(QStringList());
568     Q_EMIT changed();
569 }
570 
insertStringList(const QStringList & list,int index)571 void KEditListWidget::insertStringList(const QStringList &list, int index)
572 {
573     QStringList content = d->model->stringList();
574     if (index < 0) {
575         content += list;
576     } else {
577         for (int i = 0, j = index; i < list.count(); ++i, ++j) {
578             content.insert(j, list[i]);
579         }
580     }
581 
582     d->model->setStringList(content);
583 }
584 
insertItem(const QString & text,int index)585 void KEditListWidget::insertItem(const QString &text, int index)
586 {
587     QStringList list = d->model->stringList();
588 
589     if (index < 0) {
590         list.append(text);
591     } else {
592         list.insert(index, text);
593     }
594 
595     d->model->setStringList(list);
596 }
597 
text(int index) const598 QString KEditListWidget::text(int index) const
599 {
600     const QStringList list = d->model->stringList();
601 
602     return list[index];
603 }
604 
currentText() const605 QString KEditListWidget::currentText() const
606 {
607     QModelIndex index = d->selectedIndex();
608     if (!index.isValid()) {
609         return QString();
610     } else {
611         return text(index.row());
612     }
613 }
614 
items() const615 QStringList KEditListWidget::items() const
616 {
617     return d->model->stringList();
618 }
619 
setItems(const QStringList & items)620 void KEditListWidget::setItems(const QStringList &items)
621 {
622     d->model->setStringList(items);
623 }
624 
buttons() const625 KEditListWidget::Buttons KEditListWidget::buttons() const
626 {
627     return d->buttons;
628 }
629 
slotSelectionChanged(const QItemSelection &,const QItemSelection &)630 void KEditListWidget::slotSelectionChanged(const QItemSelection &, const QItemSelection &)
631 {
632     d->updateButtonState();
633     QModelIndex index = d->selectedIndex();
634     enableMoveButtons(index, QModelIndex());
635     if (index.isValid()) {
636         d->lineEdit->setFocus(Qt::OtherFocusReason);
637     }
638 }
639 
eventFilter(QObject * o,QEvent * e)640 bool KEditListWidget::eventFilter(QObject *o, QEvent *e)
641 {
642     if (o == d->lineEdit && e->type() == QEvent::KeyPress) {
643         QKeyEvent *keyEvent = (QKeyEvent *)e;
644         if (keyEvent->key() == Qt::Key_Down || keyEvent->key() == Qt::Key_Up) {
645             return ((QObject *)d->listView)->event(e);
646         } else if (keyEvent->key() == Qt::Key_Return || keyEvent->key() == Qt::Key_Enter) {
647             return true;
648         }
649     }
650 
651     return false;
652 }
653