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