1 /****************************************************************************
2 **
3 ** Copyright (C) 2016 The Qt Company Ltd.
4 ** Contact: https://www.qt.io/licensing/
5 **
6 ** This file is part of Qt Creator.
7 **
8 ** Commercial License Usage
9 ** Licensees holding valid commercial Qt licenses may use this file in
10 ** accordance with the commercial license agreement provided with the
11 ** Software or, alternatively, in accordance with the terms contained in
12 ** a written agreement between you and The Qt Company. For licensing terms
13 ** and conditions see https://www.qt.io/terms-conditions. For further
14 ** information use the contact form at https://www.qt.io/contact-us.
15 **
16 ** GNU General Public License Usage
17 ** Alternatively, this file may be used under the terms of the GNU
18 ** General Public License version 3 as published by the Free Software
19 ** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
20 ** included in the packaging of this file. Please review the following
21 ** information to ensure the GNU General Public License requirements will
22 ** be met: https://www.gnu.org/licenses/gpl-3.0.html.
23 **
24 ****************************************************************************/
25
26 #include "texteditoroverlay.h"
27 #include "texteditor.h"
28
29 #include <QDebug>
30 #include <QMap>
31 #include <QPainter>
32 #include <QPainterPath>
33 #include <QTextBlock>
34
35 #include <algorithm>
36 #include <utils/qtcassert.h>
37
38 using namespace TextEditor;
39 using namespace TextEditor::Internal;
40
TextEditorOverlay(TextEditorWidget * editor)41 TextEditorOverlay::TextEditorOverlay(TextEditorWidget *editor) :
42 QObject(editor),
43 m_visible(false),
44 m_alpha(true),
45 m_borderWidth(1),
46 m_dropShadowWidth(2),
47 m_firstSelectionOriginalBegin(-1),
48 m_editor(editor),
49 m_viewport(editor->viewport())
50 {
51 }
52
update()53 void TextEditorOverlay::update()
54 {
55 if (m_visible)
56 m_viewport->update();
57 }
58
59
setVisible(bool b)60 void TextEditorOverlay::setVisible(bool b)
61 {
62 if (m_visible == b)
63 return;
64 m_visible = b;
65 if (!m_selections.isEmpty())
66 m_viewport->update();
67 }
68
clear()69 void TextEditorOverlay::clear()
70 {
71 if (m_selections.isEmpty())
72 return;
73 m_selections.clear();
74 m_firstSelectionOriginalBegin = -1;
75 update();
76 }
77
addOverlaySelection(int begin,int end,const QColor & fg,const QColor & bg,uint overlaySelectionFlags)78 void TextEditorOverlay::addOverlaySelection(int begin, int end,
79 const QColor &fg, const QColor &bg,
80 uint overlaySelectionFlags)
81 {
82 if (end < begin)
83 return;
84
85 QTextDocument *document = m_editor->document();
86
87 OverlaySelection selection;
88 selection.m_fg = fg;
89 selection.m_bg = bg;
90
91 selection.m_cursor_begin = QTextCursor(document);
92 selection.m_cursor_begin.setPosition(begin);
93 selection.m_cursor_end = QTextCursor(document);
94 selection.m_cursor_end.setPosition(end);
95
96 if (overlaySelectionFlags & ExpandBegin)
97 selection.m_cursor_begin.setKeepPositionOnInsert(true);
98
99 if (overlaySelectionFlags & LockSize)
100 selection.m_fixedLength = (end - begin);
101
102 selection.m_dropShadow = (overlaySelectionFlags & DropShadow);
103
104 if (m_selections.isEmpty())
105 m_firstSelectionOriginalBegin = begin;
106 else if (begin < m_firstSelectionOriginalBegin)
107 qWarning() << "overlay selections not in order";
108
109 m_selections.append(selection);
110 update();
111 }
112
113
addOverlaySelection(const QTextCursor & cursor,const QColor & fg,const QColor & bg,uint overlaySelectionFlags)114 void TextEditorOverlay::addOverlaySelection(const QTextCursor &cursor,
115 const QColor &fg, const QColor &bg,
116 uint overlaySelectionFlags)
117 {
118 addOverlaySelection(cursor.selectionStart(), cursor.selectionEnd(), fg, bg, overlaySelectionFlags);
119 }
120
rect() const121 QRect TextEditorOverlay::rect() const
122 {
123 return m_viewport->rect();
124 }
125
createSelectionPath(const QTextCursor & begin,const QTextCursor & end,const QRect & clip)126 QPainterPath TextEditorOverlay::createSelectionPath(const QTextCursor &begin, const QTextCursor &end,
127 const QRect &clip)
128 {
129 if (begin.isNull() || end.isNull() || begin.position() > end.position())
130 return QPainterPath();
131
132 QPointF offset = m_editor->contentOffset();
133 QRect viewportRect = rect();
134 QTextDocument *document = m_editor->document();
135
136 if (m_editor->blockBoundingGeometry(begin.block()).translated(offset).top() > clip.bottom() + 10
137 || m_editor->blockBoundingGeometry(end.block()).translated(offset).bottom() < clip.top() - 10
138 )
139 return QPainterPath(); // nothing of the selection is visible
140
141
142 QTextBlock block = begin.block();
143
144 if (block.blockNumber() < m_editor->firstVisibleBlock().blockNumber() - 4)
145 block = m_editor->document()->findBlockByNumber(m_editor->firstVisibleBlock().blockNumber() - 4);
146
147 bool inSelection = false;
148
149 QVector<QRectF> selection;
150
151 if (begin.position() == end.position()) {
152 // special case empty selections
153 const QRectF blockGeometry = m_editor->blockBoundingGeometry(block);
154 QTextLayout *blockLayout = block.layout();
155 int pos = begin.position() - begin.block().position();
156 QTextLine line = blockLayout->lineForTextPosition(pos);
157 QRectF lineRect = line.naturalTextRect();
158 int x = line.cursorToX(pos);
159 lineRect.setLeft(x - m_borderWidth);
160 lineRect.setRight(x + m_borderWidth);
161 selection += lineRect.translated(blockGeometry.topLeft());
162 } else {
163 for (; block.isValid() && block.blockNumber() <= end.blockNumber(); block = block.next()) {
164 if (! block.isVisible())
165 continue;
166
167 const QRectF blockGeometry = m_editor->blockBoundingGeometry(block);
168 QTextLayout *blockLayout = block.layout();
169
170 QTextLine line = blockLayout->lineAt(0);
171 bool firstOrLastBlock = false;
172
173 int beginChar = 0;
174 if (!inSelection) {
175 if (block == begin.block()) {
176 beginChar = begin.positionInBlock();
177 line = blockLayout->lineForTextPosition(beginChar);
178 firstOrLastBlock = true;
179 }
180 inSelection = true;
181 } else {
182 // while (beginChar < block.length() && document->characterAt(block.position() + beginChar).isSpace())
183 // ++beginChar;
184 // if (beginChar == block.length())
185 // beginChar = 0;
186 }
187
188 int lastLine = blockLayout->lineCount()-1;
189 int endChar = -1;
190 if (block == end.block()) {
191 endChar = end.positionInBlock();
192 lastLine = blockLayout->lineForTextPosition(endChar).lineNumber();
193 inSelection = false;
194 firstOrLastBlock = true;
195 } else {
196 endChar = block.length();
197 while (endChar > beginChar && document->characterAt(block.position() + endChar - 1).isSpace())
198 --endChar;
199 }
200
201 QRectF lineRect = line.naturalTextRect();
202 if (beginChar < endChar) {
203 lineRect.setLeft(line.cursorToX(beginChar));
204 if (line.lineNumber() == lastLine)
205 lineRect.setRight(line.cursorToX(endChar));
206 selection += lineRect.translated(blockGeometry.topLeft());
207
208 for (int lineIndex = line.lineNumber()+1; lineIndex <= lastLine; ++lineIndex) {
209 line = blockLayout->lineAt(lineIndex);
210 lineRect = line.naturalTextRect();
211 if (lineIndex == lastLine)
212 lineRect.setRight(line.cursorToX(endChar));
213 selection += lineRect.translated(blockGeometry.topLeft());
214 }
215 } else { // empty lines
216 const int emptyLineSelectionSize = 16;
217 if (!firstOrLastBlock && !selection.isEmpty()) { // middle
218 lineRect.setLeft(selection.last().left());
219 } else if (inSelection) { // first line
220 lineRect.setLeft(line.cursorToX(beginChar));
221 } else { // last line
222 if (endChar == 0)
223 break;
224 lineRect.setLeft(line.cursorToX(endChar) - emptyLineSelectionSize);
225 }
226 lineRect.setRight(lineRect.left() + emptyLineSelectionSize);
227 selection += lineRect.translated(blockGeometry.topLeft());
228 }
229
230 if (!inSelection)
231 break;
232
233 if (blockGeometry.translated(offset).y() > 2*viewportRect.height())
234 break;
235 }
236 }
237
238
239 if (selection.isEmpty())
240 return QPainterPath();
241
242 QVector<QPointF> points;
243
244 const int margin = m_borderWidth/2;
245 const int extra = 0;
246
247 const QRectF &firstSelection = selection.at(0);
248 points += (firstSelection.topLeft() + firstSelection.topRight()) / 2 + QPointF(0, -margin);
249 points += firstSelection.topRight() + QPointF(margin+1, -margin);
250 points += firstSelection.bottomRight() + QPointF(margin+1, 0);
251
252 const int count = selection.count();
253 for (int i = 1; i < count-1; ++i) {
254 qreal x = std::max({selection.at(i - 1).right(),
255 selection.at(i).right(),
256 selection.at(i + 1).right()})
257 + margin;
258
259 points += QPointF(x+1, selection.at(i).top());
260 points += QPointF(x+1, selection.at(i).bottom());
261 }
262
263 const QRectF &lastSelection = selection.at(count-1);
264 points += lastSelection.topRight() + QPointF(margin+1, 0);
265 points += lastSelection.bottomRight() + QPointF(margin+1, margin+extra);
266 points += lastSelection.bottomLeft() + QPointF(-margin, margin+extra);
267 points += lastSelection.topLeft() + QPointF(-margin, 0);
268
269 for (int i = count-2; i > 0; --i) {
270 qreal x = std::min({selection.at(i - 1).left(),
271 selection.at(i).left(),
272 selection.at(i + 1).left()})
273 - margin;
274
275 points += QPointF(x, selection.at(i).bottom()+extra);
276 points += QPointF(x, selection.at(i).top());
277 }
278
279 points += firstSelection.bottomLeft() + QPointF(-margin, extra);
280 points += firstSelection.topLeft() + QPointF(-margin, -margin);
281
282
283 QPainterPath path;
284 const int corner = 4;
285 path.moveTo(points.at(0));
286 points += points.at(0);
287 QPointF previous = points.at(0);
288 for (int i = 1; i < points.size(); ++i) {
289 QPointF point = points.at(i);
290 if (point.y() == previous.y() && qAbs(point.x() - previous.x()) > 2*corner) {
291 QPointF tmp = QPointF(previous.x() + corner * ((point.x() > previous.x())?1:-1), previous.y());
292 path.quadTo(previous, tmp);
293 previous = tmp;
294 i--;
295 continue;
296 } else if (point.x() == previous.x() && qAbs(point.y() - previous.y()) > 2*corner) {
297 QPointF tmp = QPointF(previous.x(), previous.y() + corner * ((point.y() > previous.y())?1:-1));
298 path.quadTo(previous, tmp);
299 previous = tmp;
300 i--;
301 continue;
302 }
303
304
305 QPointF target = (previous + point) / 2;
306 path.quadTo(previous, target);
307 previous = points.at(i);
308 }
309 path.closeSubpath();
310 path.translate(offset);
311 return path.simplified();
312 }
313
paintSelection(QPainter * painter,const OverlaySelection & selection)314 void TextEditorOverlay::paintSelection(QPainter *painter,
315 const OverlaySelection &selection)
316 {
317
318 QTextCursor begin = selection.m_cursor_begin;
319
320 const QTextCursor &end= selection.m_cursor_end;
321 const QColor &fg = selection.m_fg;
322 const QColor &bg = selection.m_bg;
323
324
325 if (begin.isNull() || end.isNull() || begin.position() > end.position() || !bg.isValid())
326 return;
327
328 QPainterPath path = createSelectionPath(begin, end, m_editor->viewport()->rect());
329
330 painter->save();
331 QColor penColor = fg;
332 if (m_alpha)
333 penColor.setAlpha(220);
334 QPen pen(penColor, m_borderWidth);
335 painter->translate(-.5, -.5);
336
337 QRectF pathRect = path.controlPointRect();
338
339 if (!m_alpha || begin.blockNumber() != end.blockNumber()) {
340 // gradients are too slow for larger selections :(
341 QColor col = bg;
342 if (m_alpha)
343 col.setAlpha(50);
344 painter->setBrush(col);
345 } else {
346 QLinearGradient linearGrad(pathRect.topLeft(), pathRect.bottomLeft());
347 QColor col1 = fg.lighter(150);
348 col1.setAlpha(20);
349 QColor col2 = fg;
350 col2.setAlpha(80);
351 linearGrad.setColorAt(0, col1);
352 linearGrad.setColorAt(1, col2);
353 painter->setBrush(QBrush(linearGrad));
354 }
355
356 painter->setRenderHint(QPainter::Antialiasing);
357
358 if (selection.m_dropShadow) {
359 painter->save();
360 QPainterPath shadow = path;
361 shadow.translate(m_dropShadowWidth, m_dropShadowWidth);
362 QPainterPath clip;
363 clip.addRect(m_editor->viewport()->rect());
364 painter->setClipPath(clip - path);
365 painter->fillPath(shadow, QColor(0, 0, 0, 100));
366 painter->restore();
367 }
368
369 pen.setJoinStyle(Qt::RoundJoin);
370 painter->setPen(pen);
371 painter->drawPath(path);
372 painter->restore();
373 }
374
fillSelection(QPainter * painter,const OverlaySelection & selection,const QColor & color)375 void TextEditorOverlay::fillSelection(QPainter *painter,
376 const OverlaySelection &selection,
377 const QColor &color)
378 {
379 const QTextCursor &begin = selection.m_cursor_begin;
380 const QTextCursor &end= selection.m_cursor_end;
381 if (begin.isNull() || end.isNull() || begin.position() > end.position())
382 return;
383
384 QPainterPath path = createSelectionPath(begin, end, m_editor->viewport()->rect());
385
386 painter->save();
387 painter->translate(-.5, -.5);
388 painter->setRenderHint(QPainter::Antialiasing);
389 painter->fillPath(path, color);
390 painter->restore();
391 }
392
paint(QPainter * painter,const QRect & clip)393 void TextEditorOverlay::paint(QPainter *painter, const QRect &clip)
394 {
395 Q_UNUSED(clip)
396 for (int i = m_selections.size()-1; i >= 0; --i) {
397 const OverlaySelection &selection = m_selections.at(i);
398 if (selection.m_dropShadow)
399 continue;
400 if (selection.m_fixedLength >= 0
401 && selection.m_cursor_end.position() - selection.m_cursor_begin.position()
402 != selection.m_fixedLength)
403 continue;
404
405 paintSelection(painter, selection);
406 }
407 for (int i = m_selections.size()-1; i >= 0; --i) {
408 const OverlaySelection &selection = m_selections.at(i);
409 if (!selection.m_dropShadow)
410 continue;
411 if (selection.m_fixedLength >= 0
412 && selection.m_cursor_end.position() - selection.m_cursor_begin.position()
413 != selection.m_fixedLength)
414 continue;
415
416 paintSelection(painter, selection);
417 }
418 }
419
cursorForSelection(const OverlaySelection & selection) const420 QTextCursor TextEditorOverlay::cursorForSelection(const OverlaySelection &selection) const
421 {
422 QTextCursor cursor = selection.m_cursor_begin;
423 cursor.setPosition(selection.m_cursor_begin.position());
424 cursor.setKeepPositionOnInsert(false);
425 if (!cursor.isNull())
426 cursor.setPosition(selection.m_cursor_end.position(), QTextCursor::KeepAnchor);
427 return cursor;
428 }
429
cursorForIndex(int selectionIndex) const430 QTextCursor TextEditorOverlay::cursorForIndex(int selectionIndex) const
431 {
432 return cursorForSelection(m_selections.value(selectionIndex));
433 }
434
fill(QPainter * painter,const QColor & color,const QRect & clip)435 void TextEditorOverlay::fill(QPainter *painter, const QColor &color, const QRect &clip)
436 {
437 Q_UNUSED(clip)
438 for (int i = m_selections.size()-1; i >= 0; --i) {
439 const OverlaySelection &selection = m_selections.at(i);
440 if (selection.m_dropShadow)
441 continue;
442 if (selection.m_fixedLength >= 0
443 && selection.m_cursor_end.position() - selection.m_cursor_begin.position()
444 != selection.m_fixedLength)
445 continue;
446
447 fillSelection(painter, selection, color);
448 }
449 for (int i = m_selections.size()-1; i >= 0; --i) {
450 const OverlaySelection &selection = m_selections.at(i);
451 if (!selection.m_dropShadow)
452 continue;
453 if (selection.m_fixedLength >= 0
454 && selection.m_cursor_end.position() - selection.m_cursor_begin.position()
455 != selection.m_fixedLength)
456 continue;
457
458 fillSelection(painter, selection, color);
459 }
460 }
461
hasFirstSelectionBeginMoved() const462 bool TextEditorOverlay::hasFirstSelectionBeginMoved() const
463 {
464 if (m_firstSelectionOriginalBegin == -1 || m_selections.isEmpty())
465 return false;
466 return m_selections.at(0).m_cursor_begin.position() != m_firstSelectionOriginalBegin;
467 }
468