File indexing completed on 2024-11-10 04:48:57

0001 /*
0002   This file is part of libkdepim.
0003 
0004   SPDX-FileCopyrightText: 2008 Thomas Thrainer <tom_t@gmx.at>
0005   SPDX-FileCopyrightText: 2010 Bertjan Broeksema <broeksema@kde.org>
0006 
0007   SPDX-License-Identifier: GPL-2.0-or-later WITH Qt-Commercial-exception-1.0
0008 */
0009 
0010 #include "kcheckcombobox.h"
0011 
0012 #include "libkdepim_debug.h"
0013 
0014 #include <QAbstractItemView>
0015 #include <QKeyEvent>
0016 #include <QLineEdit>
0017 #include <QStandardItemModel>
0018 
0019 using namespace KPIM;
0020 
0021 /// Class KCheckComboBox::KCheckComboBoxPrivate
0022 
0023 namespace KPIM
0024 {
0025 class Q_DECL_HIDDEN KCheckComboBox::KCheckComboBoxPrivate
0026 {
0027 public:
0028     KCheckComboBoxPrivate(KCheckComboBox *qq)
0029         : mSeparator(QLatin1Char(','))
0030         , q(qq)
0031     {
0032     }
0033 
0034     void makeInsertedItemsCheckable(const QModelIndex &, int start, int end);
0035     [[nodiscard]] QString squeeze(const QString &text);
0036     void updateCheckedItems(const QModelIndex &topLeft = QModelIndex(), const QModelIndex &bottomRight = QModelIndex(), int role = Qt::DisplayRole);
0037     void toggleCheckState();
0038 
0039 public:
0040     QString mSeparator;
0041     QString mDefaultText;
0042     bool mSqueezeText = false;
0043     bool mIgnoreHide = false;
0044     bool mAlwaysShowDefaultText = false;
0045 
0046 private:
0047     KCheckComboBox *const q;
0048 };
0049 }
0050 
0051 void KCheckComboBox::KCheckComboBoxPrivate::makeInsertedItemsCheckable(const QModelIndex &parent, int start, int end)
0052 {
0053     Q_UNUSED(parent)
0054     auto model = qobject_cast<QStandardItemModel *>(q->model());
0055     if (model) {
0056         for (int r = start; r <= end; ++r) {
0057             QStandardItem *item = model->item(r, 0);
0058             item->setCheckable(true);
0059         }
0060     } else {
0061         qCWarning(LIBKDEPIM_LOG) << "KCheckComboBox: model is not a QStandardItemModel but a" << q->model() << ". Cannot proceed.";
0062     }
0063 }
0064 
0065 QString KCheckComboBox::KCheckComboBoxPrivate::squeeze(const QString &text)
0066 {
0067     QFontMetrics fm(q->fontMetrics());
0068     // The 4 pixels is 2 * horizontalMargin from QLineEdit.
0069     // The rest is code from QLineEdit::paintEvent, where it determines whether to scroll the text
0070     // (on my machine minLB=2 and minRB=2, so this removes 8 pixels in total)
0071     const int minLB = qMax(0, -fm.minLeftBearing());
0072     const int minRB = qMax(0, -fm.minRightBearing());
0073     const int lineEditWidth = q->lineEdit()->width() - 4 - minLB - minRB;
0074     const int textWidth = fm.boundingRect(text).width();
0075     if (textWidth > lineEditWidth) {
0076         return fm.elidedText(text, Qt::ElideMiddle, lineEditWidth);
0077     }
0078 
0079     return text;
0080 }
0081 
0082 void KCheckComboBox::KCheckComboBoxPrivate::updateCheckedItems(const QModelIndex &topLeft, const QModelIndex &bottomRight, int role)
0083 {
0084     Q_UNUSED(topLeft)
0085     Q_UNUSED(bottomRight)
0086 
0087     const QStringList items = q->checkedItems(role);
0088     QString text;
0089     if (items.isEmpty() || mAlwaysShowDefaultText) {
0090         text = mDefaultText;
0091     } else {
0092         text = items.join(mSeparator);
0093     }
0094 
0095     if (mSqueezeText) {
0096         text = squeeze(text);
0097     }
0098 
0099     q->lineEdit()->setText(text);
0100 
0101     Q_EMIT q->checkedItemsChanged(items);
0102 }
0103 
0104 void KCheckComboBox::KCheckComboBoxPrivate::toggleCheckState()
0105 {
0106     int selected = q->currentIndex();
0107     if (q->view()->isVisible() && q->itemEnabled(selected)) {
0108         const QModelIndex index = q->view()->currentIndex().siblingAtRow(selected);
0109         const QVariant value = index.data(Qt::CheckStateRole);
0110         if (value.isValid()) {
0111             const auto state = static_cast<Qt::CheckState>(value.toInt());
0112             q->model()->setData(index, state == Qt::Unchecked ? Qt::Checked : Qt::Unchecked, Qt::CheckStateRole);
0113         }
0114     }
0115 }
0116 
0117 /// Class KCheckComboBox
0118 
0119 KCheckComboBox::KCheckComboBox(QWidget *parent)
0120     : QComboBox(parent)
0121     , d(new KCheckComboBox::KCheckComboBoxPrivate(this))
0122 {
0123     connect(this, &QComboBox::activated, this, [this]() {
0124         d->toggleCheckState();
0125     });
0126     connect(model(), &QAbstractItemModel::rowsInserted, this, [this](const QModelIndex &index, int start, int end) {
0127         d->makeInsertedItemsCheckable(index, start, end);
0128     });
0129     connect(model(), &QAbstractItemModel::dataChanged, this, [this](const QModelIndex &topLeft, const QModelIndex &bottomRight) {
0130         d->updateCheckedItems(topLeft, bottomRight);
0131     });
0132 
0133     // read-only contents
0134     setEditable(true);
0135 
0136     lineEdit()->setAlignment(Qt::AlignLeft);
0137     connect(lineEdit(), &QLineEdit::textChanged, this, [this](const QString &text) {
0138         if (text.isEmpty()) {
0139             // Clear checked items
0140             setCheckedItems(QStringList());
0141         }
0142     });
0143 
0144     view()->installEventFilter(this);
0145     view()->viewport()->installEventFilter(this);
0146 
0147     lineEdit()->installEventFilter(this);
0148 
0149     d->updateCheckedItems();
0150 }
0151 
0152 KCheckComboBox::~KCheckComboBox() = default;
0153 
0154 void KCheckComboBox::hidePopup()
0155 {
0156     if (!d->mIgnoreHide) {
0157         QComboBox::hidePopup();
0158     }
0159     d->mIgnoreHide = false;
0160 }
0161 
0162 Qt::CheckState KCheckComboBox::itemCheckState(int index) const
0163 {
0164     return static_cast<Qt::CheckState>(itemData(index, Qt::CheckStateRole).toInt());
0165 }
0166 
0167 void KCheckComboBox::setItemCheckState(int index, Qt::CheckState state)
0168 {
0169     setItemData(index, state, Qt::CheckStateRole);
0170 }
0171 
0172 QStringList KCheckComboBox::checkedItems(int role) const
0173 {
0174     QStringList items;
0175     if (model()) {
0176         const QModelIndex index = model()->index(0, modelColumn(), rootModelIndex());
0177         const QModelIndexList indexes = model()->match(index, Qt::CheckStateRole, Qt::Checked, -1, Qt::MatchExactly);
0178         items.reserve(indexes.count());
0179         for (const QModelIndex &index : indexes) {
0180             items += index.data(role).toString();
0181         }
0182     }
0183     return items;
0184 }
0185 
0186 void KCheckComboBox::setCheckedItems(const QStringList &items, int role)
0187 {
0188     for (int r = 0; r < model()->rowCount(rootModelIndex()); ++r) {
0189         const QModelIndex indx = model()->index(r, modelColumn(), rootModelIndex());
0190 
0191         const QString text = indx.data(role).toString();
0192         const bool found = items.contains(text);
0193         model()->setData(indx, found ? Qt::Checked : Qt::Unchecked, Qt::CheckStateRole);
0194     }
0195     d->updateCheckedItems(QModelIndex(), QModelIndex(), role);
0196 }
0197 
0198 QString KCheckComboBox::defaultText() const
0199 {
0200     return d->mDefaultText;
0201 }
0202 
0203 void KCheckComboBox::setDefaultText(const QString &text)
0204 {
0205     if (d->mDefaultText != text) {
0206         d->mDefaultText = text;
0207         d->updateCheckedItems();
0208     }
0209 }
0210 
0211 bool KCheckComboBox::squeezeText() const
0212 {
0213     return d->mSqueezeText;
0214 }
0215 
0216 void KCheckComboBox::setSqueezeText(bool squeeze)
0217 {
0218     if (d->mSqueezeText != squeeze) {
0219         d->mSqueezeText = squeeze;
0220         d->updateCheckedItems();
0221     }
0222 }
0223 
0224 bool KCheckComboBox::itemEnabled(int index)
0225 {
0226     Q_ASSERT(index >= 0 && index <= count());
0227 
0228     auto itemModel = qobject_cast<QStandardItemModel *>(model());
0229     Q_ASSERT(itemModel);
0230 
0231     QStandardItem *item = itemModel->item(index, 0);
0232     return item->isEnabled();
0233 }
0234 
0235 void KCheckComboBox::setItemEnabled(int index, bool enabled)
0236 {
0237     Q_ASSERT(index >= 0 && index <= count());
0238 
0239     auto itemModel = qobject_cast<QStandardItemModel *>(model());
0240     Q_ASSERT(itemModel);
0241 
0242     QStandardItem *item = itemModel->item(index, 0);
0243     item->setEnabled(enabled);
0244 }
0245 
0246 QString KCheckComboBox::separator() const
0247 {
0248     return d->mSeparator;
0249 }
0250 
0251 void KCheckComboBox::setSeparator(const QString &separator)
0252 {
0253     if (d->mSeparator != separator) {
0254         d->mSeparator = separator;
0255         d->updateCheckedItems();
0256     }
0257 }
0258 
0259 void KCheckComboBox::keyPressEvent(QKeyEvent *event)
0260 {
0261     switch (event->key()) {
0262     case Qt::Key_Up:
0263     case Qt::Key_Down:
0264         showPopup();
0265         event->accept();
0266         break;
0267     case Qt::Key_Return:
0268     case Qt::Key_Enter:
0269     case Qt::Key_Escape:
0270         hidePopup();
0271         event->accept();
0272         break;
0273     default:
0274         break;
0275     }
0276     // don't call base class implementation, we don't need all that stuff in there
0277 }
0278 
0279 #ifndef QT_NO_WHEELEVENT
0280 void KCheckComboBox::wheelEvent(QWheelEvent *event)
0281 {
0282     // discard mouse wheel events on the combo box
0283     event->accept();
0284 }
0285 
0286 #endif
0287 
0288 void KCheckComboBox::resizeEvent(QResizeEvent *event)
0289 {
0290     QComboBox::resizeEvent(event);
0291     if (d->mSqueezeText) {
0292         d->updateCheckedItems();
0293     }
0294 }
0295 
0296 bool KCheckComboBox::eventFilter(QObject *receiver, QEvent *event)
0297 {
0298     switch (event->type()) {
0299     case QEvent::KeyPress:
0300     case QEvent::KeyRelease:
0301     case QEvent::ShortcutOverride:
0302         switch (static_cast<QKeyEvent *>(event)->key()) {
0303         case Qt::Key_Space:
0304             if (event->type() == QEvent::KeyPress && view()->isVisible()) {
0305                 d->toggleCheckState();
0306             }
0307             // Always eat the event: don't let QItemDelegate toggle the current index when the view is hidden.
0308             return true;
0309         case Qt::Key_Return:
0310         case Qt::Key_Enter:
0311         case Qt::Key_Escape:
0312             // ignore Enter keys, they would normally select items.
0313             // but we select with Space, because multiple selection is possible
0314             // we simply close the popup on Enter/Escape
0315             hidePopup();
0316             return true;
0317         }
0318         break;
0319     case QEvent::MouseButtonDblClick:
0320     case QEvent::MouseButtonPress:
0321     case QEvent::MouseButtonRelease:
0322         d->mIgnoreHide = true;
0323         if (receiver == lineEdit()) {
0324             showPopup();
0325             return true;
0326         }
0327         break;
0328     default:
0329         break;
0330     }
0331     return QComboBox::eventFilter(receiver, event);
0332 }
0333 
0334 bool KCheckComboBox::alwaysShowDefaultText() const
0335 {
0336     return d->mAlwaysShowDefaultText;
0337 }
0338 
0339 void KCheckComboBox::setAlwaysShowDefaultText(bool always)
0340 {
0341     if (always != d->mAlwaysShowDefaultText) {
0342         d->mAlwaysShowDefaultText = always;
0343         d->updateCheckedItems();
0344     }
0345 }
0346 
0347 #include "moc_kcheckcombobox.cpp"