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 | int 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 | |
85 | void 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 | |
96 | KateCompletionWidget *KateCompletionTree::widget() const |
97 | { |
98 | return static_cast<KateCompletionWidget *>(const_cast<QObject *>(parent())); |
99 | } |
100 | |
101 | void 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 | */ |
122 | static bool measureColumnSizes(const KateCompletionTree *tree, |
123 | QModelIndex current, |
124 | QVarLengthArray<int, 8> &columnSize, |
125 | int ¤tYPos, |
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 | |
165 | void 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 | |
307 | void KateCompletionTree::initViewItemOption(QStyleOptionViewItem *option) const |
308 | { |
309 | QTreeView::initViewItemOption(option); |
310 | option->font = widget()->view()->renderer()->currentFont(); |
311 | } |
312 | |
313 | KateCompletionModel *KateCompletionTree::kateModel() const |
314 | { |
315 | return static_cast<KateCompletionModel *>(model()); |
316 | } |
317 | |
318 | bool 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 | |
342 | bool 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 | |
367 | bool 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 | |
385 | bool 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 | |
401 | void 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 | |
414 | void KateCompletionTree::scheduleUpdate() |
415 | { |
416 | m_resizeTimer->start(msec: 0); |
417 | } |
418 | |
419 | void 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 | |