KTextEditor

expandingwidgetmodel.cpp
1 /*
2  SPDX-FileCopyrightText: 2007 David Nolden <[email protected]>
3 
4  SPDX-License-Identifier: LGPL-2.0-or-later
5 */
6 
7 #include "expandingwidgetmodel.h"
8 
9 #include <QApplication>
10 #include <QBrush>
11 #include <QIcon>
12 #include <QModelIndex>
13 #include <QTreeView>
14 
15 #include <KColorUtils>
16 #include <KTextEdit>
17 #include <ktexteditor/codecompletionmodel.h>
18 
19 #include "expandingdelegate.h"
20 #include "katepartdebug.h"
21 
22 using namespace KTextEditor;
23 
24 inline QModelIndex firstColumn(const QModelIndex &index)
25 {
26  return index.sibling(index.row(), 0);
27 }
28 
29 ExpandingWidgetModel::ExpandingWidgetModel(QWidget *parent)
30  : QAbstractItemModel(parent)
31 {
32 }
33 
34 ExpandingWidgetModel::~ExpandingWidgetModel()
35 {
36  clearExpanding();
37 }
38 
39 static QColor doAlternate(const QColor &color)
40 {
41  QColor background = QApplication::palette().window().color();
42  return KColorUtils::mix(color, background, 0.15);
43 }
44 
46 {
47  int matchQuality = contextMatchQuality(index.sibling(index.row(), 0));
48 
49  if (matchQuality > 0) {
50  bool alternate = index.row() & 1;
51 
52  QColor badMatchColor(0xff00aa44); // Blue-ish green
53  QColor goodMatchColor(0xff00ff00); // Green
54 
55  QColor background = treeView()->palette().light().color();
56 
57  QColor totalColor = KColorUtils::mix(badMatchColor, goodMatchColor, ((float)matchQuality) / 10.0);
58 
59  if (alternate) {
60  totalColor = doAlternate(totalColor);
61  }
62 
63  const qreal dynamicTint = 0.2;
64  const qreal minimumTint = 0.2;
65  qreal tintStrength = (dynamicTint * matchQuality) / 10;
66  if (tintStrength != 0.0) {
67  tintStrength += minimumTint; // Some minimum tinting strength, else it's not visible any more
68  }
69 
70  return KColorUtils::tint(background, totalColor, tintStrength).rgb();
71  } else {
72  return 0;
73  }
74 }
75 
76 QVariant ExpandingWidgetModel::data(const QModelIndex &index, int role) const
77 {
78  switch (role) {
79  case Qt::BackgroundRole: {
80  if (index.column() == 0) {
81  // Highlight by match-quality
82  uint color = matchColor(index);
83  if (color) {
84  return QBrush(color);
85  }
86  }
87  // Use a special background-color for expanded items
88  if (isExpanded(index)) {
89  if (index.row() & 1) {
90  return doAlternate(treeView()->palette().toolTipBase().color());
91  } else {
92  return treeView()->palette().toolTipBase();
93  }
94  }
95  }
96  }
97  return QVariant();
98 }
99 
100 void ExpandingWidgetModel::clearMatchQualities()
101 {
102  m_contextMatchQualities.clear();
103 }
104 
106 {
107  if (m_partiallyExpanded.isEmpty()) {
108  return QModelIndex();
109  } else {
110  return m_partiallyExpanded.constBegin().key();
111  }
112 }
113 
115 {
116  clearMatchQualities();
117  QMap<QModelIndex, ExpandingWidgetModel::ExpandingType> oldExpandState = m_expandState;
118  for (auto &widget : qAsConst(m_expandingWidgets)) {
119  if (widget) {
120  widget->deleteLater(); // By using deleteLater, we prevent crashes when an action within a widget makes the completion cancel
121  }
122  }
123  m_expandingWidgets.clear();
124  m_expandState.clear();
125  m_partiallyExpanded.clear();
126 
127  for (auto it = oldExpandState.constBegin(); it != oldExpandState.constEnd(); ++it) {
128  if (it.value() == Expanded) {
129  emit dataChanged(it.key(), it.key());
130  }
131  }
132 }
133 
134 ExpandingWidgetModel::ExpansionType ExpandingWidgetModel::isPartiallyExpanded(const QModelIndex &index) const
135 {
136  if (m_partiallyExpanded.contains(firstColumn(index))) {
137  return m_partiallyExpanded[firstColumn(index)];
138  } else {
139  return NotExpanded;
140  }
141 }
142 
143 void ExpandingWidgetModel::partiallyUnExpand(const QModelIndex &idx_)
144 {
145  QModelIndex index(firstColumn(idx_));
146  m_partiallyExpanded.remove(index);
147  m_partiallyExpanded.remove(idx_);
148 }
149 
151 {
152  return 60;
153 }
154 
156 {
157  QModelIndex idx(firstColumn(idx_));
158  if (!m_partiallyExpanded.contains(idx)) {
159  QModelIndex oldIndex = partiallyExpandedRow();
160  // Unexpand the previous partially expanded row
161  if (!m_partiallyExpanded.isEmpty()) {
163  while (!m_partiallyExpanded.isEmpty()) {
164  m_partiallyExpanded.erase(m_partiallyExpanded.begin());
165  }
166  // partiallyUnExpand( m_partiallyExpanded.begin().key() );
167  }
168  // Notify the underlying models that the item was selected, and eventually get back the text for the expanding widget.
169  if (!idx.isValid()) {
170  // All items have been unselected
171  if (oldIndex.isValid()) {
172  emit dataChanged(oldIndex, oldIndex);
173  }
174  } else {
175  QVariant variant = data(idx, CodeCompletionModel::ItemSelected);
176 
177  if (!isExpanded(idx) && variant.type() == QVariant::String) {
178  // Either expand upwards or downwards, choose in a way that
179  // the visible fields of the new selected entry are not moved.
180  if (oldIndex.isValid() && (oldIndex < idx || (!(oldIndex < idx) && oldIndex.parent() < idx.parent()))) {
181  m_partiallyExpanded.insert(idx, ExpandUpwards);
182  } else {
183  m_partiallyExpanded.insert(idx, ExpandDownwards);
184  }
185 
186  // 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)
187  if (oldIndex.isValid() && oldIndex < idx) {
188  emit dataChanged(oldIndex, idx);
189 
190  if (treeView()->verticalScrollMode() == QAbstractItemView::ScrollPerItem) {
191  // Qt fails to correctly scroll in ScrollPerItem mode, so the selected index is completely visible,
192  // so we do the scrolling by hand.
193  QRect selectedRect = treeView()->visualRect(idx);
194  QRect frameRect = treeView()->frameRect();
195 
196  if (selectedRect.bottom() > frameRect.bottom()) {
197  int diff = selectedRect.bottom() - frameRect.bottom();
198  // We need to scroll down
199  QModelIndex newTopIndex = idx;
200 
201  QModelIndex nextTopIndex = idx;
202  QRect nextRect = treeView()->visualRect(nextTopIndex);
203  while (nextTopIndex.isValid() && nextRect.isValid() && nextRect.top() >= diff) {
204  newTopIndex = nextTopIndex;
205  nextTopIndex = treeView()->indexAbove(nextTopIndex);
206  if (nextTopIndex.isValid()) {
207  nextRect = treeView()->visualRect(nextTopIndex);
208  }
209  }
210  treeView()->scrollTo(newTopIndex, QAbstractItemView::PositionAtTop);
211  }
212  }
213 
214  // This is needed to keep the item we are expanding completely visible. Qt does not scroll the view to keep the item visible.
215  // But we must make sure that it isn't too expensive.
216  // We need to make sure that scrolling is efficient, and the whole content is not repainted.
217  // Since we are scrolling anyway, we can keep the next line visible, which might be a cool feature.
218 
219  // Since this also doesn't work smoothly, leave it for now
220  // treeView()->scrollTo( nextLine, QAbstractItemView::EnsureVisible );
221  } else if (oldIndex.isValid() && idx < oldIndex) {
222  emit dataChanged(idx, oldIndex);
223 
224  // For consistency with the down-scrolling, we keep one additional line visible above the current visible.
225 
226  // Since this also doesn't work smoothly, leave it for now
227  /* QModelIndex prevLine = idx.sibling(idx.row()-1, idx.column());
228  if( prevLine.isValid() )
229  treeView()->scrollTo( prevLine );*/
230  } else {
231  emit dataChanged(idx, idx);
232  }
233  } else if (oldIndex.isValid()) {
234  // We are not partially expanding a new row, but we previously had a partially expanded row. So signalize that it has been unexpanded.
235 
236  emit dataChanged(oldIndex, oldIndex);
237  }
238  }
239  } else {
240  qCDebug(LOG_KTE) << "ExpandingWidgetModel::rowSelected: Row is already partially expanded";
241  }
242 }
243 
244 QString ExpandingWidgetModel::partialExpandText(const QModelIndex &idx) const
245 {
246  if (!idx.isValid()) {
247  return QString();
248  }
249 
250  return data(firstColumn(idx), CodeCompletionModel::ItemSelected).toString();
251 }
252 
254 {
255  QModelIndex idx(firstColumn(idx_));
256 
257  if (!idx.isValid()) {
258  return QRect();
259  }
260 
261  ExpansionType expansion = ExpandDownwards;
262 
263  if (m_partiallyExpanded.find(idx) != m_partiallyExpanded.constEnd()) {
264  expansion = m_partiallyExpanded[idx];
265  }
266 
267  // Get the whole rectangle of the row:
268  QModelIndex rightMostIndex = idx;
269  QModelIndex tempIndex = idx;
270  while ((tempIndex = rightMostIndex.sibling(rightMostIndex.row(), rightMostIndex.column() + 1)).isValid()) {
271  rightMostIndex = tempIndex;
272  }
273 
274  QRect rect = treeView()->visualRect(idx);
275  QRect rightMostRect = treeView()->visualRect(rightMostIndex);
276 
277  rect.setLeft(rect.left() + 20);
278  rect.setRight(rightMostRect.right() - 5);
279 
280  // These offsets must match exactly those used in ExpandingDelegate::sizeHint()
281  int top = rect.top() + 5;
282  int bottom = rightMostRect.bottom() - 5;
283 
284  if (expansion == ExpandDownwards) {
285  top += basicRowHeight(idx);
286  } else {
287  bottom -= basicRowHeight(idx);
288  }
289 
290  rect.setTop(top);
291  rect.setBottom(bottom);
292 
293  return rect;
294 }
295 
297 {
298  QModelIndex idx(firstColumn(idx_));
299 
300  if (!m_expandState.contains(idx)) {
301  m_expandState.insert(idx, NotExpandable);
302  QVariant v = data(idx, CodeCompletionModel::IsExpandable);
303  if (v.canConvert<bool>() && v.toBool()) {
304  m_expandState[idx] = Expandable;
305  }
306  }
307 
308  return m_expandState[idx] != NotExpandable;
309 }
310 
312 {
313  QModelIndex idx(firstColumn(idx_));
314  return m_expandState.contains(idx) && m_expandState[idx] == Expanded;
315 }
316 
318 {
319  QModelIndex idx(firstColumn(idx_));
320 
321  // qCDebug(LOG_KTE) << "Setting expand-state of row " << idx.row() << " to " << expanded;
322  if (!idx.isValid()) {
323  return;
324  }
325 
326  if (isExpandable(idx)) {
327  if (!expanded && m_expandingWidgets.contains(idx) && m_expandingWidgets[idx]) {
328  m_expandingWidgets[idx]->hide();
329  }
330 
331  m_expandState[idx] = expanded ? Expanded : Expandable;
332 
333  if (expanded) {
334  partiallyUnExpand(idx);
335  }
336 
337  if (expanded && !m_expandingWidgets.contains(idx)) {
338  QVariant v = data(idx, CodeCompletionModel::ExpandingWidget);
339 
340  if (v.canConvert<QWidget *>()) {
341  m_expandingWidgets[idx] = v.value<QWidget *>();
342  } else if (v.canConvert<QString>()) {
343  // Create a html widget that shows the given string
344  KTextEdit *edit = new KTextEdit(v.toString());
345  edit->setReadOnly(true);
346  edit->resize(200, 50); // Make the widget small so it embeds nicely.
347  m_expandingWidgets[idx] = edit;
348  } else {
349  m_expandingWidgets[idx] = nullptr;
350  }
351  }
352 
353  // Eventually partially expand the row
354  if (!expanded && firstColumn(treeView()->currentIndex()) == idx && !isPartiallyExpanded(idx)) {
355  rowSelected(idx); // Partially expand the row.
356  }
357 
358  emit dataChanged(idx, idx);
359 
360  if (treeView()) {
361  treeView()->scrollTo(idx);
362  }
363  }
364 }
365 
366 int ExpandingWidgetModel::basicRowHeight(const QModelIndex &idx_) const
367 {
368  QModelIndex idx(firstColumn(idx_));
369 
370  ExpandingDelegate *delegate = dynamic_cast<ExpandingDelegate *>(treeView()->itemDelegate(idx));
371  if (!delegate || !idx.isValid()) {
372  qCDebug(LOG_KTE) << "ExpandingWidgetModel::basicRowHeight: Could not get delegate";
373  return 15;
374  }
375  return delegate->basicSizeHint(idx).height();
376 }
377 
379 {
380  QModelIndex idx(firstColumn(idx_));
381  if (!idx.isValid() || !isExpanded(idx)) {
382  return;
383  }
384 
385  QWidget *w = m_expandingWidgets.value(idx);
386  if (!w) {
387  return;
388  }
389 
390  QRect rect = treeView()->visualRect(idx);
391 
392  if (!rect.isValid() || rect.bottom() < 0 || rect.top() >= treeView()->height()) {
393  // The item is currently not visible
394  w->hide();
395  return;
396  }
397 
398  // Find out the basic width of the row
399  rect.setLeft(rect.left() + 20);
400  for (int i = 0, numColumns = idx.model()->columnCount(idx.parent()); i < numColumns; ++i) {
401  QModelIndex rightMostIndex = idx.sibling(idx.row(), i);
402  int right = treeView()->visualRect(rightMostIndex).right();
403  if (right > rect.right()) {
404  rect.setRight(right);
405  }
406  }
407  rect.setRight(rect.right() - 5);
408 
409  // These offsets must match exactly those used in KateCompletionDeleage::sizeHint()
410  rect.setTop(rect.top() + basicRowHeight(idx) + 5);
411  rect.setHeight(w->height());
412 
413  if (w->parent() != treeView()->viewport() || w->geometry() != rect || !w->isVisible()) {
414  w->setParent(treeView()->viewport());
415 
416  w->setGeometry(rect);
417  w->show();
418  }
419 }
420 
422 {
423  for (QMap<QModelIndex, QPointer<QWidget>>::const_iterator it = m_expandingWidgets.constBegin(); it != m_expandingWidgets.constEnd(); ++it) {
424  placeExpandingWidget(it.key());
425  }
426 }
427 
429 {
430  int sum = 0;
431  for (QMap<QModelIndex, QPointer<QWidget>>::const_iterator it = m_expandingWidgets.constBegin(); it != m_expandingWidgets.constEnd(); ++it) {
432  if (isExpanded(it.key()) && (*it)) {
433  sum += (*it)->height();
434  }
435  }
436  return sum;
437 }
438 
440 {
441  QModelIndex idx(firstColumn(idx_));
442 
443  if (m_expandingWidgets.contains(idx)) {
444  return m_expandingWidgets[idx];
445  } else {
446  return nullptr;
447  }
448 }
449 
450 void ExpandingWidgetModel::cacheIcons() const
451 {
452  if (m_expandedIcon.isNull()) {
453  m_expandedIcon = QIcon::fromTheme(QStringLiteral("arrow-down"));
454  }
455 
456  if (m_collapsedIcon.isNull()) {
457  m_collapsedIcon = QIcon::fromTheme(QStringLiteral("arrow-right"));
458  }
459 }
460 
461 QList<QVariant> mergeCustomHighlighting(int leftSize, const QList<QVariant> &left, int rightSize, const QList<QVariant> &right)
462 {
463  QList<QVariant> ret = left;
464  if (left.isEmpty()) {
465  ret << QVariant(0);
466  ret << QVariant(leftSize);
468  }
469 
470  if (right.isEmpty()) {
471  ret << QVariant(leftSize);
472  ret << QVariant(rightSize);
474  } else {
476  while (it != right.constEnd()) {
477  {
479  for (int a = 0; a < 2; a++) {
480  ++testIt;
481  if (testIt == right.constEnd()) {
482  qCWarning(LOG_KTE) << "Length of input is not multiple of 3";
483  break;
484  }
485  }
486  }
487 
488  ret << QVariant((*it).toInt() + leftSize);
489  ++it;
490  ret << QVariant((*it).toInt());
491  ++it;
492  ret << *it;
493  if (!(*it).value<QTextFormat>().isValid()) {
494  qCDebug(LOG_KTE) << "Text-format is invalid";
495  }
496  ++it;
497  }
498  }
499  return ret;
500 }
501 
502 // It is assumed that between each two strings, one space is inserted
503 QList<QVariant> mergeCustomHighlighting(QStringList strings, QList<QVariantList> highlights, int grapBetweenStrings)
504 {
505  if (strings.isEmpty()) {
506  qCWarning(LOG_KTE) << "List of strings is empty";
507  return QList<QVariant>();
508  }
509 
510  if (highlights.isEmpty()) {
511  qCWarning(LOG_KTE) << "List of highlightings is empty";
512  return QList<QVariant>();
513  }
514 
515  if (strings.count() != highlights.count()) {
516  qCWarning(LOG_KTE) << "Length of string-list is " << strings.count() << " while count of highlightings is " << highlights.count() << ", should be same";
517  return QList<QVariant>();
518  }
519 
520  // Merge them together
521  QString totalString = strings[0];
522  QVariantList totalHighlighting = highlights[0];
523 
524  strings.pop_front();
525  highlights.pop_front();
526 
527  while (!strings.isEmpty()) {
528  totalHighlighting = mergeCustomHighlighting(totalString.length(), totalHighlighting, strings[0].length(), highlights[0]);
529  totalString += strings[0];
530 
531  for (int a = 0; a < grapBetweenStrings; a++) {
532  totalString += QLatin1Char(' ');
533  }
534 
535  strings.pop_front();
536  highlights.pop_front();
537  }
538  // Combine the custom-highlightings
539  return totalHighlighting;
540 }
bool canConvert(int targetTypeId) const const
void resize(int w, int h)
void setBottom(int y)
virtual void rowSelected(const QModelIndex &row)
Notifies underlying models that the item was selected, collapses any previous partially expanded line...
QModelIndex partiallyExpandedRow() const
Returns the first row that is currently partially expanded.
void clearExpanding()
Unexpand all rows and clear all cached information about them(this includes deleting the expanding-wi...
QColor light(int factor) const const
void placeExpandingWidgets()
Place or hides all expanding-widgets to the correct positions. Should be called after the view was sc...
int right() const const
int length() const const
KGUIADDONS_EXPORT QColor tint(const QColor &base, const QColor &color, qreal amount=0.3)
QMap::const_iterator constBegin() const const
bool isVisible() const const
ExpansionType isPartiallyExpanded(const QModelIndex &index) const
Returns whether the given index is currently partially expanded. Does not do any other checks like ca...
T value() const const
KGUIADDONS_EXPORT QColor mix(const QColor &c1, const QColor &c2, qreal bias=0.5)
void clear()
bool isExpandable(const QModelIndex &index) const
void setParent(QWidget *parent)
const QColor & color() const const
void placeExpandingWidget(const QModelIndex &row)
Places and shows the expanding-widget for the given row, if it should be visible and is valid...
bool isValid() const const
int count(const T &value) const const
virtual void setReadOnly(bool readOnly)
void pop_front()
int top() const const
QRgb rgb() const const
QPalette palette()
void setTop(int y)
int left() const const
QWidget * expandingWidget(const QModelIndex &row) const
bool isEmpty() const const
BackgroundRole
QMap::const_iterator constEnd() const const
int row() const const
uint matchColor(const QModelIndex &index) const
Returns the match-color for the given index, or zero if match-quality could not be computed...
void hide()
QModelIndex parent() const const
The KTextEditor namespace contains all the public API that is required to use the KTextEditor compone...
bool isExpanded(const QModelIndex &row) const
int partiallyExpandWidgetHeight() const
Amount by which the height of a row increases when it is partially expanded.
bool isValid() const const
void setRight(int x)
This is a delegate that cares, together with ExpandingWidgetModel, about embedded widgets in tree-vie...
const QAbstractItemModel * model() const const
void setExpanded(QModelIndex index, bool expanded)
Change the expand-state of the row given through index. The display will be updated.
QTextStream & right(QTextStream &s)
QModelIndex sibling(int row, int column) const const
void setHeight(int height)
virtual int columnCount(const QModelIndex &parent) const const =0
int height() const const
int bottom() const const
int column() const const
bool toBool() const const
QIcon fromTheme(const QString &name)
void show()
const QBrush & window() const const
QList::const_iterator constEnd() const const
QList::const_iterator constBegin() const const
QVariant::Type type() const const
QObject * parent() const const
QString toString() const const
void setLeft(int x)
QVariant data(const QModelIndex &index, int role=Qt::DisplayRole) const override
Does not request data from index, this only returns local data like highlighting for expanded rows an...
bool isValid() const const
int expandingWidgetsHeight() const
Returns the total height added through all open expanding-widgets.
void setGeometry(int x, int y, int w, int h)
QRect partialExpandRect(const QModelIndex &row) const
Returns the rectangle for the partially expanded part of the given row.
This file is part of the KDE documentation.
Documentation copyright © 1996-2020 The KDE developers.
Generated on Tue Oct 20 2020 23:03:12 by doxygen 1.8.11 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.