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 | |
27 | KateCompletionTree::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 | |
60 | void KateCompletionTree::currentChanged(const QModelIndex ¤t, 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 | |
70 | void KateCompletionTree::setScrollingEnabled(bool enabled) |
71 | { |
72 | m_scrollingEnabled = enabled; |
73 | } |
74 | |
75 | void 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 | |
86 | KateCompletionWidget *KateCompletionTree::widget() const |
87 | { |
88 | return static_cast<KateCompletionWidget *>(const_cast<QObject *>(parent())); |
89 | } |
90 | |
91 | void 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 | */ |
112 | static bool measureColumnSizes(const KateCompletionTree *tree, |
113 | QModelIndex current, |
114 | QVarLengthArray<int, 8> &columnSize, |
115 | int ¤tYPos, |
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 | |
155 | void 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 | |
297 | void KateCompletionTree::initViewItemOption(QStyleOptionViewItem *option) const |
298 | { |
299 | QTreeView::initViewItemOption(option); |
300 | option->font = widget()->view()->renderer()->currentFont(); |
301 | } |
302 | |
303 | KateCompletionModel *KateCompletionTree::kateModel() const |
304 | { |
305 | return static_cast<KateCompletionModel *>(model()); |
306 | } |
307 | |
308 | bool 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 | |
332 | bool 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 | |
357 | bool 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 | |
375 | bool 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 | |
391 | void 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 | |
404 | void KateCompletionTree::scheduleUpdate() |
405 | { |
406 | m_resizeTimer->start(msec: 0); |
407 | } |
408 | |
409 | void 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 | |