File indexing completed on 2024-05-12 03:48:30

0001 /*
0002     File                 : TreeViewComboBox.cpp
0003     Project              : LabPlot
0004     Description          : Provides a QTreeView in a QComboBox
0005     --------------------------------------------------------------------
0006     SPDX-FileCopyrightText: 2008-2023 Alexander Semke <alexander.semke@web.de>
0007     SPDX-FileCopyrightText: 2008 Tilman Benkert <thzs@gmx.net>
0008     SPDX-License-Identifier: GPL-2.0-or-later
0009 */
0010 
0011 #include "commonfrontend/widgets/TreeViewComboBox.h"
0012 #include "backend/core/AbstractAspect.h"
0013 #include "backend/core/AbstractColumn.h"
0014 #include "backend/core/AspectTreeModel.h"
0015 #include "backend/lib/macros.h"
0016 
0017 #include <QEvent>
0018 #include <QGroupBox>
0019 #include <QHeaderView>
0020 #include <QLineEdit>
0021 #include <QStylePainter>
0022 #include <QTreeView>
0023 #include <QVBoxLayout>
0024 
0025 #include <KLocalizedString>
0026 
0027 #include <cstring> // strcmp()
0028 
0029 /*!
0030     \class TreeViewComboBox
0031     \brief Provides a QTreeView in a QComboBox.
0032 
0033     \ingroup backend/widgets
0034 */
0035 TreeViewComboBox::TreeViewComboBox(QWidget* parent)
0036     : QComboBox(parent)
0037     , m_treeView(new QTreeView)
0038     , m_groupBox(new QGroupBox)
0039     , m_lineEdit(new QLineEdit) {
0040     auto* layout = new QVBoxLayout(this);
0041     layout->setContentsMargins(0, 0, 0, 0);
0042     layout->setSpacing(0);
0043 
0044     layout->addWidget(m_lineEdit);
0045     layout->addWidget(m_treeView);
0046 
0047     m_groupBox->setLayout(layout);
0048     m_groupBox->setParent(parent, Qt::Popup);
0049     m_groupBox->hide();
0050     m_groupBox->installEventFilter(this);
0051 
0052     m_treeView->header()->hide();
0053     m_treeView->setSelectionMode(QAbstractItemView::SingleSelection);
0054     m_treeView->setUniformRowHeights(true);
0055 
0056     m_lineEdit->setPlaceholderText(i18n("Search/Filter text"));
0057     m_lineEdit->setClearButtonEnabled(true);
0058     m_lineEdit->setFocus();
0059 
0060     addItem(QString());
0061     setCurrentIndex(0);
0062     setEditText(m_lineEditText);
0063 
0064     // signal activated() is platform dependent
0065     connect(m_treeView, &QTreeView::pressed, this, &TreeViewComboBox::treeViewIndexActivated);
0066     connect(m_lineEdit, &QLineEdit::textChanged, this, &TreeViewComboBox::filterChanged);
0067 }
0068 
0069 void TreeViewComboBox::setTopLevelClasses(const QList<AspectType>& list) {
0070     m_topLevelClasses = list;
0071 }
0072 
0073 void TreeViewComboBox::setHiddenAspects(const QList<const AbstractAspect*>& list) {
0074     m_hiddenAspects = list;
0075 }
0076 
0077 /*!
0078     Sets the \a model for the view to present.
0079 */
0080 void TreeViewComboBox::setModel(AspectTreeModel* model) {
0081     m_model = model;
0082     m_treeView->setModel(model);
0083 
0084     // show only the first column in the combo box
0085     for (int i = 1; i < model->columnCount(); i++)
0086         m_treeView->hideColumn(i);
0087 
0088     // Expand the complete tree in order to see everything in the first popup.
0089     m_treeView->expandAll();
0090 
0091     setEditText(m_lineEditText);
0092 }
0093 
0094 /*!
0095     Sets the current item to be the item at \a index and selects it.
0096     \sa currentIndex()
0097 */
0098 void TreeViewComboBox::setCurrentModelIndex(const QModelIndex& index) {
0099     m_treeView->setCurrentIndex(index);
0100     if (index.isValid()) {
0101         setToolTip(m_model->data(index, Qt::ToolTipRole).toString());
0102         QComboBox::setItemText(0, index.data().toString());
0103     }
0104 }
0105 
0106 /*!
0107     Returns the model index of the current item.
0108     \sa setCurrentModelIndex()
0109 */
0110 QModelIndex TreeViewComboBox::currentModelIndex() const {
0111     return m_treeView->currentIndex();
0112 }
0113 
0114 /*!
0115     Displays the tree view of items in the combobox.
0116     Triggers showTopLevelOnly() to show toplevel items only.
0117 */
0118 void TreeViewComboBox::showPopup() {
0119     if (!m_treeView->model() || !m_treeView->model()->hasChildren())
0120         return;
0121 
0122     QModelIndex root = m_treeView->model()->index(0, 0);
0123     showTopLevelOnly(root);
0124     m_groupBox->show();
0125     m_groupBox->resize(this->width(), 250);
0126     m_groupBox->move(mapToGlobal(this->rect().topLeft()));
0127 
0128     setEditText(m_lineEditText);
0129     m_lineEdit->setText(QString()); // delete the previous search string
0130     m_lineEdit->setFocus();
0131 }
0132 
0133 /*!
0134     \reimp
0135     TODO: why do I have to reimplement paintEvent. It should work
0136     also without
0137 */
0138 void TreeViewComboBox::paintEvent(QPaintEvent*) {
0139     QStylePainter painter(this);
0140     painter.setPen(palette().color(QPalette::Text));
0141     // draw the combobox frame, focusrect and selected etc.
0142     QStyleOptionComboBox opt;
0143     initStyleOption(&opt);
0144     opt.currentText = currentText(); // TODO: why it's not working when letting this away?
0145     painter.drawComplexControl(QStyle::CC_ComboBox, opt);
0146     // draw the icon and text
0147     painter.drawControl(QStyle::CE_ComboBoxLabel, opt);
0148 }
0149 
0150 void TreeViewComboBox::hidePopup() {
0151     m_groupBox->hide();
0152 }
0153 
0154 void TreeViewComboBox::useCurrentIndexText(const bool set) {
0155     m_useCurrentIndexText = set;
0156 }
0157 
0158 /*!
0159     \property QComboBox::currentText
0160     \brief the current text
0161     If the combo box is editable, the current text is the value displayed
0162     by the line edit. Otherwise, it is the value of the current item or
0163     an empty string if the combo box is empty or no current item is set.
0164     The setter setCurrentText() simply calls setEditText() if the combo box is editable.
0165     Otherwise, if there is a matching text in the list, currentIndex is set to the
0166     corresponding index.
0167     If m_useCurrentIndexText is false, the Text set with setText is used. The intention of displaying
0168     this text is to show a text in the case of removed element.
0169     \sa editable, setEditText()
0170 */
0171 QString TreeViewComboBox::currentText() const {
0172     if (lineEdit())
0173         return lineEdit()->text();
0174     else if (currentModelIndex().isValid() && m_useCurrentIndexText)
0175         return itemText(currentIndex());
0176     else if (!m_useCurrentIndexText)
0177         return m_lineEditText;
0178 
0179     return {};
0180 }
0181 
0182 void TreeViewComboBox::setText(const QString& text) {
0183     m_lineEditText = text;
0184 }
0185 
0186 void TreeViewComboBox::setInvalid(bool invalid, const QString& tooltip) {
0187     if (invalid) {
0188         SET_WARNING_PALETTE
0189 
0190         setToolTip(tooltip);
0191     } else {
0192         setPalette(qApp->palette());
0193         setToolTip(m_model->data(currentModelIndex(), Qt::ToolTipRole).toString());
0194     }
0195 }
0196 
0197 /*!
0198     Hides the non-toplevel items of the model used in the tree view.
0199 */
0200 void TreeViewComboBox::showTopLevelOnly(const QModelIndex& index) {
0201     int rows = index.model()->rowCount(index);
0202     for (int i = 0; i < rows; i++) {
0203         QModelIndex child = index.model()->index(i, 0, index);
0204         showTopLevelOnly(child);
0205         const auto* aspect = static_cast<const AbstractAspect*>(child.internalPointer());
0206         m_treeView->setRowHidden(i, index, !(isTopLevel(aspect) && !isHidden(aspect)));
0207     }
0208 }
0209 
0210 /*!
0211     catches the MouseButtonPress-event and hides the tree view on mouse clicking.
0212 */
0213 bool TreeViewComboBox::eventFilter(QObject* object, QEvent* event) {
0214     if ((object == m_groupBox) && event->type() == QEvent::MouseButtonPress) {
0215         m_groupBox->hide();
0216         this->setFocus();
0217         return true;
0218     }
0219     return QComboBox::eventFilter(object, event);
0220 }
0221 
0222 // SLOTs
0223 void TreeViewComboBox::treeViewIndexActivated(const QModelIndex& index) {
0224     if (index.internalPointer()) {
0225         QComboBox::setCurrentIndex(0);
0226         QComboBox::setItemText(0, index.data().toString());
0227         setToolTip(m_model->data(index, Qt::ToolTipRole).toString());
0228         Q_EMIT currentModelIndexChanged(index);
0229         m_groupBox->hide();
0230         return;
0231     }
0232 
0233     m_treeView->setCurrentIndex(QModelIndex());
0234     setCurrentIndex(0);
0235     QComboBox::setItemText(0, QString());
0236     Q_EMIT currentModelIndexChanged(QModelIndex());
0237     m_groupBox->hide();
0238 }
0239 
0240 void TreeViewComboBox::filterChanged(const QString& text) {
0241     QModelIndex root = m_treeView->model()->index(0, 0);
0242     filter(root, text);
0243 }
0244 
0245 bool TreeViewComboBox::filter(const QModelIndex& index, const QString& text) {
0246     bool childVisible = false;
0247     const int rows = index.model()->rowCount(index);
0248     for (int i = 0; i < rows; i++) {
0249         QModelIndex child = index.model()->index(i, 0, index);
0250         auto* aspect = static_cast<AbstractAspect*>(child.internalPointer());
0251         bool topLevel = isTopLevel(aspect);
0252         if (!topLevel)
0253             continue;
0254 
0255         bool visible = aspect->name().contains(text, Qt::CaseInsensitive);
0256 
0257         if (visible) {
0258             // current item is visible -> make all its children (allowed top level types only and not hidden) visible without applying the filter
0259             for (int j = 0; j < child.model()->rowCount(child); ++j) {
0260                 AbstractAspect* aspect = static_cast<AbstractAspect*>((child.model()->index(j, 0, child)).internalPointer());
0261                 m_treeView->setRowHidden(j, child, !(isTopLevel(aspect) && !isHidden(aspect)));
0262             }
0263 
0264             childVisible = true;
0265         } else {
0266             // check children items. if one of the children is visible, make the parent (current) item visible too.
0267             visible = filter(child, text);
0268             if (visible)
0269                 childVisible = true;
0270         }
0271 
0272         m_treeView->setRowHidden(i, index, !(visible && !isHidden(aspect)));
0273     }
0274 
0275     return childVisible;
0276 }
0277 
0278 /*!
0279     checks whether \c aspect is one of the allowed top level types
0280     and has selectable children
0281 */
0282 bool TreeViewComboBox::isTopLevel(const AbstractAspect* aspect) const {
0283     const auto& selectableTypes = m_model->selectableAspects();
0284     for (AspectType type : m_topLevelClasses) {
0285         if (aspect->type() == type) {
0286             // curent aspect is a top level aspect,
0287             // check whether its selectable
0288             if (selectableTypes.indexOf(type) != -1)
0289                 return true;
0290 
0291             // check whether the current aspect has selectable children
0292             bool hasSelectableAspects = false;
0293             for (auto selectableType : selectableTypes) {
0294                 const auto& children = aspect->children(selectableType, AbstractAspect::ChildIndexFlag::Recursive);
0295                 if (!children.isEmpty()) {
0296                     hasSelectableAspects = true;
0297                     break;
0298                 }
0299             }
0300 
0301             return hasSelectableAspects;
0302         }
0303 
0304         if (type == AspectType::XYAnalysisCurve)
0305             if (aspect->inherits(AspectType::XYAnalysisCurve))
0306                 return true;
0307     }
0308     return false;
0309 }
0310 
0311 bool TreeViewComboBox::isHidden(const AbstractAspect* aspect) const {
0312     return (m_hiddenAspects.indexOf(aspect) != -1);
0313 }
0314 
0315 void TreeViewComboBox::setAspect(const AbstractAspect* aspect) {
0316     if (aspect)
0317         setCurrentModelIndex(m_model->modelIndexOfAspect(aspect));
0318     else
0319         setCurrentModelIndex(QModelIndex());
0320 }
0321 
0322 AbstractAspect* TreeViewComboBox::currentAspect() const {
0323     return static_cast<AbstractAspect*>(currentModelIndex().internalPointer());
0324 }
0325 
0326 void TreeViewComboBox::setColumn(const AbstractColumn* column, const QString& path) {
0327     DEBUG(Q_FUNC_INFO)
0328     setAspect(column);
0329 
0330     // don't make the combobox red for initially created curves
0331     if (!column && path.isEmpty()) {
0332         setText(QString());
0333         setInvalid(false);
0334         return;
0335     }
0336 
0337     if (column) {
0338         useCurrentIndexText(true);
0339         setInvalid(false);
0340     } else {
0341         useCurrentIndexText(false);
0342         setInvalid(true, i18n("The column \"%1\"\nis not available anymore. It will be automatically used once it is created again.", path));
0343     }
0344     setText(path.split(QLatin1Char('/')).last());
0345 }