File indexing completed on 2024-05-19 15:46:52

0001 /*
0002     SPDX-FileCopyrightText: 2007 David Nolden <david.nolden.kdevelop@art-master.de>
0003 
0004     SPDX-License-Identifier: LGPL-2.0-or-later
0005 */
0006 
0007 #include "expandingwidgetmodel.h"
0008 
0009 #include <QTreeView>
0010 #include <QAbstractProxyModel>
0011 #include <QModelIndex>
0012 #include <QBrush>
0013 #include <QApplication>
0014 
0015 #include <KTextEditor/CodeCompletionModel>
0016 #include <KTextEdit>
0017 #include <KColorUtils>
0018 
0019 #include "expandingdelegate.h"
0020 #include <debug.h>
0021 
0022 using namespace KTextEditor;
0023 
0024 inline QModelIndex firstColumn(const QModelIndex& index)
0025 {
0026     return index.sibling(index.row(), 0);
0027 }
0028 
0029 ExpandingWidgetModel::ExpandingWidgetModel(QWidget* parent) :
0030     QAbstractTableModel(parent)
0031 {
0032 }
0033 
0034 ExpandingWidgetModel::~ExpandingWidgetModel()
0035 {
0036     clearExpanding();
0037 }
0038 
0039 static QColor doAlternate(const QColor& color)
0040 {
0041     QColor background = QApplication::palette().window().color();
0042     return KColorUtils::mix(color, background, 0.15);
0043 }
0044 
0045 uint ExpandingWidgetModel::matchColor(const QModelIndex& index) const
0046 {
0047     int matchQuality = contextMatchQuality(index.sibling(index.row(), 0));
0048 
0049     if (matchQuality > 0) {
0050         bool alternate = index.row() & 1;
0051 
0052         QColor badMatchColor(0xff00aa44); //Blue-ish green
0053         QColor goodMatchColor(0xff00ff00); //Green
0054 
0055         QColor background = treeView()->palette().light().color();
0056 
0057         QColor totalColor = KColorUtils::mix(badMatchColor, goodMatchColor, (( float )matchQuality) / 10.0);
0058 
0059         if (alternate) {
0060             totalColor = doAlternate(totalColor);
0061         }
0062 
0063         const float dynamicTint = 0.2f;
0064         const float minimumTint = 0.2f;
0065         double tintStrength = (dynamicTint * matchQuality) / 10;
0066         if (tintStrength) {
0067             tintStrength += minimumTint; //Some minimum tinting strength, else it's not visible any more
0068         }
0069         return KColorUtils::tint(background, totalColor, tintStrength).rgb();
0070     } else {
0071         return 0;
0072     }
0073 }
0074 
0075 QVariant ExpandingWidgetModel::data(const QModelIndex& index, int role) const
0076 {
0077     switch (role) {
0078     case Qt::BackgroundRole:
0079     {
0080         if (index.column() == 0) {
0081             //Highlight by match-quality
0082             uint color = matchColor(index);
0083             if (color) {
0084                 return QBrush(color);
0085             }
0086         }
0087         //Use a special background-color for expanded items
0088         if (isExpanded(index)) {
0089             if (index.row() & 1) {
0090                 return doAlternate(treeView()->palette().toolTipBase().color());
0091             } else {
0092                 return treeView()->palette().toolTipBase();
0093             }
0094         }
0095     }
0096     }
0097     return QVariant();
0098 }
0099 
0100 QModelIndex ExpandingWidgetModel::mapFromSource(const QModelIndex& index) const
0101 {
0102     const auto proxyModel = qobject_cast<QAbstractProxyModel*>(treeView()->model());
0103     Q_ASSERT(proxyModel);
0104     Q_ASSERT(!index.isValid() || index.model() == this);
0105     return proxyModel->mapFromSource(index);
0106 }
0107 
0108 QModelIndex ExpandingWidgetModel::mapToSource(const QModelIndex& index) const
0109 {
0110     const auto proxyModel = qobject_cast<QAbstractProxyModel*>(treeView()->model());
0111     Q_ASSERT(proxyModel);
0112     Q_ASSERT(!index.isValid() || index.model() == proxyModel);
0113     return proxyModel->mapToSource(index);
0114 }
0115 
0116 void ExpandingWidgetModel::clearMatchQualities()
0117 {
0118     m_contextMatchQualities.clear();
0119 }
0120 
0121 QModelIndex ExpandingWidgetModel::partiallyExpandedRow() const
0122 {
0123     if (m_partiallyExpanded.isEmpty()) {
0124         return QModelIndex();
0125     } else {
0126         return m_partiallyExpanded.constBegin().key();
0127     }
0128 }
0129 
0130 void ExpandingWidgetModel::clearExpanding()
0131 {
0132     clearMatchQualities();
0133     QMap<QModelIndex, ExpandingWidgetModel::ExpandingType> oldExpandState = m_expandState;
0134     for (auto& widget : qAsConst(m_expandingWidgets)) {
0135         delete widget;
0136     }
0137 
0138     m_expandingWidgets.clear();
0139     m_expandState.clear();
0140     m_partiallyExpanded.clear();
0141 
0142     for (QMap<QModelIndex, ExpandingWidgetModel::ExpandingType>::const_iterator it = oldExpandState.constBegin(); it != oldExpandState.constEnd(); ++it) {
0143         if (it.value() == Expanded) {
0144             emit dataChanged(it.key(), it.key());
0145         }
0146     }
0147 }
0148 
0149 ExpandingWidgetModel::ExpansionType ExpandingWidgetModel::isPartiallyExpanded(const QModelIndex& index) const
0150 {
0151     const auto expansionIt = m_partiallyExpanded.find(firstColumn(index));
0152     if (expansionIt != m_partiallyExpanded.end()) {
0153         return *expansionIt;
0154     } else {
0155         return NotExpanded;
0156     }
0157 }
0158 
0159 void ExpandingWidgetModel::partiallyUnExpand(const QModelIndex& idx_)
0160 {
0161     QModelIndex index(firstColumn(idx_));
0162     m_partiallyExpanded.remove(index);
0163     m_partiallyExpanded.remove(idx_);
0164 }
0165 
0166 int ExpandingWidgetModel::partiallyExpandWidgetHeight() const
0167 {
0168     return 60; ///@todo use font-metrics text-height*2 for 2 lines
0169 }
0170 
0171 void ExpandingWidgetModel::rowSelected(const QModelIndex& idx_)
0172 {
0173     Q_ASSERT(idx_.model() == this);
0174 
0175     QModelIndex idx(firstColumn(idx_));
0176     if (!m_partiallyExpanded.contains(idx)) {
0177         QModelIndex oldIndex = partiallyExpandedRow();
0178         //Unexpand the previous partially expanded row
0179         if (!m_partiallyExpanded.isEmpty()) { ///@todo allow multiple partially expanded rows
0180             while (!m_partiallyExpanded.isEmpty())
0181                 m_partiallyExpanded.erase(m_partiallyExpanded.begin());
0182             //partiallyUnExpand( m_partiallyExpanded.begin().key() );
0183         }
0184         //Notify the underlying models that the item was selected, and eventually get back the text for the expanding widget.
0185         if (!idx.isValid()) {
0186             //All items have been unselected
0187             if (oldIndex.isValid()) {
0188                 emit dataChanged(oldIndex, oldIndex);
0189             }
0190         } else {
0191             QVariant variant = data(idx, CodeCompletionModel::ItemSelected);
0192 
0193             if (!isExpanded(idx) && variant.type() == QVariant::String) {
0194                 //Either expand upwards or downwards, choose in a way that
0195                 //the visible fields of the new selected entry are not moved.
0196                 if (oldIndex.isValid() && (oldIndex < idx || (!(oldIndex < idx) && oldIndex.parent() < idx.parent()))) {
0197                     m_partiallyExpanded.insert(idx, ExpandUpwards);
0198                 } else {
0199                     m_partiallyExpanded.insert(idx, ExpandDownwards);
0200                 }
0201 
0202                 //Say that one row above until one row below has changed, so no items will need to be moved(the space that is taken from one item is given to the other)
0203                 if (oldIndex.isValid() && oldIndex < idx) {
0204                     emit dataChanged(oldIndex, idx);
0205 
0206                     if (treeView()->verticalScrollMode() == QAbstractItemView::ScrollPerItem) {
0207                         const QModelIndex viewIndex = mapFromSource(idx);
0208                         //Qt fails to correctly scroll in ScrollPerItem mode, so the selected index is completely visible,
0209                         //so we do the scrolling by hand.
0210                         QRect selectedRect = treeView()->visualRect(viewIndex);
0211                         QRect frameRect = treeView()->frameRect();
0212 
0213                         if (selectedRect.bottom() > frameRect.bottom()) {
0214                             int diff = selectedRect.bottom() - frameRect.bottom();
0215                             //We need to scroll down
0216                             QModelIndex newTopIndex = viewIndex;
0217 
0218                             QModelIndex nextTopIndex = viewIndex;
0219                             QRect nextRect = treeView()->visualRect(nextTopIndex);
0220                             while (nextTopIndex.isValid() && nextRect.isValid() && nextRect.top() >= diff) {
0221                                 newTopIndex = nextTopIndex;
0222                                 nextTopIndex = treeView()->indexAbove(nextTopIndex);
0223                                 if (nextTopIndex.isValid()) {
0224                                     nextRect = treeView()->visualRect(nextTopIndex);
0225                                 }
0226                             }
0227                             treeView()->scrollTo(newTopIndex, QAbstractItemView::PositionAtTop);
0228                         }
0229                     }
0230 
0231                     //This is needed to keep the item we are expanding completely visible. Qt does not scroll the view to keep the item visible.
0232                     //But we must make sure that it isn't too expensive.
0233                     //We need to make sure that scrolling is efficient, and the whole content is not repainted.
0234                     //Since we are scrolling anyway, we can keep the next line visible, which might be a cool feature.
0235 
0236                     //Since this also doesn't work smoothly, leave it for now
0237                     //treeView()->scrollTo( nextLine, QAbstractItemView::EnsureVisible );
0238                 } else if (oldIndex.isValid() &&  idx < oldIndex) {
0239                     emit dataChanged(idx, oldIndex);
0240 
0241                     //For consistency with the down-scrolling, we keep one additional line visible above the current visible.
0242 
0243                     //Since this also doesn't work smoothly, leave it for now
0244 /*            QModelIndex prevLine = idx.sibling(idx.row()-1, idx.column());
0245             if( prevLine.isValid() )
0246                 treeView()->scrollTo( prevLine );*/
0247                 } else {
0248                     emit dataChanged(idx, idx);
0249                 }
0250             } else if (oldIndex.isValid()) {
0251                 //We are not partially expanding a new row, but we previously had a partially expanded row. So signalize that it has been unexpanded.
0252 
0253                 emit dataChanged(oldIndex, oldIndex);
0254             }
0255         }
0256     } else {
0257         qCDebug(PLUGIN_QUICKOPEN) << "ExpandingWidgetModel::rowSelected: Row is already partially expanded";
0258     }
0259 }
0260 
0261 QString ExpandingWidgetModel::partialExpandText(const QModelIndex& idx) const
0262 {
0263     Q_ASSERT(idx.model() == this);
0264 
0265     if (!idx.isValid()) {
0266         return QString();
0267     }
0268 
0269     return data(firstColumn(idx), CodeCompletionModel::ItemSelected).toString();
0270 }
0271 
0272 QRect ExpandingWidgetModel::partialExpandRect(const QModelIndex& idx_) const
0273 {
0274     Q_ASSERT(idx_.model() == this);
0275 
0276     QModelIndex idx(firstColumn(idx_));
0277 
0278     if (!idx.isValid()) {
0279         return QRect();
0280     }
0281 
0282     ExpansionType expansion = ExpandDownwards;
0283 
0284     if (m_partiallyExpanded.find(idx) != m_partiallyExpanded.constEnd()) {
0285         expansion = m_partiallyExpanded[idx];
0286     }
0287 
0288     //Get the whole rectangle of the row:
0289     const QModelIndex viewIndex = mapFromSource(idx);
0290     QModelIndex rightMostIndex = viewIndex;
0291     QModelIndex tempIndex = viewIndex;
0292     while ((tempIndex = rightMostIndex.sibling(rightMostIndex.row(), rightMostIndex.column() + 1)).isValid())
0293         rightMostIndex = tempIndex;
0294 
0295     QRect rect = treeView()->visualRect(viewIndex);
0296     QRect rightMostRect = treeView()->visualRect(rightMostIndex);
0297 
0298     rect.setLeft(rect.left() + 20);
0299     rect.setRight(rightMostRect.right() - 5);
0300 
0301     //These offsets must match exactly those used in ExpandingDelegate::sizeHint()
0302     int top = rect.top() + 5;
0303     int bottom = rightMostRect.bottom() - 5;
0304 
0305     if (expansion == ExpandDownwards) {
0306         top += basicRowHeight(viewIndex);
0307     } else {
0308         bottom -= basicRowHeight(viewIndex);
0309     }
0310 
0311     rect.setTop(top);
0312     rect.setBottom(bottom);
0313 
0314     return rect;
0315 }
0316 
0317 bool ExpandingWidgetModel::isExpandable(const QModelIndex& idx_) const
0318 {
0319     Q_ASSERT(idx_.model() == this);
0320 
0321     QModelIndex idx(firstColumn(idx_));
0322 
0323     auto expandStateIt = m_expandState.find(idx);
0324     if (expandStateIt == m_expandState.end()) {
0325         expandStateIt = m_expandState.insert(idx, NotExpandable);
0326         QVariant v = data(idx, CodeCompletionModel::IsExpandable);
0327         if (v.canConvert<bool>() && v.toBool()) {
0328             *expandStateIt = Expandable;
0329         }
0330     }
0331 
0332     return *expandStateIt != NotExpandable;
0333 }
0334 
0335 bool ExpandingWidgetModel::isExpanded(const QModelIndex& idx_) const
0336 {
0337     Q_ASSERT(idx_.model() == this);
0338 
0339     QModelIndex idx(firstColumn(idx_));
0340     return m_expandState.contains(idx) && m_expandState[idx] == Expanded;
0341 }
0342 
0343 void ExpandingWidgetModel::setExpanded(const QModelIndex& idx_, bool expanded)
0344 {
0345     Q_ASSERT(idx_.model() == this);
0346 
0347     QModelIndex idx(firstColumn(idx_));
0348 
0349     qCDebug(PLUGIN_QUICKOPEN) << "Setting expand-state of row " << idx.row() << " to " << expanded;
0350     if (!idx.isValid()) {
0351         return;
0352     }
0353 
0354     if (isExpandable(idx)) {
0355         if (!expanded && m_expandingWidgets.contains(idx) && m_expandingWidgets[idx]) {
0356             m_expandingWidgets[idx]->hide();
0357         }
0358 
0359         m_expandState[idx] = expanded ? Expanded : Expandable;
0360 
0361         if (expanded) {
0362             partiallyUnExpand(idx);
0363         }
0364 
0365         if (expanded && !m_expandingWidgets.contains(idx)) {
0366             QVariant v = data(idx, CodeCompletionModel::ExpandingWidget);
0367 
0368             if (v.canConvert<QWidget*>()) {
0369                 m_expandingWidgets[idx] = v.value<QWidget*>();
0370             } else if (v.canConvert<QString>()) {
0371                 //Create a html widget that shows the given string
0372                 auto* edit = new KTextEdit(v.toString());
0373                 edit->setReadOnly(true);
0374                 edit->resize(200, 50); //Make the widget small so it embeds nicely.
0375                 m_expandingWidgets[idx] = edit;
0376             } else {
0377                 m_expandingWidgets[idx] = nullptr;
0378             }
0379         }
0380 
0381         //Eventually partially expand the row
0382         if (!expanded && firstColumn(mapToSource(treeView()->currentIndex())) == idx && (isPartiallyExpanded(idx) == ExpandingWidgetModel::ExpansionType::NotExpanded)) {
0383             rowSelected(idx); //Partially expand the row.
0384         }
0385         emit dataChanged(idx, idx);
0386 
0387         if (treeView()) {
0388             treeView()->scrollTo(mapFromSource(idx));
0389         }
0390     }
0391 }
0392 
0393 int ExpandingWidgetModel::basicRowHeight(const QModelIndex& idx_) const
0394 {
0395     Q_ASSERT(idx_.model() == treeView()->model());
0396 
0397     QModelIndex idx(firstColumn(idx_));
0398 
0399     auto* delegate = qobject_cast<ExpandingDelegate*>(treeView()->itemDelegate(idx));
0400     if (!delegate || !idx.isValid()) {
0401         qCDebug(PLUGIN_QUICKOPEN) << "ExpandingWidgetModel::basicRowHeight: Could not get delegate";
0402         return 15;
0403     }
0404     return delegate->basicSizeHint(idx).height();
0405 }
0406 
0407 void ExpandingWidgetModel::placeExpandingWidget(const QModelIndex& idx_)
0408 {
0409     Q_ASSERT(idx_.model() == this);
0410 
0411     QModelIndex idx(firstColumn(idx_));
0412 
0413     QWidget* w = nullptr;
0414     const auto widgetIt = m_expandingWidgets.constFind(idx);
0415     if (widgetIt != m_expandingWidgets.constEnd()) {
0416         w = *widgetIt;
0417     }
0418 
0419     if (w && isExpanded(idx)) {
0420         if (!idx.isValid()) {
0421             return;
0422         }
0423 
0424         const QModelIndex viewIndex = mapFromSource(idx_);
0425         QRect rect = treeView()->visualRect(viewIndex);
0426 
0427         if (!rect.isValid() || rect.bottom() < 0 || rect.top() >= treeView()->height()) {
0428             //The item is currently not visible
0429             w->hide();
0430             return;
0431         }
0432 
0433         QModelIndex rightMostIndex = viewIndex;
0434         QModelIndex tempIndex = viewIndex;
0435         while ((tempIndex = rightMostIndex.sibling(rightMostIndex.row(), rightMostIndex.column() + 1)).isValid())
0436             rightMostIndex = tempIndex;
0437 
0438         QRect rightMostRect = treeView()->visualRect(rightMostIndex);
0439 
0440         //Find out the basic height of the row
0441         rect.setLeft(rect.left() + 20);
0442         rect.setRight(rightMostRect.right() - 5);
0443 
0444         //These offsets must match exactly those used in KateCompletionDeleage::sizeHint()
0445         rect.setTop(rect.top() + basicRowHeight(viewIndex) + 5);
0446         rect.setHeight(w->height());
0447 
0448         if (w->parent() != treeView()->viewport() || w->geometry() != rect || !w->isVisible()) {
0449             w->setParent(treeView()->viewport());
0450 
0451             w->setGeometry(rect);
0452             w->show();
0453         }
0454     }
0455 }
0456 
0457 void ExpandingWidgetModel::placeExpandingWidgets()
0458 {
0459     for (QMap<QModelIndex, QPointer<QWidget> >::const_iterator it = m_expandingWidgets.constBegin(); it != m_expandingWidgets.constEnd(); ++it) {
0460         placeExpandingWidget(it.key());
0461     }
0462 }
0463 
0464 int ExpandingWidgetModel::expandingWidgetsHeight() const
0465 {
0466     int sum = 0;
0467     for (QMap<QModelIndex, QPointer<QWidget> >::const_iterator it = m_expandingWidgets.constBegin(); it != m_expandingWidgets.constEnd(); ++it) {
0468         if (isExpanded(it.key()) && (*it)) {
0469             sum += (*it)->height();
0470         }
0471     }
0472 
0473     return sum;
0474 }
0475 
0476 QWidget* ExpandingWidgetModel::expandingWidget(const QModelIndex& idx_) const
0477 {
0478     QModelIndex idx(firstColumn(idx_));
0479 
0480     const auto widgetIt = m_expandingWidgets.find(idx);
0481     if (widgetIt != m_expandingWidgets.end()) {
0482         return *widgetIt;
0483     } else {
0484         return nullptr;
0485     }
0486 }
0487 
0488 QList<QVariant> mergeCustomHighlighting(int leftSize, const QList<QVariant>& left, int rightSize, const QList<QVariant>& right)
0489 {
0490     QList<QVariant> ret = left;
0491     if (left.isEmpty()) {
0492         ret << QVariant(0);
0493         ret << QVariant(leftSize);
0494         ret << QTextFormat(QTextFormat::CharFormat);
0495     }
0496 
0497     if (right.isEmpty()) {
0498         ret << QVariant(leftSize);
0499         ret << QVariant(rightSize);
0500         ret << QTextFormat(QTextFormat::CharFormat);
0501     } else {
0502         QList<QVariant>::const_iterator it = right.constBegin();
0503         while (it != right.constEnd()) {
0504             {
0505                 QList<QVariant>::const_iterator testIt = it;
0506                 for (int a = 0; a < 2; a++) {
0507                     ++testIt;
0508                     if (testIt == right.constEnd()) {
0509                         qCWarning(PLUGIN_QUICKOPEN) << "Length of input is not multiple of 3";
0510                         break;
0511                     }
0512                 }
0513             }
0514 
0515             ret << QVariant((*it).toInt() + leftSize);
0516             ++it;
0517             ret << QVariant((*it).toInt());
0518             ++it;
0519             ret << *it;
0520             if (!(*it).value<QTextFormat>().isValid()) {
0521                 qCDebug(PLUGIN_QUICKOPEN) << "Text-format is invalid";
0522             }
0523             ++it;
0524         }
0525     }
0526     return ret;
0527 }
0528 
0529 //It is assumed that between each two strings, one space is inserted
0530 QList<QVariant> mergeCustomHighlighting(const QStringList& strings_, const QList<QVariantList>& highlights_, int grapBetweenStrings)
0531 {
0532     QStringList strings(strings_);
0533     QList<QVariantList> highlights(highlights_);
0534 
0535     if (strings.isEmpty()) {
0536         qCWarning(PLUGIN_QUICKOPEN) << "List of strings is empty";
0537         return QList<QVariant>();
0538     }
0539 
0540     if (highlights.isEmpty()) {
0541         qCWarning(PLUGIN_QUICKOPEN) << "List of highlightings is empty";
0542         return QList<QVariant>();
0543     }
0544 
0545     if (strings.count() != highlights.count()) {
0546         qCWarning(PLUGIN_QUICKOPEN) << "Length of string-list is " << strings.count() << " while count of highlightings is " << highlights.count() << ", should be same";
0547         return QList<QVariant>();
0548     }
0549 
0550     //Merge them together
0551     QString totalString = strings[0];
0552     QVariantList totalHighlighting = highlights[0];
0553 
0554     strings.pop_front();
0555     highlights.pop_front();
0556 
0557     while (!strings.isEmpty()) {
0558         const int stringLength = strings[0].length();
0559         totalHighlighting = mergeCustomHighlighting(totalString.length(), totalHighlighting, stringLength, highlights[0]);
0560         totalString.reserve(totalString.size() + stringLength + grapBetweenStrings);
0561         totalString += strings[0];
0562 
0563         for (int a = 0; a < grapBetweenStrings; a++) {
0564             totalString += QLatin1Char(' ');
0565         }
0566 
0567         strings.pop_front();
0568         highlights.pop_front();
0569     }
0570     //Combine the custom-highlightings
0571     return totalHighlighting;
0572 }
0573 
0574 #include "moc_expandingwidgetmodel.cpp"