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
24class KEditListWidgetPrivate
25{
26public:
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
50private:
51 KEditListWidget *const q;
52};
53
54void 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(left: 0, top: 0, right: 0, bottom: 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(layout: btnsLayout);
74
75 mainLayout->addLayout(layout: subLayout);
76
77 setEditor(lineEdit, representationWidget);
78
79 buttons = KEditListWidget::Buttons();
80 q->setButtons(newButtons);
81
82 q->connect(sender: listView->selectionModel(), signal: &QItemSelectionModel::selectionChanged, context: q, slot: &KEditListWidget::slotSelectionChanged);
83}
84
85void 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(index: 0, widget: editingWidget); // before subLayout
101
102 lineEdit->installEventFilter(filterObj: q);
103
104 q->connect(sender: lineEdit, signal: &QLineEdit::textChanged, context: q, slot: &KEditListWidget::typedSomething);
105 q->connect(sender: lineEdit, signal: &QLineEdit::returnPressed, context: q, slot: &KEditListWidget::addItem);
106
107 // maybe supplied lineedit has some text already
108 q->typedSomething(text: 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
131void 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
149QModelIndex 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
160class KEditListWidgetCustomEditorPrivate
161{
162public:
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
175KEditListWidget::CustomEditor::CustomEditor()
176 : d(new KEditListWidgetCustomEditorPrivate(this))
177{
178}
179
180KEditListWidget::CustomEditor::CustomEditor(QWidget *repWidget, QLineEdit *edit)
181 : d(new KEditListWidgetCustomEditorPrivate(this))
182{
183 d->representationWidget = repWidget;
184 d->lineEdit = edit;
185}
186
187KEditListWidget::CustomEditor::CustomEditor(QComboBox *combo)
188 : d(new KEditListWidgetCustomEditorPrivate(this))
189{
190 d->representationWidget = combo;
191 d->lineEdit = qobject_cast<QLineEdit *>(object: combo->lineEdit());
192 Q_ASSERT(d->lineEdit);
193}
194
195KEditListWidget::CustomEditor::~CustomEditor() = default;
196
197void KEditListWidget::CustomEditor::setRepresentationWidget(QWidget *repWidget)
198{
199 d->representationWidget = repWidget;
200}
201
202void KEditListWidget::CustomEditor::setLineEdit(QLineEdit *edit)
203{
204 d->lineEdit = edit;
205}
206
207QWidget *KEditListWidget::CustomEditor::representationWidget() const
208{
209 return d->representationWidget;
210}
211
212QLineEdit *KEditListWidget::CustomEditor::lineEdit() const
213{
214 return d->lineEdit;
215}
216
217KEditListWidget::KEditListWidget(QWidget *parent)
218 : QWidget(parent)
219 , d(new KEditListWidgetPrivate(this))
220{
221 d->init();
222}
223
224KEditListWidget::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(check: checkAtEntering, newButtons: buttons, representationWidget: custom.representationWidget());
230}
231
232KEditListWidget::~KEditListWidget() = default;
233
234void KEditListWidget::setCustomEditor(const CustomEditor &editor)
235{
236 d->setEditor(newLineEdit: editor.lineEdit(), representationWidget: editor.representationWidget());
237}
238
239QListView *KEditListWidget::listView() const
240{
241 return d->listView;
242}
243
244QLineEdit *KEditListWidget::lineEdit() const
245{
246 return d->lineEdit;
247}
248
249QPushButton *KEditListWidget::addButton() const
250{
251 return d->servNewButton;
252}
253
254QPushButton *KEditListWidget::removeButton() const
255{
256 return d->servRemoveButton;
257}
258
259QPushButton *KEditListWidget::upButton() const
260{
261 return d->servUpButton;
262}
263
264QPushButton *KEditListWidget::downButton() const
265{
266 return d->servDownButton;
267}
268
269int KEditListWidget::count() const
270{
271 return int(d->model->rowCount());
272}
273
274void 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(s: "&Add", c: "@action:button"), this);
282 d->servNewButton->setEnabled(false);
283 d->servNewButton->show();
284 connect(sender: d->servNewButton, signal: &QAbstractButton::clicked, context: this, slot: &KEditListWidget::addItem);
285
286 d->btnsLayout->insertWidget(index: 0, widget: 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(s: "&Remove", c: "@action:button"), this);
294 d->servRemoveButton->setEnabled(false);
295 d->servRemoveButton->show();
296 connect(sender: d->servRemoveButton, signal: &QAbstractButton::clicked, context: this, slot: &KEditListWidget::removeItem);
297
298 d->btnsLayout->insertWidget(index: 1, widget: 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(s: "Move &Up", c: "@action:button"), this);
306 d->servUpButton->setEnabled(false);
307 d->servUpButton->show();
308 connect(sender: d->servUpButton, signal: &QAbstractButton::clicked, context: this, slot: &KEditListWidget::moveItemUp);
309
310 d->servDownButton = new QPushButton(QIcon::fromTheme(QStringLiteral("arrow-down")), tr(s: "Move &Down", c: "@action:button"), this);
311 d->servDownButton->setEnabled(false);
312 d->servDownButton->show();
313 connect(sender: d->servDownButton, signal: &QAbstractButton::clicked, context: this, slot: &KEditListWidget::moveItemDown);
314
315 d->btnsLayout->insertWidget(index: 2, widget: d->servUpButton);
316 d->btnsLayout->insertWidget(index: 3, widget: 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
327void KEditListWidget::setCheckAtEntering(bool check)
328{
329 d->checkAtEntering = check;
330}
331
332bool KEditListWidget::checkAtEntering()
333{
334 return d->checkAtEntering;
335}
336
337void 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(b: true);
346 QModelIndex currentIndex = d->selectedIndex();
347 if (currentIndex.isValid()) {
348 d->model->setData(index: currentIndex, value: text);
349 }
350 d->listView->blockSignals(b: 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(str: text, cs: Qt::CaseSensitive);
372 d->servNewButton->setEnabled(enable);
373 }
374 }
375}
376
377void 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(row: index.row() - 1, column: index.column());
392
393 QString tmp = d->model->data(index: aboveIndex, role: Qt::DisplayRole).toString();
394 d->model->setData(index: aboveIndex, value: d->model->data(index, role: Qt::DisplayRole));
395 d->model->setData(index, value: tmp);
396
397 d->listView->selectionModel()->select(index, command: QItemSelectionModel::Deselect);
398 d->listView->selectionModel()->select(index: aboveIndex, command: QItemSelectionModel::Select);
399 }
400
401 Q_EMIT changed();
402}
403
404void 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(row: index.row() + 1, column: index.column());
419
420 QString tmp = d->model->data(index: belowIndex, role: Qt::DisplayRole).toString();
421 d->model->setData(index: belowIndex, value: d->model->data(index, role: Qt::DisplayRole));
422 d->model->setData(index, value: tmp);
423
424 d->listView->selectionModel()->select(index, command: QItemSelectionModel::Deselect);
425 d->listView->selectionModel()->select(index: belowIndex, command: QItemSelectionModel::Select);
426 }
427
428 Q_EMIT changed();
429}
430
431void 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(index: currentIndex, role: Qt::DisplayRole).toString() == currentTextLE) {
449 alreadyInList = true;
450 }
451 } else {
452 alreadyInList = d->model->stringList().contains(str: currentTextLE, cs: 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(b: true);
469 d->lineEdit->clear();
470 d->lineEdit->blockSignals(b: block);
471
472 d->listView->selectionModel()->setCurrentIndex(index: currentIndex, command: QItemSelectionModel::Deselect);
473
474 if (!alreadyInList) {
475 block = d->listView->signalsBlocked();
476
477 if (currentIndex.isValid()) {
478 d->model->setData(index: currentIndex, value: 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(text: currentTextLE); // TODO: pass the index too
487 }
488
489 d->updateButtonState();
490}
491
492int 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
502void 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(index: currentIndex, role: Qt::DisplayRole).toString();
519
520 d->model->removeRows(row: currentIndex.row(), count: 1);
521
522 d->listView->selectionModel()->clear();
523
524 Q_EMIT changed();
525
526 Q_EMIT removed(text: removedText);
527 }
528
529 d->updateButtonState();
530}
531
532void 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
564void KEditListWidget::clear()
565{
566 d->lineEdit->clear();
567 d->model->setStringList(QStringList());
568 Q_EMIT changed();
569}
570
571void 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(i: j, t: list[i]);
579 }
580 }
581
582 d->model->setStringList(content);
583}
584
585void KEditListWidget::insertItem(const QString &text, int index)
586{
587 QStringList list = d->model->stringList();
588
589 if (index < 0) {
590 list.append(t: text);
591 } else {
592 list.insert(i: index, t: text);
593 }
594
595 d->model->setStringList(list);
596}
597
598QString KEditListWidget::text(int index) const
599{
600 const QStringList list = d->model->stringList();
601
602 return list[index];
603}
604
605QString KEditListWidget::currentText() const
606{
607 QModelIndex index = d->selectedIndex();
608 if (!index.isValid()) {
609 return QString();
610 } else {
611 return text(index: index.row());
612 }
613}
614
615QStringList KEditListWidget::items() const
616{
617 return d->model->stringList();
618}
619
620void KEditListWidget::setItems(const QStringList &items)
621{
622 d->model->setStringList(items);
623}
624
625KEditListWidget::Buttons KEditListWidget::buttons() const
626{
627 return d->buttons;
628}
629
630void KEditListWidget::slotSelectionChanged(const QItemSelection &, const QItemSelection &)
631{
632 d->updateButtonState();
633 QModelIndex index = d->selectedIndex();
634 enableMoveButtons(newIndex: index, QModelIndex());
635 if (index.isValid()) {
636 d->lineEdit->setFocus(Qt::OtherFocusReason);
637 }
638}
639
640bool 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(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
654#include "moc_keditlistwidget.cpp"
655

source code of kwidgetsaddons/src/keditlistwidget.cpp