Skip to content
Snippets Groups Projects
basetexteditor.cpp 179 KiB
Newer Older
con's avatar
con committed
                        xoffset += 2;
                    }
                }
            }

            if (d->m_codeFoldingVisible) {

                bool collapseThis = false;
                bool collapseAfter = false;
                bool hasClosingCollapse = false;

                if (TextBlockUserData *userData = static_cast<TextBlockUserData*>(block.userData())) {
                    if (!userData->ifdefedOut()) {
                        collapseAfter = (userData->collapseMode() == TextBlockUserData::CollapseAfter);
                        collapseThis = (userData->collapseMode() == TextBlockUserData::CollapseThis);
                        hasClosingCollapse = userData->hasClosingCollapse() && (previousBraceDepth > 0);
                    }
                }

                int extraAreaHighlightCollapseBlockNumber = -1;
                int extraAreaHighlightCollapseEndBlockNumber = -1;
                bool endIsVisible = false;
                if (!d->m_highlightBlocksInfo.isEmpty()) {
                    extraAreaHighlightCollapseBlockNumber =  d->m_highlightBlocksInfo.open.last();
                    extraAreaHighlightCollapseEndBlockNumber =  d->m_highlightBlocksInfo.close.first();
                    endIsVisible = doc->findBlockByNumber(extraAreaHighlightCollapseEndBlockNumber).isVisible();

mae's avatar
mae committed
                    QTextBlock before = doc->findBlockByNumber(extraAreaHighlightCollapseBlockNumber-1);
                    if (TextBlockUserData::hasCollapseAfter(before)) {
                        extraAreaHighlightCollapseBlockNumber--;
                TextBlockUserData *nextBlockUserData = BaseTextDocumentLayout::testUserData(nextBlock);

                bool collapseNext = nextBlockUserData
                                    && nextBlockUserData->collapseMode() == TextBlockUserData::CollapseThis
                                    && !nextBlockUserData->ifdefedOut();

                bool nextHasClosingCollapse = nextBlockUserData
                                              && nextBlockUserData->hasClosingCollapseInside()
                                              && nextBlockUserData->ifdefedOut();

                bool drawBox = ((collapseAfter || collapseNext) && !nextHasClosingCollapse);
                bool active = blockNumber == extraAreaHighlightCollapseBlockNumber;
                bool drawStart = drawBox && active;
                bool drawEnd = blockNumber == extraAreaHighlightCollapseEndBlockNumber || (drawStart && !endIsVisible);
                bool hovered = blockNumber >= extraAreaHighlightCollapseBlockNumber
                               && blockNumber <= extraAreaHighlightCollapseEndBlockNumber;
                int boxWidth = collapseBoxWidth(fm);
                    int itop = qRound(top);
                    int ibottom = qRound(bottom);
                    QRect box = QRect(extraAreaWidth + 1, itop, boxWidth - 2, ibottom - itop);
                    drawRectBox(&painter, box, drawStart, drawEnd, pal);
                    bool expanded = nextBlock.isVisible();
                    int size = boxWidth/4;
                    QRect box(extraAreaWidth + size, top + size,
                              2 * (size) + 1, 2 * (size) + 1);
                    drawFoldingMarker(&painter, pal, box, expanded, active, hovered);
con's avatar
con committed
            }

            painter.restore();
        }


        if (d->m_revisionsVisible && block.revision() != documentLayout->lastSaveRevision) {
            painter.save();
            painter.setRenderHint(QPainter::Antialiasing, false);
            if (block.revision() < 0)
                painter.setPen(QPen(Qt::darkGreen, 2));
            else
                painter.setPen(QPen(Qt::red, 2));
            painter.drawLine(extraAreaWidth - 1, top, extraAreaWidth - 1, bottom - 1);
con's avatar
con committed
            painter.restore();
        }

        if (d->m_lineNumbersVisible) {
            const QString &number = QString::number(blockNumber + 1);
mae's avatar
mae committed
            bool selected = (
                    (selStart < block.position() + block.length()
                    && selEnd > block.position())
                    || (selStart == selEnd && selStart == block.position())
mae's avatar
mae committed
                    );
            if (selected) {
                QFont f = painter.font();
                f.setBold(d->m_currentLineNumberFormat.font().bold());
                f.setItalic(d->m_currentLineNumberFormat.font().italic());
                painter.setFont(f);
                painter.setPen(d->m_currentLineNumberFormat.foreground().color());
            painter.drawText(QRectF(markWidth, top, extraAreaWidth - markWidth - 4, height), Qt::AlignRight, number);
mae's avatar
mae committed
            if (selected)
                painter.restore();
con's avatar
con committed
        }

        block = nextVisibleBlock;
        blockNumber = nextVisibleBlockNumber;
    }
}
void BaseTextEditor::drawFoldingMarker(QPainter *painter, const QPalette &pal,
                                       const QRect &rect,
                                       bool expanded,
                                       bool active,
                                       bool hovered) const
    Q_UNUSED(active)
    Q_UNUSED(hovered)
    QStyle *s = style();
    if (ManhattanStyle *ms = qobject_cast<ManhattanStyle*>(s))
        s = ms->baseStyle();

    if (!qstrcmp(s->metaObject()->className(), "OxygenStyle")) {
        painter->save();
        painter->setPen(Qt::NoPen);
        int size = rect.size().width();
        int sqsize = 2*(size/2);

        QColor textColor = pal.buttonText().color();
        QColor brushColor = textColor;

        textColor.setAlpha(100);
        brushColor.setAlpha(100);

        QPolygon a;
        if (expanded) {
            // down arrow
            a.setPoints(3, 0, sqsize/3,  sqsize/2, sqsize  - sqsize/3,  sqsize, sqsize/3);
        } else {
            // right arrow
            a.setPoints(3, sqsize - sqsize/3, sqsize/2,  sqsize/2 - sqsize/3, 0,  sqsize/2 - sqsize/3, sqsize);
            painter->setBrush(brushColor);
        }
        painter->translate(0.5, 0.5);
        painter->setRenderHint(QPainter::Antialiasing);
        painter->translate(rect.topLeft());
        painter->setPen(textColor);
        painter->setBrush(textColor);
        painter->drawPolygon(a);
        painter->restore();
        QStyleOptionViewItemV2 opt;
        opt.rect = rect;
        opt.state = QStyle::State_Active | QStyle::State_Item | QStyle::State_Children;
        if (expanded)
            opt.state |= QStyle::State_Open;
        if (active)
            opt.state |= QStyle::State_MouseOver | QStyle::State_Enabled | QStyle::State_Selected;
        if (hovered)
            opt.palette.setBrush(QPalette::Window, pal.highlight());

         // QGtkStyle needs a small correction to draw the marker in the right place
        if (!qstrcmp(s->metaObject()->className(), "QGtkStyle"))
           opt.rect.translate(-2, 0);
        else if (!qstrcmp(s->metaObject()->className(), "QMacStyle"))
            opt.rect.translate(-1, 0);

        s->drawPrimitive(QStyle::PE_IndicatorBranch, &opt, painter, this);
con's avatar
con committed
}

void BaseTextEditor::slotModificationChanged(bool m)
{
    if (m)
        return;

    QTextDocument *doc = document();
    BaseTextDocumentLayout *documentLayout = qobject_cast<BaseTextDocumentLayout*>(doc->documentLayout());
    QTC_ASSERT(documentLayout, return);
    int oldLastSaveRevision = documentLayout->lastSaveRevision;
    documentLayout->lastSaveRevision = doc->revision();

    if (oldLastSaveRevision != documentLayout->lastSaveRevision) {
        QTextBlock block = doc->begin();
        while (block.isValid()) {
            if (block.revision() < 0 || block.revision() != oldLastSaveRevision) {
                block.setRevision(-documentLayout->lastSaveRevision - 1);
            } else {
                block.setRevision(documentLayout->lastSaveRevision);
            }
            block = block.next();
        }
    }
    d->m_extraArea->update();
}

void BaseTextEditor::slotUpdateRequest(const QRect &r, int dy)
{
    if (dy)
        d->m_extraArea->scroll(0, dy);
    else if (r.width() > 4) { // wider than cursor width, not just cursor blinking
        d->m_extraArea->update(0, r.y(), d->m_extraArea->width(), r.height());
mae's avatar
mae committed
        if (!d->m_searchExpr.isEmpty()) {
            const int m = d->m_searchResultOverlay->dropShadowWidth();
            viewport()->update(r.adjusted(-m, -m, m, m));
        }
    }

    if (r.contains(viewport()->rect()))
        slotUpdateExtraAreaWidth();
}

void BaseTextEditor::saveCurrentCursorPositionForNavigation()
{
    d->m_lastCursorChangeWasInteresting = true;
    d->m_tempNavigationState = saveState();
void BaseTextEditor::updateCurrentLineHighlight()
{
    QList<QTextEdit::ExtraSelection> extraSelections;

    if (d->m_highlightCurrentLine) {
        QTextEdit::ExtraSelection sel;
        sel.format.setBackground(d->m_currentLineFormat.background());
        sel.format.setProperty(QTextFormat::FullWidthSelection, true);
        sel.cursor = textCursor();
        sel.cursor.clearSelection();
        extraSelections.append(sel);
    }

    setExtraSelections(CurrentLineSelection, extraSelections);


    // the extra area shows information for the entire current block, not just the currentline.
    // This is why we must force a bigger update region.
    int cursorBlockNumber = textCursor().blockNumber();
    if (cursorBlockNumber != d->m_cursorBlockNumber) {
        QPointF offset = contentOffset();
        QTextBlock block = document()->findBlockByNumber(d->m_cursorBlockNumber);
        if (block.isValid())
            d->m_extraArea->update(blockBoundingGeometry(block).translated(offset).toAlignedRect());
        block = document()->findBlockByNumber(cursorBlockNumber);
        if (block.isValid())
            d->m_extraArea->update(blockBoundingGeometry(block).translated(offset).toAlignedRect());
        d->m_cursorBlockNumber = cursorBlockNumber;
    }

void BaseTextEditor::slotCursorPositionChanged()
{
#if 0
    qDebug() << "block" << textCursor().blockNumber()+1
            << "depth:" << BaseTextDocumentLayout::braceDepth(textCursor().block())
            << '/' << BaseTextDocumentLayout::braceDepth(document()->lastBlock());
    if (!d->m_contentsChanged && d->m_lastCursorChangeWasInteresting) {
        Core::EditorManager::instance()->addCurrentPositionToNavigationHistory(editableInterface(), d->m_tempNavigationState);
        d->m_lastCursorChangeWasInteresting = false;
    } else if (d->m_contentsChanged) {
        saveCurrentCursorPositionForNavigation();
    }
con's avatar
con committed
    updateHighlights();
}
con's avatar
con committed
void BaseTextEditor::updateHighlights()
{
    if (d->m_parenthesesMatchingEnabled && hasFocus()) {
        // Delay update when no matching is displayed yet, to avoid flicker
        if (extraSelections(ParenthesesMatchingSelection).isEmpty()
            && d->m_animator == 0) {
            d->m_parenthesesMatchingTimer->start(50);
        } else {
             // use 0-timer, not direct call, to give the syntax highlighter a chance
            // to update the parantheses information
            d->m_parenthesesMatchingTimer->start(0);
    if (d->m_displaySettings.m_highlightBlocks) {
        QTextCursor cursor = textCursor();
        d->extraAreaHighlightCollapseBlockNumber = cursor.blockNumber();
        d->extraAreaHighlightCollapseColumn = cursor.position() - cursor.block().position();
        d->m_highlightBlocksTimer->start(100);
    }
}

void BaseTextEditor::slotUpdateBlockNotify(const QTextBlock &block)
{
    static bool blockRecursion = false;
    if (blockRecursion)
        return;
    blockRecursion = true;
    if (d->m_overlay->isVisible()) {
        /* an overlay might draw outside the block bounderies, force
           complete viewport update */
        viewport()->update();
    } else {
        if (block.previous().isValid() && block.userState() != block.previous().userState()) {
        /* The syntax highlighting state changes. This opens up for
           the possibility that the paragraph has braces that support
           code folding. In this case, do the save thing and also
           update the previous block, which might contain a collapse
           box which now is invalid.*/
            emit requestBlockUpdate(block.previous());
        }
mae's avatar
mae committed
        if (!d->m_findScopeStart.isNull()) {
            if (block.position() < d->m_findScopeEnd.position()
                && block.position()+block.length() >= d->m_findScopeStart.position()) {
                QTextBlock b = block.document()->findBlock(d->m_findScopeStart.position());
                do {
                    emit requestBlockUpdate(b);
                    b = b.next();
mae's avatar
mae committed
                } while (b.isValid() && b.position() < d->m_findScopeEnd.position());
    blockRecursion = false;
con's avatar
con committed
void BaseTextEditor::timerEvent(QTimerEvent *e)
{
    if (e->timerId() == d->autoScrollTimer.timerId()) {
        const QPoint globalPos = QCursor::pos();
        const QPoint pos = d->m_extraArea->mapFromGlobal(globalPos);
        QRect visible = d->m_extraArea->rect();
        verticalScrollBar()->triggerAction( pos.y() < visible.center().y() ?
                                            QAbstractSlider::SliderSingleStepSub
                                            : QAbstractSlider::SliderSingleStepAdd);
        QMouseEvent ev(QEvent::MouseMove, pos, globalPos, Qt::LeftButton, Qt::LeftButton, Qt::NoModifier);
        extraAreaMouseEvent(&ev);
        int delta = qMax(pos.y() - visible.top(), visible.bottom() - pos.y()) - visible.height();
        if (delta < 7)
            delta = 7;
        int timeout = 4900 / (delta * delta);
        d->autoScrollTimer.start(timeout, this);

    } else if (e->timerId() == d->collapsedBlockTimer.timerId()) {
        d->visibleCollapsedBlockNumber = d->suggestedVisibleCollapsedBlockNumber;
        d->suggestedVisibleCollapsedBlockNumber = -1;
        d->collapsedBlockTimer.stop();
        viewport()->update();
    }
    QPlainTextEdit::timerEvent(e);
}


void BaseTextEditorPrivate::clearVisibleCollapsedBlock()
{
    if (suggestedVisibleCollapsedBlockNumber) {
        suggestedVisibleCollapsedBlockNumber = -1;
        collapsedBlockTimer.stop();
    }
    if (visibleCollapsedBlockNumber >= 0) {
        visibleCollapsedBlockNumber = -1;
        q->viewport()->update();
    }
}

void BaseTextEditor::mouseMoveEvent(QMouseEvent *e)
{
    d->m_lastEventWasBlockSelectionEvent = (e->modifiers() & Qt::AltModifier);
    if (e->buttons() == Qt::NoButton) {
        const QTextBlock collapsedBlock = collapsedBlockAt(e->pos());
        const int blockNumber = collapsedBlock.next().blockNumber();
con's avatar
con committed
        if (blockNumber < 0) {
            d->clearVisibleCollapsedBlock();
        } else if (blockNumber != d->visibleCollapsedBlockNumber) {
            d->suggestedVisibleCollapsedBlockNumber = blockNumber;
            d->collapsedBlockTimer.start(40, this);
        }

        // Update the mouse cursor
        if (collapsedBlock.isValid() && !d->m_mouseOnCollapsedMarker) {
            d->m_mouseOnCollapsedMarker = true;
            viewport()->setCursor(Qt::PointingHandCursor);
        } else if (!collapsedBlock.isValid() && d->m_mouseOnCollapsedMarker) {
            d->m_mouseOnCollapsedMarker = false;
            viewport()->setCursor(Qt::IBeamCursor);
        }
con's avatar
con committed
    } else {
        QPlainTextEdit::mouseMoveEvent(e);
    }
    if (d->m_lastEventWasBlockSelectionEvent && d->m_inBlockSelectionMode) {
        if (textCursor().atBlockEnd()) {
            d->m_blockSelectionExtraX = qMax(0, e->pos().x() - cursorRect().center().x()) / fontMetrics().averageCharWidth();
con's avatar
con committed
        } else {
            d->m_blockSelectionExtraX = 0;
        }
    }
    if (viewport()->cursor().shape() == Qt::BlankCursor)
        viewport()->setCursor(Qt::IBeamCursor);
con's avatar
con committed
}

static bool handleForwardBackwardMouseButtons(QMouseEvent *e)
{
    if (e->button() == Qt::XButton1) {
        Core::EditorManager::instance()->goBackInNavigationHistory();
        return true;
    }
    if (e->button() == Qt::XButton2) {
        Core::EditorManager::instance()->goForwardInNavigationHistory();
        return true;
    }

    return false;
}

con's avatar
con committed
void BaseTextEditor::mousePressEvent(QMouseEvent *e)
{
    if (e->button() == Qt::LeftButton) {
        d->clearBlockSelection(); // just in case, otherwise we might get strange drag and drop

        QTextBlock collapsedBlock = collapsedBlockAt(e->pos());
        if (collapsedBlock.isValid()) {
            toggleBlockVisible(collapsedBlock);
            viewport()->setCursor(Qt::IBeamCursor);
        }

        updateLink(e);

        if (d->m_currentLink.isValid())
            d->m_linkPressed = true;
con's avatar
con committed
    }

#ifdef Q_OS_LINUX
    if (handleForwardBackwardMouseButtons(e))
        return;
#endif

con's avatar
con committed
    QPlainTextEdit::mousePressEvent(e);
}

void BaseTextEditor::mouseReleaseEvent(QMouseEvent *e)
{
    if (mouseNavigationEnabled()
        && d->m_linkPressed
        && e->modifiers() & Qt::ControlModifier
        && !(e->modifiers() & Qt::ShiftModifier)
        const QTextCursor cursor = cursorForPosition(e->pos());
        if (openLink(findLinkAt(cursor))) {
            clearLink();
            return;
        }
    }

#ifndef Q_OS_LINUX
    if (handleForwardBackwardMouseButtons(e))
        return;
#endif

    QPlainTextEdit::mouseReleaseEvent(e);
}

void BaseTextEditor::leaveEvent(QEvent *e)
{
    // Clear link emulation when the mouse leaves the editor
    clearLink();
    QPlainTextEdit::leaveEvent(e);
}

void BaseTextEditor::keyReleaseEvent(QKeyEvent *e)
{
    // Clear link emulation when Ctrl is released
    if (e->key() == Qt::Key_Control)
        clearLink();

    QPlainTextEdit::keyReleaseEvent(e);
}

void BaseTextEditor::dragEnterEvent(QDragEnterEvent *e)
{
    // If the drag event contains URLs, we don't want to insert them as text
    if (e->mimeData()->hasUrls()) {
        e->ignore();
        return;
    }

    QPlainTextEdit::dragEnterEvent(e);
}

con's avatar
con committed
void BaseTextEditor::extraAreaLeaveEvent(QEvent *)
{
    // fake missing mouse move event from Qt
    QMouseEvent me(QEvent::MouseMove, QPoint(-1, -1), Qt::NoButton, 0, 0);
    extraAreaMouseEvent(&me);
con's avatar
con committed
}

void BaseTextEditor::extraAreaMouseEvent(QMouseEvent *e)
{
    QTextCursor cursor = cursorForPosition(QPoint(0, e->pos().y()));
    cursor.setPosition(cursor.block().position());

    int markWidth;
    extraAreaWidth(&markWidth);

    if (d->m_codeFoldingVisible
        && e->type() == QEvent::MouseMove && e->buttons() == 0) { // mouse tracking
        // Update which folder marker is highlighted
        const int highlightBlockNumber = d->extraAreaHighlightCollapseBlockNumber;
        const int highlightColumn = d->extraAreaHighlightCollapseColumn;
con's avatar
con committed
        d->extraAreaHighlightCollapseBlockNumber = -1;
        d->extraAreaHighlightCollapseColumn = -1;
        if (e->pos().x() > extraArea()->width() - collapseBoxWidth(fontMetrics())) {
            d->extraAreaHighlightCollapseBlockNumber = cursor.blockNumber();
mae's avatar
mae committed
            if (TextBlockUserData::canCollapse(cursor.block())
                || !TextBlockUserData::hasClosingCollapse(cursor.block()))
                d->extraAreaHighlightCollapseColumn = cursor.block().length()-1;
            if (TextBlockUserData::hasCollapseAfter(cursor.block())) {
                d->extraAreaHighlightCollapseBlockNumber++;
mae's avatar
mae committed
                d->extraAreaHighlightCollapseColumn = -1;
                if (TextBlockUserData::canCollapse(cursor.block().next())
                    || !TextBlockUserData::hasClosingCollapse(cursor.block().next()))
                    d->extraAreaHighlightCollapseColumn = cursor.block().next().length()-1;
            }
        } else if (d->m_displaySettings.m_highlightBlocks) {
            QTextCursor cursor = textCursor();
            d->extraAreaHighlightCollapseBlockNumber = cursor.blockNumber();
            d->extraAreaHighlightCollapseColumn = cursor.position() - cursor.block().position();
con's avatar
con committed
        }
        if (highlightBlockNumber != d->extraAreaHighlightCollapseBlockNumber
            || highlightColumn != d->extraAreaHighlightCollapseColumn) {
            d->m_highlightBlocksTimer->start(d->m_highlightBlocksInfo.isEmpty() ? 120 : 0);
        }
con's avatar
con committed
    }

    // Set whether the mouse cursor is a hand or normal arrow
    if (e->type() == QEvent::MouseMove) {
        bool hand = (e->pos().x() <= markWidth);
        if (hand != (d->m_extraArea->cursor().shape() == Qt::PointingHandCursor))
            d->m_extraArea->setCursor(hand ? Qt::PointingHandCursor : Qt::ArrowCursor);
    }

con's avatar
con committed
    if (e->type() == QEvent::MouseButtonPress || e->type() == QEvent::MouseButtonDblClick) {
        if (e->button() == Qt::LeftButton) {
            int boxWidth = collapseBoxWidth(fontMetrics());
            if (d->m_codeFoldingVisible && e->pos().x() > extraArea()->width() - boxWidth) {
                if (!cursor.block().next().isVisible()) {
                    toggleBlockVisible(cursor.block());
                    d->moveCursorVisible(false);
                } else if (collapseBox().contains(e->pos())) {
                    cursor.setPosition(
                            document()->findBlockByNumber(d->m_highlightBlocksInfo.open.last()).position()
                            );
                    QTextBlock c = cursor.block();
                    if (TextBlockUserData::hasCollapseAfter(c.previous()))
                        c = c.previous();
                    toggleBlockVisible(c);
                    d->moveCursorVisible(false);
                }
            } else if (d->m_lineNumbersVisible && e->pos().x() > markWidth) {
con's avatar
con committed
                QTextCursor selection = cursor;
                selection.setVisualNavigation(true);
                d->extraAreaSelectionAnchorBlockNumber = selection.blockNumber();
                selection.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor);
                selection.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor);
                setTextCursor(selection);
            } else {
                d->extraAreaToggleMarkBlockNumber = cursor.blockNumber();
            }
        } else if (d->m_marksVisible && e->button() == Qt::RightButton) {
            QMenu * contextMenu = new QMenu(this);
            emit editableInterface()->markContextMenuRequested(editableInterface(), cursor.blockNumber() + 1, contextMenu);
            if (!contextMenu->isEmpty())
                contextMenu->exec(e->globalPos());
            delete contextMenu;
con's avatar
con committed
        }
    } else if (d->extraAreaSelectionAnchorBlockNumber >= 0) {
        QTextCursor selection = cursor;
        selection.setVisualNavigation(true);
        if (e->type() == QEvent::MouseMove) {
            QTextBlock anchorBlock = document()->findBlockByNumber(d->extraAreaSelectionAnchorBlockNumber);
            selection.setPosition(anchorBlock.position());
            if (cursor.blockNumber() < d->extraAreaSelectionAnchorBlockNumber) {
                selection.movePosition(QTextCursor::EndOfBlock);
                selection.movePosition(QTextCursor::Right);
            }
            selection.setPosition(cursor.block().position(), QTextCursor::KeepAnchor);
            if (cursor.blockNumber() >= d->extraAreaSelectionAnchorBlockNumber) {
                selection.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor);
                selection.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor);
            }

            if (e->pos().y() >= 0 && e->pos().y() <= d->m_extraArea->height())
                d->autoScrollTimer.stop();
            else if (!d->autoScrollTimer.isActive())
                d->autoScrollTimer.start(100, this);

        } else {
            d->autoScrollTimer.stop();
            d->extraAreaSelectionAnchorBlockNumber = -1;
            return;
        }
        setTextCursor(selection);
    } else if (d->extraAreaToggleMarkBlockNumber >= 0 && d->m_marksVisible && d->m_requestMarkEnabled) {
        if (e->type() == QEvent::MouseButtonRelease && e->button() == Qt::LeftButton) {
            int n = d->extraAreaToggleMarkBlockNumber;
            d->extraAreaToggleMarkBlockNumber = -1;
            if (cursor.blockNumber() == n) {
                int line = n + 1;
                emit editableInterface()->markRequested(editableInterface(), line);
con's avatar
con committed
            }
        }
    }
}

void BaseTextEditor::ensureCursorVisible()
{
    QTextBlock block = textCursor().block();
    if (!block.isVisible()) {
        while (!block.isVisible() && block.previous().isValid())
            block = block.previous();
        toggleBlockVisible(block);
    }
    QPlainTextEdit::ensureCursorVisible();
}

void BaseTextEditor::toggleBlockVisible(const QTextBlock &block)
{
    BaseTextDocumentLayout *documentLayout = qobject_cast<BaseTextDocumentLayout*>(document()->documentLayout());
hjk's avatar
hjk committed
    QTC_ASSERT(documentLayout, return);
con's avatar
con committed

    bool visible = block.next().isVisible();
    TextBlockUserData::doCollapse(block, !visible);
    documentLayout->requestUpdate();
    documentLayout->emitDocumentSizeChanged();
}


const TabSettings &BaseTextEditor::tabSettings() const
{
    return d->m_document->tabSettings();
}

const DisplaySettings &BaseTextEditor::displaySettings() const
{
    return d->m_displaySettings;
}


void BaseTextEditor::indentOrUnindent(bool doIndent)
{
    QTextCursor cursor = textCursor();
    cursor.beginEditBlock();

    int pos = cursor.position();
    const TextEditor::TabSettings &tabSettings = d->m_document->tabSettings();

    QTextDocument *doc = document();
    if (cursor.hasSelection()) {
        // Indent or unindent the selected lines
con's avatar
con committed
        int anchor = cursor.anchor();
        int start = qMin(anchor, pos);
        int end = qMax(anchor, pos);

        QTextBlock startBlock = doc->findBlock(start);
        QTextBlock endBlock = doc->findBlock(end-1).next();

        for (QTextBlock block = startBlock; block != endBlock; block = block.next()) {
            QString text = block.text();
            int indentPosition = tabSettings.lineIndentPosition(text);
            if (!doIndent && !indentPosition)
                indentPosition = tabSettings.firstNonSpace(text);
            int targetColumn = tabSettings.indentedColumn(tabSettings.columnAt(text, indentPosition), doIndent);
            cursor.setPosition(block.position() + indentPosition);
            cursor.insertText(tabSettings.indentationString(0, targetColumn, block));
con's avatar
con committed
            cursor.setPosition(block.position());
            cursor.setPosition(block.position() + indentPosition, QTextCursor::KeepAnchor);
            cursor.removeSelectedText();
        }
    } else {
        // Indent or unindent at cursor position
        QTextBlock block = cursor.block();
        QString text = block.text();
        int indentPosition = cursor.positionInBlock();
        int spaces = tabSettings.spacesLeftFromPosition(text, indentPosition);
        int startColumn = tabSettings.columnAt(text, indentPosition - spaces);
        int targetColumn = tabSettings.indentedColumn(tabSettings.columnAt(text, indentPosition), doIndent);
        cursor.setPosition(block.position() + indentPosition);
        cursor.setPosition(block.position() + indentPosition - spaces, QTextCursor::KeepAnchor);
        cursor.removeSelectedText();
        cursor.insertText(tabSettings.indentationString(startColumn, targetColumn, block));
con's avatar
con committed
    }

    cursor.endEditBlock();
}

void BaseTextEditor::handleHomeKey(bool anchor)
{
    QTextCursor cursor = textCursor();
    QTextCursor::MoveMode mode = QTextCursor::MoveAnchor;

    if (anchor)
        mode = QTextCursor::KeepAnchor;

    const int initpos = cursor.position();
    int pos = cursor.block().position();
    QChar character = characterAt(pos);
    const QLatin1Char tab = QLatin1Char('\t');

    while (character == tab || character.category() == QChar::Separator_Space) {
        ++pos;
con's avatar
con committed
        character = characterAt(pos);
    }

    // Go to the start of the block when we're already at the start of the text
    if (pos == initpos)
        pos = cursor.block().position();

    cursor.setPosition(pos, mode);
    setTextCursor(cursor);
}


#define SET_AND_RETURN(cursor) setTextCursor(cursor); return  // make cursor visible and reset vertical x movement
con's avatar
con committed
void BaseTextEditor::handleBackspaceKey()
{
    QTextCursor cursor = textCursor();
    int pos = cursor.position();
hjk's avatar
hjk committed
    QTC_ASSERT(!cursor.hasSelection(), return);
con's avatar
con committed

    if (d->m_snippetOverlay->isVisible()) {
        QTextCursor snippetCursor = cursor;
        snippetCursor.movePosition(QTextCursor::Left);
        d->snippetCheckCursor(snippetCursor);
    }

con's avatar
con committed
    const TextEditor::TabSettings &tabSettings = d->m_document->tabSettings();
    if (tabSettings.m_autoIndent && autoBackspace(cursor))

    if (!tabSettings.m_smartBackspace) {
        cursor.deletePreviousChar();
        SET_AND_RETURN(cursor);
con's avatar
con committed
    QTextBlock currentBlock = cursor.block();
    int positionInBlock = pos - currentBlock.position();
con's avatar
con committed
    const QString blockText = currentBlock.text();
    if (cursor.atBlockStart() || tabSettings.firstNonSpace(blockText) < positionInBlock) {
        cursor.deletePreviousChar();
        SET_AND_RETURN(cursor);
con's avatar
con committed
    }

    int previousIndent = 0;
    const int indent = tabSettings.columnAt(blockText, positionInBlock);

    for (QTextBlock previousNonEmptyBlock = currentBlock.previous();
         previousNonEmptyBlock.isValid();
         previousNonEmptyBlock = previousNonEmptyBlock.previous()) {
        QString previousNonEmptyBlockText = previousNonEmptyBlock.text();
        if (previousNonEmptyBlockText.trimmed().isEmpty())
            continue;
        previousIndent = tabSettings.columnAt(previousNonEmptyBlockText,
                                              tabSettings.firstNonSpace(previousNonEmptyBlockText));
        if (previousIndent < indent) {
            cursor.beginEditBlock();
            cursor.setPosition(currentBlock.position(), QTextCursor::KeepAnchor);
            cursor.insertText(tabSettings.indentationString(previousNonEmptyBlockText));
            cursor.endEditBlock();
            SET_AND_RETURN(cursor);
con's avatar
con committed
    }
mae's avatar
mae committed
    cursor.deletePreviousChar();
    SET_AND_RETURN(cursor);
con's avatar
con committed
}

void BaseTextEditor::wheelEvent(QWheelEvent *e)
{
    d->clearVisibleCollapsedBlock();
    if (scrollWheelZoomingEnabled() && e->modifiers() & Qt::ControlModifier) {
con's avatar
con committed
        const int delta = e->delta();
        if (delta < 0)
            zoomOut();
        else if (delta > 0)
            zoomIn();
        return;
    }
    QPlainTextEdit::wheelEvent(e);
}

void BaseTextEditor::zoomIn(int range)
{
mae's avatar
mae committed
    d->clearVisibleCollapsedBlock();
    emit requestFontZoom(range*10);
con's avatar
con committed
}

void BaseTextEditor::zoomOut(int range)
{
    zoomIn(-range);
}

mae's avatar
mae committed
void BaseTextEditor::zoomReset()
{
    emit requestZoomReset();
}

bool BaseTextEditor::isElectricCharacter(QChar) const
con's avatar
con committed
{
    return false;
}

void BaseTextEditor::indentInsertedText(const QTextCursor &tc)
{
    indent(tc.document(), tc, QChar::Null);
}

void BaseTextEditor::countBracket(QChar open, QChar close, QChar c, int *errors, int *stillopen)
{
    if (c == open)
        ++*stillopen;
    else if (c == close)
        --*stillopen;

    if (*stillopen < 0) {
        *errors += -1 * (*stillopen);
        *stillopen = 0;
    }
}

void BaseTextEditor::countBrackets(QTextCursor cursor, int from, int end, QChar open, QChar close, int *errors, int *stillopen)
{
    cursor.setPosition(from);
    QTextBlock block = cursor.block();
    while (block.isValid() && block.position() < end) {
        TextEditor::Parentheses parenList = TextEditor::BaseTextDocumentLayout::parentheses(block);
        if (!parenList.isEmpty() && !TextEditor::BaseTextDocumentLayout::ifdefedOut(block)) {
            for (int i = 0; i < parenList.count(); ++i) {
                TextEditor::Parenthesis paren = parenList.at(i);
                int position = block.position() + paren.pos;
                if (position < from || position >= end)
                    continue;
                countBracket(open, close, paren.chr, errors, stillopen);
            }
        }
        block = block.next();
    }
}

bool BaseTextEditor::contextAllowsAutoParentheses(const QTextCursor &cursor,
                                                  const QString &textToInsert) const
{
    Q_UNUSED(cursor);
    Q_UNUSED(textToInsert);
    return false;
}

bool BaseTextEditor::contextAllowsElectricCharacters(const QTextCursor &cursor) const
{
    return contextAllowsAutoParentheses(cursor);
}

bool BaseTextEditor::isInComment(const QTextCursor &cursor) const
    Q_UNUSED(cursor);
    return false;
}

QString BaseTextEditor::insertMatchingBrace(const QTextCursor &tc, const QString &text,
                                            QChar la, int *skippedChars) const
    Q_UNUSED(text);
    Q_UNUSED(la);
    Q_UNUSED(skippedChars);
QString BaseTextEditor::insertParagraphSeparator(const QTextCursor &tc) const
{
    Q_UNUSED(tc);
    return QString();
}

QString BaseTextEditor::autoComplete(QTextCursor &cursor, const QString &textToInsert) const
{
    const bool checkBlockEnd = d->m_allowSkippingOfBlockEnd;
    d->m_allowSkippingOfBlockEnd = false; // consume blockEnd.

    if (!d->m_autoParenthesesEnabled)
        return QString();

    if (!contextAllowsAutoParentheses(cursor, textToInsert))
        return QString();

    const QString text = textToInsert;
    const QChar lookAhead = characterAt(cursor.selectionEnd());

    const QChar character = textToInsert.at(0);
    const QString parentheses = QLatin1String("()");
    const QString brackets = QLatin1String("[]");
    if (parentheses.contains(character) || brackets.contains(character)) {
        QTextCursor tmp= cursor;
con's avatar
con committed
        bool foundBlockStart = TextEditor::TextBlockUserData::findPreviousBlockOpenParenthesis(&tmp);
        int blockStart = foundBlockStart ? tmp.position() : 0;
con's avatar
con committed
        bool foundBlockEnd = TextEditor::TextBlockUserData::findNextBlockClosingParenthesis(&tmp);
        int blockEnd = foundBlockEnd ? tmp.position() : (cursor.document()->characterCount() - 1);
        const QChar openChar = parentheses.contains(character) ? QLatin1Char('(') : QLatin1Char('[');
        const QChar closeChar = parentheses.contains(character) ? QLatin1Char(')') : QLatin1Char(']');

        int errors = 0;
        int stillopen = 0;
        countBrackets(cursor, blockStart, blockEnd, openChar, closeChar, &errors, &stillopen);
        int errorsBeforeInsertion = errors + stillopen;
        errors = 0;
        stillopen = 0;
        countBrackets(cursor, blockStart, cursor.position(), openChar, closeChar, &errors, &stillopen);
        countBracket(openChar, closeChar, character, &errors, &stillopen);
        countBrackets(cursor, cursor.position(), blockEnd, openChar, closeChar, &errors, &stillopen);
        int errorsAfterInsertion = errors + stillopen;
        if (errorsAfterInsertion < errorsBeforeInsertion)
            return QString(); // insertion fixes parentheses or bracket errors, do not auto complete
    }

    int skippedChars = 0;
    const QString autoText = insertMatchingBrace(cursor, text, lookAhead, &skippedChars);

    if (checkBlockEnd && textToInsert.at(0) == QLatin1Char('}')) {
        if (textToInsert.length() > 1)
            qWarning() << "*** handle event compression";

        int startPos = cursor.selectionEnd(), pos = startPos;
        while (characterAt(pos).isSpace())
            ++pos;

        if (characterAt(pos) == QLatin1Char('}'))
            skippedChars += (pos - startPos) + 1;
    }

    if (skippedChars) {
        const int pos = cursor.position();
        cursor.setPosition(pos + skippedChars);
        cursor.setPosition(pos, QTextCursor::KeepAnchor);
    }

    return autoText;
}

bool BaseTextEditor::autoBackspace(QTextCursor &cursor)
{
    d->m_allowSkippingOfBlockEnd = false;

    if (!d->m_autoParenthesesEnabled)
        return false;

    int pos = cursor.position();
mae's avatar
mae committed
    if (pos == 0)
        return false;
    QTextCursor c = cursor;
    c.setPosition(pos - 1);

    const QChar lookAhead = characterAt(pos);
    const QChar lookBehind = characterAt(pos - 1);
    const QChar lookFurtherBehind = characterAt(pos - 2);
    const QChar character = lookBehind;
    if (character == QLatin1Char('(') || character == QLatin1Char('[')) {
        QTextCursor tmp = cursor;
        TextEditor::TextBlockUserData::findPreviousBlockOpenParenthesis(&tmp);
        int blockStart = tmp.isNull() ? 0 : tmp.position();
        tmp = cursor;
        TextEditor::TextBlockUserData::findNextBlockClosingParenthesis(&tmp);
        int blockEnd = tmp.isNull() ? (cursor.document()->characterCount()-1) : tmp.position();
        QChar openChar = character;
        QChar closeChar = (character == QLatin1Char('(')) ? QLatin1Char(')') : QLatin1Char(']');

        int errors = 0;
        int stillopen = 0;
        countBrackets(cursor, blockStart, blockEnd, openChar, closeChar, &errors, &stillopen);
        int errorsBeforeDeletion = errors + stillopen;
        errors = 0;
        stillopen = 0;
        countBrackets(cursor, blockStart, pos - 1, openChar, closeChar, &errors, &stillopen);
        countBrackets(cursor, pos, blockEnd, openChar, closeChar, &errors, &stillopen);
        int errorsAfterDeletion = errors + stillopen;

        if (errorsAfterDeletion < errorsBeforeDeletion)
            return false; // insertion fixes parentheses or bracket errors, do not auto complete
    }

    // ### this code needs to be generalized
    if    ((lookBehind == QLatin1Char('(') && lookAhead == QLatin1Char(')'))
        || (lookBehind == QLatin1Char('[') && lookAhead == QLatin1Char(']'))
        || (lookBehind == QLatin1Char('"') && lookAhead == QLatin1Char('"')
            && lookFurtherBehind != QLatin1Char('\\'))
        || (lookBehind == QLatin1Char('\'') && lookAhead == QLatin1Char('\'')
            && lookFurtherBehind != QLatin1Char('\\'))) {
        if (! isInComment(c)) {