File indexing completed on 2024-05-12 04:40:57
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"