1/*
2 SPDX-FileCopyrightText: 2006 Hamish Rodda <rodda@kde.org>
3 SPDX-FileCopyrightText: 2007-2008 David Nolden <david.nolden.kdevelop@art-master.de>
4
5 SPDX-License-Identifier: LGPL-2.0-or-later
6*/
7
8#include "katecompletiontree.h"
9
10#include <QApplication>
11#include <QHeaderView>
12#include <QList>
13#include <QScreen>
14#include <QScrollBar>
15#include <QTimer>
16
17#include "kateconfig.h"
18#include "katepartdebug.h"
19#include "katerenderer.h"
20#include "kateview.h"
21
22#include "documentation_tip.h"
23#include "katecompletiondelegate.h"
24#include "katecompletionmodel.h"
25#include "katecompletionwidget.h"
26
27KateCompletionTree::KateCompletionTree(KateCompletionWidget *parent)
28 : QTreeView(parent)
29{
30 m_scrollingEnabled = true;
31 header()->hide();
32 setRootIsDecorated(false);
33 setIndentation(0);
34 setFrameStyle(QFrame::NoFrame);
35 setAllColumnsShowFocus(true);
36 setAlternatingRowColors(true);
37 setUniformRowHeights(true);
38 header()->setMinimumSectionSize(0);
39
40 // We need ScrollPerItem, because ScrollPerPixel is too slow with a very large completion-list(see KDevelop).
41 setVerticalScrollMode(QAbstractItemView::ScrollPerItem);
42
43 m_resizeTimer = new QTimer(this);
44 m_resizeTimer->setSingleShot(true);
45
46 connect(sender: m_resizeTimer, signal: &QTimer::timeout, context: this, slot: &KateCompletionTree::resizeColumnsSlot);
47
48 // Provide custom highlighting to completion entries
49 setItemDelegate(new KateCompletionDelegate(this));
50 // make sure we adapt to size changes when the model got reset
51 // this is important for delayed creation of groups, without this
52 // the first column would never get resized to the correct size
53 connect(sender: widget()->model(), signal: &QAbstractItemModel::modelReset, context: this, slot: &KateCompletionTree::scheduleUpdate, type: Qt::QueuedConnection);
54
55 // Prevent user from expanding / collapsing with the mouse
56 setItemsExpandable(false);
57 setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
58}
59
60void KateCompletionTree::currentChanged(const QModelIndex &current, const QModelIndex &previous)
61{
62 // If config is enabled OR the tip is already visible => show / update it
63 if (widget()->view()->config()->showDocWithCompletion() || widget()->docTip()->isVisible()) {
64 widget()->showDocTip(idx: current);
65 }
66 widget()->model()->rowSelected(row: current);
67 QTreeView::currentChanged(current, previous);
68}
69
70void KateCompletionTree::setScrollingEnabled(bool enabled)
71{
72 m_scrollingEnabled = enabled;
73}
74
75void KateCompletionTree::scrollContentsBy(int dx, int dy)
76{
77 if (m_scrollingEnabled) {
78 QTreeView::scrollContentsBy(dx, dy);
79 }
80
81 if (isVisible()) {
82 scheduleUpdate();
83 }
84}
85
86KateCompletionWidget *KateCompletionTree::widget() const
87{
88 return static_cast<KateCompletionWidget *>(const_cast<QObject *>(parent()));
89}
90
91void KateCompletionTree::resizeColumnsSlot()
92{
93 if (model()) {
94 resizeColumns();
95
96 if (!widget()->docTip()->isHidden()) {
97 widget()->docTip()->updatePosition(completionWidget: widget());
98 }
99 }
100}
101
102/**
103 * Measure the width of visible columns.
104 *
105 * This iterates from the start index @p current down until a dead end is hit.
106 * In a tree model, it will recurse into child indices. Iteration is stopped if
107 * no more items are available, or the visited rows exceed the available @p maxHeight.
108 *
109 * If the model is a tree model, and @p current points to a leaf, and the max height
110 * is not exceeded, then iteration will continue from the next parent sibling.
111 */
112static bool measureColumnSizes(const KateCompletionTree *tree,
113 QModelIndex current,
114 QVarLengthArray<int, 8> &columnSize,
115 int &currentYPos,
116 const int maxHeight,
117 bool recursed = false)
118{
119 while (current.isValid() && currentYPos < maxHeight) {
120 currentYPos += tree->sizeHintForIndex(index: current).height();
121 const int row = current.row();
122 for (int a = 0; a < columnSize.size(); a++) {
123 QSize s = tree->sizeHintForIndex(index: current.sibling(arow: row, acolumn: a));
124 if (s.width() > 2000) {
125 qCDebug(LOG_KTE) << "got invalid size-hint of width " << s.width();
126 } else if (s.width() > columnSize[a]) {
127 columnSize[a] = s.width();
128 }
129 }
130
131 const QAbstractItemModel *model = current.model();
132 const int children = model->rowCount(parent: current);
133 if (children > 0) {
134 for (int i = 0; i < children; ++i) {
135 if (measureColumnSizes(tree, current: model->index(row: i, column: 0, parent: current), columnSize, currentYPos, maxHeight, recursed: true)) {
136 break;
137 }
138 }
139 }
140
141 QModelIndex oldCurrent = current;
142 current = current.sibling(arow: current.row() + 1, acolumn: 0);
143
144 // Are we at the end of a group? If yes, move up into the next group
145 // only do this when we did not recurse already
146 while (!recursed && !current.isValid() && oldCurrent.parent().isValid()) {
147 oldCurrent = oldCurrent.parent();
148 current = oldCurrent.sibling(arow: oldCurrent.row() + 1, acolumn: 0);
149 }
150 }
151
152 return currentYPos >= maxHeight;
153}
154
155void KateCompletionTree::resizeColumns(bool firstShow, bool forceResize)
156{
157 static bool preventRecursion = false;
158 if (preventRecursion) {
159 return;
160 }
161 m_resizeTimer->stop();
162
163 if (firstShow) {
164 forceResize = true;
165 }
166
167 preventRecursion = true;
168
169 widget()->setUpdatesEnabled(false);
170
171 int modelIndexOfName = kateModel()->translateColumn(sourceColumn: KTextEditor::CodeCompletionModel::Name);
172 int oldIndentWidth = columnViewportPosition(column: modelIndexOfName);
173
174 /// Step 1: Compute the needed column-sizes for the visible content
175 const int numColumns = model()->columnCount();
176 QVarLengthArray<int, 8> columnSize(numColumns);
177 for (int i = 0; i < numColumns; ++i) {
178 columnSize[i] = 0;
179 }
180 QModelIndex current = indexAt(p: QPoint(1, 1));
181 // const bool changed = current.isValid();
182 int currentYPos = 0;
183 measureColumnSizes(tree: this, current, columnSize, currentYPos, maxHeight: height());
184
185 auto totalColumnsWidth = 0;
186 auto originalViewportWidth = viewport()->width();
187
188 const int maxWidth = (widget()->parentWidget()->geometry().width()) / 2;
189
190 /// Step 2: Update column-sizes
191 // This contains several hacks to reduce the amount of resizing that happens. Generally,
192 // resizes only happen if a) More than a specific amount of space is saved by the resize, or
193 // b) the resizing is required so the list can show all of its contents.
194 int minimumResize = 0;
195 int maximumResize = 0;
196
197 for (int n = 0; n < numColumns; n++) {
198 totalColumnsWidth += columnSize[n];
199
200 int diff = columnSize[n] - columnWidth(column: n);
201 if (diff < minimumResize) {
202 minimumResize = diff;
203 }
204 if (diff > maximumResize) {
205 maximumResize = diff;
206 }
207 }
208
209 int noReduceTotalWidth = 0; // The total width of the widget of no columns are reduced
210 for (int n = 0; n < numColumns; n++) {
211 if (columnSize[n] < columnWidth(column: n)) {
212 noReduceTotalWidth += columnWidth(column: n);
213 } else {
214 noReduceTotalWidth += columnSize[n];
215 }
216 }
217
218 // Check whether we can afford to reduce none of the columns
219 // Only reduce size if we widget would else be too wide.
220 bool noReduce = noReduceTotalWidth < maxWidth && !forceResize;
221
222 if (noReduce) {
223 totalColumnsWidth = 0;
224 for (int n = 0; n < numColumns; n++) {
225 if (columnSize[n] < columnWidth(column: n)) {
226 columnSize[n] = columnWidth(column: n);
227 }
228
229 totalColumnsWidth += columnSize[n];
230 }
231 }
232
233 if (minimumResize > -40 && maximumResize == 0 && !forceResize) {
234 // No column needs to be exanded, and no column needs to be reduced by more than 40 pixels.
235 // To prevent flashing, do not resize at all.
236 totalColumnsWidth = 0;
237 for (int n = 0; n < numColumns; n++) {
238 columnSize[n] = columnWidth(column: n);
239 totalColumnsWidth += columnSize[n];
240 }
241 } else {
242 // viewport()->resize( 5000, viewport()->height() );
243 for (int n = 0; n < numColumns; n++) {
244 setColumnWidth(column: n, width: columnSize[n]);
245 }
246 // For the first column (which is arrow-down / arrow-right) we keep its width to 20
247 // to prevent glitches and weird resizes when we have no expanding items in the view
248 // qCDebug(LOG_KTE) << "resizing viewport to" << totalColumnsWidth;
249 viewport()->resize(w: totalColumnsWidth, h: viewport()->height());
250 }
251
252 /// Step 3: Update widget-size and -position
253
254 int scrollBarWidth = verticalScrollBar()->width();
255
256 int newIndentWidth = columnViewportPosition(column: modelIndexOfName);
257
258 int newWidth = qMin(a: maxWidth, b: qMax(a: 75, b: totalColumnsWidth));
259
260 if (newWidth == maxWidth) {
261 setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded);
262 } else {
263 setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
264 }
265
266 if (maximumResize > 0 || forceResize || oldIndentWidth != newIndentWidth) {
267 // qCDebug(LOG_KTE) << geometry() << "newWidth" << newWidth << "current width" << width() << "target width" << newWidth + scrollBarWidth;
268
269 if ((newWidth + scrollBarWidth) != width() && originalViewportWidth != totalColumnsWidth) {
270 auto width = newWidth + scrollBarWidth + 2;
271 widget()->resize(w: width, h: widget()->height());
272 resize(w: width, h: widget()->height() - (2 * widget()->frameWidth()));
273 }
274
275 // qCDebug(LOG_KTE) << "created geometry:" << widget()->geometry() << geometry() << "newWidth" << newWidth << "viewport" << viewport()->width();
276
277 if (viewport()->width() > totalColumnsWidth) { // Set the size of the last column to fill the whole rest of the widget
278 setColumnWidth(column: numColumns - 1, width: viewport()->width() - columnViewportPosition(column: numColumns - 1));
279 }
280
281 /* for(int a = 0; a < numColumns; ++a)
282 qCDebug(LOG_KTE) << "column" << a << columnWidth(a) << "target:" << columnSize[a];*/
283
284 if (oldIndentWidth != newIndentWidth) {
285 if (!forceResize) {
286 preventRecursion = false;
287 resizeColumns(firstShow: true, forceResize: true);
288 }
289 }
290 }
291
292 widget()->setUpdatesEnabled(true);
293
294 preventRecursion = false;
295}
296
297void KateCompletionTree::initViewItemOption(QStyleOptionViewItem *option) const
298{
299 QTreeView::initViewItemOption(option);
300 option->font = widget()->view()->renderer()->currentFont();
301}
302
303KateCompletionModel *KateCompletionTree::kateModel() const
304{
305 return static_cast<KateCompletionModel *>(model());
306}
307
308bool KateCompletionTree::nextCompletion()
309{
310 QModelIndex current;
311 QModelIndex firstCurrent = currentIndex();
312
313 do {
314 QModelIndex oldCurrent = currentIndex();
315
316 current = moveCursor(cursorAction: MoveDown, modifiers: Qt::NoModifier);
317
318 if (current != oldCurrent && current.isValid()) {
319 setCurrentIndex(current);
320 } else {
321 if (firstCurrent.isValid()) {
322 setCurrentIndex(firstCurrent);
323 }
324 return false;
325 }
326
327 } while (!kateModel()->indexIsItem(index: current));
328
329 return true;
330}
331
332bool KateCompletionTree::previousCompletion()
333{
334 QModelIndex current;
335 QModelIndex firstCurrent = currentIndex();
336
337 do {
338 QModelIndex oldCurrent = currentIndex();
339
340 current = moveCursor(cursorAction: MoveUp, modifiers: Qt::NoModifier);
341
342 if (current != oldCurrent && current.isValid()) {
343 setCurrentIndex(current);
344
345 } else {
346 if (firstCurrent.isValid()) {
347 setCurrentIndex(firstCurrent);
348 }
349 return false;
350 }
351
352 } while (!kateModel()->indexIsItem(index: current));
353
354 return true;
355}
356
357bool KateCompletionTree::pageDown()
358{
359 QModelIndex old = currentIndex();
360
361 QModelIndex current = moveCursor(cursorAction: MovePageDown, modifiers: Qt::NoModifier);
362
363 if (current.isValid()) {
364 setCurrentIndex(current);
365 if (!kateModel()->indexIsItem(index: current)) {
366 if (!nextCompletion()) {
367 previousCompletion();
368 }
369 }
370 }
371
372 return current != old;
373}
374
375bool KateCompletionTree::pageUp()
376{
377 QModelIndex old = currentIndex();
378 QModelIndex current = moveCursor(cursorAction: MovePageUp, modifiers: Qt::NoModifier);
379
380 if (current.isValid()) {
381 setCurrentIndex(current);
382 if (!kateModel()->indexIsItem(index: current)) {
383 if (!previousCompletion()) {
384 nextCompletion();
385 }
386 }
387 }
388 return current != old;
389}
390
391void KateCompletionTree::top()
392{
393 QModelIndex current = moveCursor(cursorAction: MoveHome, modifiers: Qt::NoModifier);
394 setCurrentIndex(current);
395
396 if (current.isValid()) {
397 setCurrentIndex(current);
398 if (!kateModel()->indexIsItem(index: current)) {
399 nextCompletion();
400 }
401 }
402}
403
404void KateCompletionTree::scheduleUpdate()
405{
406 m_resizeTimer->start(msec: 0);
407}
408
409void KateCompletionTree::bottom()
410{
411 QModelIndex current = moveCursor(cursorAction: MoveEnd, modifiers: Qt::NoModifier);
412 setCurrentIndex(current);
413
414 if (current.isValid()) {
415 setCurrentIndex(current);
416 if (!kateModel()->indexIsItem(index: current)) {
417 previousCompletion();
418 }
419 }
420}
421

source code of ktexteditor/src/completion/katecompletiontree.cpp