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 ¤tTextLE = 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