1/* This file is part of the KDE libraries and the Kate part.
2 *
3 * Copyright (C) 2007 David Nolden <david.nolden.kdevelop@art-master.de>
4 *
5 * This library is free software; you can redistribute it and/or
6 * modify it under the terms of the GNU Library General Public
7 * License as published by the Free Software Foundation; either
8 * version 2 of the License, or (at your option) any later version.
9 *
10 * This library is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13 * Library General Public License for more details.
14 *
15 * You should have received a copy of the GNU Library General Public License
16 * along with this library; see the file COPYING.LIB. If not, write to
17 * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
18 * Boston, MA 02110-1301, USA.
19 */
20
21#include "expandingwidgetmodel.h"
22
23#include <QTreeView>
24#include <QModelIndex>
25#include <QBrush>
26
27#include <ktexteditor/codecompletionmodel.h>
28#include <kiconloader.h>
29#include <ktextedit.h>
30#include "kcolorutils.h"
31
32#include "expandingdelegate.h"
33#include <qapplication.h>
34
35QIcon ExpandingWidgetModel::m_expandedIcon;
36QIcon ExpandingWidgetModel::m_collapsedIcon;
37
38using namespace KTextEditor;
39
40inline QModelIndex firstColumn( const QModelIndex& index ) {
41 return index.sibling(index.row(), 0);
42}
43
44ExpandingWidgetModel::ExpandingWidgetModel( QWidget* parent ) :
45 QAbstractTableModel(parent)
46{
47}
48
49ExpandingWidgetModel::~ExpandingWidgetModel() {
50 clearExpanding();
51}
52
53static QColor doAlternate(QColor color) {
54 QColor background = QApplication::palette().background().color();
55 return KColorUtils::mix(color, background, 0.15);
56}
57
58uint ExpandingWidgetModel::matchColor(const QModelIndex& index) const {
59
60 int matchQuality = contextMatchQuality( index.sibling(index.row(), 0) );
61
62 if( matchQuality > 0 )
63 {
64 bool alternate = index.row() & 1;
65
66 QColor badMatchColor(0xff00aa44); //Blue-ish green
67 QColor goodMatchColor(0xff00ff00); //Green
68
69 QColor background = treeView()->palette().light().color();
70
71 QColor totalColor = KColorUtils::mix(badMatchColor, goodMatchColor, ((float)matchQuality)/10.0);
72
73 if(alternate)
74 totalColor = doAlternate(totalColor);
75
76 const float dynamicTint = 0.2;
77 const float minimumTint = 0.2;
78 double tintStrength = (dynamicTint*matchQuality)/10;
79 if(tintStrength)
80 tintStrength += minimumTint; //Some minimum tinting strength, else it's not visible any more
81
82 return KColorUtils::tint(background, totalColor, tintStrength ).rgb();
83 }else{
84 return 0;
85 }
86}
87
88QVariant ExpandingWidgetModel::data( const QModelIndex & index, int role ) const
89{
90 switch( role ) {
91 case Qt::BackgroundRole:
92 {
93 if( index.column() == 0 ) {
94 //Highlight by match-quality
95 uint color = matchColor(index);
96 if( color )
97 return QBrush( color );
98 }
99 //Use a special background-color for expanded items
100 if( isExpanded(index) ) {
101 if( index.row() & 1 ) {
102 return doAlternate(treeView()->palette().toolTipBase().color());
103 } else {
104 return treeView()->palette().toolTipBase();
105 }
106 }
107 }
108 }
109 return QVariant();
110}
111
112void ExpandingWidgetModel::clearMatchQualities() {
113 m_contextMatchQualities.clear();
114}
115
116QModelIndex ExpandingWidgetModel::partiallyExpandedRow() const {
117 if( m_partiallyExpanded.isEmpty() )
118 return QModelIndex();
119 else
120 return m_partiallyExpanded.constBegin().key();
121}
122
123void ExpandingWidgetModel::clearExpanding() {
124
125 clearMatchQualities();
126 QMap<QModelIndex,ExpandingWidgetModel::ExpandingType> oldExpandState = m_expandState;
127 foreach( const QPointer<QWidget> &widget, m_expandingWidgets )
128 if(widget)
129 widget->deleteLater(); // By using deleteLater, we prevent crashes when an action within a widget makes the completion cancel
130 m_expandingWidgets.clear();
131 m_expandState.clear();
132 m_partiallyExpanded.clear();
133
134 for( QMap<QModelIndex, ExpandingWidgetModel::ExpandingType>::const_iterator it = oldExpandState.constBegin(); it != oldExpandState.constEnd(); ++it )
135 if(it.value() == Expanded)
136 emit dataChanged(it.key(), it.key());
137}
138
139ExpandingWidgetModel::ExpansionType ExpandingWidgetModel::isPartiallyExpanded(const QModelIndex& index) const {
140 if( m_partiallyExpanded.contains(firstColumn(index)) )
141 return m_partiallyExpanded[firstColumn(index)];
142 else
143 return NotExpanded;
144}
145
146void ExpandingWidgetModel::partiallyUnExpand(const QModelIndex& idx_)
147{
148 QModelIndex index( firstColumn(idx_) );
149 m_partiallyExpanded.remove(index);
150 m_partiallyExpanded.remove(idx_);
151}
152
153int ExpandingWidgetModel::partiallyExpandWidgetHeight() const {
154 return 60; ///@todo use font-metrics text-height*2 for 2 lines
155}
156
157void ExpandingWidgetModel::rowSelected(const QModelIndex& idx_)
158{
159 QModelIndex idx( firstColumn(idx_) );
160 if( !m_partiallyExpanded.contains( idx ) )
161 {
162 QModelIndex oldIndex = partiallyExpandedRow();
163 //Unexpand the previous partially expanded row
164 if( !m_partiallyExpanded.isEmpty() )
165 { ///@todo allow multiple partially expanded rows
166 while( !m_partiallyExpanded.isEmpty() )
167 m_partiallyExpanded.erase(m_partiallyExpanded.begin());
168 //partiallyUnExpand( m_partiallyExpanded.begin().key() );
169 }
170 //Notify the underlying models that the item was selected, and eventually get back the text for the expanding widget.
171 if( !idx.isValid() ) {
172 //All items have been unselected
173 if( oldIndex.isValid() )
174 emit dataChanged(oldIndex, oldIndex);
175 } else {
176 QVariant variant = data(idx, CodeCompletionModel::ItemSelected);
177
178 if( !isExpanded(idx) && variant.type() == QVariant::String) {
179
180 //Either expand upwards or downwards, choose in a way that
181 //the visible fields of the new selected entry are not moved.
182 if( oldIndex.isValid() && (oldIndex < idx || (!(oldIndex < idx) && oldIndex.parent() < idx.parent()) ) )
183 m_partiallyExpanded.insert(idx, ExpandUpwards);
184 else
185 m_partiallyExpanded.insert(idx, ExpandDownwards);
186
187 //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)
188 if( oldIndex.isValid() && oldIndex < idx ) {
189 emit dataChanged(oldIndex, idx);
190
191 if( treeView()->verticalScrollMode() == QAbstractItemView::ScrollPerItem )
192 {
193 //Qt fails to correctly scroll in ScrollPerItem mode, so the selected index is completely visible,
194 //so we do the scrolling by hand.
195 QRect selectedRect = treeView()->visualRect(idx);
196 QRect frameRect = treeView()->frameRect();
197
198 if( selectedRect.bottom() > frameRect.bottom() ) {
199 int diff = selectedRect.bottom() - frameRect.bottom();
200 //We need to scroll down
201 QModelIndex newTopIndex = idx;
202
203 QModelIndex nextTopIndex = idx;
204 QRect nextRect = treeView()->visualRect(nextTopIndex);
205 while( nextTopIndex.isValid() && nextRect.isValid() && nextRect.top() >= diff ) {
206 newTopIndex = nextTopIndex;
207 nextTopIndex = treeView()->indexAbove(nextTopIndex);
208 if( nextTopIndex.isValid() )
209 nextRect = treeView()->visualRect(nextTopIndex);
210 }
211 treeView()->scrollTo( newTopIndex, QAbstractItemView::PositionAtTop );
212 }
213 }
214
215 //This is needed to keep the item we are expanding completely visible. Qt does not scroll the view to keep the item visible.
216 //But we must make sure that it isn't too expensive.
217 //We need to make sure that scrolling is efficient, and the whole content is not repainted.
218 //Since we are scrolling anyway, we can keep the next line visible, which might be a cool feature.
219
220 //Since this also doesn't work smoothly, leave it for now
221 //treeView()->scrollTo( nextLine, QAbstractItemView::EnsureVisible );
222 } else if( oldIndex.isValid() && idx < oldIndex ) {
223 emit dataChanged(idx, oldIndex);
224
225 //For consistency with the down-scrolling, we keep one additional line visible above the current visible.
226
227 //Since this also doesn't work smoothly, leave it for now
228/* QModelIndex prevLine = idx.sibling(idx.row()-1, idx.column());
229 if( prevLine.isValid() )
230 treeView()->scrollTo( prevLine );*/
231 } else
232 emit dataChanged(idx, idx);
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 kDebug( 13035 ) << "ExpandingWidgetModel::rowSelected: Row is already partially expanded";
241 }
242}
243
244QString ExpandingWidgetModel::partialExpandText(const QModelIndex& idx) const {
245 if( !idx.isValid() )
246 return QString();
247
248 return data(firstColumn(idx), CodeCompletionModel::ItemSelected).toString();
249}
250
251QRect ExpandingWidgetModel::partialExpandRect(const QModelIndex& idx_) const
252{
253 QModelIndex idx(firstColumn(idx_));
254
255 if( !idx.isValid() )
256 return QRect();
257
258 ExpansionType expansion = ExpandDownwards;
259
260 if( m_partiallyExpanded.find(idx) != m_partiallyExpanded.constEnd() )
261 expansion = m_partiallyExpanded[idx];
262
263 //Get the whole rectangle of the row:
264 QModelIndex rightMostIndex = idx;
265 QModelIndex tempIndex = idx;
266 while( (tempIndex = rightMostIndex.sibling(rightMostIndex.row(), rightMostIndex.column()+1)).isValid() )
267 rightMostIndex = tempIndex;
268
269 QRect rect = treeView()->visualRect(idx);
270 QRect rightMostRect = treeView()->visualRect(rightMostIndex);
271
272 rect.setLeft( rect.left() + 20 );
273 rect.setRight( rightMostRect.right() - 5 );
274
275 //These offsets must match exactly those used in ExpandingDelegate::sizeHint()
276 int top = rect.top() + 5;
277 int bottom = rightMostRect.bottom() - 5 ;
278
279 if( expansion == ExpandDownwards )
280 top += basicRowHeight(idx);
281 else
282 bottom -= basicRowHeight(idx);
283
284 rect.setTop( top );
285 rect.setBottom( bottom );
286
287 return rect;
288}
289
290bool ExpandingWidgetModel::isExpandable(const QModelIndex& idx_) const
291{
292 QModelIndex idx(firstColumn(idx_));
293
294 if( !m_expandState.contains(idx) )
295 {
296 m_expandState.insert(idx, NotExpandable);
297 QVariant v = data(idx, CodeCompletionModel::IsExpandable);
298 if( v.canConvert<bool>() && v.value<bool>() )
299 m_expandState[idx] = Expandable;
300 }
301
302 return m_expandState[idx] != NotExpandable;
303}
304
305bool ExpandingWidgetModel::isExpanded(const QModelIndex& idx_) const
306{
307 QModelIndex idx(firstColumn(idx_));
308 return m_expandState.contains(idx) && m_expandState[idx] == Expanded;
309}
310
311void ExpandingWidgetModel::setExpanded(QModelIndex idx_, bool expanded)
312{
313 QModelIndex idx(firstColumn(idx_));
314
315 //kDebug( 13035 ) << "Setting expand-state of row " << idx.row() << " to " << expanded;
316 if( !idx.isValid() )
317 return;
318
319 if( isExpandable(idx) ) {
320 if( !expanded && m_expandingWidgets.contains(idx) && m_expandingWidgets[idx] ) {
321 m_expandingWidgets[idx]->hide();
322 }
323
324 m_expandState[idx] = expanded ? Expanded : Expandable;
325
326 if( expanded )
327 partiallyUnExpand(idx);
328
329 if( expanded && !m_expandingWidgets.contains(idx) )
330 {
331 QVariant v = data(idx, CodeCompletionModel::ExpandingWidget);
332
333 if( v.canConvert<QWidget*>() ) {
334 m_expandingWidgets[idx] = v.value<QWidget*>();
335 } else if( v.canConvert<QString>() ) {
336 //Create a html widget that shows the given string
337 KTextEdit* edit = new KTextEdit( v.value<QString>() );
338 edit->setReadOnly(true);
339 edit->resize(200, 50); //Make the widget small so it embeds nicely.
340 m_expandingWidgets[idx] = edit;
341 } else {
342 m_expandingWidgets[idx] = 0;
343 }
344 }
345
346 //Eventually partially expand the row
347 if( !expanded && firstColumn(treeView()->currentIndex()) == idx && !isPartiallyExpanded(idx) )
348 rowSelected(idx); //Partially expand the row.
349
350 emit dataChanged(idx, idx);
351
352 if(treeView())
353 treeView()->scrollTo(idx);
354 }
355}
356
357int ExpandingWidgetModel::basicRowHeight( const QModelIndex& idx_ ) const
358{
359 QModelIndex idx(firstColumn(idx_));
360
361 ExpandingDelegate* delegate = dynamic_cast<ExpandingDelegate*>( treeView()->itemDelegate(idx) );
362 if( !delegate || !idx.isValid() ) {
363 kDebug( 13035 ) << "ExpandingWidgetModel::basicRowHeight: Could not get delegate";
364 return 15;
365 }
366 return delegate->basicSizeHint( idx ).height();
367}
368
369
370void ExpandingWidgetModel::placeExpandingWidget(const QModelIndex& idx_)
371{
372 QModelIndex idx(firstColumn(idx_));
373
374 QWidget* w = 0;
375 if( m_expandingWidgets.contains(idx) )
376 w = m_expandingWidgets[idx];
377
378 if( w && isExpanded(idx) ) {
379 if( !idx.isValid() )
380 return;
381
382 QRect rect = treeView()->visualRect(idx);
383
384 if( !rect.isValid() || rect.bottom() < 0 || rect.top() >= treeView()->height() ) {
385 //The item is currently not visible
386 w->hide();
387 return;
388 }
389
390 QModelIndex rightMostIndex = idx;
391 QModelIndex tempIndex = idx;
392 while( (tempIndex = rightMostIndex.sibling(rightMostIndex.row(), rightMostIndex.column()+1)).isValid() )
393 rightMostIndex = tempIndex;
394
395 QRect rightMostRect = treeView()->visualRect(rightMostIndex);
396
397 //Find out the basic height of the row
398 rect.setLeft( rect.left() + 20 );
399 rect.setRight( rightMostRect.right() - 5 );
400
401 //These offsets must match exactly those used in KateCompletionDeleage::sizeHint()
402 rect.setTop( rect.top() + basicRowHeight(idx) + 5 );
403 rect.setHeight( w->height() );
404
405 if( w->parent() != treeView()->viewport() || w->geometry() != rect || !w->isVisible() ) {
406 w->setParent( treeView()->viewport() );
407
408 w->setGeometry(rect);
409 w->show();
410 }
411 }
412}
413
414void ExpandingWidgetModel::placeExpandingWidgets() {
415 for( QMap<QModelIndex, QPointer<QWidget> >::const_iterator it = m_expandingWidgets.constBegin(); it != m_expandingWidgets.constEnd(); ++it ) {
416 placeExpandingWidget(it.key());
417 }
418}
419
420int ExpandingWidgetModel::expandingWidgetsHeight() const
421{
422 int sum = 0;
423 for( QMap<QModelIndex, QPointer<QWidget> >::const_iterator it = m_expandingWidgets.constBegin(); it != m_expandingWidgets.constEnd(); ++it ) {
424 if( isExpanded(it.key() ) && (*it) )
425 sum += (*it)->height();
426 }
427 return sum;
428}
429
430
431QWidget* ExpandingWidgetModel::expandingWidget(const QModelIndex& idx_) const
432{
433 QModelIndex idx(firstColumn(idx_));
434
435 if( m_expandingWidgets.contains(idx) )
436 return m_expandingWidgets[idx];
437 else
438 return 0;
439}
440
441void ExpandingWidgetModel::cacheIcons() const {
442 if( m_expandedIcon.isNull() )
443 m_expandedIcon = KIconLoader::global()->loadIcon("arrow-down", KIconLoader::Small, 10);
444
445 if( m_collapsedIcon.isNull() )
446 m_collapsedIcon = KIconLoader::global()->loadIcon("arrow-right", KIconLoader::Small, 10);
447}
448
449QList<QVariant> mergeCustomHighlighting( int leftSize, const QList<QVariant>& left, int rightSize, const QList<QVariant>& right )
450{
451 QList<QVariant> ret = left;
452 if( left.isEmpty() ) {
453 ret << QVariant(0);
454 ret << QVariant(leftSize);
455 ret << QTextFormat(QTextFormat::CharFormat);
456 }
457
458 if( right.isEmpty() ) {
459 ret << QVariant(leftSize);
460 ret << QVariant(rightSize);
461 ret << QTextFormat(QTextFormat::CharFormat);
462 } else {
463 QList<QVariant>::const_iterator it = right.constBegin();
464 while( it != right.constEnd() ) {
465 {
466 QList<QVariant>::const_iterator testIt = it;
467 for(int a = 0; a < 2; a++) {
468 ++testIt;
469 if(testIt == right.constEnd()) {
470 kWarning() << "Length of input is not multiple of 3";
471 break;
472 }
473 }
474 }
475
476 ret << QVariant( (*it).toInt() + leftSize );
477 ++it;
478 ret << QVariant( (*it).toInt() );
479 ++it;
480 ret << *it;
481 if(!(*it).value<QTextFormat>().isValid())
482 kDebug( 13035 ) << "Text-format is invalid";
483 ++it;
484 }
485 }
486 return ret;
487}
488
489//It is assumed that between each two strings, one space is inserted
490QList<QVariant> mergeCustomHighlighting( QStringList strings, QList<QVariantList> highlights, int grapBetweenStrings )
491{
492 if(strings.isEmpty()) {
493 kWarning() << "List of strings is empty";
494 return QList<QVariant>();
495 }
496
497 if(highlights.isEmpty()) {
498 kWarning() << "List of highlightings is empty";
499 return QList<QVariant>();
500 }
501
502 if(strings.count() != highlights.count()) {
503 kWarning() << "Length of string-list is " << strings.count() << " while count of highlightings is " << highlights.count() << ", should be same";
504 return QList<QVariant>();
505 }
506
507 //Merge them together
508 QString totalString = strings[0];
509 QVariantList totalHighlighting = highlights[0];
510
511 strings.pop_front();
512 highlights.pop_front();
513
514 while( !strings.isEmpty() ) {
515 totalHighlighting = mergeCustomHighlighting( totalString.length(), totalHighlighting, strings[0].length(), highlights[0] );
516 totalString += strings[0];
517
518 for(int a = 0; a < grapBetweenStrings; a++)
519 totalString += ' ';
520
521 strings.pop_front();
522 highlights.pop_front();
523
524 }
525 //Combine the custom-highlightings
526 return totalHighlighting;
527}
528#include "expandingwidgetmodel.moc"
529
530