File indexing completed on 2024-04-28 15:32:02

0001 /*
0002     This file is part of the KDE libraries
0003     SPDX-FileCopyrightText: 2000 David Faure <faure@kde.org>
0004     SPDX-FileCopyrightText: 2000 Alexander Neundorf <neundorf@kde.org>
0005     SPDX-FileCopyrightText: 2000, 2002 Carsten Pfeiffer <pfeiffer@kde.org>
0006     SPDX-FileCopyrightText: 2010 Sebastian Trueg <trueg@kde.org>
0007 
0008     SPDX-License-Identifier: LGPL-2.0-or-later
0009 */
0010 
0011 #include "keditlistwidget.h"
0012 
0013 #include <QApplication>
0014 #include <QComboBox>
0015 #include <QHBoxLayout>
0016 #include <QKeyEvent>
0017 #include <QLineEdit>
0018 #include <QListView>
0019 #include <QPushButton>
0020 #include <QStringList>
0021 #include <QStringListModel>
0022 #include <QVBoxLayout>
0023 
0024 class KEditListWidgetPrivate
0025 {
0026 public:
0027     KEditListWidgetPrivate(KEditListWidget *parent)
0028         : q(parent)
0029     {
0030     }
0031     QListView *listView = nullptr;
0032     QPushButton *servUpButton = nullptr;
0033     QPushButton *servDownButton = nullptr;
0034     QPushButton *servNewButton = nullptr;
0035     QPushButton *servRemoveButton = nullptr;
0036     QLineEdit *lineEdit = nullptr;
0037     QWidget *editingWidget = nullptr;
0038     QVBoxLayout *mainLayout = nullptr;
0039     QVBoxLayout *btnsLayout = nullptr;
0040     QStringListModel *model = nullptr;
0041 
0042     bool checkAtEntering;
0043     KEditListWidget::Buttons buttons;
0044 
0045     void init(bool check = false, KEditListWidget::Buttons buttons = KEditListWidget::All, QWidget *representationWidget = nullptr);
0046     void setEditor(QLineEdit *lineEdit, QWidget *representationWidget = nullptr);
0047     void updateButtonState();
0048     QModelIndex selectedIndex();
0049 
0050 private:
0051     KEditListWidget *const q;
0052 };
0053 
0054 void KEditListWidgetPrivate::init(bool check, KEditListWidget::Buttons newButtons, QWidget *representationWidget)
0055 {
0056     checkAtEntering = check;
0057 
0058     servNewButton = servRemoveButton = servUpButton = servDownButton = nullptr;
0059     q->setSizePolicy(QSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred));
0060 
0061     mainLayout = new QVBoxLayout(q);
0062     mainLayout->setContentsMargins(0, 0, 0, 0);
0063 
0064     QHBoxLayout *subLayout = new QHBoxLayout;
0065     btnsLayout = new QVBoxLayout;
0066     btnsLayout->addStretch();
0067 
0068     model = new QStringListModel(q);
0069     listView = new QListView(q);
0070     listView->setModel(model);
0071 
0072     subLayout->addWidget(listView);
0073     subLayout->addLayout(btnsLayout);
0074 
0075     mainLayout->addLayout(subLayout);
0076 
0077     setEditor(lineEdit, representationWidget);
0078 
0079     buttons = KEditListWidget::Buttons();
0080     q->setButtons(newButtons);
0081 
0082     q->connect(listView->selectionModel(), &QItemSelectionModel::selectionChanged, q, &KEditListWidget::slotSelectionChanged);
0083 }
0084 
0085 void KEditListWidgetPrivate::setEditor(QLineEdit *newLineEdit, QWidget *representationWidget)
0086 {
0087     if (editingWidget != lineEdit && editingWidget != representationWidget) {
0088         delete editingWidget;
0089     }
0090     if (lineEdit != newLineEdit) {
0091         delete lineEdit;
0092     }
0093     lineEdit = newLineEdit ? newLineEdit : new QLineEdit(q);
0094     editingWidget = representationWidget ? representationWidget : lineEdit;
0095 
0096     if (representationWidget) {
0097         representationWidget->setParent(q);
0098     }
0099 
0100     mainLayout->insertWidget(0, editingWidget); // before subLayout
0101 
0102     lineEdit->installEventFilter(q);
0103 
0104     q->connect(lineEdit, &QLineEdit::textChanged, q, &KEditListWidget::typedSomething);
0105     q->connect(lineEdit, &QLineEdit::returnPressed, q, &KEditListWidget::addItem);
0106 
0107     // maybe supplied lineedit has some text already
0108     q->typedSomething(lineEdit->text());
0109 
0110     // fix tab ordering
0111     q->setTabOrder(editingWidget, listView);
0112     QWidget *w = listView;
0113     if (servNewButton) {
0114         q->setTabOrder(w, servNewButton);
0115         w = servNewButton;
0116     }
0117     if (servRemoveButton) {
0118         q->setTabOrder(w, servRemoveButton);
0119         w = servRemoveButton;
0120     }
0121     if (servUpButton) {
0122         q->setTabOrder(w, servUpButton);
0123         w = servUpButton;
0124     }
0125     if (servDownButton) {
0126         q->setTabOrder(w, servDownButton);
0127         w = servDownButton;
0128     }
0129 }
0130 
0131 void KEditListWidgetPrivate::updateButtonState()
0132 {
0133     const bool hasSelectedItem = selectedIndex().isValid();
0134 
0135     // TODO: merge with enableMoveButtons()
0136     QPushButton *const buttons[3] = {servUpButton, servDownButton, servRemoveButton};
0137 
0138     for (QPushButton *button : buttons) {
0139         if (button) {
0140             // keep focus in widget
0141             if (!hasSelectedItem && button->hasFocus()) {
0142                 lineEdit->setFocus(Qt::OtherFocusReason);
0143             }
0144             button->setEnabled(hasSelectedItem);
0145         }
0146     }
0147 }
0148 
0149 QModelIndex KEditListWidgetPrivate::selectedIndex()
0150 {
0151     QItemSelectionModel *selection = listView->selectionModel();
0152     const QModelIndexList selectedIndexes = selection->selectedIndexes();
0153     if (!selectedIndexes.isEmpty() && selectedIndexes[0].isValid()) {
0154         return selectedIndexes[0];
0155     } else {
0156         return QModelIndex();
0157     }
0158 }
0159 
0160 class KEditListWidgetCustomEditorPrivate
0161 {
0162 public:
0163     KEditListWidgetCustomEditorPrivate(KEditListWidget::CustomEditor *qq)
0164         : q(qq)
0165         , representationWidget(nullptr)
0166         , lineEdit(nullptr)
0167     {
0168     }
0169 
0170     KEditListWidget::CustomEditor *q;
0171     QWidget *representationWidget;
0172     QLineEdit *lineEdit;
0173 };
0174 
0175 KEditListWidget::CustomEditor::CustomEditor()
0176     : d(new KEditListWidgetCustomEditorPrivate(this))
0177 {
0178 }
0179 
0180 KEditListWidget::CustomEditor::CustomEditor(QWidget *repWidget, QLineEdit *edit)
0181     : d(new KEditListWidgetCustomEditorPrivate(this))
0182 {
0183     d->representationWidget = repWidget;
0184     d->lineEdit = edit;
0185 }
0186 
0187 KEditListWidget::CustomEditor::CustomEditor(QComboBox *combo)
0188     : d(new KEditListWidgetCustomEditorPrivate(this))
0189 {
0190     d->representationWidget = combo;
0191     d->lineEdit = qobject_cast<QLineEdit *>(combo->lineEdit());
0192     Q_ASSERT(d->lineEdit);
0193 }
0194 
0195 KEditListWidget::CustomEditor::~CustomEditor() = default;
0196 
0197 void KEditListWidget::CustomEditor::setRepresentationWidget(QWidget *repWidget)
0198 {
0199     d->representationWidget = repWidget;
0200 }
0201 
0202 void KEditListWidget::CustomEditor::setLineEdit(QLineEdit *edit)
0203 {
0204     d->lineEdit = edit;
0205 }
0206 
0207 QWidget *KEditListWidget::CustomEditor::representationWidget() const
0208 {
0209     return d->representationWidget;
0210 }
0211 
0212 QLineEdit *KEditListWidget::CustomEditor::lineEdit() const
0213 {
0214     return d->lineEdit;
0215 }
0216 
0217 KEditListWidget::KEditListWidget(QWidget *parent)
0218     : QWidget(parent)
0219     , d(new KEditListWidgetPrivate(this))
0220 {
0221     d->init();
0222 }
0223 
0224 KEditListWidget::KEditListWidget(const CustomEditor &custom, QWidget *parent, bool checkAtEntering, Buttons buttons)
0225     : QWidget(parent)
0226     , d(new KEditListWidgetPrivate(this))
0227 {
0228     d->lineEdit = custom.lineEdit();
0229     d->init(checkAtEntering, buttons, custom.representationWidget());
0230 }
0231 
0232 KEditListWidget::~KEditListWidget() = default;
0233 
0234 void KEditListWidget::setCustomEditor(const CustomEditor &editor)
0235 {
0236     d->setEditor(editor.lineEdit(), editor.representationWidget());
0237 }
0238 
0239 QListView *KEditListWidget::listView() const
0240 {
0241     return d->listView;
0242 }
0243 
0244 QLineEdit *KEditListWidget::lineEdit() const
0245 {
0246     return d->lineEdit;
0247 }
0248 
0249 QPushButton *KEditListWidget::addButton() const
0250 {
0251     return d->servNewButton;
0252 }
0253 
0254 QPushButton *KEditListWidget::removeButton() const
0255 {
0256     return d->servRemoveButton;
0257 }
0258 
0259 QPushButton *KEditListWidget::upButton() const
0260 {
0261     return d->servUpButton;
0262 }
0263 
0264 QPushButton *KEditListWidget::downButton() const
0265 {
0266     return d->servDownButton;
0267 }
0268 
0269 int KEditListWidget::count() const
0270 {
0271     return int(d->model->rowCount());
0272 }
0273 
0274 void KEditListWidget::setButtons(Buttons buttons)
0275 {
0276     if (d->buttons == buttons) {
0277         return;
0278     }
0279 
0280     if ((buttons & Add) && !d->servNewButton) {
0281         d->servNewButton = new QPushButton(QIcon::fromTheme(QStringLiteral("list-add")), tr("&Add", "@action:button"), this);
0282         d->servNewButton->setEnabled(false);
0283         d->servNewButton->show();
0284         connect(d->servNewButton, &QAbstractButton::clicked, this, &KEditListWidget::addItem);
0285 
0286         d->btnsLayout->insertWidget(0, d->servNewButton);
0287     } else if ((buttons & Add) == 0 && d->servNewButton) {
0288         delete d->servNewButton;
0289         d->servNewButton = nullptr;
0290     }
0291 
0292     if ((buttons & Remove) && !d->servRemoveButton) {
0293         d->servRemoveButton = new QPushButton(QIcon::fromTheme(QStringLiteral("list-remove")), tr("&Remove", "@action:button"), this);
0294         d->servRemoveButton->setEnabled(false);
0295         d->servRemoveButton->show();
0296         connect(d->servRemoveButton, &QAbstractButton::clicked, this, &KEditListWidget::removeItem);
0297 
0298         d->btnsLayout->insertWidget(1, d->servRemoveButton);
0299     } else if ((buttons & Remove) == 0 && d->servRemoveButton) {
0300         delete d->servRemoveButton;
0301         d->servRemoveButton = nullptr;
0302     }
0303 
0304     if ((buttons & UpDown) && !d->servUpButton) {
0305         d->servUpButton = new QPushButton(QIcon::fromTheme(QStringLiteral("arrow-up")), tr("Move &Up", "@action:button"), this);
0306         d->servUpButton->setEnabled(false);
0307         d->servUpButton->show();
0308         connect(d->servUpButton, &QAbstractButton::clicked, this, &KEditListWidget::moveItemUp);
0309 
0310         d->servDownButton = new QPushButton(QIcon::fromTheme(QStringLiteral("arrow-down")), tr("Move &Down", "@action:button"), this);
0311         d->servDownButton->setEnabled(false);
0312         d->servDownButton->show();
0313         connect(d->servDownButton, &QAbstractButton::clicked, this, &KEditListWidget::moveItemDown);
0314 
0315         d->btnsLayout->insertWidget(2, d->servUpButton);
0316         d->btnsLayout->insertWidget(3, d->servDownButton);
0317     } else if ((buttons & UpDown) == 0 && d->servUpButton) {
0318         delete d->servUpButton;
0319         d->servUpButton = nullptr;
0320         delete d->servDownButton;
0321         d->servDownButton = nullptr;
0322     }
0323 
0324     d->buttons = buttons;
0325 }
0326 
0327 void KEditListWidget::setCheckAtEntering(bool check)
0328 {
0329     d->checkAtEntering = check;
0330 }
0331 
0332 bool KEditListWidget::checkAtEntering()
0333 {
0334     return d->checkAtEntering;
0335 }
0336 
0337 void KEditListWidget::typedSomething(const QString &text)
0338 {
0339     if (currentItem() >= 0) {
0340         if (currentText() != d->lineEdit->text()) {
0341             // IMHO changeItem() shouldn't do anything with the value
0342             // of currentItem() ... like changing it or emitting signals ...
0343             // but TT disagree with me on this one (it's been that way since ages ... grrr)
0344             bool block = d->listView->signalsBlocked();
0345             d->listView->blockSignals(true);
0346             QModelIndex currentIndex = d->selectedIndex();
0347             if (currentIndex.isValid()) {
0348                 d->model->setData(currentIndex, text);
0349             }
0350             d->listView->blockSignals(block);
0351             Q_EMIT changed();
0352         }
0353     }
0354 
0355     if (!d->servNewButton) {
0356         return;
0357     }
0358 
0359     if (!d->lineEdit->hasAcceptableInput()) {
0360         d->servNewButton->setEnabled(false);
0361         return;
0362     }
0363 
0364     if (!d->checkAtEntering) {
0365         d->servNewButton->setEnabled(!text.isEmpty());
0366     } else {
0367         if (text.isEmpty()) {
0368             d->servNewButton->setEnabled(false);
0369         } else {
0370             QStringList list = d->model->stringList();
0371             bool enable = !list.contains(text, Qt::CaseSensitive);
0372             d->servNewButton->setEnabled(enable);
0373         }
0374     }
0375 }
0376 
0377 void KEditListWidget::moveItemUp()
0378 {
0379     if (!d->listView->isEnabled()) {
0380         QApplication::beep();
0381         return;
0382     }
0383 
0384     QModelIndex index = d->selectedIndex();
0385     if (index.isValid()) {
0386         if (index.row() == 0) {
0387             QApplication::beep();
0388             return;
0389         }
0390 
0391         QModelIndex aboveIndex = d->model->index(index.row() - 1, index.column());
0392 
0393         QString tmp = d->model->data(aboveIndex, Qt::DisplayRole).toString();
0394         d->model->setData(aboveIndex, d->model->data(index, Qt::DisplayRole));
0395         d->model->setData(index, tmp);
0396 
0397         d->listView->selectionModel()->select(index, QItemSelectionModel::Deselect);
0398         d->listView->selectionModel()->select(aboveIndex, QItemSelectionModel::Select);
0399     }
0400 
0401     Q_EMIT changed();
0402 }
0403 
0404 void KEditListWidget::moveItemDown()
0405 {
0406     if (!d->listView->isEnabled()) {
0407         QApplication::beep();
0408         return;
0409     }
0410 
0411     QModelIndex index = d->selectedIndex();
0412     if (index.isValid()) {
0413         if (index.row() == d->model->rowCount() - 1) {
0414             QApplication::beep();
0415             return;
0416         }
0417 
0418         QModelIndex belowIndex = d->model->index(index.row() + 1, index.column());
0419 
0420         QString tmp = d->model->data(belowIndex, Qt::DisplayRole).toString();
0421         d->model->setData(belowIndex, d->model->data(index, Qt::DisplayRole));
0422         d->model->setData(index, tmp);
0423 
0424         d->listView->selectionModel()->select(index, QItemSelectionModel::Deselect);
0425         d->listView->selectionModel()->select(belowIndex, QItemSelectionModel::Select);
0426     }
0427 
0428     Q_EMIT changed();
0429 }
0430 
0431 void KEditListWidget::addItem()
0432 {
0433     // when checkAtEntering is true, the add-button is disabled, but this
0434     // slot can still be called through Key_Return/Key_Enter. So we guard
0435     // against this.
0436     if (!d->servNewButton || !d->servNewButton->isEnabled()) {
0437         return;
0438     }
0439 
0440     QModelIndex currentIndex = d->selectedIndex();
0441 
0442     const QString &currentTextLE = d->lineEdit->text();
0443     bool alreadyInList(false);
0444     // if we didn't check for dupes at the inserting we have to do it now
0445     if (!d->checkAtEntering) {
0446         // first check current item instead of dumb iterating the entire list
0447         if (currentIndex.isValid()) {
0448             if (d->model->data(currentIndex, Qt::DisplayRole).toString() == currentTextLE) {
0449                 alreadyInList = true;
0450             }
0451         } else {
0452             alreadyInList = d->model->stringList().contains(currentTextLE, Qt::CaseSensitive);
0453         }
0454     }
0455     if (d->servNewButton) {
0456         // prevent losing the focus by it being moved outside of this widget
0457         // as well as support the user workflow a little by moving the focus
0458         // to the lineedit. chances are that users will add some items consecutively,
0459         // so this will save a manual focus change, and it is also consistent
0460         // to what happens on the click on the Remove button
0461         if (d->servNewButton->hasFocus()) {
0462             d->lineEdit->setFocus(Qt::OtherFocusReason);
0463         }
0464         d->servNewButton->setEnabled(false);
0465     }
0466 
0467     bool block = d->lineEdit->signalsBlocked();
0468     d->lineEdit->blockSignals(true);
0469     d->lineEdit->clear();
0470     d->lineEdit->blockSignals(block);
0471 
0472     d->listView->selectionModel()->setCurrentIndex(currentIndex, QItemSelectionModel::Deselect);
0473 
0474     if (!alreadyInList) {
0475         block = d->listView->signalsBlocked();
0476 
0477         if (currentIndex.isValid()) {
0478             d->model->setData(currentIndex, currentTextLE);
0479         } else {
0480             QStringList lst;
0481             lst << currentTextLE;
0482             lst << d->model->stringList();
0483             d->model->setStringList(lst);
0484         }
0485         Q_EMIT changed();
0486         Q_EMIT added(currentTextLE); // TODO: pass the index too
0487     }
0488 
0489     d->updateButtonState();
0490 }
0491 
0492 int KEditListWidget::currentItem() const
0493 {
0494     QModelIndex selectedIndex = d->selectedIndex();
0495     if (selectedIndex.isValid()) {
0496         return selectedIndex.row();
0497     } else {
0498         return -1;
0499     }
0500 }
0501 
0502 void KEditListWidget::removeItem()
0503 {
0504     QModelIndex currentIndex = d->selectedIndex();
0505     if (!currentIndex.isValid()) {
0506         return;
0507     }
0508 
0509     if (currentIndex.row() >= 0) {
0510         // prevent losing the focus by it being moved outside of this widget
0511         // as well as support the user workflow a little by moving the focus
0512         // to the lineedit. chances are that users will add some item next,
0513         // so this will save a manual focus change,
0514         if (d->servRemoveButton && d->servRemoveButton->hasFocus()) {
0515             d->lineEdit->setFocus(Qt::OtherFocusReason);
0516         }
0517 
0518         QString removedText = d->model->data(currentIndex, Qt::DisplayRole).toString();
0519 
0520         d->model->removeRows(currentIndex.row(), 1);
0521 
0522         d->listView->selectionModel()->clear();
0523 
0524         Q_EMIT changed();
0525 
0526         Q_EMIT removed(removedText);
0527     }
0528 
0529     d->updateButtonState();
0530 }
0531 
0532 void KEditListWidget::enableMoveButtons(const QModelIndex &newIndex, const QModelIndex &)
0533 {
0534     int index = newIndex.row();
0535 
0536     // Update the lineEdit when we select a different line.
0537     if (currentText() != d->lineEdit->text()) {
0538         d->lineEdit->setText(currentText());
0539     }
0540 
0541     bool moveEnabled = d->servUpButton && d->servDownButton;
0542 
0543     if (moveEnabled) {
0544         if (d->model->rowCount() <= 1) {
0545             d->servUpButton->setEnabled(false);
0546             d->servDownButton->setEnabled(false);
0547         } else if (index == (d->model->rowCount() - 1)) {
0548             d->servUpButton->setEnabled(true);
0549             d->servDownButton->setEnabled(false);
0550         } else if (index == 0) {
0551             d->servUpButton->setEnabled(false);
0552             d->servDownButton->setEnabled(true);
0553         } else {
0554             d->servUpButton->setEnabled(true);
0555             d->servDownButton->setEnabled(true);
0556         }
0557     }
0558 
0559     if (d->servRemoveButton) {
0560         d->servRemoveButton->setEnabled(true);
0561     }
0562 }
0563 
0564 void KEditListWidget::clear()
0565 {
0566     d->lineEdit->clear();
0567     d->model->setStringList(QStringList());
0568     Q_EMIT changed();
0569 }
0570 
0571 void KEditListWidget::insertStringList(const QStringList &list, int index)
0572 {
0573     QStringList content = d->model->stringList();
0574     if (index < 0) {
0575         content += list;
0576     } else {
0577         for (int i = 0, j = index; i < list.count(); ++i, ++j) {
0578             content.insert(j, list[i]);
0579         }
0580     }
0581 
0582     d->model->setStringList(content);
0583 }
0584 
0585 void KEditListWidget::insertItem(const QString &text, int index)
0586 {
0587     QStringList list = d->model->stringList();
0588 
0589     if (index < 0) {
0590         list.append(text);
0591     } else {
0592         list.insert(index, text);
0593     }
0594 
0595     d->model->setStringList(list);
0596 }
0597 
0598 QString KEditListWidget::text(int index) const
0599 {
0600     const QStringList list = d->model->stringList();
0601 
0602     return list[index];
0603 }
0604 
0605 QString KEditListWidget::currentText() const
0606 {
0607     QModelIndex index = d->selectedIndex();
0608     if (!index.isValid()) {
0609         return QString();
0610     } else {
0611         return text(index.row());
0612     }
0613 }
0614 
0615 QStringList KEditListWidget::items() const
0616 {
0617     return d->model->stringList();
0618 }
0619 
0620 void KEditListWidget::setItems(const QStringList &items)
0621 {
0622     d->model->setStringList(items);
0623 }
0624 
0625 KEditListWidget::Buttons KEditListWidget::buttons() const
0626 {
0627     return d->buttons;
0628 }
0629 
0630 void KEditListWidget::slotSelectionChanged(const QItemSelection &, const QItemSelection &)
0631 {
0632     d->updateButtonState();
0633     QModelIndex index = d->selectedIndex();
0634     enableMoveButtons(index, QModelIndex());
0635     if (index.isValid()) {
0636         d->lineEdit->setFocus(Qt::OtherFocusReason);
0637     }
0638 }
0639 
0640 bool KEditListWidget::eventFilter(QObject *o, QEvent *e)
0641 {
0642     if (o == d->lineEdit && e->type() == QEvent::KeyPress) {
0643         QKeyEvent *keyEvent = (QKeyEvent *)e;
0644         if (keyEvent->key() == Qt::Key_Down || keyEvent->key() == Qt::Key_Up) {
0645             return ((QObject *)d->listView)->event(e);
0646         } else if (keyEvent->key() == Qt::Key_Return || keyEvent->key() == Qt::Key_Enter) {
0647             return true;
0648         }
0649     }
0650 
0651     return false;
0652 }
0653 
0654 #include "moc_keditlistwidget.cpp"