File indexing completed on 2024-05-12 11:57:35

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 <QApplication>
0010 #include <QBrush>
0011 #include <QModelIndex>
0012 #include <QTreeView>
0013 
0014 #include <KColorUtils>
0015 #include <KTextEdit>
0016 #include <ktexteditor/codecompletionmodel.h>
0017 
0018 #include "katecompletiondelegate.h"
0019 #include "katepartdebug.h"
0020 
0021 using namespace KTextEditor;
0022 
0023 inline QModelIndex firstColumn(const QModelIndex &index)
0024 {
0025     return index.sibling(index.row(), 0);
0026 }
0027 
0028 ExpandingWidgetModel::ExpandingWidgetModel(QWidget *parent)
0029     : QAbstractItemModel(parent)
0030 {
0031 }
0032 
0033 ExpandingWidgetModel::~ExpandingWidgetModel()
0034 {
0035     clearExpanding();
0036 }
0037 
0038 static QColor doAlternate(const QColor &color)
0039 {
0040     QColor background = QApplication::palette().window().color();
0041     return KColorUtils::mix(color, background, 0.15);
0042 }
0043 
0044 uint ExpandingWidgetModel::matchColor(const QModelIndex &index) const
0045 {
0046     int matchQuality = contextMatchQuality(index.sibling(index.row(), 0));
0047 
0048     if (matchQuality > 0) {
0049         bool alternate = index.row() & 1;
0050 
0051         QColor badMatchColor(0xff00aa44); // Blue-ish green
0052         QColor goodMatchColor(0xff00ff00); // Green
0053 
0054         QColor background = treeView()->palette().light().color();
0055 
0056         QColor totalColor = KColorUtils::mix(badMatchColor, goodMatchColor, ((float)matchQuality) / 10.0);
0057 
0058         if (alternate) {
0059             totalColor = doAlternate(totalColor);
0060         }
0061 
0062         const qreal dynamicTint = 0.2;
0063         const qreal minimumTint = 0.2;
0064         qreal tintStrength = (dynamicTint * matchQuality) / 10;
0065         if (tintStrength != 0.0) {
0066             tintStrength += minimumTint; // Some minimum tinting strength, else it's not visible any more
0067         }
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         if (index.column() == 0) {
0080             // Highlight by match-quality
0081             uint color = matchColor(index);
0082             if (color) {
0083                 return QBrush(color);
0084             }
0085         }
0086         // Use a special background-color for expanded items
0087         if (isExpanded(index)) {
0088             if (index.row() & 1) {
0089                 return doAlternate(treeView()->palette().toolTipBase().color());
0090             } else {
0091                 return treeView()->palette().toolTipBase();
0092             }
0093         }
0094     }
0095     }
0096     return QVariant();
0097 }
0098 
0099 void ExpandingWidgetModel::clearExpanding()
0100 {
0101     QMap<QModelIndex, ExpandingWidgetModel::ExpandingType> oldExpandState = m_expandState;
0102     for (auto &widget : std::as_const(m_expandingWidgets)) {
0103         if (widget) {
0104             widget->deleteLater(); // By using deleteLater, we prevent crashes when an action within a widget makes the completion cancel
0105         }
0106     }
0107     m_expandingWidgets.clear();
0108     m_expandState.clear();
0109 
0110     for (auto it = oldExpandState.constBegin(); it != oldExpandState.constEnd(); ++it) {
0111         if (it.value() == Expanded) {
0112             Q_EMIT dataChanged(it.key(), it.key());
0113         }
0114     }
0115 }
0116 
0117 bool ExpandingWidgetModel::isExpandable(const QModelIndex &idx_) const
0118 {
0119     QModelIndex idx(firstColumn(idx_));
0120 
0121     if (!m_expandState.contains(idx)) {
0122         m_expandState.insert(idx, NotExpandable);
0123         QVariant v = data(idx, CodeCompletionModel::IsExpandable);
0124         if (v.canConvert<bool>() && v.toBool()) {
0125             m_expandState[idx] = Expandable;
0126         }
0127     }
0128 
0129     return m_expandState[idx] != NotExpandable;
0130 }
0131 
0132 bool ExpandingWidgetModel::isExpanded(const QModelIndex &idx_) const
0133 {
0134     QModelIndex idx(firstColumn(idx_));
0135     return m_expandState.contains(idx) && m_expandState[idx] == Expanded;
0136 }
0137 
0138 void ExpandingWidgetModel::setExpanded(QModelIndex idx_, bool expanded)
0139 {
0140     QModelIndex idx(firstColumn(idx_));
0141 
0142     // qCDebug(LOG_KTE) << "Setting expand-state of row " << idx.row() << " to " << expanded;
0143     if (!idx.isValid()) {
0144         return;
0145     }
0146 
0147     if (isExpandable(idx)) {
0148         if (!expanded && m_expandingWidgets.contains(idx) && m_expandingWidgets[idx]) {
0149             m_expandingWidgets[idx]->hide();
0150         }
0151 
0152         m_expandState[idx] = expanded ? Expanded : Expandable;
0153 
0154         if (expanded && !m_expandingWidgets.contains(idx)) {
0155             QVariant v = data(idx, CodeCompletionModel::ExpandingWidget);
0156 
0157             if (v.canConvert<QWidget *>()) {
0158                 m_expandingWidgets[idx] = v.value<QWidget *>();
0159             } else if (v.canConvert<QString>()) {
0160                 // Create a html widget that shows the given string
0161                 QTextEdit *edit = new QTextEdit(v.toString());
0162                 edit->setReadOnly(true);
0163                 edit->resize(200, 50); // Make the widget small so it embeds nicely.
0164                 m_expandingWidgets[idx] = edit;
0165             } else {
0166                 m_expandingWidgets[idx] = nullptr;
0167             }
0168         }
0169 
0170         Q_EMIT dataChanged(idx, idx);
0171 
0172         if (treeView()) {
0173             treeView()->scrollTo(idx);
0174         }
0175     }
0176 }
0177 
0178 int ExpandingWidgetModel::basicRowHeight(const QModelIndex &idx_) const
0179 {
0180     QModelIndex idx(firstColumn(idx_));
0181 
0182     auto *delegate = (KateCompletionDelegate *)treeView()->itemDelegate(idx);
0183     if (!delegate || !idx.isValid()) {
0184         qCDebug(LOG_KTE) << "ExpandingWidgetModel::basicRowHeight: Could not get delegate";
0185         return 15;
0186     }
0187     return delegate->basicSizeHint(idx).height();
0188 }
0189 
0190 void ExpandingWidgetModel::placeExpandingWidget(const QModelIndex &idx_)
0191 {
0192     QModelIndex idx(firstColumn(idx_));
0193     if (!idx.isValid() || !isExpanded(idx)) {
0194         return;
0195     }
0196 
0197     QWidget *w = m_expandingWidgets.value(idx);
0198     if (!w) {
0199         return;
0200     }
0201 
0202     QRect rect = treeView()->visualRect(idx);
0203 
0204     if (!rect.isValid() || rect.bottom() < 0 || rect.top() >= treeView()->height()) {
0205         // The item is currently not visible
0206         w->hide();
0207         return;
0208     }
0209 
0210     // Find out the basic width of the row
0211     const int numColumns = idx.model()->columnCount(idx.parent());
0212     int left = 0;
0213     for (int i = 0; i < numColumns; ++i) {
0214         auto index = idx.sibling(idx.row(), i);
0215         auto text = index.data().toString();
0216         if (!index.data(Qt::DecorationRole).isNull()) {
0217             left += 24;
0218         }
0219 
0220         if (!text.isEmpty()) {
0221             left += treeView()->visualRect(index).left();
0222             break;
0223         }
0224     }
0225     rect.setLeft(rect.left() + left);
0226 
0227     for (int i = 0; i < numColumns; ++i) {
0228         QModelIndex rightMostIndex = idx.sibling(idx.row(), i);
0229         int right = treeView()->visualRect(rightMostIndex).right();
0230         if (right > rect.right()) {
0231             rect.setRight(right);
0232         }
0233     }
0234     rect.setRight(rect.right() - 5);
0235 
0236     // These offsets must match exactly those used in KateCompletionDeleage::sizeHint()
0237     rect.setTop(rect.top() + basicRowHeight(idx));
0238     rect.setHeight(w->height());
0239 
0240     if (w->parent() != treeView()->viewport() || w->geometry() != rect || !w->isVisible()) {
0241         w->setParent(treeView()->viewport());
0242 
0243         w->setGeometry(rect);
0244         w->show();
0245     }
0246 }
0247 
0248 void ExpandingWidgetModel::placeExpandingWidgets()
0249 {
0250     for (QMap<QModelIndex, QPointer<QWidget>>::const_iterator it = m_expandingWidgets.constBegin(); it != m_expandingWidgets.constEnd(); ++it) {
0251         placeExpandingWidget(it.key());
0252     }
0253 }
0254 
0255 QWidget *ExpandingWidgetModel::expandingWidget(const QModelIndex &idx_) const
0256 {
0257     QModelIndex idx(firstColumn(idx_));
0258 
0259     if (m_expandingWidgets.contains(idx)) {
0260         return m_expandingWidgets[idx];
0261     } else {
0262         return nullptr;
0263     }
0264 }
0265 
0266 void ExpandingWidgetModel::cacheIcons() const
0267 {
0268     if (m_expandedIcon.isNull()) {
0269         m_expandedIcon = QIcon::fromTheme(QStringLiteral("arrow-down"));
0270     }
0271 
0272     if (m_collapsedIcon.isNull()) {
0273         m_collapsedIcon = QIcon::fromTheme(QStringLiteral("arrow-right"));
0274     }
0275 }
0276 
0277 QList<QVariant> mergeCustomHighlighting(int leftSize, const QList<QVariant> &left, int rightSize, const QList<QVariant> &right)
0278 {
0279     QList<QVariant> ret = left;
0280     if (left.isEmpty()) {
0281         ret << QVariant(0);
0282         ret << QVariant(leftSize);
0283         ret << QTextFormat(QTextFormat::CharFormat);
0284     }
0285 
0286     if (right.isEmpty()) {
0287         ret << QVariant(leftSize);
0288         ret << QVariant(rightSize);
0289         ret << QTextFormat(QTextFormat::CharFormat);
0290     } else {
0291         QList<QVariant>::const_iterator it = right.constBegin();
0292         while (it != right.constEnd()) {
0293             {
0294                 QList<QVariant>::const_iterator testIt = it;
0295                 for (int a = 0; a < 2; a++) {
0296                     ++testIt;
0297                     if (testIt == right.constEnd()) {
0298                         qCWarning(LOG_KTE) << "Length of input is not multiple of 3";
0299                         break;
0300                     }
0301                 }
0302             }
0303 
0304             ret << QVariant((*it).toInt() + leftSize);
0305             ++it;
0306             ret << QVariant((*it).toInt());
0307             ++it;
0308             ret << *it;
0309             if (!(*it).value<QTextFormat>().isValid()) {
0310                 qCDebug(LOG_KTE) << "Text-format is invalid";
0311             }
0312             ++it;
0313         }
0314     }
0315     return ret;
0316 }
0317 
0318 // It is assumed that between each two strings, one space is inserted
0319 QList<QVariant> mergeCustomHighlighting(QStringList strings, QList<QVariantList> highlights, int grapBetweenStrings)
0320 {
0321     if (strings.isEmpty()) {
0322         qCWarning(LOG_KTE) << "List of strings is empty";
0323         return QList<QVariant>();
0324     }
0325 
0326     if (highlights.isEmpty()) {
0327         qCWarning(LOG_KTE) << "List of highlightings is empty";
0328         return QList<QVariant>();
0329     }
0330 
0331     if (strings.count() != highlights.count()) {
0332         qCWarning(LOG_KTE) << "Length of string-list is " << strings.count() << " while count of highlightings is " << highlights.count() << ", should be same";
0333         return QList<QVariant>();
0334     }
0335 
0336     // Merge them together
0337     QString totalString = strings[0];
0338     QVariantList totalHighlighting = highlights[0];
0339 
0340     strings.pop_front();
0341     highlights.pop_front();
0342 
0343     while (!strings.isEmpty()) {
0344         totalHighlighting = mergeCustomHighlighting(totalString.length(), totalHighlighting, strings[0].length(), highlights[0]);
0345         totalString += strings[0];
0346 
0347         for (int a = 0; a < grapBetweenStrings; a++) {
0348             totalString += QLatin1Char(' ');
0349         }
0350 
0351         strings.pop_front();
0352         highlights.pop_front();
0353     }
0354     // Combine the custom-highlightings
0355     return totalHighlighting;
0356 }
0357 
0358 #include "moc_expandingwidgetmodel.cpp"