texteditoroverlay.cpp 17.8 KB
Newer Older
mae's avatar
mae committed
1 2 3 4
/**************************************************************************
**
** This file is part of Qt Creator
**
hjk's avatar
hjk committed
5
** Copyright (c) 2010 Nokia Corporation and/or its subsidiary(-ies).
mae's avatar
mae committed
6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
**
** Contact: Nokia Corporation (qt-info@nokia.com)
**
** Commercial Usage
**
** Licensees holding valid Qt Commercial licenses may use this file in
** accordance with the Qt Commercial License Agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and Nokia.
**
** GNU Lesser General Public License Usage
**
** Alternatively, this file may be used under the terms of the GNU Lesser
** General Public License version 2.1 as published by the Free Software
** Foundation and appearing in the file LICENSE.LGPL included in the
** packaging of this file.  Please review the following information to
** ensure the GNU Lesser General Public License version 2.1 requirements
** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
**
** If you are unsure which license is appropriate for your use, please
** contact the sales department at http://qt.nokia.com/contact.
**
**************************************************************************/

30
#include "texteditoroverlay.h"
31
#include "basetexteditor.h"
32

33
#include <QtCore/QMap>
34 35
#include <QtGui/QPainter>
#include <QtGui/QTextBlock>
36 37 38 39

using namespace TextEditor;
using namespace TextEditor::Internal;

40 41 42 43 44 45 46 47 48
TextEditorOverlay::TextEditorOverlay(BaseTextEditor *editor) :
    QObject(editor),
    m_visible(false),
    m_borderWidth(1),
    m_dropShadowWidth(2),
    m_alpha(true),
    m_editor(editor),
    m_viewport(editor->viewport())
{
49 50 51 52 53 54 55 56 57 58 59 60 61
}

void TextEditorOverlay::update()
{
    if (m_visible)
        m_viewport->update();
}


void TextEditorOverlay::setVisible(bool b)
{
    if (m_visible == b)
        return;
62
    m_visible = b;
mae's avatar
mae committed
63 64
    if (!m_selections.isEmpty())
        m_viewport->update();
65 66 67 68
}

void TextEditorOverlay::clear()
{
mae's avatar
mae committed
69 70
    if (m_selections.isEmpty())
        return;
71 72 73 74
    m_selections.clear();
    update();
}

75
void TextEditorOverlay::addOverlaySelection(int begin, int end,
76
                                            const QColor &fg, const QColor &bg,
mae's avatar
mae committed
77
                                            uint overlaySelectionFlags)
78
{
mae's avatar
mae committed
79
    if (end < begin)
80 81 82 83 84
        return;

    QTextDocument *document = m_editor->document();

    OverlaySelection selection;
85 86
    selection.m_fg = fg;
    selection.m_bg = bg;
87

mae's avatar
mae committed
88 89 90
    selection.m_cursor_begin = QTextCursor(document->docHandle(), begin);
    selection.m_cursor_end = QTextCursor(document->docHandle(), end);

91 92
    if (overlaySelectionFlags & ExpandBegin) {
        if (begin > 0 && begin < end) { // not empty
mae's avatar
mae committed
93
            selection.m_cursor_begin.setKeepPositionOnInsert(true);
94 95 96
        }
    }

97

98
    if (overlaySelectionFlags & LockSize)
mae's avatar
mae committed
99 100
        selection.m_fixedLength = (end - begin);

101

102 103 104
    selection.m_dropShadow = (overlaySelectionFlags & DropShadow);

    m_selections.append(selection);
105 106 107 108
    update();
}


109
void TextEditorOverlay::addOverlaySelection(const QTextCursor &cursor,
110
                                            const QColor &fg, const QColor &bg,
mae's avatar
mae committed
111
                                            uint overlaySelectionFlags)
112
{
mae's avatar
mae committed
113
    addOverlaySelection(cursor.selectionStart(), cursor.selectionEnd(), fg, bg, overlaySelectionFlags);
114 115 116 117 118 119 120
}

QRect TextEditorOverlay::rect() const
{
    return m_viewport->rect();
}

121
QPainterPath TextEditorOverlay::createSelectionPath(const QTextCursor &begin, const QTextCursor &end,
mae's avatar
mae committed
122
                                                    const QRect &clip)
123 124
{
    if (begin.isNull() || end.isNull() || begin.position() > end.position())
125
        return QPainterPath();
126 127 128 129 130

    QPointF offset = m_editor->contentOffset();
    QRect viewportRect = rect();
    QTextDocument *document = m_editor->document();

131 132
    if (m_editor->blockBoundingGeometry(begin.block()).translated(offset).top() > clip.bottom() + 10
        || m_editor->blockBoundingGeometry(end.block()).translated(offset).bottom() < clip.top() - 10
133
        )
134
        return QPainterPath(); // nothing of the selection is visible
135

136

137 138
    QTextBlock block = begin.block();

mae's avatar
mae committed
139 140 141
    if (block.blockNumber() < m_editor->firstVisibleBlock().blockNumber() - 4)
        block = m_editor->document()->findBlockByNumber(m_editor->firstVisibleBlock().blockNumber() - 4);

142 143 144 145
    bool inSelection = false;

    QVector<QRectF> selection;

146 147
    if (begin.position() == end.position()) {
        // special case empty selections
148 149
        const QRectF blockGeometry = m_editor->blockBoundingGeometry(block);
        QTextLayout *blockLayout = block.layout();
150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165
        int pos = begin.position() - begin.block().position();
        QTextLine line = blockLayout->lineForTextPosition(pos);
        QRectF lineRect = line.naturalTextRect();
        int x = line.cursorToX(pos);
        lineRect.setLeft(x - m_borderWidth);
        lineRect.setRight(x + m_borderWidth);
        selection += lineRect.translated(blockGeometry.topLeft());
    } else {
        for (; block.isValid() && block.blockNumber() <= end.blockNumber(); block = block.next()) {
            if (! block.isVisible())
                continue;

            const QRectF blockGeometry = m_editor->blockBoundingGeometry(block);
            QTextLayout *blockLayout = block.layout();

            QTextLine line = blockLayout->lineAt(0);
166
            bool firstOrLastBlock = false;
167 168 169

            int beginChar = 0;
            if (!inSelection) {
170 171 172 173 174
                if (block == begin.block()) {
                    beginChar = begin.positionInBlock();
                    line = blockLayout->lineForTextPosition(beginChar);
                    firstOrLastBlock = true;
                }
175 176
                inSelection = true;
            } else {
mae's avatar
mae committed
177 178 179 180
//                while (beginChar < block.length() && document->characterAt(block.position() + beginChar).isSpace())
//                    ++beginChar;
//                if (beginChar == block.length())
//                    beginChar = 0;
181
            }
182

183 184 185
            int lastLine = blockLayout->lineCount()-1;
            int endChar = -1;
            if (block == end.block()) {
186
                endChar = end.positionInBlock();
187 188
                lastLine = blockLayout->lineForTextPosition(endChar).lineNumber();
                inSelection = false;
189
                firstOrLastBlock = true;
190 191 192 193 194
            } else {
                endChar = block.length();
                while (endChar > beginChar && document->characterAt(block.position() + endChar - 1).isSpace())
                    --endChar;
            }
195

196 197 198 199
            QRectF lineRect = line.naturalTextRect();
            if (beginChar < endChar) {
                lineRect.setLeft(line.cursorToX(beginChar));
                if (line.lineNumber() == lastLine)
200 201
                    lineRect.setRight(line.cursorToX(endChar));
                selection += lineRect.translated(blockGeometry.topLeft());
202 203 204 205 206 207 208 209

                for (int lineIndex = line.lineNumber()+1; lineIndex <= lastLine; ++lineIndex) {
                    line = blockLayout->lineAt(lineIndex);
                    lineRect = line.naturalTextRect();
                    if (lineIndex == lastLine)
                        lineRect.setRight(line.cursorToX(endChar));
                    selection += lineRect.translated(blockGeometry.topLeft());
                }
mae's avatar
mae committed
210
            } else { // empty lines
211 212
                const int emptyLineSelectionSize = 16;
                if (!firstOrLastBlock && !selection.isEmpty()) { // middle
213
                    lineRect.setLeft(selection.last().left());
214 215 216 217 218 219 220 221
                } else if (inSelection) { // first line
                    lineRect.setLeft(line.cursorToX(beginChar));
                } else { // last line
                    if (endChar == 0)
                        break;
                    lineRect.setLeft(line.cursorToX(endChar) - emptyLineSelectionSize);
                }
                lineRect.setRight(lineRect.left() + emptyLineSelectionSize);
222
                selection += lineRect.translated(blockGeometry.topLeft());
223 224
            }

225 226
            if (!inSelection)
                break;
227

228 229 230
            if (blockGeometry.translated(offset).y() > 2*viewportRect.height())
                break;
        }
231 232 233 234
    }


    if (selection.isEmpty())
235
        return QPainterPath();
236 237 238 239

    QVector<QPointF> points;

    const int margin = m_borderWidth/2;
240
    const int extra = 0;
241 242 243 244 245 246 247 248 249 250 251 252 253 254

    points += (selection.at(0).topLeft() + selection.at(0).topRight()) / 2 + QPointF(0, -margin);
    points += selection.at(0).topRight() + QPointF(margin+1, -margin);
    points += selection.at(0).bottomRight() + QPointF(margin+1, 0);


    for(int i = 1; i < selection.count()-1; ++i) {

#define MAX3(a,b,c) qMax(a, qMax(b,c))
        qreal x = MAX3(selection.at(i-1).right(),
                       selection.at(i).right(),
                       selection.at(i+1).right()) + margin;

        points += QPointF(x+1, selection.at(i).top());
mae's avatar
mae committed
255
        points += QPointF(x+1, selection.at(i).bottom());
256 257 258 259

    }

    points += selection.at(selection.count()-1).topRight() + QPointF(margin+1, 0);
260 261
    points += selection.at(selection.count()-1).bottomRight() + QPointF(margin+1, margin+extra);
    points += selection.at(selection.count()-1).bottomLeft() + QPointF(-margin, margin+extra);
262 263 264 265 266 267 268 269
    points += selection.at(selection.count()-1).topLeft() + QPointF(-margin, 0);

    for(int i = selection.count()-2; i > 0; --i) {
#define MIN3(a,b,c) qMin(a, qMin(b,c))
        qreal x = MIN3(selection.at(i-1).left(),
                       selection.at(i).left(),
                       selection.at(i+1).left()) - margin;

270
        points += QPointF(x, selection.at(i).bottom()+extra);
271 272 273
        points += QPointF(x, selection.at(i).top());
    }

274
    points += selection.at(0).bottomLeft() + QPointF(-margin, extra);
275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304
    points += selection.at(0).topLeft() + QPointF(-margin, -margin);


    QPainterPath path;
    const int corner = 4;
    path.moveTo(points.at(0));
    points += points.at(0);
    QPointF previous = points.at(0);
    for (int i = 1; i < points.size(); ++i) {
        QPointF point = points.at(i);
        if (point.y() == previous.y() && qAbs(point.x() - previous.x()) > 2*corner) {
            QPointF tmp = QPointF(previous.x() + corner * ((point.x() > previous.x())?1:-1), previous.y());
            path.quadTo(previous, tmp);
            previous = tmp;
            i--;
            continue;
        } else if (point.x() == previous.x() && qAbs(point.y() - previous.y()) > 2*corner) {
            QPointF tmp = QPointF(previous.x(), previous.y() + corner * ((point.y() > previous.y())?1:-1));
            path.quadTo(previous, tmp);
            previous = tmp;
            i--;
            continue;
        }


        QPointF target = (previous + point) / 2;
        path.quadTo(previous, target);
        previous = points.at(i);
    }
    path.closeSubpath();
305 306 307 308
    path.translate(offset);
    return path;
}

309 310
void TextEditorOverlay::paintSelection(QPainter *painter,
                                       const OverlaySelection &selection)
311
{
312

313 314
    QTextCursor begin = selection.m_cursor_begin;

315 316 317 318 319
    const QTextCursor &end= selection.m_cursor_end;
    const QColor &fg = selection.m_fg;
    const QColor &bg = selection.m_bg;


320 321 322
    if (begin.isNull()
        || end.isNull()
        || begin.position() > end.position())
323 324
        return;

mae's avatar
mae committed
325
    QPainterPath path = createSelectionPath(begin, end, m_editor->viewport()->rect());
326 327

    painter->save();
328
    QColor penColor = fg;
329 330
    if (m_alpha)
        penColor.setAlpha(220);
331
    QPen pen(penColor, m_borderWidth);
332
    painter->translate(-.5, -.5);
333 334 335

    QRectF pathRect = path.controlPointRect();

336
    if (bg.isValid()) {
337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352
        if (!m_alpha || begin.blockNumber() != end.blockNumber()) {
            // gradients are too slow for larger selections :(
            QColor col = bg;
            if (m_alpha)
                col.setAlpha(50);
            painter->setBrush(col);
        } else {
            QLinearGradient linearGrad(pathRect.topLeft(), pathRect.bottomLeft());
            QColor col1 = fg.lighter(150);
            col1.setAlpha(20);
            QColor col2 = fg;
            col2.setAlpha(80);
            linearGrad.setColorAt(0, col1);
            linearGrad.setColorAt(1, col2);
            painter->setBrush(QBrush(linearGrad));
        }
353 354 355
    } else {
        painter->setBrush(QBrush());
    }
356

357
    painter->setRenderHint(QPainter::Antialiasing);
358 359 360

    if (selection.m_dropShadow) {
        painter->save();
361 362
        QPainterPath shadow = path;
        shadow.translate(m_dropShadowWidth, m_dropShadowWidth);
Roberto Raggi's avatar
Roberto Raggi committed
363 364 365
        QPainterPath clip;
        clip.addRect(m_editor->viewport()->rect());
        painter->setClipPath(clip - path);
366
        painter->fillPath(shadow, QColor(0, 0, 0, 100));
367 368 369
        painter->restore();
    }

370 371 372 373 374 375
    pen.setJoinStyle(Qt::RoundJoin);
    painter->setPen(pen);
    painter->drawPath(path);
    painter->restore();
}

376 377 378
void TextEditorOverlay::fillSelection(QPainter *painter,
                                      const OverlaySelection &selection,
                                      const QColor &color)
379
{
380 381
    const QTextCursor &begin = selection.m_cursor_begin;
    const QTextCursor &end= selection.m_cursor_end;
382 383 384
    if (begin.isNull() || end.isNull() || begin.position() > end.position())
        return;

mae's avatar
mae committed
385
    QPainterPath path = createSelectionPath(begin, end, m_editor->viewport()->rect());
386 387 388 389 390 391 392

    painter->save();
    painter->translate(-.5, -.5);
    painter->setRenderHint(QPainter::Antialiasing);
    painter->fillPath(path, color);
    painter->restore();
}
393 394 395 396

void TextEditorOverlay::paint(QPainter *painter, const QRect &clip)
{
    Q_UNUSED(clip);
397
    for (int i = m_selections.size()-1; i >= 0; --i) {
398
        const OverlaySelection &selection = m_selections.at(i);
399 400 401 402 403 404 405 406 407 408 409 410 411
        if (selection.m_dropShadow)
            continue;
        if (selection.m_fixedLength >= 0
            && selection.m_cursor_end.position() - selection.m_cursor_begin.position()
            != selection.m_fixedLength)
            continue;

        paintSelection(painter, selection);
    }
    for (int i = m_selections.size()-1; i >= 0; --i) {
        const OverlaySelection &selection = m_selections.at(i);
        if (!selection.m_dropShadow)
            continue;
mae's avatar
mae committed
412 413 414 415 416
        if (selection.m_fixedLength >= 0
            && selection.m_cursor_end.position() - selection.m_cursor_begin.position()
            != selection.m_fixedLength)
            continue;

417
        paintSelection(painter, selection);
418 419 420 421 422 423
    }
}

void TextEditorOverlay::fill(QPainter *painter, const QColor &color, const QRect &clip)
{
    Q_UNUSED(clip);
424 425 426 427 428 429 430 431 432 433 434 435
    for (int i = m_selections.size()-1; i >= 0; --i) {
        const OverlaySelection &selection = m_selections.at(i);
        if (selection.m_dropShadow)
            continue;
        if (selection.m_fixedLength >= 0
            && selection.m_cursor_end.position() - selection.m_cursor_begin.position()
            != selection.m_fixedLength)
            continue;

        fillSelection(painter, selection, color);
    }
    for (int i = m_selections.size()-1; i >= 0; --i) {
436
        const OverlaySelection &selection = m_selections.at(i);
437 438
        if (!selection.m_dropShadow)
            continue;
439 440 441 442 443
        if (selection.m_fixedLength >= 0
            && selection.m_cursor_end.position() - selection.m_cursor_begin.position()
            != selection.m_fixedLength)
            continue;

444
        fillSelection(painter, selection, color);
445 446 447
    }
}

448 449 450
/*! \returns true if any selection contains \a cursor, where a cursor on the
             start or end of a selection is counted as contained.
*/
451
bool TextEditorOverlay::hasCursorInSelection(const QTextCursor &cursor) const
452 453 454 455 456 457 458
{
    if (selectionIndexForCursor(cursor) != -1)
        return true;
    return false;
}

int TextEditorOverlay::selectionIndexForCursor(const QTextCursor &cursor) const
459 460 461
{
    for (int i = 0; i < m_selections.size(); ++i) {
        const OverlaySelection &selection = m_selections.at(i);
462
        if (cursor.position() >= selection.m_cursor_begin.position()
463
            && cursor.position() <= selection.m_cursor_end.position())
464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525
            return i;
    }
    return -1;
}

QString TextEditorOverlay::selectionText(int selectionIndex) const
{
    return assembleCursorForSelection(selectionIndex).selectedText();
}

QTextCursor TextEditorOverlay::assembleCursorForSelection(int selectionIndex) const
{
    const OverlaySelection &selection = m_selections.at(selectionIndex);
    QTextCursor cursor(m_editor->document());
    cursor.setPosition(selection.m_cursor_begin.position());
    cursor.setPosition(selection.m_cursor_end.position(), QTextCursor::KeepAnchor);
    return cursor;
}

void TextEditorOverlay::mapEquivalentSelections()
{
    m_equivalentSelections.clear();
    m_equivalentSelections.resize(m_selections.size());

    QMap<QString, int> all;
    for (int i = 0; i < m_selections.size(); ++i)
        all.insertMulti(selectionText(i), i);

    const QList<QString> &uniqueKeys = all.uniqueKeys();
    foreach (const QString &key, uniqueKeys) {
        QList<int> indexes;
        QMap<QString, int>::const_iterator lbit = all.lowerBound(key);
        QMap<QString, int>::const_iterator ubit = all.upperBound(key);
        while (lbit != ubit) {
            indexes.append(lbit.value());
            ++lbit;
        }

        foreach (int index, indexes)
            m_equivalentSelections[index] = indexes;
    }
}

void TextEditorOverlay::updateEquivalentSelections(const QTextCursor &cursor)
{
    int selectionIndex = selectionIndexForCursor(cursor);
    if (selectionIndex == -1)
        return;

    const QString &currentText = selectionText(selectionIndex);
    const QList<int> &equivalents = m_equivalentSelections.at(selectionIndex);
    foreach (int i, equivalents) {
        if (i == selectionIndex)
            continue;
        const QString &equivalentText = selectionText(i);
        if (currentText != equivalentText) {
            QTextCursor selectionCursor = assembleCursorForSelection(i);
            selectionCursor.joinPreviousEditBlock();
            selectionCursor.removeSelectedText();
            selectionCursor.insertText(currentText);
            selectionCursor.endEditBlock();
        }
526 527
    }
}