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
75int KateCompletionTree::textColumnOffset() const
76{
77 QStyleOptionViewItem item;
78 item.index = currentIndex();
79 initViewItemOption(option: &item);
80 int margins = style()->pixelMetric(metric: QStyle::PM_FocusFrameHMargin) * 3;
81 int nameColumn = kateModel()->translateColumn(sourceColumn: KTextEditor::CodeCompletionModel::Name);
82 return margins + item.decorationSize.width() + columnViewportPosition(column: nameColumn);
83}
84
85void KateCompletionTree::scrollContentsBy(int dx, int dy)
86{
87 if (m_scrollingEnabled) {
88 QTreeView::scrollContentsBy(dx, dy);
89 }
90
91 if (isVisible()) {
92 scheduleUpdate();
93 }
94}
95
96KateCompletionWidget *KateCompletionTree::widget() const
97{
98 return static_cast<KateCompletionWidget *>(const_cast<QObject *>(parent()));
99}
100
101void KateCompletionTree::resizeColumnsSlot()
102{
103 if (model()) {
104 resizeColumns();
105
106 if (!widget()->docTip()->isHidden()) {
107 widget()->docTip()->updatePosition(completionWidget: widget());
108 }
109 }
110}
111
112/**
113 * Measure the width of visible columns.
114 *
115 * This iterates from the start index @p current down until a dead end is hit.
116 * In a tree model, it will recurse into child indices. Iteration is stopped if
117 * no more items are available, or the visited rows exceed the available @p maxHeight.
118 *
119 * If the model is a tree model, and @p current points to a leaf, and the max height
120 * is not exceeded, then iteration will continue from the next parent sibling.
121 */
122static bool measureColumnSizes(const KateCompletionTree *tree,
123 QModelIndex current,
124 QVarLengthArray<int, 8> &columnSize,
125 int &currentYPos,
126 const int maxHeight,
127 bool recursed = false)
128{
129 while (current.isValid() && currentYPos < maxHeight) {
130 currentYPos += tree->sizeHintForIndex(index: current).height();
131 const int row = current.row();
132 for (int a = 0; a < columnSize.size(); a++) {
133 QSize s = tree->sizeHintForIndex(index: current.sibling(arow: row, acolumn: a));
134 if (s.width() > 2000) {
135 qCDebug(LOG_KTE) << "got invalid size-hint of width " << s.width();
136 } else if (s.width() > columnSize[a]) {
137 columnSize[a] = s.width();
138 }
139 }
140
141 const QAbstractItemModel *model = current.model();
142 const int children = model->rowCount(parent: current);
143 if (children > 0) {
144 for (int i = 0; i < children; ++i) {
145 if (measureColumnSizes(tree, current: model->index(row: i, column: 0, parent: current), columnSize, currentYPos, maxHeight, recursed: true)) {
146 break;
147 }
148 }
149 }
150
151 QModelIndex oldCurrent = current;
152 current = current.sibling(arow: current.row() + 1, acolumn: 0);
153
154 // Are we at the end of a group? If yes, move up into the next group
155 // only do this when we did not recurse already
156 while (!recursed && !current.isValid() && oldCurrent.parent().isValid()) {
157 oldCurrent = oldCurrent.parent();
158 current = oldCurrent.sibling(arow: oldCurrent.row() + 1, acolumn: 0);
159 }
160 }
161
162 return currentYPos >= maxHeight;
163}
164
165void KateCompletionTree::resizeColumns(bool firstShow, bool forceResize)
166{
167 static bool preventRecursion = false;
168 if (preventRecursion) {
169 return;
170 }
171 m_resizeTimer->stop();
172
173 if (firstShow) {
174 forceResize = true;
175 }
176
177 preventRecursion = true;
178
179 widget()->setUpdatesEnabled(false);
180
181 int modelIndexOfName = kateModel()->translateColumn(sourceColumn: KTextEditor::CodeCompletionModel::Name);
182 int oldIndentWidth = columnViewportPosition(column: modelIndexOfName);
183
184 /// Step 1: Compute the needed column-sizes for the visible content
185 const int numColumns = model()->columnCount();
186 QVarLengthArray<int, 8> columnSize(numColumns);
187 for (int i = 0; i < numColumns; ++i) {
188 columnSize[i] = 0;
189 }
190 QModelIndex current = indexAt(p: QPoint(1, 1));
191 // const bool changed = current.isValid();
192 int currentYPos = 0;
193 measureColumnSizes(tree: this, current, columnSize, currentYPos, maxHeight: height());
194
195 auto totalColumnsWidth = 0;
196 auto originalViewportWidth = viewport()->width();
197
198 const int maxWidth = (widget()->parentWidget()->geometry().width()) / 2;
199
200 /// Step 2: Update column-sizes
201 // This contains several hacks to reduce the amount of resizing that happens. Generally,
202 // resizes only happen if a) More than a specific amount of space is saved by the resize, or
203 // b) the resizing is required so the list can show all of its contents.
204 int minimumResize = 0;
205 int maximumResize = 0;
206
207 for (int n = 0; n < numColumns; n++) {
208 totalColumnsWidth += columnSize[n];
209
210 int diff = columnSize[n] - columnWidth(column: n);
211 if (diff < minimumResize) {
212 minimumResize = diff;
213 }
214 if (diff > maximumResize) {
215 maximumResize = diff;
216 }
217 }
218
219 int noReduceTotalWidth = 0; // The total width of the widget of no columns are reduced
220 for (int n = 0; n < numColumns; n++) {
221 if (columnSize[n] < columnWidth(column: n)) {
222 noReduceTotalWidth += columnWidth(column: n);
223 } else {
224 noReduceTotalWidth += columnSize[n];
225 }
226 }
227
228 // Check whether we can afford to reduce none of the columns
229 // Only reduce size if we widget would else be too wide.
230 bool noReduce = noReduceTotalWidth < maxWidth && !forceResize;
231
232 if (noReduce) {
233 totalColumnsWidth = 0;
234 for (int n = 0; n < numColumns; n++) {
235 if (columnSize[n] < columnWidth(column: n)) {
236 columnSize[n] = columnWidth(column: n);
237 }
238
239 totalColumnsWidth += columnSize[n];
240 }
241 }
242
243 if (minimumResize > -40 && maximumResize == 0 && !forceResize) {
244 // No column needs to be exanded, and no column needs to be reduced by more than 40 pixels.
245 // To prevent flashing, do not resize at all.
246 totalColumnsWidth = 0;
247 for (int n = 0; n < numColumns; n++) {
248 columnSize[n] = columnWidth(column: n);
249 totalColumnsWidth += columnSize[n];
250 }
251 } else {
252 // viewport()->resize( 5000, viewport()->height() );
253 for (int n = 0; n < numColumns; n++) {
254 setColumnWidth(column: n, width: columnSize[n]);
255 }
256 // For the first column (which is arrow-down / arrow-right) we keep its width to 20
257 // to prevent glitches and weird resizes when we have no expanding items in the view
258 // qCDebug(LOG_KTE) << "resizing viewport to" << totalColumnsWidth;
259 viewport()->resize(w: totalColumnsWidth, h: viewport()->height());
260 }
261
262 /// Step 3: Update widget-size and -position
263
264 int scrollBarWidth = verticalScrollBar()->width();
265
266 int newIndentWidth = columnViewportPosition(column: modelIndexOfName);
267
268 int newWidth = qMin(a: maxWidth, b: qMax(a: 75, b: totalColumnsWidth));
269
270 if (newWidth == maxWidth) {
271 setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded);
272 } else {
273 setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
274 }
275
276 if (maximumResize > 0 || forceResize || oldIndentWidth != newIndentWidth) {
277 // qCDebug(LOG_KTE) << geometry() << "newWidth" << newWidth << "current width" << width() << "target width" << newWidth + scrollBarWidth;
278
279 if ((newWidth + scrollBarWidth) != width() && originalViewportWidth != totalColumnsWidth) {
280 auto width = newWidth + scrollBarWidth + 2;
281 widget()->resize(w: width, h: widget()->height());
282 resize(w: width - (2 * widget()->frameWidth()), h: widget()->height() - (2 * widget()->frameWidth()));
283 }
284
285 // qCDebug(LOG_KTE) << "created geometry:" << widget()->geometry() << geometry() << "newWidth" << newWidth << "viewport" << viewport()->width();
286
287 if (viewport()->width() > totalColumnsWidth) { // Set the size of the last column to fill the whole rest of the widget
288 setColumnWidth(column: numColumns - 1, width: viewport()->width() - columnViewportPosition(column: numColumns - 1));
289 }
290
291 /* for(int a = 0; a < numColumns; ++a)
292 qCDebug(LOG_KTE) << "column" << a << columnWidth(a) << "target:" << columnSize[a];*/
293
294 if (oldIndentWidth != newIndentWidth) {
295 if (!forceResize) {
296 preventRecursion = false;
297 resizeColumns(firstShow: true, forceResize: true);
298 }
299 }
300 }
301
302 widget()->setUpdatesEnabled(true);
303
304 preventRecursion = false;
305}
306
307void KateCompletionTree::initViewItemOption(QStyleOptionViewItem *option) const
308{
309 QTreeView::initViewItemOption(option);
310 option->font = widget()->view()->renderer()->currentFont();
311}
312
313KateCompletionModel *KateCompletionTree::kateModel() const
314{
315 return static_cast<KateCompletionModel *>(model());
316}
317
318bool KateCompletionTree::nextCompletion()
319{
320 QModelIndex current;
321 QModelIndex firstCurrent = currentIndex();
322
323 do {
324 QModelIndex oldCurrent = currentIndex();
325
326 current = moveCursor(cursorAction: MoveDown, modifiers: Qt::NoModifier);
327
328 if (current != oldCurrent && current.isValid()) {
329 setCurrentIndex(current);
330 } else {
331 if (firstCurrent.isValid()) {
332 setCurrentIndex(firstCurrent);
333 }
334 return false;
335 }
336
337 } while (!kateModel()->indexIsItem(index: current));
338
339 return true;
340}
341
342bool KateCompletionTree::previousCompletion()
343{
344 QModelIndex current;
345 QModelIndex firstCurrent = currentIndex();
346
347 do {
348 QModelIndex oldCurrent = currentIndex();
349
350 current = moveCursor(cursorAction: MoveUp, modifiers: Qt::NoModifier);
351
352 if (current != oldCurrent && current.isValid()) {
353 setCurrentIndex(current);
354
355 } else {
356 if (firstCurrent.isValid()) {
357 setCurrentIndex(firstCurrent);
358 }
359 return false;
360 }
361
362 } while (!kateModel()->indexIsItem(index: current));
363
364 return true;
365}
366
367bool KateCompletionTree::pageDown()
368{
369 QModelIndex old = currentIndex();
370
371 QModelIndex current = moveCursor(cursorAction: MovePageDown, modifiers: Qt::NoModifier);
372
373 if (current.isValid()) {
374 setCurrentIndex(current);
375 if (!kateModel()->indexIsItem(index: current)) {
376 if (!nextCompletion()) {
377 previousCompletion();
378 }
379 }
380 }
381
382 return current != old;
383}
384
385bool KateCompletionTree::pageUp()
386{
387 QModelIndex old = currentIndex();
388 QModelIndex current = moveCursor(cursorAction: MovePageUp, modifiers: Qt::NoModifier);
389
390 if (current.isValid()) {
391 setCurrentIndex(current);
392 if (!kateModel()->indexIsItem(index: current)) {
393 if (!previousCompletion()) {
394 nextCompletion();
395 }
396 }
397 }
398 return current != old;
399}
400
401void KateCompletionTree::top()
402{
403 QModelIndex current = moveCursor(cursorAction: MoveHome, modifiers: Qt::NoModifier);
404 setCurrentIndex(current);
405
406 if (current.isValid()) {
407 setCurrentIndex(current);
408 if (!kateModel()->indexIsItem(index: current)) {
409 nextCompletion();
410 }
411 }
412}
413
414void KateCompletionTree::scheduleUpdate()
415{
416 m_resizeTimer->start(msec: 0);
417}
418
419void KateCompletionTree::bottom()
420{
421 QModelIndex current = moveCursor(cursorAction: MoveEnd, modifiers: Qt::NoModifier);
422 setCurrentIndex(current);
423
424 if (current.isValid()) {
425 setCurrentIndex(current);
426 if (!kateModel()->indexIsItem(index: current)) {
427 previousCompletion();
428 }
429 }
430}
431

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