forked from qt-creator/qt-creator
Replace the main cursor with a block selection instead of a normal selection when using alt+shift+LMB. This is more in line with other IDEs and also feels more natural. Change-Id: Id4dba7cec65ddeb34ab525d90a41aebf78457d0d Reviewed-by: Andrii Semkiv <andrii.semkiv@qt.io> Reviewed-by: Marcus Tillmanns <marcus.tillmanns@qt.io>
10741 lines
383 KiB
C++
10741 lines
383 KiB
C++
// Copyright (C) 2016 The Qt Company Ltd.
|
|
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
|
|
|
|
#include "texteditor.h"
|
|
|
|
#include "autocompleter.h"
|
|
#include "basehoverhandler.h"
|
|
#include "behaviorsettings.h"
|
|
#include "bookmarkmanager.h"
|
|
#include "circularclipboard.h"
|
|
#include "circularclipboardassist.h"
|
|
#include "codeassist/assistinterface.h"
|
|
#include "codeassist/codeassistant.h"
|
|
#include "codeassist/completionassistprovider.h"
|
|
#include "codeassist/documentcontentcompletion.h"
|
|
#include "completionsettings.h"
|
|
#include "displaysettings.h"
|
|
#include "extraencodingsettings.h"
|
|
#include "fontsettings.h"
|
|
#include "highlighter.h"
|
|
#include "highlighterhelper.h"
|
|
#include "highlightersettings.h"
|
|
#include "icodestylepreferences.h"
|
|
#include "linenumberfilter.h"
|
|
#include "marginsettings.h"
|
|
#include "refactoroverlay.h"
|
|
#include "snippets/snippetoverlay.h"
|
|
#include "storagesettings.h"
|
|
#include "tabsettings.h"
|
|
#include "textdocument.h"
|
|
#include "textdocumentlayout.h"
|
|
#include "texteditorconstants.h"
|
|
#include "texteditoroverlay.h"
|
|
#include "texteditorsettings.h"
|
|
#include "texteditortr.h"
|
|
#include "typehierarchy.h"
|
|
#include "typingsettings.h"
|
|
|
|
#include <aggregation/aggregate.h>
|
|
|
|
#include <coreplugin/actionmanager/actioncontainer.h>
|
|
#include <coreplugin/actionmanager/actionmanager.h>
|
|
#include <coreplugin/actionmanager/command.h>
|
|
#include <coreplugin/coreconstants.h>
|
|
#include <coreplugin/dialogs/codecselector.h>
|
|
#include <coreplugin/find/basetextfind.h>
|
|
#include <coreplugin/find/highlightscrollbarcontroller.h>
|
|
#include <coreplugin/icore.h>
|
|
#include <coreplugin/locator/locatormanager.h>
|
|
#include <coreplugin/manhattanstyle.h>
|
|
#include <coreplugin/navigationwidget.h>
|
|
|
|
#include <utils/algorithm.h>
|
|
#include <utils/async.h>
|
|
#include <utils/camelcasecursor.h>
|
|
#include <utils/dropsupport.h>
|
|
#include <utils/fadingindicator.h>
|
|
#include <utils/filesearch.h>
|
|
#include <utils/fileutils.h>
|
|
#include <utils/hostosinfo.h>
|
|
#include <utils/infobar.h>
|
|
#include <utils/mimeutils.h>
|
|
#include <utils/minimizableinfobars.h>
|
|
#include <utils/multitextcursor.h>
|
|
#include <utils/qtcassert.h>
|
|
#include <utils/searchresultitem.h>
|
|
#include <utils/styledbar.h>
|
|
#include <utils/stylehelper.h>
|
|
#include <utils/textutils.h>
|
|
#include <utils/theme/theme.h>
|
|
#include <utils/tooltip/tooltip.h>
|
|
#include <utils/uncommentselection.h>
|
|
|
|
#include <QAbstractTextDocumentLayout>
|
|
#include <QActionGroup>
|
|
#include <QApplication>
|
|
#include <QClipboard>
|
|
#include <QComboBox>
|
|
#include <QCoreApplication>
|
|
#include <QDebug>
|
|
#include <QDesktopServices>
|
|
#include <QDrag>
|
|
#include <QFutureWatcher>
|
|
#include <QGridLayout>
|
|
#include <QKeyEvent>
|
|
#include <QLoggingCategory>
|
|
#include <QMap>
|
|
#include <QMenu>
|
|
#include <QMessageBox>
|
|
#include <QMimeData>
|
|
#include <QPainter>
|
|
#include <QPainterPath>
|
|
#include <QPrintDialog>
|
|
#include <QPrinter>
|
|
#include <QPropertyAnimation>
|
|
#include <QRegularExpression>
|
|
#include <QScopeGuard>
|
|
#include <QScreen>
|
|
#include <QScrollBar>
|
|
#include <QSequentialAnimationGroup>
|
|
#include <QShortcut>
|
|
#include <QStyle>
|
|
#include <QStyleFactory>
|
|
#include <QTextBlock>
|
|
#include <QTextCodec>
|
|
#include <QTextCursor>
|
|
#include <QTextDocumentFragment>
|
|
#include <QTextLayout>
|
|
#include <QTime>
|
|
#include <QTimeLine>
|
|
#include <QTimer>
|
|
#include <QToolBar>
|
|
#include <QToolButton>
|
|
|
|
/*!
|
|
\namespace TextEditor
|
|
\brief The TextEditor namespace contains the base text editor and several classes which
|
|
provide supporting functionality like snippets, highlighting, \l{CodeAssist}{code assist},
|
|
indentation and style, and others.
|
|
*/
|
|
|
|
/*!
|
|
\namespace TextEditor::Internal
|
|
\internal
|
|
*/
|
|
|
|
/*!
|
|
\class TextEditor::BaseTextEditor
|
|
\brief The BaseTextEditor class is base implementation for QPlainTextEdit-based
|
|
text editors. It can use the Kate text highlighting definitions, and some basic
|
|
auto indentation.
|
|
|
|
The corresponding document base class is BaseTextDocument, the corresponding
|
|
widget base class is BaseTextEditorWidget.
|
|
|
|
It is the default editor for text files used by \QC, if no other editor
|
|
implementation matches the MIME type.
|
|
*/
|
|
|
|
using namespace Core;
|
|
using namespace Utils;
|
|
|
|
namespace TextEditor {
|
|
|
|
using namespace Internal;
|
|
|
|
namespace Internal {
|
|
|
|
enum { NExtraSelectionKinds = 12 };
|
|
|
|
using TransformationMethod = QString(const QString &);
|
|
using ListTransformationMethod = void(QStringList &);
|
|
|
|
static constexpr char dropProperty[] = "dropProp";
|
|
|
|
class LineColumnButtonPrivate
|
|
{
|
|
public:
|
|
QSize m_maxSize;
|
|
TextEditorWidget *m_editor;
|
|
};
|
|
|
|
} // namespace Internal
|
|
|
|
LineColumnButton::LineColumnButton(TextEditorWidget *parent)
|
|
: QToolButton(parent)
|
|
, m_d(new LineColumnButtonPrivate)
|
|
{
|
|
m_d->m_editor = parent;
|
|
connect(m_d->m_editor, &QPlainTextEdit::cursorPositionChanged, this, &LineColumnButton::update);
|
|
connect(this, &QToolButton::clicked, ActionManager::instance(), [this] {
|
|
m_d->m_editor->setFocus();
|
|
QMetaObject::invokeMethod(
|
|
ActionManager::instance(),
|
|
[] {
|
|
if (Command *cmd = ActionManager::command(Core::Constants::GOTO)) {
|
|
if (QAction *act = cmd->action())
|
|
act->trigger();
|
|
}
|
|
},
|
|
Qt::QueuedConnection);
|
|
});
|
|
}
|
|
|
|
LineColumnButton::~LineColumnButton() = default;
|
|
|
|
void LineColumnButton::update()
|
|
{
|
|
const Utils::MultiTextCursor &cursors = m_d->m_editor->multiTextCursor();
|
|
QString text;
|
|
if (cursors.hasMultipleCursors()) {
|
|
text = Tr::tr("Cursors: %2").arg(cursors.cursorCount());
|
|
} else {
|
|
const QTextCursor cursor = cursors.mainCursor();
|
|
const QTextBlock block = cursor.block();
|
|
const int line = block.blockNumber() + 1;
|
|
const TabSettings &tabSettings = m_d->m_editor->textDocument()->tabSettings();
|
|
const int column = tabSettings.columnAt(block.text(), cursor.positionInBlock()) + 1;
|
|
text = Tr::tr("Line: %1, Col: %2").arg(line).arg(column);
|
|
const QString toolTipText = Tr::tr("Cursor position: %1");
|
|
setToolTip(toolTipText.arg(cursor.position()));
|
|
}
|
|
int selection = 0;
|
|
for (const QTextCursor &cursor : cursors)
|
|
selection += cursor.selectionEnd() - cursor.selectionStart();
|
|
if (selection > 0)
|
|
text += " " + Tr::tr("(Sel: %1)").arg(selection);
|
|
setText(text);
|
|
}
|
|
|
|
bool LineColumnButton::event(QEvent *event)
|
|
{
|
|
if (event->type() == QEvent::Leave)
|
|
ToolTip::hideImmediately();
|
|
|
|
if (event->type() != QEvent::ToolTip)
|
|
return QToolButton::event(event);
|
|
|
|
QString tooltipText = "<table cellpadding='2'>\n";
|
|
|
|
const MultiTextCursor multiCursor = m_d->m_editor->multiTextCursor();
|
|
const QList<QTextCursor> cursors = multiCursor.cursors().mid(0, 15);
|
|
|
|
tooltipText += "<tr>";
|
|
tooltipText += QString("<th align='left'>%1</th>").arg(Tr::tr("Cursors:"));
|
|
tooltipText += QString("<td>%1</td>").arg(multiCursor.cursorCount());
|
|
tooltipText += "</tr>\n";
|
|
|
|
auto addRow = [&](const QString header, auto cellText) {
|
|
tooltipText += "<tr>";
|
|
tooltipText += QString("<th align='left'>%1</th>").arg(header);
|
|
for (const QTextCursor &c : cursors)
|
|
tooltipText += QString("<td>%1</td>").arg(cellText(c));
|
|
if (multiCursor.cursorCount() > cursors.count())
|
|
tooltipText += QString("<td>...</td>");
|
|
tooltipText += "</tr>\n";
|
|
};
|
|
|
|
addRow(Tr::tr("Line:"), [](const QTextCursor &c) { return c.blockNumber() + 1; });
|
|
|
|
const TabSettings &tabSettings = m_d->m_editor->textDocument()->tabSettings();
|
|
addRow(Tr::tr("Column:"), [&](const QTextCursor &c) {
|
|
return tabSettings.columnAt(c.block().text(), c.positionInBlock()) + 1;
|
|
});
|
|
|
|
addRow(Tr::tr("Selection length:"),
|
|
[](const QTextCursor &c) { return c.selectionEnd() - c.selectionStart(); });
|
|
|
|
addRow(Tr::tr("Position in document:"), [](const QTextCursor &c) { return c.position(); });
|
|
|
|
addRow(Tr::tr("Anchor:"), [](const QTextCursor &c) { return c.anchor(); });
|
|
|
|
tooltipText += "</table>\n";
|
|
|
|
ToolTip::show(static_cast<const QHelpEvent *>(event)->globalPos(), tooltipText, Qt::RichText);
|
|
event->accept();
|
|
return true;
|
|
}
|
|
|
|
QSize LineColumnButton::sizeHint() const
|
|
{
|
|
const QSize size = QToolButton::sizeHint();
|
|
auto wider = [](const QSize &left, const QSize &right) { return left.width() < right.width(); };
|
|
if (m_d->m_editor->multiTextCursor().hasSelection())
|
|
return std::max(m_d->m_maxSize, size, wider); // do not save the size if we have a selection
|
|
m_d->m_maxSize = std::max(m_d->m_maxSize, size, wider);
|
|
return m_d->m_maxSize;
|
|
}
|
|
|
|
namespace Internal {
|
|
|
|
class TabSettingsButton : public QToolButton
|
|
{
|
|
public:
|
|
TabSettingsButton(TextEditorWidget *parent)
|
|
: QToolButton(parent)
|
|
{
|
|
connect(this, &QToolButton::clicked, this, &TabSettingsButton::showMenu);
|
|
}
|
|
|
|
void setDocument(TextDocument *doc)
|
|
{
|
|
if (m_doc)
|
|
disconnect(m_doc, &TextDocument::tabSettingsChanged, this, &TabSettingsButton::update);
|
|
m_doc = doc;
|
|
if (QTC_GUARD(m_doc)) {
|
|
connect(m_doc, &TextDocument::tabSettingsChanged, this, &TabSettingsButton::update);
|
|
update();
|
|
}
|
|
}
|
|
|
|
private:
|
|
void update()
|
|
{
|
|
QTC_ASSERT(m_doc, return);
|
|
const TabSettings ts = m_doc->tabSettings();
|
|
QString policy;
|
|
switch (ts.m_tabPolicy) {
|
|
case TabSettings::SpacesOnlyTabPolicy:
|
|
policy = Tr::tr("Spaces");
|
|
break;
|
|
case TabSettings::TabsOnlyTabPolicy:
|
|
policy = Tr::tr("Tabs");
|
|
break;
|
|
}
|
|
setText(QString("%1: %2").arg(policy).arg(ts.m_indentSize));
|
|
}
|
|
|
|
void showMenu()
|
|
{
|
|
QTC_ASSERT(m_doc, return);
|
|
auto menu = new QMenu(this);
|
|
menu->addAction(ActionManager::command(Constants::AUTO_INDENT_SELECTION)->action());
|
|
menu->setAttribute(Qt::WA_DeleteOnClose);
|
|
if (auto indenter = m_doc->indenter(); indenter && indenter->respectsTabSettings()) {
|
|
auto documentSettings = menu->addMenu(Tr::tr("Document Settings"));
|
|
|
|
auto modifyTabSettings =
|
|
[this](std::function<void(TabSettings & tabSettings)> modifier) {
|
|
return [this, modifier]() {
|
|
auto ts = m_doc->tabSettings();
|
|
ts.m_autoDetect = false;
|
|
modifier(ts);
|
|
m_doc->setTabSettings(ts);
|
|
};
|
|
};
|
|
documentSettings->addAction(
|
|
Tr::tr("Auto detect"),
|
|
modifyTabSettings([doc = m_doc->document()](TabSettings &tabSettings) {
|
|
tabSettings.m_autoDetect = true;
|
|
}));
|
|
auto tabSettings = documentSettings->addMenu(Tr::tr("Tab Settings"));
|
|
tabSettings->addAction(Tr::tr("Spaces"), modifyTabSettings([](TabSettings &tabSettings) {
|
|
tabSettings.m_tabPolicy = TabSettings::SpacesOnlyTabPolicy;
|
|
}));
|
|
tabSettings->addAction(Tr::tr("Tabs"), modifyTabSettings([](TabSettings &tabSettings) {
|
|
tabSettings.m_tabPolicy = TabSettings::TabsOnlyTabPolicy;
|
|
}));
|
|
auto indentSize = documentSettings->addMenu(Tr::tr("Indent Size"));
|
|
auto indentSizeGroup = new QActionGroup(indentSize);
|
|
indentSizeGroup->setExclusive(true);
|
|
for (int i = 1; i <= 8; ++i) {
|
|
auto action = indentSizeGroup->addAction(QString::number(i));
|
|
action->setCheckable(true);
|
|
action->setChecked(i == m_doc->tabSettings().m_indentSize);
|
|
connect(action, &QAction::triggered, modifyTabSettings([i](TabSettings &tabSettings) {
|
|
tabSettings.m_indentSize = i;
|
|
}));
|
|
}
|
|
indentSize->addActions(indentSizeGroup->actions());
|
|
auto tabSize = documentSettings->addMenu(Tr::tr("Tab Size"));
|
|
auto tabSizeGroup = new QActionGroup(tabSize);
|
|
tabSizeGroup->setExclusive(true);
|
|
for (int i = 1; i <= 8; ++i) {
|
|
auto action = tabSizeGroup->addAction(QString::number(i));
|
|
action->setCheckable(true);
|
|
action->setChecked(i == m_doc->tabSettings().m_tabSize);
|
|
connect(action, &QAction::triggered, modifyTabSettings([i](TabSettings &tabSettings) {
|
|
tabSettings.m_tabSize = i;
|
|
}));
|
|
}
|
|
tabSize->addActions(tabSizeGroup->actions());
|
|
}
|
|
|
|
Id globalSettingsCategory;
|
|
if (auto codeStyle = m_doc->codeStyle())
|
|
globalSettingsCategory = codeStyle->globalSettingsCategory();
|
|
if (!globalSettingsCategory.isValid())
|
|
globalSettingsCategory = Constants::TEXT_EDITOR_BEHAVIOR_SETTINGS;
|
|
menu->addAction(Tr::tr("Global Settings..."), [globalSettingsCategory] {
|
|
Core::ICore::showOptionsDialog(globalSettingsCategory);
|
|
});
|
|
|
|
menu->popup(QCursor::pos());
|
|
}
|
|
|
|
TextDocument *m_doc = nullptr;
|
|
};
|
|
|
|
class TextEditorAnimator : public QObject
|
|
{
|
|
Q_OBJECT
|
|
|
|
public:
|
|
TextEditorAnimator(QObject *parent);
|
|
|
|
void init(const QTextCursor &cursor, const QFont &f, const QPalette &pal);
|
|
inline QTextCursor cursor() const { return m_cursor; }
|
|
|
|
void draw(QPainter *p, const QPointF &pos);
|
|
QRectF rect() const;
|
|
|
|
inline qreal value() const { return m_value; }
|
|
inline QPointF lastDrawPos() const { return m_lastDrawPos; }
|
|
|
|
void finish();
|
|
|
|
bool isRunning() const;
|
|
|
|
signals:
|
|
void updateRequest(const QTextCursor &cursor, QPointF lastPos, QRectF rect);
|
|
|
|
private:
|
|
void step(qreal v);
|
|
|
|
QTimeLine m_timeline;
|
|
qreal m_value;
|
|
QTextCursor m_cursor;
|
|
QPointF m_lastDrawPos;
|
|
QFont m_font;
|
|
QPalette m_palette;
|
|
QString m_text;
|
|
QSizeF m_size;
|
|
};
|
|
|
|
class TextEditExtraArea : public QWidget
|
|
{
|
|
public:
|
|
TextEditExtraArea(TextEditorWidget *edit)
|
|
: QWidget(edit)
|
|
{
|
|
textEdit = edit;
|
|
setAutoFillBackground(true);
|
|
}
|
|
|
|
protected:
|
|
QSize sizeHint() const override {
|
|
return {textEdit->extraAreaWidth(), 0};
|
|
}
|
|
void paintEvent(QPaintEvent *event) override {
|
|
textEdit->extraAreaPaintEvent(event);
|
|
}
|
|
void mousePressEvent(QMouseEvent *event) override {
|
|
textEdit->extraAreaMouseEvent(event);
|
|
}
|
|
void mouseMoveEvent(QMouseEvent *event) override {
|
|
textEdit->extraAreaMouseEvent(event);
|
|
}
|
|
void mouseReleaseEvent(QMouseEvent *event) override {
|
|
textEdit->extraAreaMouseEvent(event);
|
|
}
|
|
void leaveEvent(QEvent *event) override {
|
|
textEdit->extraAreaLeaveEvent(event);
|
|
}
|
|
void contextMenuEvent(QContextMenuEvent *event) override {
|
|
textEdit->extraAreaContextMenuEvent(event);
|
|
}
|
|
void changeEvent(QEvent *event) override {
|
|
if (event->type() == QEvent::PaletteChange)
|
|
QCoreApplication::sendEvent(textEdit, event);
|
|
}
|
|
void wheelEvent(QWheelEvent *event) override {
|
|
QCoreApplication::sendEvent(textEdit->viewport(), event);
|
|
}
|
|
bool event(QEvent *event) override
|
|
{
|
|
if (event->type() == QEvent::ToolTip) {
|
|
textEdit->extraAreaToolTipEvent(static_cast<QHelpEvent *>(event));
|
|
return true;
|
|
}
|
|
return QWidget::event(event);
|
|
}
|
|
|
|
private:
|
|
TextEditorWidget *textEdit;
|
|
};
|
|
|
|
class BaseTextEditorPrivate
|
|
{
|
|
public:
|
|
BaseTextEditorPrivate() = default;
|
|
|
|
TextEditorFactoryPrivate *m_origin = nullptr;
|
|
QByteArray m_savedNavigationState;
|
|
};
|
|
|
|
class HoverHandlerRunner
|
|
{
|
|
public:
|
|
using Callback = std::function<void(TextEditorWidget *, BaseHoverHandler *, int)>;
|
|
using FallbackCallback = std::function<void(TextEditorWidget *)>;
|
|
|
|
HoverHandlerRunner(TextEditorWidget *widget, QList<BaseHoverHandler *> &handlers)
|
|
: m_widget(widget)
|
|
, m_handlers(handlers)
|
|
{
|
|
}
|
|
|
|
~HoverHandlerRunner() { abortHandlers(); }
|
|
|
|
void startChecking(const QTextCursor &textCursor, const Callback &callback, const FallbackCallback &fallbackCallback)
|
|
{
|
|
if (m_handlers.empty()) {
|
|
fallbackCallback(m_widget);
|
|
return;
|
|
}
|
|
|
|
// Does the last handler still applies?
|
|
const int documentRevision = textCursor.document()->revision();
|
|
const int position = Text::wordStartCursor(textCursor).position();
|
|
if (m_lastHandlerInfo.applies(documentRevision, position, m_widget)) {
|
|
callback(m_widget, m_lastHandlerInfo.handler, position);
|
|
return;
|
|
}
|
|
|
|
if (isCheckRunning(documentRevision, position))
|
|
return;
|
|
|
|
// Update invocation data
|
|
m_documentRevision = documentRevision;
|
|
m_position = position;
|
|
m_callback = callback;
|
|
m_fallbackCallback = fallbackCallback;
|
|
|
|
restart();
|
|
}
|
|
|
|
bool isCheckRunning(int documentRevision, int position) const
|
|
{
|
|
return m_currentHandlerIndex >= 0
|
|
&& m_documentRevision == documentRevision
|
|
&& m_position == position;
|
|
}
|
|
|
|
void checkNext()
|
|
{
|
|
QTC_ASSERT(m_currentHandlerIndex >= 0, return);
|
|
QTC_ASSERT(m_currentHandlerIndex < m_handlers.size(), return);
|
|
BaseHoverHandler *currentHandler = m_handlers[m_currentHandlerIndex];
|
|
|
|
currentHandler->checkPriority(m_widget, m_position, [this](int priority) {
|
|
onHandlerFinished(m_documentRevision, m_position, priority);
|
|
});
|
|
}
|
|
|
|
void onHandlerFinished(int documentRevision, int position, int priority)
|
|
{
|
|
QTC_ASSERT(m_currentHandlerIndex >= 0, return);
|
|
QTC_ASSERT(m_currentHandlerIndex < m_handlers.size(), return);
|
|
QTC_ASSERT(documentRevision == m_documentRevision, return);
|
|
QTC_ASSERT(position == m_position, return);
|
|
|
|
BaseHoverHandler *currentHandler = m_handlers[m_currentHandlerIndex];
|
|
if (priority > m_highestHandlerPriority) {
|
|
m_highestHandlerPriority = priority;
|
|
m_bestHandler = currentHandler;
|
|
}
|
|
|
|
// There are more, check next
|
|
++m_currentHandlerIndex;
|
|
if (m_currentHandlerIndex < m_handlers.size()) {
|
|
checkNext();
|
|
return;
|
|
}
|
|
m_currentHandlerIndex = -1;
|
|
|
|
// All were queried, run the best
|
|
if (m_bestHandler) {
|
|
m_lastHandlerInfo = LastHandlerInfo(m_bestHandler, m_documentRevision, m_position);
|
|
m_callback(m_widget, m_bestHandler, m_position);
|
|
} else {
|
|
m_fallbackCallback(m_widget);
|
|
}
|
|
}
|
|
|
|
void handlerRemoved(BaseHoverHandler *handler)
|
|
{
|
|
if (m_lastHandlerInfo.handler == handler)
|
|
m_lastHandlerInfo = LastHandlerInfo();
|
|
if (m_currentHandlerIndex >= 0)
|
|
restart();
|
|
}
|
|
|
|
void abortHandlers()
|
|
{
|
|
for (BaseHoverHandler *handler : m_handlers)
|
|
handler->abort();
|
|
m_currentHandlerIndex = -1;
|
|
}
|
|
|
|
private:
|
|
void restart()
|
|
{
|
|
abortHandlers();
|
|
|
|
if (m_handlers.empty())
|
|
return;
|
|
|
|
// Re-initialize process data
|
|
m_currentHandlerIndex = 0;
|
|
m_bestHandler = nullptr;
|
|
m_highestHandlerPriority = BaseHoverHandler::Priority_None;
|
|
|
|
// Start checking
|
|
checkNext();
|
|
}
|
|
|
|
TextEditorWidget *m_widget;
|
|
const QList<BaseHoverHandler *> &m_handlers;
|
|
|
|
struct LastHandlerInfo {
|
|
LastHandlerInfo() = default;
|
|
LastHandlerInfo(BaseHoverHandler *handler, int documentRevision, int cursorPosition)
|
|
: handler(handler)
|
|
, documentRevision(documentRevision)
|
|
, cursorPosition(cursorPosition)
|
|
{}
|
|
|
|
bool applies(int documentRevision, int cursorPosition, TextEditorWidget *widget) const
|
|
{
|
|
return handler
|
|
&& handler->lastHelpItemAppliesTo(widget)
|
|
&& documentRevision == this->documentRevision
|
|
&& cursorPosition == this->cursorPosition;
|
|
}
|
|
|
|
BaseHoverHandler *handler = nullptr;
|
|
int documentRevision = -1;
|
|
int cursorPosition = -1;
|
|
} m_lastHandlerInfo;
|
|
|
|
// invocation data
|
|
Callback m_callback;
|
|
FallbackCallback m_fallbackCallback;
|
|
int m_position = -1;
|
|
int m_documentRevision = -1;
|
|
|
|
// processing data
|
|
int m_currentHandlerIndex = -1;
|
|
int m_highestHandlerPriority = BaseHoverHandler::Priority_None;
|
|
BaseHoverHandler *m_bestHandler = nullptr;
|
|
};
|
|
|
|
struct CursorData
|
|
{
|
|
QTextLayout *layout = nullptr;
|
|
QPointF offset;
|
|
int pos = 0;
|
|
QPen pen;
|
|
};
|
|
|
|
struct PaintEventData
|
|
{
|
|
PaintEventData(TextEditorWidget *editor, QPaintEvent *event, QPointF offset)
|
|
: offset(offset)
|
|
, viewportRect(editor->viewport()->rect())
|
|
, eventRect(event->rect())
|
|
, doc(editor->document())
|
|
, documentLayout(qobject_cast<TextDocumentLayout *>(doc->documentLayout()))
|
|
, documentWidth(int(doc->size().width()))
|
|
, textCursor(editor->textCursor())
|
|
, textCursorBlock(textCursor.block())
|
|
, isEditable(!editor->isReadOnly())
|
|
, fontSettings(editor->textDocument()->fontSettings())
|
|
, lineSpacing(fontSettings.lineSpacing())
|
|
, searchScopeFormat(fontSettings.toTextCharFormat(C_SEARCH_SCOPE))
|
|
, searchResultFormat(fontSettings.toTextCharFormat(C_SEARCH_RESULT))
|
|
, visualWhitespaceFormat(fontSettings.toTextCharFormat(C_VISUAL_WHITESPACE))
|
|
, ifdefedOutFormat(fontSettings.toTextCharFormat(C_DISABLED_CODE))
|
|
, suppressSyntaxInIfdefedOutBlock(ifdefedOutFormat.foreground()
|
|
!= fontSettings.toTextCharFormat(C_TEXT).foreground())
|
|
, tabSettings(editor->textDocument()->tabSettings())
|
|
|
|
{ }
|
|
QPointF offset;
|
|
const QRect viewportRect;
|
|
const QRect eventRect;
|
|
qreal rightMargin = -1;
|
|
const QTextDocument *doc;
|
|
TextDocumentLayout *documentLayout;
|
|
const int documentWidth;
|
|
const QTextCursor textCursor;
|
|
const QTextBlock textCursorBlock;
|
|
const bool isEditable;
|
|
const FontSettings fontSettings;
|
|
const int lineSpacing;
|
|
const QTextCharFormat searchScopeFormat;
|
|
const QTextCharFormat searchResultFormat;
|
|
const QTextCharFormat visualWhitespaceFormat;
|
|
const QTextCharFormat ifdefedOutFormat;
|
|
const bool suppressSyntaxInIfdefedOutBlock;
|
|
QAbstractTextDocumentLayout::PaintContext context;
|
|
QTextBlock visibleCollapsedBlock;
|
|
QPointF visibleCollapsedBlockOffset;
|
|
QTextBlock block;
|
|
QList<CursorData> cursors;
|
|
const TabSettings tabSettings;
|
|
};
|
|
|
|
struct PaintEventBlockData
|
|
{
|
|
QRectF boundingRect;
|
|
QVector<QTextLayout::FormatRange> selections;
|
|
QTextLayout *layout = nullptr;
|
|
int position = 0;
|
|
int length = 0;
|
|
};
|
|
|
|
struct ExtraAreaPaintEventData;
|
|
|
|
struct TextEditorPrivateHighlightBlocks
|
|
{
|
|
QList<int> open;
|
|
QList<int> close;
|
|
QList<int> visualIndent;
|
|
inline int count() const { return visualIndent.size(); }
|
|
inline bool isEmpty() const { return open.isEmpty() || close.isEmpty() || visualIndent.isEmpty(); }
|
|
inline bool operator==(const TextEditorPrivateHighlightBlocks &o) const {
|
|
return (open == o.open && close == o.close && visualIndent == o.visualIndent);
|
|
}
|
|
inline bool operator!=(const TextEditorPrivateHighlightBlocks &o) const { return !(*this == o); }
|
|
};
|
|
|
|
class TextEditorWidgetPrivate : public QObject
|
|
{
|
|
public:
|
|
TextEditorWidgetPrivate(TextEditorWidget *parent);
|
|
~TextEditorWidgetPrivate() override;
|
|
|
|
void updateLineSelectionColor();
|
|
|
|
void print(QPrinter *printer);
|
|
|
|
void maybeSelectLine();
|
|
void duplicateSelection(bool comment);
|
|
void updateCannotDecodeInfo();
|
|
void collectToCircularClipboard();
|
|
void setClipboardSelection();
|
|
|
|
void setDocument(const QSharedPointer<TextDocument> &doc);
|
|
void handleHomeKey(bool anchor, bool block);
|
|
void handleBackspaceKey();
|
|
void moveLineUpDown(bool up);
|
|
void copyLineUpDown(bool up);
|
|
void addSelectionNextFindMatch();
|
|
void addCursorsToLineEnds();
|
|
void saveCurrentCursorPositionForNavigation();
|
|
void updateHighlights();
|
|
void updateCurrentLineInScrollbar();
|
|
void updateCurrentLineHighlight();
|
|
int indentDepthForBlock(const QTextBlock &block, const PaintEventData &data);
|
|
|
|
void drawFoldingMarker(QPainter *painter, const QPalette &pal,
|
|
const QRect &rect,
|
|
bool expanded,
|
|
bool active,
|
|
bool hovered) const;
|
|
bool updateAnnotationBounds(TextBlockUserData *blockUserData, TextDocumentLayout *layout,
|
|
bool annotationsVisible);
|
|
void updateLineAnnotation(const PaintEventData &data, const PaintEventBlockData &blockData,
|
|
QPainter &painter);
|
|
void paintRightMarginArea(PaintEventData &data, QPainter &painter) const;
|
|
void paintRightMarginLine(const PaintEventData &data, QPainter &painter) const;
|
|
void paintBlockHighlight(const PaintEventData &data, QPainter &painter) const;
|
|
void paintSearchResultOverlay(const PaintEventData &data, QPainter &painter) const;
|
|
void paintSelectionOverlay(const PaintEventData &data, QPainter &painter) const;
|
|
void paintIfDefedOutBlocks(const PaintEventData &data, QPainter &painter) const;
|
|
void paintFindScope(const PaintEventData &data, QPainter &painter) const;
|
|
void paintCurrentLineHighlight(const PaintEventData &data, QPainter &painter) const;
|
|
QRectF cursorBlockRect(const QTextDocument *doc,
|
|
const QTextBlock &block,
|
|
int cursorPosition,
|
|
QRectF blockBoundingRect = {},
|
|
bool *doSelection = nullptr) const;
|
|
void paintCursorAsBlock(const PaintEventData &data,
|
|
QPainter &painter,
|
|
PaintEventBlockData &blockData,
|
|
int cursorPosition) const;
|
|
void paintAdditionalVisualWhitespaces(PaintEventData &data, QPainter &painter, qreal top) const;
|
|
void paintIndentDepth(PaintEventData &data, QPainter &painter, const PaintEventBlockData &blockData);
|
|
void paintReplacement(PaintEventData &data, QPainter &painter, qreal top) const;
|
|
void paintWidgetBackground(const PaintEventData &data, QPainter &painter) const;
|
|
void paintOverlays(const PaintEventData &data, QPainter &painter) const;
|
|
void paintCursor(const PaintEventData &data, QPainter &painter) const;
|
|
|
|
void setupBlockLayout(const PaintEventData &data, QPainter &painter,
|
|
PaintEventBlockData &blockData) const;
|
|
void setupSelections(const PaintEventData &data, PaintEventBlockData &blockData) const;
|
|
void addCursorsPosition(PaintEventData &data,
|
|
QPainter &painter,
|
|
const PaintEventBlockData &blockData) const;
|
|
QTextBlock nextVisibleBlock(const QTextBlock &block) const;
|
|
void scheduleCleanupAnnotationCache();
|
|
void cleanupAnnotationCache();
|
|
|
|
// extra area paint methods
|
|
void paintLineNumbers(QPainter &painter, const ExtraAreaPaintEventData &data,
|
|
const QRectF &blockBoundingRect) const;
|
|
void paintTextMarks(QPainter &painter, const ExtraAreaPaintEventData &data,
|
|
const QRectF &blockBoundingRect) const;
|
|
void paintCodeFolding(QPainter &painter, const ExtraAreaPaintEventData &data,
|
|
const QRectF &blockBoundingRect) const;
|
|
void paintRevisionMarker(QPainter &painter, const ExtraAreaPaintEventData &data,
|
|
const QRectF &blockBoundingRect) const;
|
|
|
|
void toggleBlockVisible(const QTextBlock &block);
|
|
QRect foldBox();
|
|
|
|
QTextBlock foldedBlockAt(const QPoint &pos, QRect *box = nullptr) const;
|
|
|
|
bool isMouseNavigationEvent(QMouseEvent *e) const;
|
|
void requestUpdateLink(QMouseEvent *e);
|
|
void updateLink();
|
|
void showLink(const Utils::Link &);
|
|
void clearLink();
|
|
|
|
void universalHelper(); // test function for development
|
|
|
|
bool cursorMoveKeyEvent(QKeyEvent *e);
|
|
|
|
void processTooltipRequest(const QTextCursor &c);
|
|
bool processAnnotaionTooltipRequest(const QTextBlock &block, const QPoint &pos) const;
|
|
void showTextMarksToolTip(const QPoint &pos,
|
|
const TextMarks &marks,
|
|
const TextMark *mainTextMark = nullptr) const;
|
|
|
|
void transformSelection(TransformationMethod method);
|
|
|
|
void slotUpdateExtraAreaWidth(std::optional<int> width = {});
|
|
void slotUpdateRequest(const QRect &r, int dy);
|
|
void slotUpdateBlockNotify(const QTextBlock &);
|
|
void updateTabStops();
|
|
void applyTabSettings();
|
|
void applyFontSettingsDelayed();
|
|
void markRemoved(TextMark *mark);
|
|
|
|
void editorContentsChange(int position, int charsRemoved, int charsAdded);
|
|
void documentAboutToBeReloaded();
|
|
void documentReloadFinished(bool success);
|
|
void highlightSearchResultsSlot(const QString &txt, FindFlags findFlags);
|
|
void setupScrollBar();
|
|
void highlightSearchResultsInScrollBar();
|
|
void scheduleUpdateHighlightScrollBar();
|
|
void updateHighlightScrollBarNow();
|
|
struct SearchResult {
|
|
int start;
|
|
int length;
|
|
};
|
|
void addSearchResultsToScrollBar(const QVector<SearchResult> &results);
|
|
void addSelectionHighlightToScrollBar(const QVector<SearchResult> &selections);
|
|
void adjustScrollBarRanges();
|
|
|
|
void setFindScope(const MultiTextCursor &scope);
|
|
|
|
void updateCursorPosition();
|
|
|
|
// parentheses matcher
|
|
void _q_matchParentheses();
|
|
void _q_highlightBlocks();
|
|
void autocompleterHighlight(const QTextCursor &cursor = QTextCursor());
|
|
void updateAnimator(QPointer<TextEditorAnimator> animator, QPainter &painter);
|
|
void cancelCurrentAnimations();
|
|
void slotSelectionChanged();
|
|
void _q_animateUpdate(const QTextCursor &cursor, QPointF lastPos, QRectF rect);
|
|
void updateCodeFoldingVisible();
|
|
void updateFileLineEndingVisible();
|
|
void updateTabSettingsButtonVisible();
|
|
|
|
void reconfigure();
|
|
void updateSyntaxInfoBar(const HighlighterHelper::Definitions &definitions, const QString &fileName);
|
|
void removeSyntaxInfoBar();
|
|
void configureGenericHighlighter(const KSyntaxHighlighting::Definition &definition);
|
|
void setupFromDefinition(const KSyntaxHighlighting::Definition &definition);
|
|
KSyntaxHighlighting::Definition currentDefinition();
|
|
void rememberCurrentSyntaxDefinition();
|
|
void openLinkUnderCursor(bool openInNextSplit);
|
|
void openTypeUnderCursor(bool openInNextSplit);
|
|
qreal charWidth() const;
|
|
|
|
std::unique_ptr<EmbeddedWidgetInterface> insertWidget(QWidget *widget, int line);
|
|
void forceUpdateScrollbarSize();
|
|
|
|
// actions
|
|
void registerActions();
|
|
void updateActions();
|
|
void updateOptionalActions();
|
|
void updateRedoAction();
|
|
void updateUndoAction();
|
|
void updateCopyAction(bool on);
|
|
|
|
public:
|
|
TextEditorWidget *q;
|
|
QWidget *m_toolBarWidget = nullptr;
|
|
QToolBar *m_toolBar = nullptr;
|
|
QWidget *m_stretchWidget = nullptr;
|
|
QAction *m_stretchAction = nullptr;
|
|
QAction *m_toolbarOutlineAction = nullptr;
|
|
LineColumnButton *m_cursorPositionButton = nullptr;
|
|
TabSettingsButton *m_tabSettingsButton = nullptr;
|
|
QToolButton *m_fileEncodingButton = nullptr;
|
|
QAction *m_fileEncodingLabelAction = nullptr;
|
|
BaseTextFind *m_find = nullptr;
|
|
|
|
QToolButton *m_fileLineEnding = nullptr;
|
|
QAction *m_fileLineEndingAction = nullptr;
|
|
|
|
uint m_optionalActionMask = OptionalActions::None;
|
|
bool m_contentsChanged = false;
|
|
bool m_lastCursorChangeWasInteresting = false;
|
|
std::shared_ptr<void> m_suggestionBlocker;
|
|
|
|
QSharedPointer<TextDocument> m_document;
|
|
QList<QMetaObject::Connection> m_documentConnections;
|
|
QByteArray m_tempState;
|
|
|
|
bool m_parenthesesMatchingEnabled = false;
|
|
QTimer m_parenthesesMatchingTimer;
|
|
|
|
QWidget *m_extraArea = nullptr;
|
|
|
|
Id m_tabSettingsId;
|
|
DisplaySettings m_displaySettings;
|
|
bool m_annotationsrRight = true;
|
|
MarginSettings m_marginSettings;
|
|
// apply when making visible the first time, for the split case
|
|
bool m_fontSettingsNeedsApply = true;
|
|
bool m_wasNotYetShown = true;
|
|
BehaviorSettings m_behaviorSettings;
|
|
|
|
int extraAreaSelectionAnchorBlockNumber = -1;
|
|
int extraAreaToggleMarkBlockNumber = -1;
|
|
int extraAreaHighlightFoldedBlockNumber = -1;
|
|
int extraAreaPreviousMarkTooltipRequestedLine = -1;
|
|
|
|
TextEditorOverlay *m_overlay = nullptr;
|
|
SnippetOverlay *m_snippetOverlay = nullptr;
|
|
TextEditorOverlay *m_searchResultOverlay = nullptr;
|
|
TextEditorOverlay *m_selectionHighlightOverlay = nullptr;
|
|
bool snippetCheckCursor(const QTextCursor &cursor);
|
|
void snippetTabOrBacktab(bool forward);
|
|
|
|
struct AnnotationRect
|
|
{
|
|
QRectF rect;
|
|
const TextMark *mark;
|
|
friend bool operator==(const AnnotationRect &a, const AnnotationRect &b)
|
|
{ return a.mark == b.mark && a.rect == b.rect; }
|
|
};
|
|
bool cleanupAnnotationRectsScheduled = false;
|
|
QMap<int, QList<AnnotationRect>> m_annotationRects;
|
|
QRectF getLastLineLineRect(const QTextBlock &block);
|
|
|
|
RefactorOverlay *m_refactorOverlay = nullptr;
|
|
HelpItem m_contextHelpItem;
|
|
|
|
QBasicTimer foldedBlockTimer;
|
|
int visibleFoldedBlockNumber = -1;
|
|
int suggestedVisibleFoldedBlockNumber = -1;
|
|
void clearVisibleFoldedBlock();
|
|
bool m_mouseOnFoldedMarker = false;
|
|
void foldLicenseHeader();
|
|
|
|
QBasicTimer autoScrollTimer;
|
|
uint m_marksVisible : 1;
|
|
uint m_codeFoldingVisible : 1;
|
|
uint m_codeFoldingSupported : 1;
|
|
uint m_revisionsVisible : 1;
|
|
uint m_lineNumbersVisible : 1;
|
|
uint m_highlightCurrentLine : 1;
|
|
uint m_requestMarkEnabled : 1;
|
|
uint m_lineSeparatorsAllowed : 1;
|
|
uint m_maybeFakeTooltipEvent : 1;
|
|
int m_visibleWrapColumn = 0;
|
|
|
|
Utils::Link m_currentLink;
|
|
bool m_linkPressed = false;
|
|
QTextCursor m_pendingLinkUpdate;
|
|
QTextCursor m_lastLinkUpdate;
|
|
|
|
QRegularExpression m_searchExpr;
|
|
QString m_findText;
|
|
FindFlags m_findFlags;
|
|
void highlightSearchResults(const QTextBlock &block, const PaintEventData &data) const;
|
|
void highlightSelection(const QTextBlock &block) const;
|
|
QTimer m_delayedUpdateTimer;
|
|
|
|
void setExtraSelections(Utils::Id kind, const QList<QTextEdit::ExtraSelection> &selections);
|
|
QHash<Utils::Id, QList<QTextEdit::ExtraSelection>> m_extraSelections;
|
|
|
|
void startCursorFlashTimer();
|
|
void resetCursorFlashTimer();
|
|
QBasicTimer m_cursorFlashTimer;
|
|
bool m_cursorVisible = false;
|
|
bool m_moveLineUndoHack = false;
|
|
void updateCursorSelections();
|
|
void moveCursor(QTextCursor::MoveOperation operation,
|
|
QTextCursor::MoveMode mode = QTextCursor::MoveAnchor);
|
|
QRect cursorUpdateRect(const MultiTextCursor &cursor);
|
|
|
|
Utils::MultiTextCursor m_findScope;
|
|
|
|
QTextCursor m_selectBlockAnchor;
|
|
|
|
void moveCursorVisible(bool ensureVisible = true);
|
|
|
|
int visualIndent(const QTextBlock &block) const;
|
|
TextEditorPrivateHighlightBlocks m_highlightBlocksInfo;
|
|
QTimer m_highlightBlocksTimer;
|
|
|
|
CodeAssistant m_codeAssistant;
|
|
|
|
QList<BaseHoverHandler *> m_hoverHandlers; // Not owned
|
|
HoverHandlerRunner m_hoverHandlerRunner;
|
|
|
|
QPointer<QSequentialAnimationGroup> m_navigationAnimation;
|
|
|
|
QPointer<TextEditorAnimator> m_bracketsAnimator;
|
|
|
|
// Animation and highlighting of auto completed text
|
|
QPointer<TextEditorAnimator> m_autocompleteAnimator;
|
|
bool m_animateAutoComplete = true;
|
|
bool m_highlightAutoComplete = true;
|
|
bool m_skipAutoCompletedText = true;
|
|
bool m_skipFormatOnPaste = false;
|
|
bool m_removeAutoCompletedText = true;
|
|
bool m_keepAutoCompletionHighlight = false;
|
|
QList<QTextCursor> m_autoCompleteHighlightPos;
|
|
void updateAutoCompleteHighlight();
|
|
|
|
QSet<int> m_cursorBlockNumbers;
|
|
int m_blockCount = 0;
|
|
|
|
QPoint m_markDragStart;
|
|
bool m_markDragging = false;
|
|
QCursor m_markDragCursor;
|
|
TextMark* m_dragMark = nullptr;
|
|
QTextCursor m_dndCursor;
|
|
|
|
QScopedPointer<AutoCompleter> m_autoCompleter;
|
|
CommentDefinition m_commentDefinition;
|
|
|
|
QFuture<SearchResultItems> m_searchFuture;
|
|
QFuture<SearchResultItems> m_selectionHighlightFuture;
|
|
QVector<SearchResult> m_searchResults;
|
|
QVector<SearchResult> m_selectionResults;
|
|
QTimer m_scrollBarUpdateTimer;
|
|
HighlightScrollBarController *m_highlightScrollBarController = nullptr;
|
|
bool m_scrollBarUpdateScheduled = false;
|
|
|
|
const MultiTextCursor m_cursors;
|
|
struct BlockSelection
|
|
{
|
|
int blockNumber = -1;
|
|
int column = -1;
|
|
int anchorBlockNumber = -1;
|
|
int anchorColumn = -1;
|
|
};
|
|
QList<BlockSelection> m_blockSelections;
|
|
QList<QTextCursor> generateCursorsForBlockSelection(const BlockSelection &blockSelection);
|
|
void initBlockSelection();
|
|
void clearBlockSelection();
|
|
void handleMoveBlockSelection(QTextCursor::MoveOperation op);
|
|
|
|
class UndoCursor
|
|
{
|
|
public:
|
|
int position = 0;
|
|
int anchor = 0;
|
|
};
|
|
using UndoMultiCursor = QList<UndoCursor>;
|
|
QStack<UndoMultiCursor> m_undoCursorStack;
|
|
QList<int> m_visualIndentCache;
|
|
int m_visualIndentOffset = 0;
|
|
|
|
void insertSuggestion(std::unique_ptr<TextSuggestion> &&suggestion);
|
|
void updateSuggestion();
|
|
void clearCurrentSuggestion();
|
|
QTextBlock m_suggestionBlock;
|
|
int m_numEmbeddedWidgets = 0;
|
|
|
|
Context m_editorContext;
|
|
QAction *m_undoAction = nullptr;
|
|
QAction *m_redoAction = nullptr;
|
|
QAction *m_copyAction = nullptr;
|
|
QAction *m_copyHtmlAction = nullptr;
|
|
QAction *m_cutAction = nullptr;
|
|
QAction *m_autoIndentAction = nullptr;
|
|
QAction *m_autoFormatAction = nullptr;
|
|
QAction *m_visualizeWhitespaceAction = nullptr;
|
|
QAction *m_textWrappingAction = nullptr;
|
|
QAction *m_unCommentSelectionAction = nullptr;
|
|
QAction *m_unfoldAllAction = nullptr;
|
|
QAction *m_followSymbolAction = nullptr;
|
|
QAction *m_followSymbolInNextSplitAction = nullptr;
|
|
QAction *m_followToTypeAction = nullptr;
|
|
QAction *m_followToTypeInNextSplitAction = nullptr;
|
|
QAction *m_findUsageAction = nullptr;
|
|
QAction *m_openCallHierarchyAction = nullptr;
|
|
QAction *m_openTypeHierarchyAction = nullptr;
|
|
QAction *m_renameSymbolAction = nullptr;
|
|
QAction *m_jumpToFileAction = nullptr;
|
|
QAction *m_jumpToFileInNextSplitAction = nullptr;
|
|
QList<QAction *> m_modifyingActions;
|
|
};
|
|
|
|
class TextEditorWidgetFind : public BaseTextFind
|
|
{
|
|
public:
|
|
TextEditorWidgetFind(TextEditorWidget *editor)
|
|
: BaseTextFind(editor)
|
|
, m_editor(editor)
|
|
{
|
|
setMultiTextCursorProvider([editor]() { return editor->multiTextCursor(); });
|
|
}
|
|
|
|
~TextEditorWidgetFind() override { cancelCurrentSelectAll(); }
|
|
|
|
bool supportsSelectAll() const override { return true; }
|
|
void selectAll(const QString &txt, FindFlags findFlags) override;
|
|
|
|
static void cancelCurrentSelectAll();
|
|
|
|
private:
|
|
TextEditorWidget * const m_editor;
|
|
static QFutureWatcher<SearchResultItems> *m_selectWatcher;
|
|
};
|
|
|
|
static QTextCursor selectRange(QTextDocument *textDocument, const Text::Range &range,
|
|
TextEditorWidgetPrivate::SearchResult *searchResult = nullptr)
|
|
{
|
|
const int startLine = qMax(range.begin.line - 1, 0);
|
|
const int startColumn = qMax(range.begin.column, 0);
|
|
const int endLine = qMax(range.end.line - 1, 0);
|
|
const int endColumn = qMax(range.end.column, 0);
|
|
const int startPosition = textDocument->findBlockByNumber(startLine).position() + startColumn;
|
|
const int endPosition = textDocument->findBlockByNumber(endLine).position() + endColumn;
|
|
QTextCursor textCursor(textDocument);
|
|
textCursor.setPosition(startPosition);
|
|
textCursor.setPosition(endPosition, QTextCursor::KeepAnchor);
|
|
if (searchResult)
|
|
*searchResult = {startPosition + 1, endPosition + 1};
|
|
return textCursor;
|
|
}
|
|
|
|
QFutureWatcher<SearchResultItems> *TextEditorWidgetFind::m_selectWatcher = nullptr;
|
|
|
|
void TextEditorWidgetFind::selectAll(const QString &txt, FindFlags findFlags)
|
|
{
|
|
if (txt.isEmpty())
|
|
return;
|
|
|
|
cancelCurrentSelectAll();
|
|
|
|
m_selectWatcher = new QFutureWatcher<SearchResultItems>();
|
|
connect(m_selectWatcher, &QFutureWatcher<SearchResultItems>::finished, this, [this] {
|
|
const QFuture<SearchResultItems> future = m_selectWatcher->future();
|
|
m_selectWatcher->deleteLater();
|
|
m_selectWatcher = nullptr;
|
|
if (future.resultCount() <= 0)
|
|
return;
|
|
const SearchResultItems &results = future.result();
|
|
if (results.isEmpty())
|
|
return;
|
|
const auto cursorForResult = [this](const SearchResultItem &item) {
|
|
return selectRange(m_editor->document(), item.mainRange());
|
|
};
|
|
QList<QTextCursor> cursors = Utils::transform(results, cursorForResult);
|
|
cursors = Utils::filtered(cursors, [this](const QTextCursor &c) {
|
|
return m_editor->inFindScope(c);
|
|
});
|
|
m_editor->setMultiTextCursor(MultiTextCursor(cursors));
|
|
m_editor->setFocus();
|
|
});
|
|
|
|
m_selectWatcher->setFuture(Utils::asyncRun(Utils::searchInContents, txt, findFlags,
|
|
m_editor->textDocument()->filePath(),
|
|
m_editor->textDocument()->plainText()));
|
|
}
|
|
|
|
void TextEditorWidgetFind::cancelCurrentSelectAll()
|
|
{
|
|
if (m_selectWatcher) {
|
|
m_selectWatcher->disconnect();
|
|
m_selectWatcher->cancel();
|
|
m_selectWatcher->deleteLater();
|
|
m_selectWatcher = nullptr;
|
|
}
|
|
}
|
|
|
|
TextEditorWidgetPrivate::TextEditorWidgetPrivate(TextEditorWidget *parent)
|
|
: q(parent)
|
|
, m_suggestionBlocker((void *) this, [](void *) {})
|
|
, m_overlay(new TextEditorOverlay(q))
|
|
, m_snippetOverlay(new SnippetOverlay(q))
|
|
, m_searchResultOverlay(new TextEditorOverlay(q))
|
|
, m_selectionHighlightOverlay(new TextEditorOverlay(q))
|
|
, m_refactorOverlay(new RefactorOverlay(q))
|
|
, m_marksVisible(false)
|
|
, m_codeFoldingVisible(false)
|
|
, m_codeFoldingSupported(false)
|
|
, m_revisionsVisible(false)
|
|
, m_lineNumbersVisible(true)
|
|
, m_highlightCurrentLine(true)
|
|
, m_requestMarkEnabled(true)
|
|
, m_lineSeparatorsAllowed(false)
|
|
, m_maybeFakeTooltipEvent(false)
|
|
, m_codeAssistant(parent)
|
|
, m_hoverHandlerRunner(parent, m_hoverHandlers)
|
|
, m_autoCompleter(new AutoCompleter)
|
|
, m_editorContext(Id::generate())
|
|
{
|
|
m_selectionHighlightOverlay->show();
|
|
m_find = new TextEditorWidgetFind(q);
|
|
connect(m_find, &BaseTextFind::highlightAllRequested,
|
|
this, &TextEditorWidgetPrivate::highlightSearchResultsSlot);
|
|
connect(m_find, &BaseTextFind::findScopeChanged,
|
|
this, &TextEditorWidgetPrivate::setFindScope);
|
|
Aggregation::aggregate({q, m_find});
|
|
|
|
m_extraArea = new TextEditExtraArea(q);
|
|
m_extraArea->setMouseTracking(true);
|
|
|
|
auto toolBarLayout = new QHBoxLayout;
|
|
toolBarLayout->setContentsMargins(0, 0, 0, 0);
|
|
toolBarLayout->setSpacing(0);
|
|
m_toolBarWidget = new Utils::StyledBar;
|
|
m_toolBarWidget->setLayout(toolBarLayout);
|
|
m_stretchWidget = new QWidget;
|
|
m_stretchWidget->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);
|
|
m_toolBar = new QToolBar;
|
|
m_toolBar->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Minimum);
|
|
m_stretchAction = m_toolBar->addWidget(m_stretchWidget);
|
|
m_toolBarWidget->layout()->addWidget(m_toolBar);
|
|
|
|
const int spacing = q->style()->pixelMetric(QStyle::PM_LayoutHorizontalSpacing) / 2;
|
|
m_cursorPositionButton = new LineColumnButton(q);
|
|
m_cursorPositionButton->setContentsMargins(spacing, 0, spacing, 0);
|
|
m_toolBarWidget->layout()->addWidget(m_cursorPositionButton);
|
|
|
|
m_tabSettingsButton = new TabSettingsButton(q);
|
|
m_tabSettingsButton->setContentsMargins(spacing, 0, spacing, 0);
|
|
m_toolBarWidget->layout()->addWidget(m_tabSettingsButton);
|
|
updateTabSettingsButtonVisible();
|
|
|
|
m_fileLineEnding = new QToolButton(q);
|
|
m_fileLineEnding->setContentsMargins(spacing, 0, spacing, 0);
|
|
m_fileLineEndingAction = m_toolBar->addWidget(m_fileLineEnding);
|
|
updateFileLineEndingVisible();
|
|
|
|
m_fileEncodingButton = new QToolButton;
|
|
m_fileEncodingButton->setContentsMargins(spacing, 0, spacing, 0);
|
|
m_fileEncodingLabelAction = m_toolBar->addWidget(m_fileEncodingButton);
|
|
|
|
m_extraSelections.reserve(NExtraSelectionKinds);
|
|
|
|
connect(&m_codeAssistant, &CodeAssistant::finished,
|
|
q, &TextEditorWidget::assistFinished);
|
|
|
|
connect(q, &QPlainTextEdit::blockCountChanged, this, [this] { slotUpdateExtraAreaWidth(); });
|
|
|
|
connect(q, &QPlainTextEdit::modificationChanged,
|
|
m_extraArea, QOverload<>::of(&QWidget::update));
|
|
|
|
connect(q, &QPlainTextEdit::cursorPositionChanged,
|
|
q, &TextEditorWidget::slotCursorPositionChanged);
|
|
|
|
connect(q, &QPlainTextEdit::cursorPositionChanged,
|
|
this, &TextEditorWidgetPrivate::updateCursorPosition);
|
|
|
|
connect(q, &QPlainTextEdit::updateRequest,
|
|
this, &TextEditorWidgetPrivate::slotUpdateRequest);
|
|
|
|
connect(q, &QPlainTextEdit::selectionChanged,
|
|
this, &TextEditorWidgetPrivate::slotSelectionChanged);
|
|
|
|
connect(q, &QPlainTextEdit::undoAvailable,
|
|
this, &TextEditorWidgetPrivate::updateUndoAction);
|
|
|
|
connect(q, &QPlainTextEdit::redoAvailable,
|
|
this, &TextEditorWidgetPrivate::updateRedoAction);
|
|
|
|
connect(q, &QPlainTextEdit::copyAvailable,
|
|
this, &TextEditorWidgetPrivate::updateCopyAction);
|
|
|
|
m_parenthesesMatchingTimer.setSingleShot(true);
|
|
m_parenthesesMatchingTimer.setInterval(50);
|
|
connect(&m_parenthesesMatchingTimer, &QTimer::timeout,
|
|
this, &TextEditorWidgetPrivate::_q_matchParentheses);
|
|
|
|
m_highlightBlocksTimer.setSingleShot(true);
|
|
connect(&m_highlightBlocksTimer, &QTimer::timeout,
|
|
this, &TextEditorWidgetPrivate::_q_highlightBlocks);
|
|
|
|
m_scrollBarUpdateTimer.setSingleShot(true);
|
|
connect(&m_scrollBarUpdateTimer, &QTimer::timeout,
|
|
this, &TextEditorWidgetPrivate::highlightSearchResultsInScrollBar);
|
|
|
|
m_delayedUpdateTimer.setSingleShot(true);
|
|
connect(&m_delayedUpdateTimer, &QTimer::timeout,
|
|
q->viewport(), QOverload<>::of(&QWidget::update));
|
|
|
|
connect(m_fileEncodingButton, &QToolButton::clicked,
|
|
q, &TextEditorWidget::selectEncoding);
|
|
|
|
connect(m_fileLineEnding, &QToolButton::clicked, ActionManager::instance(), [this] {
|
|
QMenu *menu = new QMenu(q);
|
|
menu->setAttribute(Qt::WA_DeleteOnClose);
|
|
menu->addAction(Tr::tr("Unix Line Endings (LF)"),
|
|
[this] { q->selectLineEnding(TextFileFormat::LFLineTerminator); });
|
|
menu->addAction(Tr::tr("Windows Line Endings (CRLF)"),
|
|
[this] { q->selectLineEnding(TextFileFormat::CRLFLineTerminator); });
|
|
menu->popup(QCursor::pos());
|
|
});
|
|
|
|
TextEditorSettings *settings = TextEditorSettings::instance();
|
|
|
|
// Connect to settings change signals
|
|
connect(settings, &TextEditorSettings::typingSettingsChanged,
|
|
q, &TextEditorWidget::setTypingSettings);
|
|
connect(settings, &TextEditorSettings::storageSettingsChanged,
|
|
q, &TextEditorWidget::setStorageSettings);
|
|
connect(settings, &TextEditorSettings::behaviorSettingsChanged,
|
|
q, &TextEditorWidget::setBehaviorSettings);
|
|
connect(settings, &TextEditorSettings::marginSettingsChanged,
|
|
q, &TextEditorWidget::setMarginSettings);
|
|
connect(settings, &TextEditorSettings::displaySettingsChanged,
|
|
q, &TextEditorWidget::setDisplaySettings);
|
|
connect(settings, &TextEditorSettings::completionSettingsChanged,
|
|
q, &TextEditorWidget::setCompletionSettings);
|
|
connect(settings, &TextEditorSettings::extraEncodingSettingsChanged,
|
|
q, &TextEditorWidget::setExtraEncodingSettings);
|
|
|
|
auto context = new Core::IContext(this);
|
|
context->setWidget(q);
|
|
context->setContext(m_editorContext);
|
|
Core::ICore::addContextObject(context);
|
|
|
|
registerActions();
|
|
updateActions();
|
|
}
|
|
|
|
TextEditorWidgetPrivate::~TextEditorWidgetPrivate()
|
|
{
|
|
QTextDocument *doc = m_document->document();
|
|
QTC_CHECK(doc);
|
|
auto documentLayout = qobject_cast<TextDocumentLayout*>(doc->documentLayout());
|
|
QTC_CHECK(documentLayout);
|
|
QTC_CHECK(m_document.data());
|
|
documentLayout->disconnect(this);
|
|
documentLayout->disconnect(m_extraArea);
|
|
doc->disconnect(this);
|
|
m_document.data()->disconnect(this);
|
|
q->disconnect(documentLayout);
|
|
q->disconnect(this);
|
|
delete m_toolBarWidget;
|
|
delete m_highlightScrollBarController;
|
|
if (m_searchFuture.isRunning())
|
|
m_searchFuture.cancel();
|
|
if (m_selectionHighlightFuture.isRunning())
|
|
m_selectionHighlightFuture.cancel();
|
|
}
|
|
|
|
static QFrame *createSeparator(const QString &styleSheet)
|
|
{
|
|
QFrame* separator = new QFrame();
|
|
separator->setStyleSheet(styleSheet);
|
|
separator->setFrameShape(QFrame::HLine);
|
|
QSizePolicy sizePolicy = separator->sizePolicy();
|
|
sizePolicy.setHorizontalPolicy(QSizePolicy::MinimumExpanding);
|
|
separator->setSizePolicy(sizePolicy);
|
|
|
|
return separator;
|
|
}
|
|
|
|
static QLayout *createSeparatorLayout()
|
|
{
|
|
QString styleSheet = "color: gray";
|
|
|
|
QFrame* separator1 = createSeparator(styleSheet);
|
|
QFrame* separator2 = createSeparator(styleSheet);
|
|
auto label = new QLabel(Tr::tr("Other annotations"));
|
|
label->setStyleSheet(styleSheet);
|
|
|
|
auto layout = new QHBoxLayout;
|
|
layout->addWidget(separator1);
|
|
layout->addWidget(label);
|
|
layout->addWidget(separator2);
|
|
|
|
return layout;
|
|
}
|
|
|
|
void TextEditorWidgetPrivate::showTextMarksToolTip(const QPoint &pos,
|
|
const TextMarks &marks,
|
|
const TextMark *mainTextMark) const
|
|
{
|
|
if (!mainTextMark && marks.isEmpty())
|
|
return; // Nothing to show
|
|
|
|
TextMarks allMarks = marks;
|
|
|
|
auto layout = new QGridLayout;
|
|
layout->setContentsMargins(0, 0, 0, 0);
|
|
layout->setSpacing(2);
|
|
|
|
if (mainTextMark) {
|
|
mainTextMark->addToToolTipLayout(layout);
|
|
if (allMarks.size() > 1)
|
|
layout->addLayout(createSeparatorLayout(), layout->rowCount(), 0, 1, -1);
|
|
}
|
|
|
|
Utils::sort(allMarks, [](const TextMark *mark1, const TextMark *mark2) {
|
|
return mark1->priority() > mark2->priority();
|
|
});
|
|
|
|
for (const TextMark *mark : std::as_const(allMarks)) {
|
|
if (mark != mainTextMark)
|
|
mark->addToToolTipLayout(layout);
|
|
}
|
|
|
|
layout->addWidget(DisplaySettings::createAnnotationSettingsLink(),
|
|
layout->rowCount(), 0, 1, -1, Qt::AlignRight);
|
|
ToolTip::show(pos, layout, q);
|
|
}
|
|
|
|
} // namespace Internal
|
|
|
|
QString TextEditorWidget::plainTextFromSelection(const QTextCursor &cursor) const
|
|
{
|
|
// Copy the selected text as plain text
|
|
QString text = cursor.selectedText();
|
|
return TextDocument::convertToPlainText(text);
|
|
}
|
|
|
|
QString TextEditorWidget::plainTextFromSelection(const Utils::MultiTextCursor &cursor) const
|
|
{
|
|
return TextDocument::convertToPlainText(cursor.selectedText());
|
|
}
|
|
|
|
static const char kTextBlockMimeType[] = "application/vnd.qtcreator.blocktext";
|
|
|
|
Id TextEditorWidget::SnippetPlaceholderSelection("TextEdit.SnippetPlaceHolderSelection");
|
|
Id TextEditorWidget::CurrentLineSelection("TextEdit.CurrentLineSelection");
|
|
Id TextEditorWidget::ParenthesesMatchingSelection("TextEdit.ParenthesesMatchingSelection");
|
|
Id TextEditorWidget::AutoCompleteSelection("TextEdit.AutoCompleteSelection");
|
|
Id TextEditorWidget::CodeWarningsSelection("TextEdit.CodeWarningsSelection");
|
|
Id TextEditorWidget::CodeSemanticsSelection("TextEdit.CodeSemanticsSelection");
|
|
Id TextEditorWidget::CursorSelection("TextEdit.CursorSelection");
|
|
Id TextEditorWidget::UndefinedSymbolSelection("TextEdit.UndefinedSymbolSelection");
|
|
Id TextEditorWidget::UnusedSymbolSelection("TextEdit.UnusedSymbolSelection");
|
|
Id TextEditorWidget::OtherSelection("TextEdit.OtherSelection");
|
|
Id TextEditorWidget::ObjCSelection("TextEdit.ObjCSelection");
|
|
Id TextEditorWidget::DebuggerExceptionSelection("TextEdit.DebuggerExceptionSelection");
|
|
Id TextEditorWidget::FakeVimSelection("TextEdit.FakeVimSelection");
|
|
|
|
TextEditorWidget::TextEditorWidget(QWidget *parent)
|
|
: QPlainTextEdit(parent)
|
|
{
|
|
// "Needed", as the creation below triggers ChildEvents that are
|
|
// passed to this object's event() which uses 'd'.
|
|
d = std::make_unique<Internal::TextEditorWidgetPrivate>(this);
|
|
setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn);
|
|
setLayoutDirection(Qt::LeftToRight);
|
|
viewport()->setMouseTracking(true);
|
|
setFrameStyle(QFrame::NoFrame);
|
|
}
|
|
|
|
TextEditorWidget::~TextEditorWidget() = default;
|
|
|
|
void TextEditorWidget::setTextDocument(const QSharedPointer<TextDocument> &doc)
|
|
{
|
|
d->setDocument(doc);
|
|
}
|
|
|
|
void TextEditorWidgetPrivate::setupScrollBar()
|
|
{
|
|
if (m_displaySettings.m_scrollBarHighlights) {
|
|
if (!m_highlightScrollBarController)
|
|
m_highlightScrollBarController = new HighlightScrollBarController();
|
|
|
|
m_highlightScrollBarController->setScrollArea(q);
|
|
highlightSearchResultsInScrollBar();
|
|
scheduleUpdateHighlightScrollBar();
|
|
} else if (m_highlightScrollBarController) {
|
|
delete m_highlightScrollBarController;
|
|
m_highlightScrollBarController = nullptr;
|
|
}
|
|
}
|
|
|
|
void TextEditorWidgetPrivate::setDocument(const QSharedPointer<TextDocument> &doc)
|
|
{
|
|
QSharedPointer<TextDocument> previousDocument = m_document;
|
|
for (const QMetaObject::Connection &connection : m_documentConnections)
|
|
disconnect(connection);
|
|
m_documentConnections.clear();
|
|
|
|
m_document = doc;
|
|
q->QPlainTextEdit::setDocument(doc->document());
|
|
m_tabSettingsButton->setDocument(q->textDocument());
|
|
previousDocument.clear();
|
|
q->setCursorWidth(2); // Applies to the document layout
|
|
|
|
auto documentLayout = qobject_cast<TextDocumentLayout *>(
|
|
m_document->document()->documentLayout());
|
|
QTC_CHECK(documentLayout);
|
|
|
|
m_documentConnections << connect(documentLayout,
|
|
&QPlainTextDocumentLayout::updateBlock,
|
|
this,
|
|
&TextEditorWidgetPrivate::slotUpdateBlockNotify);
|
|
|
|
m_documentConnections << connect(documentLayout,
|
|
&TextDocumentLayout::updateExtraArea,
|
|
m_extraArea,
|
|
QOverload<>::of(&QWidget::update));
|
|
|
|
m_documentConnections << connect(q,
|
|
&TextEditorWidget::requestBlockUpdate,
|
|
documentLayout,
|
|
&QPlainTextDocumentLayout::updateBlock);
|
|
|
|
m_documentConnections << connect(documentLayout,
|
|
&TextDocumentLayout::updateExtraArea,
|
|
this,
|
|
&TextEditorWidgetPrivate::scheduleUpdateHighlightScrollBar);
|
|
|
|
m_documentConnections << connect(documentLayout,
|
|
&TextDocumentLayout::parenthesesChanged,
|
|
&m_parenthesesMatchingTimer,
|
|
QOverload<>::of(&QTimer::start));
|
|
|
|
m_documentConnections << connect(documentLayout,
|
|
&QAbstractTextDocumentLayout::documentSizeChanged,
|
|
this,
|
|
&TextEditorWidgetPrivate::scheduleUpdateHighlightScrollBar);
|
|
|
|
m_documentConnections << connect(documentLayout,
|
|
&QAbstractTextDocumentLayout::update,
|
|
this,
|
|
&TextEditorWidgetPrivate::scheduleUpdateHighlightScrollBar);
|
|
|
|
m_documentConnections << connect(m_document->document(),
|
|
&QTextDocument::contentsChange,
|
|
this,
|
|
&TextEditorWidgetPrivate::editorContentsChange);
|
|
|
|
m_documentConnections << connect(m_document->document(),
|
|
&QTextDocument::modificationChanged,
|
|
q,
|
|
&TextEditorWidget::updateTextCodecLabel);
|
|
|
|
m_documentConnections << connect(m_document->document(),
|
|
&QTextDocument::modificationChanged,
|
|
q,
|
|
&TextEditorWidget::updateTextLineEndingLabel);
|
|
|
|
m_documentConnections << connect(m_document.data(),
|
|
&TextDocument::aboutToReload,
|
|
this,
|
|
&TextEditorWidgetPrivate::documentAboutToBeReloaded);
|
|
|
|
m_documentConnections << connect(m_document.data(),
|
|
&TextDocument::reloadFinished,
|
|
this,
|
|
&TextEditorWidgetPrivate::documentReloadFinished);
|
|
|
|
m_documentConnections << connect(m_document.data(),
|
|
&TextDocument::tabSettingsChanged,
|
|
this,
|
|
&TextEditorWidgetPrivate::applyTabSettings);
|
|
|
|
m_documentConnections << connect(m_document.data(),
|
|
&TextDocument::fontSettingsChanged,
|
|
this,
|
|
&TextEditorWidgetPrivate::applyFontSettingsDelayed);
|
|
|
|
m_documentConnections << connect(m_document.data(),
|
|
&TextDocument::markRemoved,
|
|
this,
|
|
&TextEditorWidgetPrivate::markRemoved);
|
|
|
|
m_documentConnections << connect(m_document.data(),
|
|
&TextDocument::aboutToOpen,
|
|
q,
|
|
&TextEditorWidget::aboutToOpen);
|
|
|
|
m_documentConnections << connect(m_document.data(),
|
|
&TextDocument::openFinishedSuccessfully,
|
|
q,
|
|
&TextEditorWidget::openFinishedSuccessfully);
|
|
|
|
m_documentConnections << connect(TextEditorSettings::instance(),
|
|
&TextEditorSettings::fontSettingsChanged,
|
|
m_document.data(),
|
|
&TextDocument::setFontSettings);
|
|
|
|
slotUpdateExtraAreaWidth();
|
|
|
|
// Apply current settings
|
|
// the document might already have the same settings as we set here in which case we do not
|
|
// get an update, so we have to trigger updates manually here
|
|
const FontSettings fontSettings = TextEditorSettings::fontSettings();
|
|
if (m_document->fontSettings() == fontSettings)
|
|
applyFontSettingsDelayed();
|
|
else
|
|
m_document->setFontSettings(fontSettings);
|
|
const TabSettings tabSettings = TextEditorSettings::codeStyle()->tabSettings();
|
|
if (m_document->tabSettings() == tabSettings)
|
|
applyTabSettings();
|
|
else
|
|
m_document->setTabSettings(tabSettings); // also set through code style ???
|
|
|
|
q->setTypingSettings(globalTypingSettings());
|
|
q->setStorageSettings(globalStorageSettings());
|
|
q->setBehaviorSettings(globalBehaviorSettings());
|
|
q->setMarginSettings(TextEditorSettings::marginSettings());
|
|
q->setDisplaySettings(TextEditorSettings::displaySettings());
|
|
q->setCompletionSettings(TextEditorSettings::completionSettings());
|
|
q->setExtraEncodingSettings(globalExtraEncodingSettings());
|
|
q->textDocument()->setCodeStyle(TextEditorSettings::codeStyle(m_tabSettingsId));
|
|
|
|
m_blockCount = doc->document()->blockCount();
|
|
|
|
// from RESEARCH
|
|
|
|
extraAreaSelectionAnchorBlockNumber = -1;
|
|
extraAreaToggleMarkBlockNumber = -1;
|
|
extraAreaHighlightFoldedBlockNumber = -1;
|
|
visibleFoldedBlockNumber = -1;
|
|
suggestedVisibleFoldedBlockNumber = -1;
|
|
|
|
if (m_bracketsAnimator)
|
|
m_bracketsAnimator->finish();
|
|
if (m_autocompleteAnimator)
|
|
m_autocompleteAnimator->finish();
|
|
|
|
slotUpdateExtraAreaWidth();
|
|
updateHighlights();
|
|
|
|
m_moveLineUndoHack = false;
|
|
|
|
updateCannotDecodeInfo();
|
|
|
|
q->updateTextCodecLabel();
|
|
q->updateTextLineEndingLabel();
|
|
setupFromDefinition(currentDefinition());
|
|
}
|
|
|
|
void TextEditorWidget::print(QPrinter *printer)
|
|
{
|
|
const bool oldFullPage = printer->fullPage();
|
|
printer->setFullPage(true);
|
|
auto dlg = new QPrintDialog(printer, this);
|
|
dlg->setWindowTitle(Tr::tr("Print Document"));
|
|
if (dlg->exec() == QDialog::Accepted)
|
|
d->print(printer);
|
|
printer->setFullPage(oldFullPage);
|
|
delete dlg;
|
|
}
|
|
|
|
static int foldBoxWidth(const QFontMetrics &fm)
|
|
{
|
|
const int lineSpacing = fm.lineSpacing();
|
|
return lineSpacing + lineSpacing % 2 + 1;
|
|
}
|
|
|
|
static int foldBoxWidth()
|
|
{
|
|
const int lineSpacing = TextEditorSettings::fontSettings().lineSpacing();
|
|
return lineSpacing + lineSpacing % 2 + 1;
|
|
}
|
|
|
|
static void printPage(int index, QPainter *painter, const QTextDocument *doc,
|
|
const QRectF &body, const QRectF &titleBox,
|
|
const QString &title)
|
|
{
|
|
painter->save();
|
|
|
|
painter->translate(body.left(), body.top() - (index - 1) * body.height());
|
|
const QRectF view(0, (index - 1) * body.height(), body.width(), body.height());
|
|
|
|
QAbstractTextDocumentLayout *layout = doc->documentLayout();
|
|
QAbstractTextDocumentLayout::PaintContext ctx;
|
|
|
|
painter->setFont(QFont(doc->defaultFont()));
|
|
const QRectF box = titleBox.translated(0, view.top());
|
|
const int dpix = painter->device()->logicalDpiX();
|
|
const int dpiy = painter->device()->logicalDpiY();
|
|
const int mx = int(5 * dpix / 72.0);
|
|
const int my = int(2 * dpiy / 72.0);
|
|
painter->fillRect(box.adjusted(-mx, -my, mx, my), QColor(210, 210, 210));
|
|
if (!title.isEmpty())
|
|
painter->drawText(box, Qt::AlignCenter, title);
|
|
const QString pageString = QString::number(index);
|
|
painter->drawText(box, Qt::AlignRight, pageString);
|
|
|
|
painter->setClipRect(view);
|
|
ctx.clip = view;
|
|
// don't use the system palette text as default text color, on HP/UX
|
|
// for example that's white, and white text on white paper doesn't
|
|
// look that nice
|
|
ctx.palette.setColor(QPalette::Text, Qt::black);
|
|
|
|
layout->draw(painter, ctx);
|
|
|
|
painter->restore();
|
|
}
|
|
|
|
Q_LOGGING_CATEGORY(printLog, "qtc.editor.print", QtWarningMsg)
|
|
|
|
void TextEditorWidgetPrivate::print(QPrinter *printer)
|
|
{
|
|
QTextDocument *doc = q->document();
|
|
|
|
QString title = m_document->displayName();
|
|
if (!title.isEmpty())
|
|
printer->setDocName(title);
|
|
|
|
|
|
QPainter p(printer);
|
|
|
|
// Check that there is a valid device to print to.
|
|
if (!p.isActive())
|
|
return;
|
|
|
|
QRectF pageRect(printer->pageLayout().paintRectPixels(printer->resolution()));
|
|
if (pageRect.isEmpty())
|
|
return;
|
|
|
|
doc = doc->clone(doc);
|
|
const QScopeGuard cleanup([doc] { delete doc; });
|
|
|
|
QTextOption opt = doc->defaultTextOption();
|
|
opt.setWrapMode(QTextOption::WrapAtWordBoundaryOrAnywhere);
|
|
doc->setDefaultTextOption(opt);
|
|
|
|
(void)doc->documentLayout(); // make sure that there is a layout
|
|
|
|
|
|
QColor background = m_document->fontSettings().toTextCharFormat(C_TEXT).background().color();
|
|
bool backgroundIsDark = background.value() < 128;
|
|
|
|
for (QTextBlock srcBlock = q->document()->firstBlock(), dstBlock = doc->firstBlock();
|
|
srcBlock.isValid() && dstBlock.isValid();
|
|
srcBlock = srcBlock.next(), dstBlock = dstBlock.next()) {
|
|
|
|
|
|
QVector<QTextLayout::FormatRange> formatList = srcBlock.layout()->formats();
|
|
if (backgroundIsDark) {
|
|
// adjust syntax highlighting colors for better contrast
|
|
for (int i = formatList.count() - 1; i >= 0; --i) {
|
|
QTextCharFormat &format = formatList[i].format;
|
|
if (format.background().color() == background) {
|
|
QBrush brush = format.foreground();
|
|
QColor color = brush.color();
|
|
int h,s,v,a;
|
|
color.getHsv(&h, &s, &v, &a);
|
|
color.setHsv(h, s, qMin(128, v), a);
|
|
brush.setColor(color);
|
|
format.setForeground(brush);
|
|
}
|
|
format.setBackground(Qt::white);
|
|
}
|
|
}
|
|
|
|
dstBlock.layout()->setFormats(formatList);
|
|
}
|
|
|
|
QAbstractTextDocumentLayout *layout = doc->documentLayout();
|
|
layout->setPaintDevice(p.device());
|
|
|
|
int dpiy = qRound(QGuiApplication::primaryScreen()->logicalDotsPerInchY());
|
|
int margin = int((2/2.54)*dpiy); // 2 cm margins
|
|
|
|
QTextFrameFormat fmt = doc->rootFrame()->frameFormat();
|
|
fmt.setMargin(margin);
|
|
doc->rootFrame()->setFrameFormat(fmt);
|
|
|
|
QRectF body = QRectF(0, 0, pageRect.width(), pageRect.height());
|
|
QFontMetrics fontMetrics(doc->defaultFont(), p.device());
|
|
|
|
QRectF titleBox(margin,
|
|
body.top() + margin
|
|
- fontMetrics.height()
|
|
- 6 * dpiy / 72.0,
|
|
body.width() - 2*margin,
|
|
fontMetrics.height());
|
|
doc->setPageSize(body.size());
|
|
|
|
int docCopies;
|
|
int pageCopies;
|
|
if (printer->collateCopies() == true) {
|
|
docCopies = 1;
|
|
pageCopies = printer->copyCount();
|
|
} else {
|
|
docCopies = printer->copyCount();
|
|
pageCopies = 1;
|
|
}
|
|
|
|
int fromPage = printer->fromPage();
|
|
int toPage = printer->toPage();
|
|
bool ascending = true;
|
|
|
|
if (fromPage == 0 && toPage == 0) {
|
|
fromPage = 1;
|
|
toPage = doc->pageCount();
|
|
}
|
|
// paranoia check
|
|
fromPage = qMax(1, fromPage);
|
|
toPage = qMin(doc->pageCount(), toPage);
|
|
|
|
if (printer->pageOrder() == QPrinter::LastPageFirst) {
|
|
int tmp = fromPage;
|
|
fromPage = toPage;
|
|
toPage = tmp;
|
|
ascending = false;
|
|
}
|
|
|
|
qCDebug(printLog) << "Printing " << m_document->filePath() << ":\n"
|
|
<< " number of copies:" << printer->copyCount() << '\n'
|
|
<< " from page" << fromPage << "to" << toPage << '\n'
|
|
<< " document page count:" << doc->pageCount() << '\n'
|
|
<< " page rectangle:" << pageRect << '\n'
|
|
<< " title box:" << titleBox << '\n';
|
|
|
|
for (int i = 0; i < docCopies; ++i) {
|
|
|
|
int page = fromPage;
|
|
while (true) {
|
|
for (int j = 0; j < pageCopies; ++j) {
|
|
if (printer->printerState() == QPrinter::Aborted
|
|
|| printer->printerState() == QPrinter::Error)
|
|
return;
|
|
printPage(page, &p, doc, body, titleBox, title);
|
|
if (j < pageCopies - 1)
|
|
printer->newPage();
|
|
}
|
|
|
|
if (page == toPage)
|
|
break;
|
|
|
|
if (ascending)
|
|
++page;
|
|
else
|
|
--page;
|
|
|
|
printer->newPage();
|
|
}
|
|
|
|
if ( i < docCopies - 1)
|
|
printer->newPage();
|
|
}
|
|
}
|
|
|
|
|
|
int TextEditorWidgetPrivate::visualIndent(const QTextBlock &block) const
|
|
{
|
|
if (!block.isValid())
|
|
return 0;
|
|
const QTextDocument *document = block.document();
|
|
int i = 0;
|
|
while (i < block.length()) {
|
|
if (!document->characterAt(block.position() + i).isSpace()) {
|
|
QTextCursor cursor(block);
|
|
cursor.setPosition(block.position() + i);
|
|
return q->cursorRect(cursor).x();
|
|
}
|
|
++i;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
void TextEditorWidgetPrivate::updateAutoCompleteHighlight()
|
|
{
|
|
const QTextCharFormat matchFormat = m_document->fontSettings().toTextCharFormat(C_AUTOCOMPLETE);
|
|
|
|
QList<QTextEdit::ExtraSelection> extraSelections;
|
|
for (const QTextCursor &cursor : std::as_const(m_autoCompleteHighlightPos)) {
|
|
QTextEdit::ExtraSelection sel;
|
|
sel.cursor = cursor;
|
|
sel.format.setBackground(matchFormat.background());
|
|
extraSelections.append(sel);
|
|
}
|
|
q->setExtraSelections(TextEditorWidget::AutoCompleteSelection, extraSelections);
|
|
}
|
|
|
|
QList<QTextCursor> TextEditorWidgetPrivate::generateCursorsForBlockSelection(
|
|
const BlockSelection &blockSelection)
|
|
{
|
|
const TabSettings tabSettings = m_document->tabSettings();
|
|
|
|
QList<QTextCursor> result;
|
|
QTextBlock block = m_document->document()->findBlockByNumber(blockSelection.anchorBlockNumber);
|
|
QTextCursor cursor(block);
|
|
cursor.setPosition(block.position()
|
|
+ tabSettings.positionAtColumn(block.text(), blockSelection.anchorColumn));
|
|
|
|
const bool forward = blockSelection.blockNumber > blockSelection.anchorBlockNumber
|
|
|| (blockSelection.blockNumber == blockSelection.anchorBlockNumber
|
|
&& blockSelection.column == blockSelection.anchorColumn);
|
|
|
|
while (block.isValid()) {
|
|
const QString &blockText = block.text();
|
|
const int columnCount = tabSettings.columnCountForText(blockText);
|
|
if (blockSelection.anchorColumn <= columnCount || blockSelection.column <= columnCount) {
|
|
const int anchor = tabSettings.positionAtColumn(blockText, blockSelection.anchorColumn);
|
|
const int position = tabSettings.positionAtColumn(blockText, blockSelection.column);
|
|
cursor.setPosition(block.position() + anchor);
|
|
cursor.setPosition(block.position() + position, QTextCursor::KeepAnchor);
|
|
result.append(cursor);
|
|
}
|
|
if (block.blockNumber() == blockSelection.blockNumber)
|
|
break;
|
|
block = forward ? block.next() : block.previous();
|
|
}
|
|
return result;
|
|
}
|
|
|
|
void TextEditorWidgetPrivate::initBlockSelection()
|
|
{
|
|
const TabSettings tabSettings = m_document->tabSettings();
|
|
for (const QTextCursor &cursor : m_cursors) {
|
|
const int column = tabSettings.columnAtCursorPosition(cursor);
|
|
QTextCursor anchor = cursor;
|
|
anchor.setPosition(anchor.anchor());
|
|
const int anchorColumn = tabSettings.columnAtCursorPosition(anchor);
|
|
m_blockSelections.append({cursor.blockNumber(), column, anchor.blockNumber(), anchorColumn});
|
|
}
|
|
}
|
|
|
|
void TextEditorWidgetPrivate::clearBlockSelection()
|
|
{
|
|
m_blockSelections.clear();
|
|
}
|
|
|
|
void TextEditorWidgetPrivate::handleMoveBlockSelection(QTextCursor::MoveOperation op)
|
|
{
|
|
if (m_blockSelections.isEmpty())
|
|
initBlockSelection();
|
|
QList<QTextCursor> cursors;
|
|
for (BlockSelection &blockSelection : m_blockSelections) {
|
|
switch (op) {
|
|
case QTextCursor::Up:
|
|
blockSelection.blockNumber = qMax(0, blockSelection.blockNumber - 1);
|
|
break;
|
|
case QTextCursor::Down:
|
|
blockSelection.blockNumber = qMin(m_document->document()->blockCount() - 1,
|
|
blockSelection.blockNumber + 1);
|
|
break;
|
|
case QTextCursor::NextCharacter:
|
|
++blockSelection.column;
|
|
break;
|
|
case QTextCursor::PreviousCharacter:
|
|
blockSelection.column = qMax(0, blockSelection.column - 1);
|
|
break;
|
|
default:
|
|
return;
|
|
}
|
|
cursors.append(generateCursorsForBlockSelection(blockSelection));
|
|
}
|
|
q->setMultiTextCursor(MultiTextCursor(cursors));
|
|
}
|
|
|
|
void TextEditorWidgetPrivate::insertSuggestion(std::unique_ptr<TextSuggestion> &&suggestion)
|
|
{
|
|
clearCurrentSuggestion();
|
|
|
|
if (m_suggestionBlocker.use_count() > 1)
|
|
return;
|
|
|
|
auto cursor = q->textCursor();
|
|
cursor.setPosition(suggestion->currentPosition());
|
|
QTextOption option = suggestion->replacementDocument()->defaultTextOption();
|
|
option.setTabStopDistance(charWidth() * m_document->tabSettings().m_tabSize);
|
|
suggestion->replacementDocument()->setDefaultTextOption(option);
|
|
auto options = suggestion->replacementDocument()->defaultTextOption();
|
|
m_suggestionBlock = cursor.block();
|
|
m_document->insertSuggestion(std::move(suggestion));
|
|
forceUpdateScrollbarSize();
|
|
}
|
|
|
|
void TextEditorWidgetPrivate::updateSuggestion()
|
|
{
|
|
if (!m_suggestionBlock.isValid())
|
|
return;
|
|
const QTextCursor cursor = m_cursors.mainCursor();
|
|
if (cursor.block() == m_suggestionBlock) {
|
|
TextSuggestion *suggestion = TextDocumentLayout::suggestion(m_suggestionBlock);
|
|
if (QTC_GUARD(suggestion)) {
|
|
const int pos = cursor.position();
|
|
if (pos >= suggestion->currentPosition()) {
|
|
suggestion->setCurrentPosition(pos);
|
|
if (suggestion->filterSuggestions(q)) {
|
|
TextDocumentLayout::updateSuggestionFormats(
|
|
m_suggestionBlock, m_document->fontSettings());
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
clearCurrentSuggestion();
|
|
}
|
|
|
|
void TextEditorWidgetPrivate::clearCurrentSuggestion()
|
|
{
|
|
if (TextBlockUserData *userData = TextDocumentLayout::textUserData(m_suggestionBlock)) {
|
|
userData->clearSuggestion();
|
|
m_document->updateLayout();
|
|
}
|
|
m_suggestionBlock = QTextBlock();
|
|
}
|
|
|
|
void TextEditorWidget::selectEncoding()
|
|
{
|
|
TextDocument *doc = d->m_document.data();
|
|
const CodecSelectorResult result = Core::askForCodec(Core::ICore::dialogParent(), doc);
|
|
switch (result.action) {
|
|
case Core::CodecSelectorResult::Reload: {
|
|
if (Result res = doc->reload(result.codec); !res) {
|
|
QMessageBox::critical(this, Tr::tr("File Error"), res.error());
|
|
break;
|
|
}
|
|
break;
|
|
}
|
|
case Core::CodecSelectorResult::Save:
|
|
doc->setCodec(result.codec);
|
|
EditorManager::saveDocument(textDocument());
|
|
updateTextCodecLabel();
|
|
break;
|
|
case Core::CodecSelectorResult::Cancel:
|
|
break;
|
|
}
|
|
}
|
|
|
|
void TextEditorWidget::selectLineEnding(TextFileFormat::LineTerminationMode lineEnding)
|
|
{
|
|
if (d->m_document->lineTerminationMode() != lineEnding) {
|
|
d->m_document->setLineTerminationMode(lineEnding);
|
|
d->q->document()->setModified(true);
|
|
updateTextLineEndingLabel();
|
|
}
|
|
}
|
|
|
|
void TextEditorWidget::updateTextLineEndingLabel()
|
|
{
|
|
const TextFileFormat::LineTerminationMode lineEnding = d->m_document->lineTerminationMode();
|
|
if (lineEnding == TextFileFormat::LFLineTerminator)
|
|
d->m_fileLineEnding->setText(Tr::tr("LF"));
|
|
else if (lineEnding == TextFileFormat::CRLFLineTerminator)
|
|
d->m_fileLineEnding->setText(Tr::tr("CRLF"));
|
|
else
|
|
QTC_ASSERT_STRING("Unsupported line ending mode.");
|
|
}
|
|
|
|
void TextEditorWidget::updateTextCodecLabel()
|
|
{
|
|
QString text = QString::fromLatin1(d->m_document->codec()->name());
|
|
d->m_fileEncodingButton->setText(text);
|
|
}
|
|
|
|
QString TextEditorWidget::msgTextTooLarge(quint64 size)
|
|
{
|
|
return Tr::tr("The text is too large to be displayed (%1 MB).").
|
|
arg(size >> 20);
|
|
}
|
|
|
|
void TextEditorWidget::insertPlainText(const QString &text)
|
|
{
|
|
MultiTextCursor cursor = d->m_cursors;
|
|
cursor.insertText(text);
|
|
setMultiTextCursor(cursor);
|
|
}
|
|
|
|
QString TextEditorWidget::selectedText() const
|
|
{
|
|
return d->m_cursors.selectedText();
|
|
}
|
|
|
|
void TextEditorWidget::setVisualIndentOffset(int offset)
|
|
{
|
|
d->m_visualIndentOffset = qMax(0, offset);
|
|
}
|
|
|
|
void TextEditorWidget::updateUndoRedoActions()
|
|
{
|
|
d->updateUndoAction();
|
|
d->updateRedoAction();
|
|
}
|
|
|
|
void TextEditorWidgetPrivate::updateCannotDecodeInfo()
|
|
{
|
|
q->setReadOnly(m_document->hasDecodingError());
|
|
InfoBar *infoBar = m_document->infoBar();
|
|
Id selectEncodingId(Constants::SELECT_ENCODING);
|
|
if (m_document->hasDecodingError()) {
|
|
if (!infoBar->canInfoBeAdded(selectEncodingId))
|
|
return;
|
|
InfoBarEntry info(selectEncodingId,
|
|
Tr::tr("<b>Error:</b> Could not decode \"%1\" with \"%2\"-encoding. Editing not possible.")
|
|
.arg(m_document->displayName(), QString::fromLatin1(m_document->codec()->name())));
|
|
info.addCustomButton(Tr::tr("Select Encoding"), [this] { q->selectEncoding(); });
|
|
infoBar->addInfo(info);
|
|
} else {
|
|
infoBar->removeInfo(selectEncodingId);
|
|
}
|
|
}
|
|
|
|
// Skip over shebang to license header (Python, Perl, sh)
|
|
// '#!/bin/sh'
|
|
// ''
|
|
// '###############'
|
|
|
|
static QTextBlock skipShebang(const QTextBlock &block)
|
|
{
|
|
if (!block.isValid() || !block.text().startsWith("#!"))
|
|
return block;
|
|
const QTextBlock nextBlock1 = block.next();
|
|
if (!nextBlock1.isValid() || !nextBlock1.text().isEmpty())
|
|
return block;
|
|
const QTextBlock nextBlock2 = nextBlock1.next();
|
|
return nextBlock2.isValid() && nextBlock2.text().startsWith('#') ? nextBlock2 : block;
|
|
}
|
|
|
|
/*
|
|
Collapses the first comment in a file, if there is only whitespace/shebang line
|
|
above
|
|
*/
|
|
void TextEditorWidgetPrivate::foldLicenseHeader()
|
|
{
|
|
QTextDocument *doc = q->document();
|
|
auto documentLayout = qobject_cast<TextDocumentLayout*>(doc->documentLayout());
|
|
QTC_ASSERT(documentLayout, return);
|
|
QTextBlock block = skipShebang(doc->firstBlock());
|
|
while (block.isValid() && block.isVisible()) {
|
|
QString text = block.text();
|
|
if (TextDocumentLayout::canFold(block) && block.next().isVisible()) {
|
|
const QString trimmedText = text.trimmed();
|
|
QStringList commentMarker;
|
|
QStringList docMarker;
|
|
HighlighterHelper::Definition def;
|
|
if (auto highlighter = qobject_cast<Highlighter *>(q->textDocument()->syntaxHighlighter()))
|
|
def = highlighter->definition();
|
|
|
|
if (def.isValid()) {
|
|
for (const QString &marker :
|
|
{def.singleLineCommentMarker(), def.multiLineCommentMarker().first}) {
|
|
if (!marker.isEmpty())
|
|
commentMarker << marker;
|
|
}
|
|
} else {
|
|
commentMarker = QStringList({"/*", "#"});
|
|
docMarker = QStringList({"/*!", "/**"});
|
|
}
|
|
|
|
if (Utils::anyOf(commentMarker, [&](const QString &marker) {
|
|
return trimmedText.startsWith(marker);
|
|
})) {
|
|
if (Utils::anyOf(docMarker, [&](const QString &marker) {
|
|
return trimmedText.startsWith(marker)
|
|
&& (trimmedText.size() == marker.size()
|
|
|| trimmedText.at(marker.size()).isSpace());
|
|
})) {
|
|
break;
|
|
}
|
|
TextDocumentLayout::doFoldOrUnfold(block, false);
|
|
moveCursorVisible();
|
|
documentLayout->requestUpdate();
|
|
documentLayout->emitDocumentSizeChanged();
|
|
break;
|
|
}
|
|
}
|
|
if (TabSettings::firstNonSpace(text) < text.size())
|
|
break;
|
|
block = block.next();
|
|
}
|
|
}
|
|
|
|
TextDocument *TextEditorWidget::textDocument() const
|
|
{
|
|
return d->m_document.data();
|
|
}
|
|
|
|
void TextEditorWidget::aboutToOpen(const Utils::FilePath &filePath, const Utils::FilePath &realFilePath)
|
|
{
|
|
Q_UNUSED(filePath)
|
|
Q_UNUSED(realFilePath)
|
|
}
|
|
|
|
void TextEditorWidget::openFinishedSuccessfully()
|
|
{
|
|
d->moveCursor(QTextCursor::Start);
|
|
d->updateCannotDecodeInfo();
|
|
updateTextCodecLabel();
|
|
updateVisualWrapColumn();
|
|
}
|
|
|
|
TextDocumentPtr TextEditorWidget::textDocumentPtr() const
|
|
{
|
|
return d->m_document;
|
|
}
|
|
|
|
TextEditorWidget *TextEditorWidget::currentTextEditorWidget()
|
|
{
|
|
return fromEditor(EditorManager::currentEditor());
|
|
}
|
|
|
|
TextEditorWidget *TextEditorWidget::fromEditor(const IEditor *editor)
|
|
{
|
|
if (editor)
|
|
return Aggregation::query<TextEditorWidget>(editor->widget());
|
|
return nullptr;
|
|
}
|
|
|
|
void TextEditorWidgetPrivate::editorContentsChange(int position, int charsRemoved, int charsAdded)
|
|
{
|
|
updateSuggestion();
|
|
|
|
if (m_bracketsAnimator)
|
|
m_bracketsAnimator->finish();
|
|
|
|
m_contentsChanged = true;
|
|
QTextDocument *doc = q->document();
|
|
auto documentLayout = static_cast<TextDocumentLayout*>(doc->documentLayout());
|
|
const QTextBlock posBlock = doc->findBlock(position);
|
|
|
|
// Keep the line numbers and the block information for the text marks updated
|
|
if (charsRemoved != 0) {
|
|
documentLayout->updateMarksLineNumber();
|
|
documentLayout->updateMarksBlock(posBlock);
|
|
} else {
|
|
const QTextBlock nextBlock = doc->findBlock(position + charsAdded);
|
|
if (posBlock != nextBlock) {
|
|
documentLayout->updateMarksLineNumber();
|
|
documentLayout->updateMarksBlock(posBlock);
|
|
documentLayout->updateMarksBlock(nextBlock);
|
|
} else {
|
|
documentLayout->updateMarksBlock(posBlock);
|
|
}
|
|
}
|
|
|
|
if (m_snippetOverlay->isVisible()) {
|
|
QTextCursor cursor = q->textCursor();
|
|
cursor.setPosition(position);
|
|
snippetCheckCursor(cursor);
|
|
}
|
|
|
|
if ((charsAdded != 0 && q->document()->characterAt(position + charsAdded - 1).isPrint()) || charsRemoved != 0)
|
|
m_codeAssistant.notifyChange();
|
|
|
|
int newBlockCount = doc->blockCount();
|
|
if (!q->hasFocus() && newBlockCount != m_blockCount) {
|
|
// lines were inserted or removed from outside, keep viewport on same part of text
|
|
if (q->firstVisibleBlock().blockNumber() > posBlock.blockNumber())
|
|
q->verticalScrollBar()->setValue(q->verticalScrollBar()->value() + newBlockCount - m_blockCount);
|
|
}
|
|
m_blockCount = newBlockCount;
|
|
m_scrollBarUpdateTimer.start(500);
|
|
m_visualIndentCache.clear();
|
|
}
|
|
|
|
void TextEditorWidgetPrivate::slotSelectionChanged()
|
|
{
|
|
if (!q->textCursor().hasSelection() && !m_selectBlockAnchor.isNull())
|
|
m_selectBlockAnchor = QTextCursor();
|
|
// Clear any link which might be showing when the selection changes
|
|
clearLink();
|
|
setClipboardSelection();
|
|
}
|
|
|
|
void TextEditorWidget::gotoBlockStart()
|
|
{
|
|
if (multiTextCursor().hasMultipleCursors())
|
|
return;
|
|
|
|
QTextCursor cursor = textCursor();
|
|
if (TextBlockUserData::findPreviousOpenParenthesis(&cursor, false)) {
|
|
setTextCursor(cursor);
|
|
d->_q_matchParentheses();
|
|
}
|
|
}
|
|
|
|
void TextEditorWidget::gotoBlockEnd()
|
|
{
|
|
if (multiTextCursor().hasMultipleCursors())
|
|
return;
|
|
|
|
QTextCursor cursor = textCursor();
|
|
if (TextBlockUserData::findNextClosingParenthesis(&cursor, false)) {
|
|
setTextCursor(cursor);
|
|
d->_q_matchParentheses();
|
|
}
|
|
}
|
|
|
|
void TextEditorWidget::gotoBlockStartWithSelection()
|
|
{
|
|
if (multiTextCursor().hasMultipleCursors())
|
|
return;
|
|
|
|
QTextCursor cursor = textCursor();
|
|
if (TextBlockUserData::findPreviousOpenParenthesis(&cursor, true)) {
|
|
setTextCursor(cursor);
|
|
d->_q_matchParentheses();
|
|
}
|
|
}
|
|
|
|
void TextEditorWidget::gotoBlockEndWithSelection()
|
|
{
|
|
if (multiTextCursor().hasMultipleCursors())
|
|
return;
|
|
|
|
QTextCursor cursor = textCursor();
|
|
if (TextBlockUserData::findNextClosingParenthesis(&cursor, true)) {
|
|
setTextCursor(cursor);
|
|
d->_q_matchParentheses();
|
|
}
|
|
}
|
|
|
|
void TextEditorWidget::gotoDocumentStart()
|
|
{
|
|
d->moveCursor(QTextCursor::Start);
|
|
}
|
|
|
|
void TextEditorWidget::gotoDocumentEnd()
|
|
{
|
|
d->moveCursor(QTextCursor::End);
|
|
}
|
|
|
|
void TextEditorWidget::gotoLineStart()
|
|
{
|
|
d->handleHomeKey(false, true);
|
|
}
|
|
|
|
void TextEditorWidget::gotoLineStartWithSelection()
|
|
{
|
|
d->handleHomeKey(true, true);
|
|
}
|
|
|
|
void TextEditorWidget::gotoLineEnd()
|
|
{
|
|
d->moveCursor(QTextCursor::EndOfLine);
|
|
}
|
|
|
|
void TextEditorWidget::gotoLineEndWithSelection()
|
|
{
|
|
d->moveCursor(QTextCursor::EndOfLine, QTextCursor::KeepAnchor);
|
|
}
|
|
|
|
void TextEditorWidget::gotoNextLine()
|
|
{
|
|
d->moveCursor(QTextCursor::Down);
|
|
}
|
|
|
|
void TextEditorWidget::gotoNextLineWithSelection()
|
|
{
|
|
d->moveCursor(QTextCursor::Down, QTextCursor::KeepAnchor);
|
|
}
|
|
|
|
void TextEditorWidget::gotoPreviousLine()
|
|
{
|
|
d->moveCursor(QTextCursor::Up);
|
|
}
|
|
|
|
void TextEditorWidget::gotoPreviousLineWithSelection()
|
|
{
|
|
d->moveCursor(QTextCursor::Up, QTextCursor::KeepAnchor);
|
|
}
|
|
|
|
void TextEditorWidget::gotoPreviousCharacter()
|
|
{
|
|
d->moveCursor(QTextCursor::PreviousCharacter);
|
|
}
|
|
|
|
void TextEditorWidget::gotoPreviousCharacterWithSelection()
|
|
{
|
|
d->moveCursor(QTextCursor::PreviousCharacter, QTextCursor::KeepAnchor);
|
|
}
|
|
|
|
void TextEditorWidget::gotoNextCharacter()
|
|
{
|
|
d->moveCursor(QTextCursor::NextCharacter);
|
|
}
|
|
|
|
void TextEditorWidget::gotoNextCharacterWithSelection()
|
|
{
|
|
d->moveCursor(QTextCursor::NextCharacter, QTextCursor::KeepAnchor);
|
|
}
|
|
|
|
void TextEditorWidget::gotoPreviousWord()
|
|
{
|
|
d->moveCursor(QTextCursor::PreviousWord);
|
|
}
|
|
|
|
void TextEditorWidget::gotoPreviousWordWithSelection()
|
|
{
|
|
d->moveCursor(QTextCursor::PreviousWord, QTextCursor::KeepAnchor);
|
|
}
|
|
|
|
void TextEditorWidget::gotoNextWord()
|
|
{
|
|
d->moveCursor(QTextCursor::NextWord);
|
|
}
|
|
|
|
void TextEditorWidget::gotoNextWordWithSelection()
|
|
{
|
|
d->moveCursor(QTextCursor::NextWord, QTextCursor::KeepAnchor);
|
|
}
|
|
|
|
void TextEditorWidget::gotoPreviousWordCamelCase()
|
|
{
|
|
MultiTextCursor cursor = multiTextCursor();
|
|
CamelCaseCursor::left(&cursor, this, QTextCursor::MoveAnchor);
|
|
setMultiTextCursor(cursor);
|
|
}
|
|
|
|
void TextEditorWidget::gotoPreviousWordCamelCaseWithSelection()
|
|
{
|
|
MultiTextCursor cursor = multiTextCursor();
|
|
CamelCaseCursor::left(&cursor, this, QTextCursor::KeepAnchor);
|
|
setMultiTextCursor(cursor);
|
|
}
|
|
|
|
void TextEditorWidget::gotoNextWordCamelCase()
|
|
{
|
|
MultiTextCursor cursor = multiTextCursor();
|
|
CamelCaseCursor::right(&cursor, this, QTextCursor::MoveAnchor);
|
|
setMultiTextCursor(cursor);
|
|
}
|
|
|
|
void TextEditorWidget::gotoNextWordCamelCaseWithSelection()
|
|
{
|
|
MultiTextCursor cursor = multiTextCursor();
|
|
CamelCaseCursor::right(&cursor, this, QTextCursor::KeepAnchor);
|
|
setMultiTextCursor(cursor);
|
|
}
|
|
|
|
bool TextEditorWidget::selectBlockUp()
|
|
{
|
|
if (multiTextCursor().hasMultipleCursors())
|
|
return false;
|
|
|
|
QTextCursor cursor = textCursor();
|
|
if (!cursor.hasSelection())
|
|
d->m_selectBlockAnchor = cursor;
|
|
else
|
|
cursor.setPosition(cursor.selectionStart());
|
|
|
|
if (!TextBlockUserData::findPreviousOpenParenthesis(&cursor, false))
|
|
return false;
|
|
if (!TextBlockUserData::findNextClosingParenthesis(&cursor, true))
|
|
return false;
|
|
|
|
setTextCursor(Text::flippedCursor(cursor));
|
|
d->_q_matchParentheses();
|
|
return true;
|
|
}
|
|
|
|
bool TextEditorWidget::selectBlockDown()
|
|
{
|
|
if (multiTextCursor().hasMultipleCursors())
|
|
return false;
|
|
|
|
QTextCursor tc = textCursor();
|
|
QTextCursor cursor = d->m_selectBlockAnchor;
|
|
|
|
if (!tc.hasSelection() || cursor.isNull())
|
|
return false;
|
|
tc.setPosition(tc.selectionStart());
|
|
|
|
forever {
|
|
QTextCursor ahead = cursor;
|
|
if (!TextBlockUserData::findPreviousOpenParenthesis(&ahead, false))
|
|
break;
|
|
if (ahead.position() <= tc.position())
|
|
break;
|
|
cursor = ahead;
|
|
}
|
|
if ( cursor != d->m_selectBlockAnchor)
|
|
TextBlockUserData::findNextClosingParenthesis(&cursor, true);
|
|
|
|
setTextCursor(Text::flippedCursor(cursor));
|
|
d->_q_matchParentheses();
|
|
return true;
|
|
}
|
|
|
|
void TextEditorWidget::selectWordUnderCursor()
|
|
{
|
|
MultiTextCursor cursor = multiTextCursor();
|
|
for (QTextCursor &c : cursor) {
|
|
if (!c.hasSelection())
|
|
c.select(QTextCursor::WordUnderCursor);
|
|
}
|
|
setMultiTextCursor(cursor);
|
|
}
|
|
|
|
void TextEditorWidget::clearSelection()
|
|
{
|
|
MultiTextCursor cursor = multiTextCursor();
|
|
cursor.clearSelection();
|
|
setMultiTextCursor(cursor);
|
|
}
|
|
|
|
void TextEditorWidget::showContextMenu()
|
|
{
|
|
QTextCursor tc = textCursor();
|
|
const QPoint cursorPos = mapToGlobal(cursorRect(tc).bottomRight() + QPoint(1,1));
|
|
qGuiApp->postEvent(
|
|
this, new QContextMenuEvent(QContextMenuEvent::Keyboard, cursorPos, QCursor::pos()));
|
|
}
|
|
|
|
void TextEditorWidget::copyLineUp()
|
|
{
|
|
d->copyLineUpDown(true);
|
|
}
|
|
|
|
void TextEditorWidget::copyLineDown()
|
|
{
|
|
d->copyLineUpDown(false);
|
|
}
|
|
|
|
// @todo: Potential reuse of some code around the following functions...
|
|
void TextEditorWidgetPrivate::copyLineUpDown(bool up)
|
|
{
|
|
if (q->multiTextCursor().hasMultipleCursors())
|
|
return;
|
|
QTextCursor cursor = q->textCursor();
|
|
QTextCursor move = cursor;
|
|
move.beginEditBlock();
|
|
|
|
bool hasSelection = cursor.hasSelection();
|
|
|
|
if (hasSelection) {
|
|
move.setPosition(cursor.selectionStart());
|
|
move.movePosition(QTextCursor::StartOfBlock);
|
|
move.setPosition(cursor.selectionEnd(), QTextCursor::KeepAnchor);
|
|
move.movePosition(move.atBlockStart() ? QTextCursor::Left: QTextCursor::EndOfBlock,
|
|
QTextCursor::KeepAnchor);
|
|
} else {
|
|
move.movePosition(QTextCursor::StartOfBlock);
|
|
move.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor);
|
|
}
|
|
|
|
QString text = move.selectedText();
|
|
|
|
if (up) {
|
|
move.setPosition(cursor.selectionStart());
|
|
move.movePosition(QTextCursor::StartOfBlock);
|
|
move.insertBlock();
|
|
move.movePosition(QTextCursor::Left);
|
|
} else {
|
|
move.movePosition(QTextCursor::EndOfBlock);
|
|
if (move.atBlockStart()) {
|
|
move.movePosition(QTextCursor::NextBlock);
|
|
move.insertBlock();
|
|
move.movePosition(QTextCursor::Left);
|
|
} else {
|
|
move.insertBlock();
|
|
}
|
|
}
|
|
|
|
int start = move.position();
|
|
move.clearSelection();
|
|
move.insertText(text);
|
|
int end = move.position();
|
|
|
|
move.setPosition(start);
|
|
move.setPosition(end, QTextCursor::KeepAnchor);
|
|
|
|
m_document->autoIndent(move);
|
|
move.endEditBlock();
|
|
|
|
q->setTextCursor(move);
|
|
}
|
|
|
|
void TextEditorWidget::joinLines()
|
|
{
|
|
MultiTextCursor cursor = multiTextCursor();
|
|
cursor.beginEditBlock();
|
|
for (QTextCursor &c : cursor) {
|
|
QTextCursor start = c;
|
|
QTextCursor end = c;
|
|
|
|
start.setPosition(c.selectionStart());
|
|
end.setPosition(c.selectionEnd() - 1);
|
|
|
|
int lineCount = qMax(1, end.blockNumber() - start.blockNumber());
|
|
|
|
c.setPosition(c.selectionStart());
|
|
while (lineCount--) {
|
|
c.movePosition(QTextCursor::NextBlock);
|
|
c.movePosition(QTextCursor::StartOfBlock);
|
|
c.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor);
|
|
QString cutLine = c.selectedText();
|
|
|
|
// Collapse leading whitespaces to one or insert whitespace
|
|
static const QRegularExpression regexp("^\\s*");
|
|
cutLine.replace(regexp, QLatin1String(" "));
|
|
c.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor);
|
|
c.removeSelectedText();
|
|
|
|
c.movePosition(QTextCursor::PreviousBlock);
|
|
c.movePosition(QTextCursor::EndOfBlock);
|
|
|
|
c.insertText(cutLine);
|
|
}
|
|
}
|
|
cursor.endEditBlock();
|
|
cursor.mergeCursors();
|
|
setMultiTextCursor(cursor);
|
|
}
|
|
|
|
void TextEditorWidget::insertLineAbove()
|
|
{
|
|
MultiTextCursor cursor = multiTextCursor();
|
|
cursor.beginEditBlock();
|
|
for (QTextCursor &c : cursor) {
|
|
// If the cursor is at the beginning of the document,
|
|
// it should still insert a line above the current line.
|
|
c.movePosition(QTextCursor::StartOfBlock, QTextCursor::MoveAnchor);
|
|
c.insertBlock();
|
|
c.movePosition(QTextCursor::PreviousBlock, QTextCursor::MoveAnchor);
|
|
d->m_document->autoIndent(c);
|
|
}
|
|
cursor.endEditBlock();
|
|
setMultiTextCursor(cursor);
|
|
}
|
|
|
|
void TextEditorWidget::insertLineBelow()
|
|
{
|
|
MultiTextCursor cursor = multiTextCursor();
|
|
cursor.beginEditBlock();
|
|
for (QTextCursor &c : cursor) {
|
|
c.movePosition(QTextCursor::EndOfBlock, QTextCursor::MoveAnchor);
|
|
c.insertBlock();
|
|
d->m_document->autoIndent(c);
|
|
}
|
|
cursor.endEditBlock();
|
|
setMultiTextCursor(cursor);
|
|
}
|
|
|
|
void TextEditorWidget::moveLineUp()
|
|
{
|
|
d->moveLineUpDown(true);
|
|
}
|
|
|
|
void TextEditorWidget::moveLineDown()
|
|
{
|
|
d->moveLineUpDown(false);
|
|
}
|
|
|
|
void TextEditorWidget::uppercaseSelection()
|
|
{
|
|
d->transformSelection([](const QString &str) { return str.toUpper(); });
|
|
}
|
|
|
|
void TextEditorWidget::lowercaseSelection()
|
|
{
|
|
d->transformSelection([](const QString &str) { return str.toLower(); });
|
|
}
|
|
|
|
void TextEditorWidget::sortLines()
|
|
{
|
|
if (d->m_cursors.hasMultipleCursors())
|
|
return;
|
|
|
|
QTextCursor cursor = textCursor();
|
|
if (!cursor.hasSelection()) {
|
|
// try to get a sensible scope for the sort
|
|
const QTextBlock currentBlock = cursor.block();
|
|
QString text = currentBlock.text();
|
|
if (text.simplified().isEmpty())
|
|
return;
|
|
const TabSettings ts = textDocument()->tabSettings();
|
|
const int currentIndent = ts.columnAt(text, TabSettings::firstNonSpace(text));
|
|
|
|
int anchor = currentBlock.position();
|
|
for (auto block = currentBlock.previous(); block.isValid(); block = block.previous()) {
|
|
text = block.text();
|
|
if (text.simplified().isEmpty()
|
|
|| ts.columnAt(text, TabSettings::firstNonSpace(text)) != currentIndent) {
|
|
break;
|
|
}
|
|
anchor = block.position();
|
|
}
|
|
|
|
int pos = currentBlock.position();
|
|
for (auto block = currentBlock.next(); block.isValid(); block = block.next()) {
|
|
text = block.text();
|
|
if (text.simplified().isEmpty()
|
|
|| ts.columnAt(text, TabSettings::firstNonSpace(text)) != currentIndent) {
|
|
break;
|
|
}
|
|
pos = block.position();
|
|
}
|
|
if (anchor == pos)
|
|
return;
|
|
|
|
cursor.setPosition(anchor);
|
|
cursor.setPosition(pos, QTextCursor::KeepAnchor);
|
|
cursor.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor);
|
|
}
|
|
|
|
const bool downwardDirection = cursor.anchor() < cursor.position();
|
|
int startPosition = cursor.selectionStart();
|
|
int endPosition = cursor.selectionEnd();
|
|
|
|
cursor.setPosition(startPosition);
|
|
cursor.movePosition(QTextCursor::StartOfBlock);
|
|
startPosition = cursor.position();
|
|
|
|
cursor.setPosition(endPosition, QTextCursor::KeepAnchor);
|
|
if (cursor.positionInBlock() == 0)
|
|
cursor.movePosition(QTextCursor::PreviousBlock, QTextCursor::KeepAnchor);
|
|
cursor.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor);
|
|
endPosition = qMax(cursor.position(), endPosition);
|
|
|
|
const QString text = cursor.selectedText();
|
|
QStringList lines = text.split(QChar::ParagraphSeparator);
|
|
lines.sort();
|
|
cursor.insertText(lines.join(QChar::ParagraphSeparator));
|
|
|
|
// (re)select the changed lines
|
|
// Note: this assumes the transformation did not change the length
|
|
cursor.setPosition(downwardDirection ? startPosition : endPosition);
|
|
cursor.setPosition(downwardDirection ? endPosition : startPosition, QTextCursor::KeepAnchor);
|
|
setTextCursor(cursor);
|
|
}
|
|
|
|
void TextEditorWidget::indent()
|
|
{
|
|
setMultiTextCursor(textDocument()->indent(multiTextCursor()));
|
|
}
|
|
|
|
void TextEditorWidget::unindent()
|
|
{
|
|
setMultiTextCursor(textDocument()->unindent(multiTextCursor()));
|
|
}
|
|
|
|
void TextEditorWidget::undo()
|
|
{
|
|
doSetTextCursor(multiTextCursor().mainCursor());
|
|
QPlainTextEdit::undo();
|
|
}
|
|
|
|
void TextEditorWidget::redo()
|
|
{
|
|
doSetTextCursor(multiTextCursor().mainCursor());
|
|
QPlainTextEdit::redo();
|
|
}
|
|
|
|
bool TextEditorWidget::isUndoAvailable() const
|
|
{
|
|
return document()->isUndoAvailable();
|
|
}
|
|
|
|
bool TextEditorWidget::isRedoAvailable() const
|
|
{
|
|
return document()->isRedoAvailable();
|
|
}
|
|
|
|
void TextEditorWidget::openLinkUnderCursor()
|
|
{
|
|
d->openLinkUnderCursor(alwaysOpenLinksInNextSplit());
|
|
}
|
|
|
|
void TextEditorWidget::openLinkUnderCursorInNextSplit()
|
|
{
|
|
d->openLinkUnderCursor(!alwaysOpenLinksInNextSplit());
|
|
}
|
|
|
|
void TextEditorWidget::openTypeUnderCursor()
|
|
{
|
|
d->openTypeUnderCursor(alwaysOpenLinksInNextSplit());
|
|
}
|
|
|
|
void TextEditorWidget::openTypeUnderCursorInNextSplit()
|
|
{
|
|
d->openTypeUnderCursor(!alwaysOpenLinksInNextSplit());
|
|
}
|
|
|
|
void TextEditorWidget::findUsages()
|
|
{
|
|
emit requestUsages(textCursor());
|
|
}
|
|
|
|
void TextEditorWidget::renameSymbolUnderCursor()
|
|
{
|
|
emit requestRename(textCursor());
|
|
}
|
|
|
|
void TextEditorWidget::openCallHierarchy()
|
|
{
|
|
emit requestCallHierarchy(textCursor());
|
|
}
|
|
|
|
void TextEditorWidget::abortAssist()
|
|
{
|
|
d->m_codeAssistant.destroyContext();
|
|
}
|
|
|
|
void TextEditorWidgetPrivate::moveLineUpDown(bool up)
|
|
{
|
|
if (m_cursors.hasMultipleCursors())
|
|
return;
|
|
QTextCursor cursor = q->textCursor();
|
|
QTextCursor move = cursor;
|
|
|
|
move.setVisualNavigation(false); // this opens folded items instead of destroying them
|
|
|
|
if (m_moveLineUndoHack)
|
|
move.joinPreviousEditBlock();
|
|
else
|
|
move.beginEditBlock();
|
|
|
|
bool hasSelection = cursor.hasSelection();
|
|
|
|
if (hasSelection) {
|
|
move.setPosition(cursor.selectionStart());
|
|
move.movePosition(QTextCursor::StartOfBlock);
|
|
move.setPosition(cursor.selectionEnd(), QTextCursor::KeepAnchor);
|
|
move.movePosition(move.atBlockStart() ? QTextCursor::PreviousCharacter: QTextCursor::EndOfBlock,
|
|
QTextCursor::KeepAnchor);
|
|
} else {
|
|
move.movePosition(QTextCursor::StartOfBlock);
|
|
move.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor);
|
|
}
|
|
QString text = move.selectedText();
|
|
|
|
RefactorMarkers affectedMarkers;
|
|
RefactorMarkers nonAffectedMarkers;
|
|
QList<int> markerOffsets;
|
|
|
|
const QList<RefactorMarker> markers = m_refactorOverlay->markers();
|
|
for (const RefactorMarker &marker : markers) {
|
|
//test if marker is part of the selection to be moved
|
|
if ((move.selectionStart() <= marker.cursor.position())
|
|
&& (move.selectionEnd() >= marker.cursor.position())) {
|
|
affectedMarkers.append(marker);
|
|
//remember the offset of markers in text
|
|
int offset = marker.cursor.position() - move.selectionStart();
|
|
markerOffsets.append(offset);
|
|
} else {
|
|
nonAffectedMarkers.append(marker);
|
|
}
|
|
}
|
|
|
|
move.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor);
|
|
move.removeSelectedText();
|
|
|
|
if (up) {
|
|
move.movePosition(QTextCursor::PreviousBlock);
|
|
move.insertBlock();
|
|
move.movePosition(QTextCursor::PreviousCharacter);
|
|
} else {
|
|
move.movePosition(QTextCursor::EndOfBlock);
|
|
if (move.atBlockStart()) { // empty block
|
|
move.movePosition(QTextCursor::NextBlock);
|
|
move.insertBlock();
|
|
move.movePosition(QTextCursor::PreviousCharacter);
|
|
} else {
|
|
move.insertBlock();
|
|
}
|
|
}
|
|
|
|
int start = move.position();
|
|
move.clearSelection();
|
|
move.insertText(text);
|
|
int end = move.position();
|
|
|
|
if (hasSelection) {
|
|
move.setPosition(end);
|
|
move.setPosition(start, QTextCursor::KeepAnchor);
|
|
} else {
|
|
move.setPosition(start);
|
|
}
|
|
|
|
//update positions of affectedMarkers
|
|
for (int i=0;i < affectedMarkers.count(); i++) {
|
|
int newPosition = start + markerOffsets.at(i);
|
|
affectedMarkers[i].cursor.setPosition(newPosition);
|
|
}
|
|
m_refactorOverlay->setMarkers(nonAffectedMarkers + affectedMarkers);
|
|
|
|
bool shouldReindent = true;
|
|
if (m_commentDefinition.isValid()) {
|
|
if (m_commentDefinition.hasMultiLineStyle()) {
|
|
// Don't have any single line comments; try multi line.
|
|
if (text.startsWith(m_commentDefinition.multiLineStart)
|
|
&& text.endsWith(m_commentDefinition.multiLineEnd)) {
|
|
shouldReindent = false;
|
|
}
|
|
}
|
|
if (shouldReindent && m_commentDefinition.hasSingleLineStyle()) {
|
|
shouldReindent = false;
|
|
QTextBlock block = move.block();
|
|
while (block.isValid() && block.position() < end) {
|
|
if (!block.text().startsWith(m_commentDefinition.singleLine))
|
|
shouldReindent = true;
|
|
block = block.next();
|
|
}
|
|
}
|
|
}
|
|
|
|
if (shouldReindent) {
|
|
// The text was not commented at all; re-indent.
|
|
m_document->autoReindent(move);
|
|
}
|
|
move.endEditBlock();
|
|
|
|
q->setTextCursor(move);
|
|
m_moveLineUndoHack = true;
|
|
}
|
|
|
|
void TextEditorWidget::cleanWhitespace()
|
|
{
|
|
d->m_document->cleanWhitespace(textCursor());
|
|
}
|
|
|
|
bool TextEditorWidgetPrivate::cursorMoveKeyEvent(QKeyEvent *e)
|
|
{
|
|
MultiTextCursor cursor = m_cursors;
|
|
if (cursor.handleMoveKeyEvent(e, q, q->camelCaseNavigationEnabled())) {
|
|
resetCursorFlashTimer();
|
|
q->setMultiTextCursor(cursor);
|
|
q->ensureCursorVisible();
|
|
updateCurrentLineHighlight();
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void TextEditorWidget::viewPageUp()
|
|
{
|
|
verticalScrollBar()->triggerAction(QAbstractSlider::SliderPageStepSub);
|
|
}
|
|
|
|
void TextEditorWidget::viewPageDown()
|
|
{
|
|
verticalScrollBar()->triggerAction(QAbstractSlider::SliderPageStepAdd);
|
|
}
|
|
|
|
void TextEditorWidget::viewLineUp()
|
|
{
|
|
verticalScrollBar()->triggerAction(QAbstractSlider::SliderSingleStepSub);
|
|
}
|
|
|
|
void TextEditorWidget::viewLineDown()
|
|
{
|
|
verticalScrollBar()->triggerAction(QAbstractSlider::SliderSingleStepAdd);
|
|
}
|
|
|
|
static inline bool isModifier(QKeyEvent *e)
|
|
{
|
|
if (!e)
|
|
return false;
|
|
switch (e->key()) {
|
|
case Qt::Key_Shift:
|
|
case Qt::Key_Control:
|
|
case Qt::Key_Meta:
|
|
case Qt::Key_Alt:
|
|
return true;
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
static inline bool isPrintableText(const QString &text)
|
|
{
|
|
return !text.isEmpty() && (text.at(0).isPrint() || text.at(0) == QLatin1Char('\t'));
|
|
}
|
|
|
|
void TextEditorWidget::keyPressEvent(QKeyEvent *e)
|
|
{
|
|
ICore::restartTrimmer();
|
|
|
|
QScopeGuard cleanup([&] { d->clearBlockSelection(); });
|
|
|
|
if (!isModifier(e) && mouseHidingEnabled())
|
|
viewport()->setCursor(Qt::BlankCursor);
|
|
ToolTip::hide();
|
|
|
|
d->m_moveLineUndoHack = false;
|
|
d->clearVisibleFoldedBlock();
|
|
|
|
MultiTextCursor cursor = multiTextCursor();
|
|
|
|
if (e->key() == Qt::Key_Alt
|
|
&& d->m_behaviorSettings.m_keyboardTooltips) {
|
|
d->m_maybeFakeTooltipEvent = true;
|
|
} else {
|
|
d->m_maybeFakeTooltipEvent = false;
|
|
if (e->key() == Qt::Key_Escape ) {
|
|
TextEditorWidgetFind::cancelCurrentSelectAll();
|
|
if (d->m_suggestionBlock.isValid()) {
|
|
d->clearCurrentSuggestion();
|
|
e->accept();
|
|
return;
|
|
}
|
|
if (d->m_snippetOverlay->isVisible()) {
|
|
e->accept();
|
|
d->m_snippetOverlay->accept();
|
|
QTextCursor cursor = textCursor();
|
|
cursor.clearSelection();
|
|
setTextCursor(cursor);
|
|
return;
|
|
}
|
|
if (cursor.hasMultipleCursors()) {
|
|
QTextCursor c = cursor.mainCursor();
|
|
c.setPosition(c.position(), QTextCursor::MoveAnchor);
|
|
doSetTextCursor(c);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
const bool ro = isReadOnly();
|
|
const bool inOverwriteMode = overwriteMode();
|
|
const bool hasMultipleCursors = cursor.hasMultipleCursors();
|
|
|
|
if (TextSuggestion *suggestion = TextDocumentLayout::suggestion(d->m_suggestionBlock)) {
|
|
if (e->matches(QKeySequence::MoveToNextWord)) {
|
|
e->accept();
|
|
if (suggestion->applyWord(this))
|
|
d->clearCurrentSuggestion();
|
|
return;
|
|
} else if (e->modifiers() == Qt::NoModifier
|
|
&& (e->key() == Qt::Key_Tab || e->key() == Qt::Key_Backtab)) {
|
|
e->accept();
|
|
if (suggestion->apply())
|
|
d->clearCurrentSuggestion();
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (!ro
|
|
&& (e == QKeySequence::InsertParagraphSeparator
|
|
|| (!d->m_lineSeparatorsAllowed && e == QKeySequence::InsertLineSeparator))) {
|
|
if (d->m_snippetOverlay->isVisible()) {
|
|
e->accept();
|
|
d->m_snippetOverlay->accept();
|
|
QTextCursor cursor = textCursor();
|
|
cursor.movePosition(QTextCursor::EndOfBlock);
|
|
setTextCursor(cursor);
|
|
return;
|
|
}
|
|
|
|
e->accept();
|
|
cursor.beginEditBlock();
|
|
for (QTextCursor &cursor : cursor) {
|
|
const TabSettings ts = d->m_document->tabSettings();
|
|
const TypingSettings &tps = d->m_document->typingSettings();
|
|
|
|
int extraBlocks = d->m_autoCompleter->paragraphSeparatorAboutToBeInserted(cursor);
|
|
|
|
QString previousIndentationString;
|
|
if (tps.m_autoIndent) {
|
|
cursor.insertBlock();
|
|
d->m_document->autoIndent(cursor);
|
|
} else {
|
|
cursor.insertBlock();
|
|
|
|
// After inserting the block, to avoid duplicating whitespace on the same line
|
|
const QString &previousBlockText = cursor.block().previous().text();
|
|
previousIndentationString = ts.indentationString(previousBlockText);
|
|
if (!previousIndentationString.isEmpty())
|
|
cursor.insertText(previousIndentationString);
|
|
}
|
|
|
|
if (extraBlocks > 0) {
|
|
const int cursorPosition = cursor.position();
|
|
QTextCursor ensureVisible = cursor;
|
|
while (extraBlocks > 0) {
|
|
--extraBlocks;
|
|
ensureVisible.movePosition(QTextCursor::NextBlock);
|
|
if (tps.m_autoIndent)
|
|
d->m_document->autoIndent(ensureVisible, QChar::Null, cursorPosition);
|
|
else if (!previousIndentationString.isEmpty())
|
|
ensureVisible.insertText(previousIndentationString);
|
|
if (d->m_animateAutoComplete || d->m_highlightAutoComplete) {
|
|
QTextCursor tc = ensureVisible;
|
|
tc.movePosition(QTextCursor::EndOfBlock);
|
|
tc.movePosition(QTextCursor::StartOfBlock, QTextCursor::KeepAnchor);
|
|
tc.movePosition(QTextCursor::NextWord, QTextCursor::KeepAnchor);
|
|
d->autocompleterHighlight(tc);
|
|
}
|
|
}
|
|
cursor.setPosition(cursorPosition);
|
|
}
|
|
}
|
|
cursor.endEditBlock();
|
|
setMultiTextCursor(cursor);
|
|
ensureCursorVisible();
|
|
return;
|
|
} else if (!ro
|
|
&& (e == QKeySequence::MoveToStartOfBlock || e == QKeySequence::SelectStartOfBlock
|
|
|| e == QKeySequence::MoveToStartOfLine
|
|
|| e == QKeySequence::SelectStartOfLine)) {
|
|
const bool blockOp = e == QKeySequence::MoveToStartOfBlock || e == QKeySequence::SelectStartOfBlock;
|
|
const bool select = e == QKeySequence::SelectStartOfLine || e == QKeySequence::SelectStartOfBlock;
|
|
d->handleHomeKey(select, blockOp);
|
|
e->accept();
|
|
return;
|
|
} else if (!ro && e == QKeySequence::DeleteStartOfWord) {
|
|
e->accept();
|
|
if (!cursor.hasSelection()) {
|
|
if (camelCaseNavigationEnabled())
|
|
CamelCaseCursor::left(&cursor, this, QTextCursor::KeepAnchor);
|
|
else
|
|
cursor.movePosition(QTextCursor::PreviousWord, QTextCursor::KeepAnchor);
|
|
}
|
|
cursor.removeSelectedText();
|
|
setMultiTextCursor(cursor);
|
|
return;
|
|
} else if (!ro && e == QKeySequence::DeleteEndOfWord) {
|
|
e->accept();
|
|
if (!cursor.hasSelection()) {
|
|
if (camelCaseNavigationEnabled())
|
|
CamelCaseCursor::right(&cursor, this, QTextCursor::KeepAnchor);
|
|
else
|
|
cursor.movePosition(QTextCursor::NextWord, QTextCursor::KeepAnchor);
|
|
}
|
|
cursor.removeSelectedText();
|
|
setMultiTextCursor(cursor);
|
|
return;
|
|
} else if (!ro && e == QKeySequence::DeleteCompleteLine) {
|
|
e->accept();
|
|
for (QTextCursor &c : cursor)
|
|
c.select(QTextCursor::BlockUnderCursor);
|
|
cursor.mergeCursors();
|
|
cursor.removeSelectedText();
|
|
setMultiTextCursor(cursor);
|
|
return;
|
|
} else
|
|
switch (e->key()) {
|
|
#if 0
|
|
case Qt::Key_Dollar: {
|
|
d->m_overlay->setVisible(!d->m_overlay->isVisible());
|
|
d->m_overlay->setCursor(textCursor());
|
|
e->accept();
|
|
return;
|
|
|
|
} break;
|
|
#endif
|
|
case Qt::Key_Tab:
|
|
case Qt::Key_Backtab: {
|
|
if (ro) break;
|
|
if (d->m_snippetOverlay->isVisible() && !d->m_snippetOverlay->isEmpty()) {
|
|
d->snippetTabOrBacktab(e->key() == Qt::Key_Tab);
|
|
e->accept();
|
|
return;
|
|
}
|
|
QTextCursor cursor = textCursor();
|
|
if (d->m_skipAutoCompletedText && e->key() == Qt::Key_Tab) {
|
|
bool skippedAutoCompletedText = false;
|
|
while (!d->m_autoCompleteHighlightPos.isEmpty()
|
|
&& d->m_autoCompleteHighlightPos.last().selectionStart() == cursor.position()) {
|
|
skippedAutoCompletedText = true;
|
|
cursor.setPosition(d->m_autoCompleteHighlightPos.last().selectionEnd());
|
|
d->m_autoCompleteHighlightPos.pop_back();
|
|
}
|
|
if (skippedAutoCompletedText) {
|
|
setTextCursor(cursor);
|
|
e->accept();
|
|
d->updateAutoCompleteHighlight();
|
|
return;
|
|
}
|
|
}
|
|
int newPosition;
|
|
if (!hasMultipleCursors
|
|
&& d->m_document->typingSettings().tabShouldIndent(document(), cursor, &newPosition)) {
|
|
if (newPosition != cursor.position() && !cursor.hasSelection()) {
|
|
cursor.setPosition(newPosition);
|
|
setTextCursor(cursor);
|
|
}
|
|
d->m_document->autoIndent(cursor);
|
|
} else {
|
|
if (e->key() == Qt::Key_Tab)
|
|
indent();
|
|
else
|
|
unindent();
|
|
}
|
|
e->accept();
|
|
return;
|
|
} break;
|
|
case Qt::Key_Backspace:
|
|
if (ro) break;
|
|
if ((e->modifiers() & (Qt::ControlModifier
|
|
| Qt::ShiftModifier
|
|
| Qt::AltModifier
|
|
| Qt::MetaModifier)) == Qt::NoModifier) {
|
|
e->accept();
|
|
if (d->m_suggestionBlock.isValid())
|
|
d->clearCurrentSuggestion();
|
|
if (cursor.hasSelection()) {
|
|
cursor.removeSelectedText();
|
|
setMultiTextCursor(cursor);
|
|
} else {
|
|
d->handleBackspaceKey();
|
|
}
|
|
ensureCursorVisible();
|
|
return;
|
|
}
|
|
break;
|
|
case Qt::Key_Insert:
|
|
if (ro) break;
|
|
if (e->modifiers() == Qt::NoModifier) {
|
|
setOverwriteMode(!inOverwriteMode);
|
|
if (inOverwriteMode) {
|
|
for (const QTextCursor &cursor : multiTextCursor()) {
|
|
const QRectF oldBlockRect = d->cursorBlockRect(document(),
|
|
cursor.block(),
|
|
cursor.position());
|
|
viewport()->update(oldBlockRect.toAlignedRect());
|
|
}
|
|
}
|
|
e->accept();
|
|
return;
|
|
}
|
|
break;
|
|
case Qt::Key_Delete:
|
|
if (hasMultipleCursors && !ro
|
|
&& (e->modifiers() == Qt::NoModifier || e->modifiers() == Qt::KeypadModifier)) {
|
|
if (cursor.hasSelection()) {
|
|
cursor.removeSelectedText();
|
|
} else {
|
|
cursor.beginEditBlock();
|
|
for (QTextCursor c : cursor)
|
|
c.deleteChar();
|
|
cursor.mergeCursors();
|
|
cursor.endEditBlock();
|
|
}
|
|
e->accept();
|
|
return;
|
|
}
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
const QString eventText = e->text();
|
|
|
|
if (e->key() == Qt::Key_H
|
|
&& e->modifiers() == Qt::KeyboardModifiers(HostOsInfo::controlModifier())) {
|
|
d->universalHelper();
|
|
e->accept();
|
|
return;
|
|
}
|
|
|
|
if (ro || !isPrintableText(eventText)) {
|
|
QTextCursor::MoveOperation blockSelectionOperation = QTextCursor::NoMove;
|
|
if (e->modifiers() == (Qt::AltModifier | Qt::ShiftModifier)
|
|
&& !Utils::HostOsInfo::isMacHost()) {
|
|
if (MultiTextCursor::multiCursorEvent(
|
|
e, QKeySequence::MoveToNextLine, Qt::ShiftModifier)) {
|
|
blockSelectionOperation = QTextCursor::Down;
|
|
} else if (MultiTextCursor::multiCursorEvent(
|
|
e, QKeySequence::MoveToPreviousLine, Qt::ShiftModifier)) {
|
|
blockSelectionOperation = QTextCursor::Up;
|
|
} else if (MultiTextCursor::multiCursorEvent(
|
|
e, QKeySequence::MoveToNextChar, Qt::ShiftModifier)) {
|
|
blockSelectionOperation = QTextCursor::NextCharacter;
|
|
} else if (MultiTextCursor::multiCursorEvent(
|
|
e, QKeySequence::MoveToPreviousChar, Qt::ShiftModifier)) {
|
|
blockSelectionOperation = QTextCursor::PreviousCharacter;
|
|
}
|
|
}
|
|
|
|
if (blockSelectionOperation != QTextCursor::NoMove) {
|
|
cleanup.dismiss();
|
|
d->handleMoveBlockSelection(blockSelectionOperation);
|
|
} else if (!d->cursorMoveKeyEvent(e)) {
|
|
QTextCursor cursor = textCursor();
|
|
bool cursorWithinSnippet = false;
|
|
if (d->m_snippetOverlay->isVisible()
|
|
&& (e->key() == Qt::Key_Delete || e->key() == Qt::Key_Backspace)) {
|
|
cursorWithinSnippet = d->snippetCheckCursor(cursor);
|
|
}
|
|
if (cursorWithinSnippet)
|
|
cursor.beginEditBlock();
|
|
|
|
QPlainTextEdit::keyPressEvent(e);
|
|
|
|
if (cursorWithinSnippet) {
|
|
cursor.endEditBlock();
|
|
d->m_snippetOverlay->updateEquivalentSelections(textCursor());
|
|
}
|
|
}
|
|
} else if (hasMultipleCursors) {
|
|
if (inOverwriteMode) {
|
|
cursor.beginEditBlock();
|
|
for (QTextCursor &c : cursor) {
|
|
QTextBlock block = c.block();
|
|
int eolPos = block.position() + block.length() - 1;
|
|
int selEndPos = qMin(c.position() + eventText.length(), eolPos);
|
|
c.setPosition(selEndPos, QTextCursor::KeepAnchor);
|
|
c.insertText(eventText);
|
|
}
|
|
cursor.endEditBlock();
|
|
} else {
|
|
cursor.insertText(eventText);
|
|
}
|
|
setMultiTextCursor(cursor);
|
|
} else if ((e->modifiers() & (Qt::ControlModifier|Qt::AltModifier)) != Qt::ControlModifier){
|
|
// only go here if control is not pressed, except if also alt is pressed
|
|
// because AltGr maps to Alt + Ctrl
|
|
QTextCursor cursor = textCursor();
|
|
QString autoText;
|
|
if (!inOverwriteMode) {
|
|
const bool skipChar = d->m_skipAutoCompletedText
|
|
&& !d->m_autoCompleteHighlightPos.isEmpty()
|
|
&& cursor == d->m_autoCompleteHighlightPos.last();
|
|
autoText = autoCompleter()->autoComplete(cursor, eventText, skipChar);
|
|
}
|
|
const bool cursorWithinSnippet = d->snippetCheckCursor(cursor);
|
|
|
|
QChar electricChar;
|
|
if (d->m_document->typingSettings().m_autoIndent) {
|
|
for (const QChar c : eventText) {
|
|
if (d->m_document->indenter()->isElectricCharacter(c)) {
|
|
electricChar = c;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
bool doEditBlock = !electricChar.isNull() || !autoText.isEmpty() || cursorWithinSnippet;
|
|
if (doEditBlock)
|
|
cursor.beginEditBlock();
|
|
|
|
if (inOverwriteMode) {
|
|
if (!doEditBlock)
|
|
cursor.beginEditBlock();
|
|
QTextBlock block = cursor.block();
|
|
int eolPos = block.position() + block.length() - 1;
|
|
int selEndPos = qMin(cursor.position() + eventText.length(), eolPos);
|
|
cursor.setPosition(selEndPos, QTextCursor::KeepAnchor);
|
|
cursor.insertText(eventText);
|
|
if (!doEditBlock)
|
|
cursor.endEditBlock();
|
|
} else {
|
|
cursor.insertText(eventText);
|
|
}
|
|
|
|
if (!autoText.isEmpty()) {
|
|
int pos = cursor.position();
|
|
cursor.insertText(autoText);
|
|
cursor.movePosition(QTextCursor::PreviousCharacter, QTextCursor::KeepAnchor);
|
|
d->autocompleterHighlight(cursor);
|
|
//Select the inserted text, to be able to re-indent the inserted text
|
|
cursor.setPosition(pos, QTextCursor::KeepAnchor);
|
|
}
|
|
if (!electricChar.isNull() && d->m_autoCompleter->contextAllowsElectricCharacters(cursor))
|
|
d->m_document->autoIndent(cursor, electricChar, cursor.position());
|
|
if (!autoText.isEmpty())
|
|
cursor.setPosition(autoText.length() == 1 ? cursor.position() : cursor.anchor());
|
|
|
|
setTextCursor(cursor);
|
|
|
|
if (doEditBlock) {
|
|
cursor.endEditBlock();
|
|
if (cursorWithinSnippet)
|
|
d->m_snippetOverlay->updateEquivalentSelections(textCursor());
|
|
}
|
|
}
|
|
|
|
if (!ro && e->key() == Qt::Key_Delete && d->m_parenthesesMatchingEnabled)
|
|
d->m_parenthesesMatchingTimer.start();
|
|
|
|
if (!ro && d->m_contentsChanged && isPrintableText(eventText) && !inOverwriteMode)
|
|
d->m_codeAssistant.process();
|
|
}
|
|
|
|
class PositionedPart : public ParsedSnippet::Part
|
|
{
|
|
public:
|
|
explicit PositionedPart(const ParsedSnippet::Part &part) : ParsedSnippet::Part(part) {}
|
|
int start;
|
|
int end;
|
|
};
|
|
|
|
class CursorPart : public ParsedSnippet::Part
|
|
{
|
|
public:
|
|
CursorPart(const PositionedPart &part, QTextDocument *doc)
|
|
: ParsedSnippet::Part(part)
|
|
, cursor(doc)
|
|
{
|
|
cursor.setPosition(part.start);
|
|
cursor.setPosition(part.end, QTextCursor::KeepAnchor);
|
|
}
|
|
QTextCursor cursor;
|
|
};
|
|
|
|
void TextEditorWidget::insertCodeSnippet(int basePosition,
|
|
const QString &snippet,
|
|
const SnippetParser &parse)
|
|
{
|
|
SnippetParseResult result = parse(snippet);
|
|
if (std::holds_alternative<SnippetParseError>(result)) {
|
|
const auto &error = std::get<SnippetParseError>(result);
|
|
QMessageBox::warning(this, Tr::tr("Snippet Parse Error"), error.htmlMessage());
|
|
return;
|
|
}
|
|
QTC_ASSERT(std::holds_alternative<ParsedSnippet>(result), return);
|
|
ParsedSnippet data = std::get<ParsedSnippet>(result);
|
|
|
|
QTextCursor cursor = textCursor();
|
|
cursor.setPosition(basePosition, QTextCursor::KeepAnchor);
|
|
cursor.beginEditBlock();
|
|
cursor.removeSelectedText();
|
|
const int startCursorPosition = cursor.position();
|
|
|
|
d->m_snippetOverlay->accept();
|
|
|
|
QList<PositionedPart> positionedParts;
|
|
for (const ParsedSnippet::Part &part : std::as_const(data.parts)) {
|
|
if (part.variableIndex >= 0) {
|
|
PositionedPart posPart(part);
|
|
posPart.start = cursor.position();
|
|
cursor.insertText(part.text);
|
|
posPart.end = cursor.position();
|
|
positionedParts << posPart;
|
|
} else {
|
|
cursor.insertText(part.text);
|
|
}
|
|
}
|
|
|
|
QList<CursorPart> cursorParts = Utils::transform(positionedParts,
|
|
[doc = document()](const PositionedPart &part) {
|
|
return CursorPart(part, doc);
|
|
});
|
|
|
|
cursor.setPosition(startCursorPosition, QTextCursor::KeepAnchor);
|
|
d->m_document->autoIndent(cursor);
|
|
cursor.endEditBlock();
|
|
|
|
const QColor occurrencesColor
|
|
= textDocument()->fontSettings().toTextCharFormat(C_OCCURRENCES).background().color();
|
|
const QColor renameColor
|
|
= textDocument()->fontSettings().toTextCharFormat(C_OCCURRENCES_RENAME).background().color();
|
|
|
|
for (const CursorPart &part : cursorParts) {
|
|
const QColor &color = part.cursor.hasSelection() ? occurrencesColor : renameColor;
|
|
if (part.finalPart) {
|
|
d->m_snippetOverlay->setFinalSelection(part.cursor, color);
|
|
} else {
|
|
d->m_snippetOverlay->addSnippetSelection(part.cursor,
|
|
color,
|
|
part.mangler,
|
|
part.variableIndex);
|
|
}
|
|
}
|
|
|
|
cursor = d->m_snippetOverlay->firstSelectionCursor();
|
|
if (!cursor.isNull()) {
|
|
setTextCursor(cursor);
|
|
if (d->m_snippetOverlay->isFinalSelection(cursor))
|
|
d->m_snippetOverlay->accept();
|
|
else
|
|
d->m_snippetOverlay->setVisible(true);
|
|
}
|
|
}
|
|
|
|
void TextEditorWidgetPrivate::universalHelper()
|
|
{
|
|
// Test function for development. Place your new fangled experiment here to
|
|
// give it proper scrutiny before pushing it onto others.
|
|
}
|
|
|
|
void TextEditorWidget::doSetTextCursor(const QTextCursor &cursor, bool keepMultiSelection)
|
|
{
|
|
// workaround for QTextControl bug
|
|
bool selectionChange = cursor.hasSelection() || textCursor().hasSelection();
|
|
QTextCursor c = cursor;
|
|
c.setVisualNavigation(true);
|
|
const MultiTextCursor oldCursor = d->m_cursors;
|
|
if (!keepMultiSelection)
|
|
const_cast<MultiTextCursor &>(d->m_cursors).setCursors({c});
|
|
else
|
|
const_cast<MultiTextCursor &>(d->m_cursors).replaceMainCursor(c);
|
|
d->updateCursorSelections();
|
|
d->resetCursorFlashTimer();
|
|
QPlainTextEdit::doSetTextCursor(c);
|
|
if (oldCursor != d->m_cursors) {
|
|
QRect updateRect = d->cursorUpdateRect(oldCursor);
|
|
if (d->m_highlightCurrentLine)
|
|
updateRect = QRect(0, updateRect.y(), viewport()->rect().width(), updateRect.height());
|
|
updateRect |= d->cursorUpdateRect(d->m_cursors);
|
|
viewport()->update(updateRect);
|
|
emit cursorPositionChanged();
|
|
}
|
|
if (selectionChange)
|
|
d->slotSelectionChanged();
|
|
}
|
|
|
|
void TextEditorWidget::doSetTextCursor(const QTextCursor &cursor)
|
|
{
|
|
doSetTextCursor(cursor, false);
|
|
}
|
|
|
|
void TextEditorWidget::gotoLine(int line, int column, bool centerLine, bool animate)
|
|
{
|
|
d->m_lastCursorChangeWasInteresting = false; // avoid adding the previous position to history
|
|
const int blockNumber = qMin(line, document()->blockCount()) - 1;
|
|
const QTextBlock &block = document()->findBlockByNumber(blockNumber);
|
|
if (block.isValid()) {
|
|
QTextCursor cursor(block);
|
|
if (column > 0) {
|
|
cursor.movePosition(QTextCursor::Right, QTextCursor::MoveAnchor, column);
|
|
} else {
|
|
int pos = cursor.position();
|
|
while (document()->characterAt(pos).category() == QChar::Separator_Space) {
|
|
++pos;
|
|
}
|
|
cursor.setPosition(pos);
|
|
}
|
|
|
|
const DisplaySettings &ds = d->m_displaySettings;
|
|
if (animate && ds.m_animateNavigationWithinFile) {
|
|
QScrollBar *scrollBar = verticalScrollBar();
|
|
const int start = scrollBar->value();
|
|
|
|
ensureBlockIsUnfolded(block);
|
|
setUpdatesEnabled(false);
|
|
setTextCursor(cursor);
|
|
if (centerLine)
|
|
centerCursor();
|
|
else
|
|
ensureCursorVisible();
|
|
const int end = scrollBar->value();
|
|
scrollBar->setValue(start);
|
|
setUpdatesEnabled(true);
|
|
|
|
const int delta = end - start;
|
|
// limit the number of steps for the animation otherwise you wont be able to tell
|
|
// the direction of the animantion for large delta values
|
|
const int steps = qMax(-ds.m_animateWithinFileTimeMax,
|
|
qMin(ds.m_animateWithinFileTimeMax, delta));
|
|
// limit the duration of the animation to at least 4 pictures on a 60Hz Monitor and
|
|
// at most to the number of absolute steps
|
|
const int durationMinimum = int (4 // number of pictures
|
|
* float(1) / 60 // on a 60 Hz Monitor
|
|
* 1000); // milliseconds
|
|
const int duration = qMax(durationMinimum, qAbs(steps));
|
|
|
|
d->m_navigationAnimation = new QSequentialAnimationGroup(this);
|
|
auto startAnimation = new QPropertyAnimation(verticalScrollBar(), "value");
|
|
startAnimation->setEasingCurve(QEasingCurve::InExpo);
|
|
startAnimation->setStartValue(start);
|
|
startAnimation->setEndValue(start + steps / 2);
|
|
startAnimation->setDuration(duration / 2);
|
|
d->m_navigationAnimation->addAnimation(startAnimation);
|
|
auto endAnimation = new QPropertyAnimation(verticalScrollBar(), "value");
|
|
endAnimation->setEasingCurve(QEasingCurve::OutExpo);
|
|
endAnimation->setStartValue(end - steps / 2);
|
|
endAnimation->setEndValue(end);
|
|
endAnimation->setDuration(duration / 2);
|
|
d->m_navigationAnimation->addAnimation(endAnimation);
|
|
d->m_navigationAnimation->start(QAbstractAnimation::DeleteWhenStopped);
|
|
} else {
|
|
setTextCursor(cursor);
|
|
if (centerLine)
|
|
centerCursor();
|
|
else
|
|
ensureCursorVisible();
|
|
}
|
|
}
|
|
d->saveCurrentCursorPositionForNavigation();
|
|
}
|
|
|
|
int TextEditorWidget::position(TextPositionOperation posOp, int at) const
|
|
{
|
|
QTextCursor tc = textCursor();
|
|
|
|
if (at != -1)
|
|
tc.setPosition(at);
|
|
|
|
if (posOp == CurrentPosition)
|
|
return tc.position();
|
|
|
|
switch (posOp) {
|
|
case EndOfLinePosition:
|
|
tc.movePosition(QTextCursor::EndOfLine);
|
|
return tc.position();
|
|
case StartOfLinePosition:
|
|
tc.movePosition(QTextCursor::StartOfLine);
|
|
return tc.position();
|
|
case AnchorPosition:
|
|
if (tc.hasSelection())
|
|
return tc.anchor();
|
|
break;
|
|
case EndOfDocPosition:
|
|
tc.movePosition(QTextCursor::End);
|
|
return tc.position();
|
|
default:
|
|
break;
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
QTextCursor TextEditorWidget::textCursorAt(int position) const
|
|
{
|
|
QTextCursor c = textCursor();
|
|
c.setPosition(position);
|
|
return c;
|
|
}
|
|
|
|
Text::Position TextEditorWidget::lineColumn() const
|
|
{
|
|
return Utils::Text::Position::fromCursor(textCursor());
|
|
}
|
|
|
|
QRect TextEditorWidget::cursorRect(int pos) const
|
|
{
|
|
QTextCursor tc = textCursor();
|
|
if (pos >= 0)
|
|
tc.setPosition(pos);
|
|
QRect result = cursorRect(tc);
|
|
result.moveTo(viewport()->mapToGlobal(result.topLeft()));
|
|
return result;
|
|
}
|
|
|
|
void TextEditorWidget::convertPosition(int pos, int *line, int *column) const
|
|
{
|
|
Text::convertPosition(document(), pos, line, column);
|
|
}
|
|
|
|
bool TextEditorWidget::event(QEvent *e)
|
|
{
|
|
if (!d)
|
|
return QPlainTextEdit::event(e);
|
|
|
|
// FIXME: That's far too heavy, and triggers e.g for ChildEvent
|
|
if (e->type() != QEvent::InputMethodQuery)
|
|
d->m_contentsChanged = false;
|
|
switch (e->type()) {
|
|
case QEvent::ShortcutOverride: {
|
|
auto ke = static_cast<QKeyEvent *>(e);
|
|
if (ke->key() == Qt::Key_Escape
|
|
&& (d->m_snippetOverlay->isVisible()
|
|
|| multiTextCursor().hasMultipleCursors()
|
|
|| d->m_suggestionBlock.isValid()
|
|
|| d->m_numEmbeddedWidgets > 0)) {
|
|
if (d->m_numEmbeddedWidgets > 0)
|
|
emit embeddedWidgetsShouldClose();
|
|
e->accept();
|
|
} else {
|
|
// hack copied from QInputControl::isCommonTextEditShortcut
|
|
// Fixes: QTCREATORBUG-22854
|
|
e->setAccepted((ke->modifiers() == Qt::NoModifier || ke->modifiers() == Qt::ShiftModifier
|
|
|| ke->modifiers() == Qt::KeypadModifier)
|
|
&& (ke->key() < Qt::Key_Escape));
|
|
d->m_maybeFakeTooltipEvent = false;
|
|
}
|
|
return true;
|
|
}
|
|
case QEvent::ApplicationPaletteChange: {
|
|
// slight hack: ignore palette changes
|
|
// at this point the palette has changed already,
|
|
// so undo it by re-setting the palette:
|
|
applyFontSettings();
|
|
return true;
|
|
}
|
|
case QEvent::ReadOnlyChange:
|
|
d->updateFileLineEndingVisible();
|
|
d->updateTabSettingsButtonVisible();
|
|
if (isReadOnly())
|
|
setTextInteractionFlags(textInteractionFlags() | Qt::TextSelectableByKeyboard);
|
|
d->updateActions();
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
return QPlainTextEdit::event(e);
|
|
}
|
|
|
|
void TextEditorWidget::contextMenuEvent(QContextMenuEvent *e)
|
|
{
|
|
showDefaultContextMenu(e, Id());
|
|
}
|
|
|
|
void TextEditorWidgetPrivate::documentAboutToBeReloaded()
|
|
{
|
|
//memorize cursor position
|
|
m_tempState = q->saveState();
|
|
|
|
// remove extra selections (loads of QTextCursor objects)
|
|
|
|
m_extraSelections.clear();
|
|
m_extraSelections.reserve(NExtraSelectionKinds);
|
|
q->QPlainTextEdit::setExtraSelections(QList<QTextEdit::ExtraSelection>());
|
|
|
|
// clear all overlays
|
|
m_overlay->clear();
|
|
m_snippetOverlay->clear();
|
|
m_searchResultOverlay->clear();
|
|
m_selectionHighlightOverlay->clear();
|
|
m_refactorOverlay->clear();
|
|
|
|
// clear search results
|
|
m_searchResults.clear();
|
|
m_selectionResults.clear();
|
|
}
|
|
|
|
void TextEditorWidgetPrivate::documentReloadFinished(bool success)
|
|
{
|
|
if (!success)
|
|
return;
|
|
|
|
// restore cursor position
|
|
q->restoreState(m_tempState);
|
|
updateCannotDecodeInfo();
|
|
}
|
|
|
|
QByteArray TextEditorWidget::saveState() const
|
|
{
|
|
QByteArray state;
|
|
QDataStream stream(&state, QIODevice::WriteOnly);
|
|
stream << 2; // version number
|
|
stream << verticalScrollBar()->value();
|
|
stream << horizontalScrollBar()->value();
|
|
int line, column;
|
|
convertPosition(textCursor().position(), &line, &column);
|
|
stream << line;
|
|
stream << column;
|
|
|
|
// store code folding state
|
|
QList<int> foldedBlocks;
|
|
QTextBlock block = document()->firstBlock();
|
|
while (block.isValid()) {
|
|
if (block.userData() && static_cast<TextBlockUserData*>(block.userData())->folded()) {
|
|
int number = block.blockNumber();
|
|
foldedBlocks += number;
|
|
}
|
|
block = block.next();
|
|
}
|
|
stream << foldedBlocks;
|
|
|
|
stream << firstVisibleBlockNumber();
|
|
stream << lastVisibleBlockNumber();
|
|
|
|
return state;
|
|
}
|
|
|
|
bool TextEditorWidget::singleShotAfterHighlightingDone(std::function<void()> &&f)
|
|
{
|
|
if (d->m_document->syntaxHighlighter()
|
|
&& !d->m_document->syntaxHighlighter()->syntaxHighlighterUpToDate()) {
|
|
connect(d->m_document->syntaxHighlighter(),
|
|
&SyntaxHighlighter::finished,
|
|
this,
|
|
[f = std::move(f)] { f(); }, Qt::SingleShotConnection);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void TextEditorWidget::restoreState(const QByteArray &state)
|
|
{
|
|
const auto callFoldLicenseHeader = [this] {
|
|
auto callFold = [this] {
|
|
if (d->m_displaySettings.m_autoFoldFirstComment)
|
|
d->foldLicenseHeader();
|
|
};
|
|
|
|
if (!singleShotAfterHighlightingDone(callFold))
|
|
callFold();
|
|
};
|
|
|
|
if (state.isEmpty()) {
|
|
callFoldLicenseHeader();
|
|
return;
|
|
}
|
|
|
|
int version;
|
|
int vval;
|
|
int hval;
|
|
int lineVal;
|
|
int columnVal;
|
|
QDataStream stream(state);
|
|
stream >> version;
|
|
stream >> vval;
|
|
stream >> hval;
|
|
stream >> lineVal;
|
|
stream >> columnVal;
|
|
|
|
if (version >= 1) {
|
|
QList<int> collapsedBlocks;
|
|
stream >> collapsedBlocks;
|
|
auto foldingRestore = [this, collapsedBlocks] {
|
|
QTextDocument *doc = document();
|
|
bool layoutChanged = false;
|
|
for (const int blockNumber : std::as_const(collapsedBlocks)) {
|
|
QTextBlock block = doc->findBlockByNumber(qMax(0, blockNumber));
|
|
if (block.isValid()) {
|
|
TextDocumentLayout::doFoldOrUnfold(block, false);
|
|
layoutChanged = true;
|
|
}
|
|
}
|
|
if (layoutChanged) {
|
|
auto documentLayout = qobject_cast<TextDocumentLayout *>(doc->documentLayout());
|
|
QTC_ASSERT(documentLayout, return);
|
|
documentLayout->requestUpdate();
|
|
documentLayout->emitDocumentSizeChanged();
|
|
d->updateCursorPosition();
|
|
}
|
|
};
|
|
if (!singleShotAfterHighlightingDone(foldingRestore))
|
|
foldingRestore();
|
|
} else {
|
|
callFoldLicenseHeader();
|
|
}
|
|
|
|
d->m_lastCursorChangeWasInteresting = false; // avoid adding last position to history
|
|
// line is 1-based, column is 0-based
|
|
gotoLine(lineVal, columnVal);
|
|
verticalScrollBar()->setValue(vval);
|
|
horizontalScrollBar()->setValue(hval);
|
|
|
|
if (version >= 2) {
|
|
int originalFirstBlock, originalLastBlock;
|
|
stream >> originalFirstBlock;
|
|
stream >> originalLastBlock;
|
|
// If current line was visible in the old state, make sure it is visible in the new state.
|
|
// This can happen if the height of the editor changed in the meantime
|
|
const int lineBlock = lineVal - 1; // line is 1-based, blocks are 0-based
|
|
const bool originalCursorVisible = (originalFirstBlock <= lineBlock
|
|
&& lineBlock <= originalLastBlock);
|
|
const int firstBlock = firstVisibleBlockNumber();
|
|
const int lastBlock = lastVisibleBlockNumber();
|
|
const bool cursorVisible = (firstBlock <= lineBlock && lineBlock <= lastBlock);
|
|
if (originalCursorVisible && !cursorVisible)
|
|
centerCursor();
|
|
}
|
|
|
|
d->saveCurrentCursorPositionForNavigation();
|
|
}
|
|
|
|
void TextEditorWidget::setParenthesesMatchingEnabled(bool b)
|
|
{
|
|
d->m_parenthesesMatchingEnabled = b;
|
|
}
|
|
|
|
bool TextEditorWidget::isParenthesesMatchingEnabled() const
|
|
{
|
|
return d->m_parenthesesMatchingEnabled;
|
|
}
|
|
|
|
void TextEditorWidget::setHighlightCurrentLine(bool b)
|
|
{
|
|
d->m_highlightCurrentLine = b;
|
|
d->updateCurrentLineHighlight();
|
|
}
|
|
|
|
bool TextEditorWidget::highlightCurrentLine() const
|
|
{
|
|
return d->m_highlightCurrentLine;
|
|
}
|
|
|
|
void TextEditorWidget::setLineNumbersVisible(bool b)
|
|
{
|
|
d->m_lineNumbersVisible = b;
|
|
d->slotUpdateExtraAreaWidth();
|
|
}
|
|
|
|
bool TextEditorWidget::lineNumbersVisible() const
|
|
{
|
|
return d->m_lineNumbersVisible;
|
|
}
|
|
|
|
void TextEditorWidget::setAlwaysOpenLinksInNextSplit(bool b)
|
|
{
|
|
d->m_displaySettings.m_openLinksInNextSplit = b;
|
|
}
|
|
|
|
bool TextEditorWidget::alwaysOpenLinksInNextSplit() const
|
|
{
|
|
return d->m_displaySettings.m_openLinksInNextSplit;
|
|
}
|
|
|
|
void TextEditorWidget::setMarksVisible(bool b)
|
|
{
|
|
d->m_marksVisible = b;
|
|
d->slotUpdateExtraAreaWidth();
|
|
}
|
|
|
|
bool TextEditorWidget::marksVisible() const
|
|
{
|
|
return d->m_marksVisible;
|
|
}
|
|
|
|
void TextEditorWidget::setRequestMarkEnabled(bool b)
|
|
{
|
|
d->m_requestMarkEnabled = b;
|
|
}
|
|
|
|
bool TextEditorWidget::requestMarkEnabled() const
|
|
{
|
|
return d->m_requestMarkEnabled;
|
|
}
|
|
|
|
void TextEditorWidget::setLineSeparatorsAllowed(bool b)
|
|
{
|
|
d->m_lineSeparatorsAllowed = b;
|
|
}
|
|
|
|
bool TextEditorWidget::lineSeparatorsAllowed() const
|
|
{
|
|
return d->m_lineSeparatorsAllowed;
|
|
}
|
|
|
|
void TextEditorWidgetPrivate::updateCodeFoldingVisible()
|
|
{
|
|
const bool visible = m_codeFoldingSupported && m_displaySettings.m_displayFoldingMarkers;
|
|
if (m_codeFoldingVisible != visible) {
|
|
m_codeFoldingVisible = visible;
|
|
slotUpdateExtraAreaWidth();
|
|
}
|
|
}
|
|
|
|
void TextEditorWidgetPrivate::updateFileLineEndingVisible()
|
|
{
|
|
m_fileLineEndingAction->setVisible(m_displaySettings.m_displayFileLineEnding && !q->isReadOnly());
|
|
}
|
|
|
|
void TextEditorWidgetPrivate::updateTabSettingsButtonVisible()
|
|
{
|
|
m_tabSettingsButton->setVisible(m_displaySettings.m_displayTabSettings && !q->isReadOnly());
|
|
}
|
|
|
|
void TextEditorWidgetPrivate::reconfigure()
|
|
{
|
|
m_document->setMimeType(
|
|
Utils::mimeTypeForFile(m_document->filePath(),
|
|
MimeMatchMode::MatchDefaultAndRemote).name());
|
|
q->configureGenericHighlighter();
|
|
}
|
|
|
|
void TextEditorWidgetPrivate::updateSyntaxInfoBar(const HighlighterHelper::Definitions &definitions,
|
|
const QString &fileName)
|
|
{
|
|
Id missing(Constants::INFO_MISSING_SYNTAX_DEFINITION);
|
|
Id multiple(Constants::INFO_MULTIPLE_SYNTAX_DEFINITIONS);
|
|
InfoBar *infoBar = m_document->infoBar();
|
|
|
|
if (definitions.isEmpty() && infoBar->canInfoBeAdded(missing)
|
|
&& !TextEditorSettings::highlighterSettings().isIgnoredFilePattern(fileName)) {
|
|
InfoBarEntry info(missing,
|
|
Tr::tr("A highlight definition was not found for this file. "
|
|
"Would you like to download additional highlight definition files?"),
|
|
InfoBarEntry::GlobalSuppression::Enabled);
|
|
info.addCustomButton(Tr::tr("Download Definitions"), [missing, this]() {
|
|
m_document->infoBar()->removeInfo(missing);
|
|
HighlighterHelper::downloadDefinitions();
|
|
});
|
|
|
|
infoBar->removeInfo(multiple);
|
|
infoBar->addInfo(info);
|
|
return;
|
|
}
|
|
|
|
infoBar->removeInfo(multiple);
|
|
infoBar->removeInfo(missing);
|
|
|
|
if (definitions.size() > 1) {
|
|
InfoBarEntry info(multiple,
|
|
Tr::tr("More than one highlight definition was found for this file. "
|
|
"Which one should be used to highlight this file?"));
|
|
info.setComboInfo(Utils::transform(definitions, &HighlighterHelper::Definition::name),
|
|
[this](const InfoBarEntry::ComboInfo &info) {
|
|
this->configureGenericHighlighter(HighlighterHelper::definitionForName(info.displayText));
|
|
});
|
|
|
|
info.addCustomButton(Tr::tr("Remember My Choice"), [multiple, this]() {
|
|
m_document->infoBar()->removeInfo(multiple);
|
|
rememberCurrentSyntaxDefinition();
|
|
});
|
|
|
|
infoBar->addInfo(info);
|
|
}
|
|
}
|
|
|
|
void TextEditorWidgetPrivate::removeSyntaxInfoBar()
|
|
{
|
|
InfoBar *infoBar = m_document->infoBar();
|
|
infoBar->removeInfo(Constants::INFO_MISSING_SYNTAX_DEFINITION);
|
|
infoBar->removeInfo(Constants::INFO_MULTIPLE_SYNTAX_DEFINITIONS);
|
|
}
|
|
|
|
void TextEditorWidgetPrivate::configureGenericHighlighter(
|
|
const KSyntaxHighlighting::Definition &definition)
|
|
{
|
|
if (definition.isValid()) {
|
|
setupFromDefinition(definition);
|
|
} else {
|
|
q->setCodeFoldingSupported(false);
|
|
}
|
|
|
|
const QString definitionFilesPath
|
|
= TextEditorSettings::highlighterSettings().definitionFilesPath().toString();
|
|
m_document->resetSyntaxHighlighter([definitionFilesPath, definition] {
|
|
auto highlighter = new Highlighter(definitionFilesPath);
|
|
highlighter->setDefinition(definition);
|
|
return highlighter;
|
|
});
|
|
|
|
m_document->setFontSettings(TextEditorSettings::fontSettings());
|
|
}
|
|
|
|
void TextEditorWidgetPrivate::setupFromDefinition(const KSyntaxHighlighting::Definition &definition)
|
|
{
|
|
const TypingSettings::CommentPosition commentPosition
|
|
= m_document->typingSettings().m_commentPosition;
|
|
m_commentDefinition.isAfterWhitespace = commentPosition != TypingSettings::StartOfLine;
|
|
if (!definition.isValid())
|
|
return;
|
|
m_commentDefinition.singleLine = definition.singleLineCommentMarker();
|
|
m_commentDefinition.multiLineStart = definition.multiLineCommentMarker().first;
|
|
m_commentDefinition.multiLineEnd = definition.multiLineCommentMarker().second;
|
|
if (commentPosition == TypingSettings::Automatic) {
|
|
m_commentDefinition.isAfterWhitespace
|
|
= definition.singleLineCommentPosition()
|
|
== KSyntaxHighlighting::CommentPosition::AfterWhitespace;
|
|
}
|
|
q->setCodeFoldingSupported(true);
|
|
}
|
|
|
|
KSyntaxHighlighting::Definition TextEditorWidgetPrivate::currentDefinition()
|
|
{
|
|
if (auto *highlighter = qobject_cast<Highlighter *>(m_document->syntaxHighlighter()))
|
|
return highlighter->definition();
|
|
return {};
|
|
}
|
|
|
|
void TextEditorWidgetPrivate::rememberCurrentSyntaxDefinition()
|
|
{
|
|
const HighlighterHelper::Definition &definition = currentDefinition();
|
|
if (definition.isValid())
|
|
HighlighterHelper::rememberDefinitionForDocument(definition, m_document.data());
|
|
}
|
|
|
|
void TextEditorWidgetPrivate::openLinkUnderCursor(bool openInNextSplit)
|
|
{
|
|
q->findLinkAt(
|
|
q->textCursor(),
|
|
[openInNextSplit, self = QPointer<TextEditorWidget>(q)](const Link &symbolLink) {
|
|
if (self)
|
|
self->openLink(symbolLink, openInNextSplit);
|
|
},
|
|
true,
|
|
openInNextSplit);
|
|
}
|
|
|
|
void TextEditorWidgetPrivate::openTypeUnderCursor(bool openInNextSplit)
|
|
{
|
|
q->findTypeAt(
|
|
q->textCursor(),
|
|
[openInNextSplit, self = QPointer<TextEditorWidget>(q)](const Link &symbolLink) {
|
|
if (self)
|
|
self->openLink(symbolLink, openInNextSplit);
|
|
},
|
|
true,
|
|
openInNextSplit);
|
|
}
|
|
|
|
qreal TextEditorWidgetPrivate::charWidth() const
|
|
{
|
|
return QFontMetricsF(q->font()).horizontalAdvance(QLatin1Char('x'));
|
|
}
|
|
class CarrierWidget : public QWidget
|
|
{
|
|
public:
|
|
CarrierWidget(TextEditorWidget *textEditorWidget, QWidget *embed)
|
|
: QWidget(textEditorWidget->viewport())
|
|
, m_embed(embed)
|
|
, m_textEditorWidget(textEditorWidget)
|
|
{
|
|
QVBoxLayout *layout = new QVBoxLayout(this);
|
|
layout->setContentsMargins(0, 0, 0, 0);
|
|
layout->addWidget(m_embed);
|
|
|
|
setFixedWidth(m_textEditorWidget->width() - m_textEditorWidget->extraAreaWidth());
|
|
setFixedHeight(m_embed->minimumSizeHint().height());
|
|
|
|
connect(m_textEditorWidget, &TextEditorWidget::resized, this, [this] {
|
|
setFixedWidth(m_textEditorWidget->width() - m_textEditorWidget->extraAreaWidth());
|
|
});
|
|
}
|
|
|
|
int embedHeight() { return m_embed->sizeHint().height(); }
|
|
|
|
private:
|
|
QWidget *m_embed;
|
|
TextEditorWidget *m_textEditorWidget;
|
|
};
|
|
|
|
EmbeddedWidgetInterface::~EmbeddedWidgetInterface()
|
|
{
|
|
close();
|
|
}
|
|
|
|
void EmbeddedWidgetInterface::resize()
|
|
{
|
|
emit resized();
|
|
}
|
|
|
|
void EmbeddedWidgetInterface::close()
|
|
{
|
|
emit closed();
|
|
}
|
|
|
|
void TextEditorWidgetPrivate::forceUpdateScrollbarSize()
|
|
{
|
|
// We use resizeEvent here as a workaround as we can't get access to the
|
|
// scrollarea which is a private part of the QPlainTextEdit.
|
|
// During the resizeEvent the plain text edit will resize its scrollbars.
|
|
// The TextEditorWidget will also update its scrollbar overlays.
|
|
q->resizeEvent(new QResizeEvent(q->size(), q->size()));
|
|
}
|
|
|
|
std::unique_ptr<EmbeddedWidgetInterface> TextEditorWidgetPrivate::insertWidget(
|
|
QWidget *widget, int line)
|
|
{
|
|
QPointer<CarrierWidget> carrier = new CarrierWidget(q, widget);
|
|
std::unique_ptr<EmbeddedWidgetInterface> result(new EmbeddedWidgetInterface());
|
|
|
|
connect(
|
|
q,
|
|
&TextEditorWidget::embeddedWidgetsShouldClose,
|
|
result.get(),
|
|
&EmbeddedWidgetInterface::shouldClose);
|
|
|
|
struct State
|
|
{
|
|
int height = 0;
|
|
QTextCursor cursor;
|
|
QTextBlock block;
|
|
};
|
|
|
|
std::shared_ptr<State> pState = std::make_shared<State>();
|
|
pState->cursor = QTextCursor(q->document());
|
|
pState->cursor.setPosition(line);
|
|
pState->cursor.movePosition(QTextCursor::StartOfBlock);
|
|
|
|
auto position = [this, pState, carrier] {
|
|
QTextBlock block = pState->cursor.block();
|
|
QTC_ASSERT(block.isValid(), return);
|
|
|
|
TextBlockUserData *userData = TextDocumentLayout::userData(block);
|
|
if (block != pState->block) {
|
|
TextBlockUserData *previousUserData = TextDocumentLayout::userData(pState->block);
|
|
if (previousUserData && userData != previousUserData) {
|
|
// We have swapped into a different block, remove it from the previous block
|
|
previousUserData->removeEmbeddedWidget(carrier);
|
|
}
|
|
userData->addEmbeddedWidget(carrier);
|
|
pState->block = block;
|
|
pState->height = 0;
|
|
}
|
|
|
|
QRectF r = cursorBlockRect(m_document->document(), block, block.position());
|
|
|
|
int y = 0;
|
|
for (const auto &wdgt : userData->embeddedWidgets()) {
|
|
if (wdgt == carrier)
|
|
break;
|
|
y += wdgt->height();
|
|
}
|
|
|
|
QPoint pos = r.topLeft().toPoint()
|
|
+ QPoint(0, TextEditorSettings::fontSettings().lineSpacing() + y);
|
|
|
|
int h = carrier->embedHeight();
|
|
if (h == pState->height && pos == carrier->pos())
|
|
return;
|
|
|
|
carrier->move(pos);
|
|
carrier->setFixedHeight(h);
|
|
|
|
pState->height = h;
|
|
|
|
qobject_cast<TextDocumentLayout *>(q->document()->documentLayout())->scheduleUpdate();
|
|
};
|
|
|
|
position();
|
|
|
|
connect(widget, &QWidget::destroyed, this, [pState, carrier, this] {
|
|
if (carrier)
|
|
carrier->deleteLater();
|
|
if (!q->document())
|
|
return;
|
|
QTextBlock block = pState->cursor.block();
|
|
auto userData = TextDocumentLayout::userData(block);
|
|
userData->removeEmbeddedWidget(carrier);
|
|
m_numEmbeddedWidgets--;
|
|
forceUpdateScrollbarSize();
|
|
});
|
|
connect(q->document()->documentLayout(), &QAbstractTextDocumentLayout::update, carrier, position);
|
|
connect(result.get(), &EmbeddedWidgetInterface::resized, carrier, [position, this]() {
|
|
position();
|
|
forceUpdateScrollbarSize();
|
|
});
|
|
connect(result.get(), &EmbeddedWidgetInterface::closed, this, [this, carrier] {
|
|
if (carrier)
|
|
carrier->deleteLater();
|
|
QAbstractTextDocumentLayout *layout = q->document()->documentLayout();
|
|
QTimer::singleShot(0, layout, [layout] { layout->update(); });
|
|
});
|
|
|
|
m_numEmbeddedWidgets++;
|
|
|
|
carrier->show();
|
|
|
|
forceUpdateScrollbarSize();
|
|
return result;
|
|
}
|
|
|
|
void TextEditorWidgetPrivate::registerActions()
|
|
{
|
|
using namespace Core::Constants;
|
|
using namespace TextEditor::Constants;
|
|
|
|
ActionBuilder(this, Constants::COMPLETE_THIS)
|
|
.setContext(m_editorContext)
|
|
.addOnTriggered(this, [this] { q->invokeAssist(Completion); });
|
|
|
|
ActionBuilder(this, Constants::FUNCTION_HINT)
|
|
.setContext(m_editorContext)
|
|
.addOnTriggered(this, [this] { q->invokeAssist(FunctionHint); });
|
|
|
|
ActionBuilder(this, Constants::QUICKFIX_THIS)
|
|
.setContext(m_editorContext)
|
|
.addOnTriggered(this, [this] { q->invokeAssist(QuickFix); });
|
|
|
|
ActionBuilder(this, Constants::SHOWCONTEXTMENU)
|
|
.setContext(m_editorContext)
|
|
.addOnTriggered(this, [this] { q->showContextMenu(); });
|
|
|
|
m_undoAction = ActionBuilder(this, UNDO)
|
|
.setContext(m_editorContext)
|
|
.addOnTriggered([this] { q->undo(); })
|
|
.setScriptable(true)
|
|
.contextAction();
|
|
m_redoAction = ActionBuilder(this, REDO)
|
|
.setContext(m_editorContext)
|
|
.addOnTriggered([this] { q->redo(); })
|
|
.setScriptable(true)
|
|
.contextAction();
|
|
m_copyAction = ActionBuilder(this, COPY)
|
|
.setContext(m_editorContext)
|
|
.addOnTriggered([this] { q->copy(); })
|
|
.setScriptable(true)
|
|
.contextAction();
|
|
m_cutAction = ActionBuilder(this, CUT)
|
|
.setContext(m_editorContext)
|
|
.addOnTriggered([this] { q->cut(); })
|
|
.setScriptable(true)
|
|
.contextAction();
|
|
m_modifyingActions << ActionBuilder(this, PASTE)
|
|
.setContext(m_editorContext)
|
|
.addOnTriggered([this] { q->paste(); })
|
|
.setScriptable(true)
|
|
.contextAction();
|
|
ActionBuilder(this, SELECTALL)
|
|
.setContext(m_editorContext)
|
|
.setScriptable(true)
|
|
.addOnTriggered([this] { q->selectAll(); });
|
|
ActionBuilder(this, GOTO).setContext(m_editorContext).addOnTriggered([] {
|
|
LocatorManager::showFilter(lineNumberFilter());
|
|
});
|
|
ActionBuilder(this, PRINT)
|
|
.setContext(m_editorContext)
|
|
.addOnTriggered([this] { q->print(ICore::printer()); })
|
|
.contextAction();
|
|
m_modifyingActions << ActionBuilder(this, DELETE_LINE)
|
|
.setContext(m_editorContext)
|
|
.addOnTriggered([this] { q->deleteLine(); })
|
|
.setScriptable(true)
|
|
.contextAction();
|
|
m_modifyingActions << ActionBuilder(this, DELETE_END_OF_LINE)
|
|
.setContext(m_editorContext)
|
|
.addOnTriggered([this] { q->deleteEndOfLine(); })
|
|
.setScriptable(true)
|
|
.contextAction();
|
|
m_modifyingActions << ActionBuilder(this, DELETE_END_OF_WORD)
|
|
.setContext(m_editorContext)
|
|
.addOnTriggered([this] { q->deleteEndOfWord(); })
|
|
.setScriptable(true)
|
|
.contextAction();
|
|
m_modifyingActions << ActionBuilder(this, DELETE_END_OF_WORD_CAMEL_CASE)
|
|
.setContext(m_editorContext)
|
|
.addOnTriggered([this] { q->deleteEndOfWordCamelCase(); })
|
|
.setScriptable(true)
|
|
.contextAction();
|
|
m_modifyingActions << ActionBuilder(this, DELETE_START_OF_LINE)
|
|
.setContext(m_editorContext)
|
|
.addOnTriggered([this] { q->deleteStartOfLine(); })
|
|
.setScriptable(true)
|
|
.contextAction();
|
|
m_modifyingActions << ActionBuilder(this, DELETE_START_OF_WORD)
|
|
.setContext(m_editorContext)
|
|
.addOnTriggered([this] { q->deleteStartOfWord(); })
|
|
.setScriptable(true)
|
|
.contextAction();
|
|
m_modifyingActions << ActionBuilder(this, DELETE_START_OF_WORD_CAMEL_CASE)
|
|
.setContext(m_editorContext)
|
|
.addOnTriggered([this] { q->deleteStartOfWordCamelCase(); })
|
|
.setScriptable(true)
|
|
.contextAction();
|
|
ActionBuilder(this, GOTO_BLOCK_START_WITH_SELECTION)
|
|
.setContext(m_editorContext)
|
|
.addOnTriggered([this] { q->gotoBlockStartWithSelection(); })
|
|
.setScriptable(true);
|
|
ActionBuilder(this, GOTO_BLOCK_END_WITH_SELECTION)
|
|
.setContext(m_editorContext)
|
|
.addOnTriggered([this] { q->gotoBlockEndWithSelection(); })
|
|
.setScriptable(true);
|
|
m_modifyingActions << ActionBuilder(this, MOVE_LINE_UP)
|
|
.setContext(m_editorContext)
|
|
.addOnTriggered([this] { q->moveLineUp(); })
|
|
.setScriptable(true)
|
|
.contextAction();
|
|
m_modifyingActions << ActionBuilder(this, MOVE_LINE_DOWN)
|
|
.setContext(m_editorContext)
|
|
.addOnTriggered([this] { q->moveLineDown(); })
|
|
.setScriptable(true)
|
|
.contextAction();
|
|
m_modifyingActions << ActionBuilder(this, COPY_LINE_UP)
|
|
.setContext(m_editorContext)
|
|
.addOnTriggered([this] { q->copyLineUp(); })
|
|
.setScriptable(true)
|
|
.contextAction();
|
|
m_modifyingActions << ActionBuilder(this, COPY_LINE_DOWN)
|
|
.setContext(m_editorContext)
|
|
.addOnTriggered([this] { q->copyLineDown(); })
|
|
.setScriptable(true)
|
|
.contextAction();
|
|
m_modifyingActions << ActionBuilder(this, JOIN_LINES)
|
|
.setContext(m_editorContext)
|
|
.addOnTriggered([this] { q->joinLines(); })
|
|
.setScriptable(true)
|
|
.contextAction();
|
|
m_modifyingActions << ActionBuilder(this, INSERT_LINE_ABOVE)
|
|
.setContext(m_editorContext)
|
|
.addOnTriggered([this] { q->insertLineAbove(); })
|
|
.setScriptable(true)
|
|
.contextAction();
|
|
m_modifyingActions << ActionBuilder(this, INSERT_LINE_BELOW)
|
|
.setContext(m_editorContext)
|
|
.addOnTriggered([this] { q->insertLineBelow(); })
|
|
.setScriptable(true)
|
|
.contextAction();
|
|
m_modifyingActions << ActionBuilder(this, SWITCH_UTF8BOM)
|
|
.setContext(m_editorContext)
|
|
.addOnTriggered([this] { q->switchUtf8bom(); })
|
|
.contextAction();
|
|
m_modifyingActions << ActionBuilder(this, INDENT)
|
|
.setContext(m_editorContext)
|
|
.addOnTriggered([this] { q->indent(); })
|
|
.setScriptable(true)
|
|
.contextAction();
|
|
m_modifyingActions << ActionBuilder(this, UNINDENT)
|
|
.setContext(m_editorContext)
|
|
.addOnTriggered([this] { q->unindent(); })
|
|
.setScriptable(true)
|
|
.contextAction();
|
|
m_followSymbolAction = ActionBuilder(this, FOLLOW_SYMBOL_UNDER_CURSOR)
|
|
.setContext(m_editorContext)
|
|
.addOnTriggered([this] { q->openLinkUnderCursor(); })
|
|
.contextAction();
|
|
m_followSymbolInNextSplitAction = ActionBuilder(this, FOLLOW_SYMBOL_UNDER_CURSOR_IN_NEXT_SPLIT)
|
|
.setContext(m_editorContext)
|
|
.addOnTriggered(
|
|
[this] { q->openLinkUnderCursorInNextSplit(); })
|
|
.contextAction();
|
|
m_followToTypeAction = ActionBuilder(this, FOLLOW_SYMBOL_TO_TYPE)
|
|
.setContext(m_editorContext)
|
|
.addOnTriggered([this] { q->openTypeUnderCursor(); })
|
|
.contextAction();
|
|
m_followToTypeInNextSplitAction = ActionBuilder(this, FOLLOW_SYMBOL_TO_TYPE_IN_NEXT_SPLIT)
|
|
.setContext(m_editorContext)
|
|
.addOnTriggered(
|
|
[this] { q->openTypeUnderCursorInNextSplit(); })
|
|
.contextAction();
|
|
m_findUsageAction = ActionBuilder(this, FIND_USAGES)
|
|
.setContext(m_editorContext)
|
|
.addOnTriggered([this] { q->findUsages(); })
|
|
.contextAction();
|
|
m_renameSymbolAction = ActionBuilder(this, RENAME_SYMBOL)
|
|
.setContext(m_editorContext)
|
|
.addOnTriggered([this] { q->renameSymbolUnderCursor(); })
|
|
.contextAction();
|
|
m_jumpToFileAction = ActionBuilder(this, JUMP_TO_FILE_UNDER_CURSOR)
|
|
.setContext(m_editorContext)
|
|
.addOnTriggered([this] { q->openLinkUnderCursor(); })
|
|
.contextAction();
|
|
m_jumpToFileInNextSplitAction = ActionBuilder(this, JUMP_TO_FILE_UNDER_CURSOR_IN_NEXT_SPLIT)
|
|
.setContext(m_editorContext)
|
|
.addOnTriggered(
|
|
[this] { q->openLinkUnderCursorInNextSplit(); })
|
|
.contextAction();
|
|
m_openCallHierarchyAction = ActionBuilder(this, OPEN_CALL_HIERARCHY)
|
|
.setContext(m_editorContext)
|
|
.addOnTriggered([this] { q->openCallHierarchy(); })
|
|
.setScriptable(true)
|
|
.contextAction();
|
|
m_openTypeHierarchyAction = ActionBuilder(this, OPEN_TYPE_HIERARCHY)
|
|
.setContext(m_editorContext)
|
|
.addOnTriggered([] {
|
|
updateTypeHierarchy(NavigationWidget::activateSubWidget(
|
|
Constants::TYPE_HIERARCHY_FACTORY_ID, Side::Left));
|
|
})
|
|
.setScriptable(true)
|
|
.contextAction();
|
|
ActionBuilder(this, VIEW_PAGE_UP)
|
|
.setContext(m_editorContext)
|
|
.addOnTriggered([this] { q->viewPageUp(); })
|
|
.setScriptable(true);
|
|
ActionBuilder(this, VIEW_PAGE_DOWN)
|
|
.setContext(m_editorContext)
|
|
.addOnTriggered([this] { q->viewPageDown(); })
|
|
.setScriptable(true);
|
|
ActionBuilder(this, VIEW_LINE_UP)
|
|
.setContext(m_editorContext)
|
|
.addOnTriggered([this] { q->viewLineUp(); })
|
|
.setScriptable(true);
|
|
ActionBuilder(this, VIEW_LINE_DOWN)
|
|
.setContext(m_editorContext)
|
|
.addOnTriggered([this] { q->viewLineDown(); })
|
|
.setScriptable(true);
|
|
|
|
ActionBuilder(this, SELECT_ENCODING).setContext(m_editorContext).addOnTriggered([this] {
|
|
q->selectEncoding();
|
|
});
|
|
m_modifyingActions << ActionBuilder(this, CIRCULAR_PASTE)
|
|
.setContext(m_editorContext)
|
|
.addOnTriggered([this] { q->circularPaste(); })
|
|
.contextAction();
|
|
m_modifyingActions << ActionBuilder(this, NO_FORMAT_PASTE)
|
|
.setContext(m_editorContext)
|
|
.addOnTriggered([this] { q->pasteWithoutFormat(); })
|
|
.setScriptable(true)
|
|
.contextAction();
|
|
|
|
m_autoIndentAction = ActionBuilder(this, AUTO_INDENT_SELECTION)
|
|
.setContext(m_editorContext)
|
|
.addOnTriggered([this] { q->autoIndent(); })
|
|
.setScriptable(true)
|
|
.contextAction();
|
|
m_autoFormatAction = ActionBuilder(this, AUTO_FORMAT_SELECTION)
|
|
.setContext(m_editorContext)
|
|
.addOnTriggered([this] { q->autoFormat(); })
|
|
.setScriptable(true)
|
|
.contextAction();
|
|
m_modifyingActions << ActionBuilder(this, REWRAP_PARAGRAPH)
|
|
.setContext(m_editorContext)
|
|
.addOnTriggered([this] { q->rewrapParagraph(); })
|
|
.setScriptable(true)
|
|
.contextAction();
|
|
m_visualizeWhitespaceAction = ActionBuilder(this, VISUALIZE_WHITESPACE)
|
|
.setContext(m_editorContext)
|
|
.setCheckable(true)
|
|
.addOnToggled(
|
|
this,
|
|
[this](bool checked) {
|
|
DisplaySettings ds = q->displaySettings();
|
|
ds.m_visualizeWhitespace = checked;
|
|
q->setDisplaySettings(ds);
|
|
})
|
|
.contextAction();
|
|
m_modifyingActions << ActionBuilder(this, CLEAN_WHITESPACE)
|
|
.setContext(m_editorContext)
|
|
.addOnTriggered([this] { q->cleanWhitespace(); })
|
|
.setScriptable(true)
|
|
.contextAction();
|
|
m_textWrappingAction = ActionBuilder(this, TEXT_WRAPPING)
|
|
.setContext(m_editorContext)
|
|
.setCheckable(true)
|
|
.addOnToggled(
|
|
this,
|
|
[this](bool checked) {
|
|
DisplaySettings ds = q->displaySettings();
|
|
ds.m_textWrapping = checked;
|
|
q->setDisplaySettings(ds);
|
|
})
|
|
.contextAction();
|
|
m_unCommentSelectionAction = ActionBuilder(this, UN_COMMENT_SELECTION)
|
|
.setContext(m_editorContext)
|
|
.addOnTriggered([this] { q->unCommentSelection(); })
|
|
.setScriptable(true)
|
|
.contextAction();
|
|
m_modifyingActions << ActionBuilder(this, CUT_LINE)
|
|
.setContext(m_editorContext)
|
|
.addOnTriggered([this] { q->cutLine(); })
|
|
.setScriptable(true)
|
|
.contextAction();
|
|
ActionBuilder(this, COPY_LINE)
|
|
.setContext(m_editorContext)
|
|
.addOnTriggered([this] { q->copyLine(); })
|
|
.setScriptable(true);
|
|
m_copyHtmlAction = ActionBuilder(this, COPY_WITH_HTML)
|
|
.setContext(m_editorContext)
|
|
.addOnTriggered([this] { q->copyWithHtml(); })
|
|
.setScriptable(true)
|
|
.contextAction();
|
|
ActionBuilder(this, ADD_CURSORS_TO_LINE_ENDS)
|
|
.setContext(m_editorContext)
|
|
.addOnTriggered([this] { q->addCursorsToLineEnds(); })
|
|
.setScriptable(true);
|
|
ActionBuilder(this, ADD_SELECT_NEXT_FIND_MATCH)
|
|
.setContext(m_editorContext)
|
|
.addOnTriggered([this] { q->addSelectionNextFindMatch(); })
|
|
.setScriptable(true);
|
|
m_modifyingActions << ActionBuilder(this, DUPLICATE_SELECTION)
|
|
.setContext(m_editorContext)
|
|
.addOnTriggered([this] { q->duplicateSelection(); })
|
|
.setScriptable(true)
|
|
.contextAction();
|
|
m_modifyingActions << ActionBuilder(this, DUPLICATE_SELECTION_AND_COMMENT)
|
|
.setContext(m_editorContext)
|
|
.addOnTriggered([this] { q->duplicateSelectionAndComment(); })
|
|
.setScriptable(true)
|
|
.contextAction();
|
|
m_modifyingActions << ActionBuilder(this, UPPERCASE_SELECTION)
|
|
.setContext(m_editorContext)
|
|
.addOnTriggered([this] { q->uppercaseSelection(); })
|
|
.setScriptable(true)
|
|
.contextAction();
|
|
m_modifyingActions << ActionBuilder(this, LOWERCASE_SELECTION)
|
|
.setContext(m_editorContext)
|
|
.addOnTriggered([this] { q->lowercaseSelection(); })
|
|
.setScriptable(true)
|
|
.contextAction();
|
|
m_modifyingActions << ActionBuilder(this, SORT_LINES)
|
|
.setContext(m_editorContext)
|
|
.addOnTriggered([this] { q->sortLines(); })
|
|
.setScriptable(true)
|
|
.contextAction();
|
|
ActionBuilder(this, FOLD)
|
|
.setContext(m_editorContext)
|
|
.addOnTriggered([this] { q->foldCurrentBlock(); })
|
|
.setScriptable(true);
|
|
ActionBuilder(this, UNFOLD)
|
|
.setContext(m_editorContext)
|
|
.addOnTriggered([this] { q->unfoldCurrentBlock(); })
|
|
.setScriptable(true);
|
|
ActionBuilder(this, FOLD_RECURSIVELY)
|
|
.setContext(m_editorContext)
|
|
.addOnTriggered([this] { q->fold(q->textCursor().block(), true); })
|
|
.setScriptable(true);
|
|
ActionBuilder(this, UNFOLD_RECURSIVELY)
|
|
.setContext(m_editorContext)
|
|
.addOnTriggered([this] { q->unfold(q->textCursor().block(), true); })
|
|
.setScriptable(true);
|
|
m_unfoldAllAction = ActionBuilder(this, UNFOLD_ALL)
|
|
.setContext(m_editorContext)
|
|
.addOnTriggered([this] { q->toggleFoldAll(); })
|
|
.setScriptable(true)
|
|
.contextAction();
|
|
ActionBuilder(this, INCREASE_FONT_SIZE).setContext(m_editorContext).addOnTriggered([this] {
|
|
q->increaseFontZoom();
|
|
});
|
|
ActionBuilder(this, DECREASE_FONT_SIZE).setContext(m_editorContext).addOnTriggered([this] {
|
|
q->decreaseFontZoom();
|
|
});
|
|
ActionBuilder(this, RESET_FONT_SIZE).setContext(m_editorContext).addOnTriggered([this] {
|
|
q->zoomReset();
|
|
});
|
|
ActionBuilder(this, GOTO_BLOCK_START)
|
|
.setContext(m_editorContext)
|
|
.addOnTriggered([this] { q->gotoBlockStart(); })
|
|
.setScriptable(true);
|
|
ActionBuilder(this, GOTO_BLOCK_END)
|
|
.setContext(m_editorContext)
|
|
.addOnTriggered([this] { q->gotoBlockEnd(); })
|
|
.setScriptable(true);
|
|
ActionBuilder(this, SELECT_BLOCK_UP)
|
|
.setContext(m_editorContext)
|
|
.addOnTriggered([this] { q->selectBlockUp(); })
|
|
.setScriptable(true);
|
|
ActionBuilder(this, SELECT_BLOCK_DOWN)
|
|
.setContext(m_editorContext)
|
|
.addOnTriggered([this] { q->selectBlockDown(); })
|
|
.setScriptable(true);
|
|
ActionBuilder(this, SELECT_WORD_UNDER_CURSOR)
|
|
.setContext(m_editorContext)
|
|
.addOnTriggered([this] { q->selectWordUnderCursor(); })
|
|
.setScriptable(true);
|
|
ActionBuilder(this, CLEAR_SELECTION)
|
|
.setContext(m_editorContext)
|
|
.addOnTriggered([this] { q->clearSelection(); })
|
|
.setScriptable(true);
|
|
|
|
ActionBuilder(this, GOTO_DOCUMENT_START)
|
|
.setContext(m_editorContext)
|
|
.addOnTriggered([this] { q->gotoDocumentStart(); })
|
|
.setScriptable(true);
|
|
ActionBuilder(this, GOTO_DOCUMENT_END)
|
|
.setContext(m_editorContext)
|
|
.addOnTriggered([this] { q->gotoDocumentEnd(); })
|
|
.setScriptable(true);
|
|
ActionBuilder(this, GOTO_LINE_START)
|
|
.setContext(m_editorContext)
|
|
.addOnTriggered([this] { q->gotoLineStart(); })
|
|
.setScriptable(true);
|
|
ActionBuilder(this, GOTO_LINE_END)
|
|
.setContext(m_editorContext)
|
|
.addOnTriggered([this] { q->gotoLineEnd(); })
|
|
.setScriptable(true);
|
|
ActionBuilder(this, GOTO_NEXT_LINE)
|
|
.setContext(m_editorContext)
|
|
.addOnTriggered([this] { q->gotoNextLine(); })
|
|
.setScriptable(true);
|
|
ActionBuilder(this, GOTO_PREVIOUS_LINE)
|
|
.setContext(m_editorContext)
|
|
.addOnTriggered([this] { q->gotoPreviousLine(); })
|
|
.setScriptable(true);
|
|
ActionBuilder(this, GOTO_PREVIOUS_CHARACTER)
|
|
.setContext(m_editorContext)
|
|
.addOnTriggered([this] { q->gotoPreviousCharacter(); })
|
|
.setScriptable(true);
|
|
ActionBuilder(this, GOTO_NEXT_CHARACTER)
|
|
.setContext(m_editorContext)
|
|
.addOnTriggered([this] { q->gotoNextCharacter(); })
|
|
.setScriptable(true);
|
|
ActionBuilder(this, GOTO_PREVIOUS_WORD)
|
|
.setContext(m_editorContext)
|
|
.addOnTriggered([this] { q->gotoPreviousWord(); })
|
|
.setScriptable(true);
|
|
ActionBuilder(this, GOTO_NEXT_WORD)
|
|
.setContext(m_editorContext)
|
|
.addOnTriggered([this] { q->gotoNextWord(); })
|
|
.setScriptable(true);
|
|
ActionBuilder(this, GOTO_PREVIOUS_WORD_CAMEL_CASE)
|
|
.setContext(m_editorContext)
|
|
.addOnTriggered([this] { q->gotoPreviousWordCamelCase(); })
|
|
.setScriptable(true);
|
|
ActionBuilder(this, GOTO_NEXT_WORD_CAMEL_CASE)
|
|
.setContext(m_editorContext)
|
|
.addOnTriggered([this] { q->gotoNextWordCamelCase(); })
|
|
.setScriptable(true);
|
|
|
|
ActionBuilder(this, GOTO_LINE_START_WITH_SELECTION)
|
|
.setContext(m_editorContext)
|
|
.addOnTriggered([this] { q->gotoLineStartWithSelection(); })
|
|
.setScriptable(true);
|
|
ActionBuilder(this, GOTO_LINE_END_WITH_SELECTION)
|
|
.setContext(m_editorContext)
|
|
.addOnTriggered([this] { q->gotoLineEndWithSelection(); })
|
|
.setScriptable(true);
|
|
ActionBuilder(this, GOTO_NEXT_LINE_WITH_SELECTION)
|
|
.setContext(m_editorContext)
|
|
.addOnTriggered([this] { q->gotoNextLineWithSelection(); })
|
|
.setScriptable(true);
|
|
ActionBuilder(this, GOTO_PREVIOUS_LINE_WITH_SELECTION)
|
|
.setContext(m_editorContext)
|
|
.addOnTriggered([this] { q->gotoPreviousLineWithSelection(); })
|
|
.setScriptable(true);
|
|
ActionBuilder(this, GOTO_PREVIOUS_CHARACTER_WITH_SELECTION)
|
|
.setContext(m_editorContext)
|
|
.addOnTriggered([this] { q->gotoPreviousCharacterWithSelection(); })
|
|
.setScriptable(true);
|
|
ActionBuilder(this, GOTO_NEXT_CHARACTER_WITH_SELECTION)
|
|
.setContext(m_editorContext)
|
|
.addOnTriggered([this] { q->gotoNextCharacterWithSelection(); })
|
|
.setScriptable(true);
|
|
ActionBuilder(this, GOTO_PREVIOUS_WORD_WITH_SELECTION)
|
|
.setContext(m_editorContext)
|
|
.addOnTriggered([this] { q->gotoPreviousWordWithSelection(); })
|
|
.setScriptable(true);
|
|
ActionBuilder(this, GOTO_NEXT_WORD_WITH_SELECTION)
|
|
.setContext(m_editorContext)
|
|
.addOnTriggered([this] { q->gotoNextWordWithSelection(); })
|
|
.setScriptable(true);
|
|
ActionBuilder(this, GOTO_PREVIOUS_WORD_CAMEL_CASE_WITH_SELECTION)
|
|
.setContext(m_editorContext)
|
|
.addOnTriggered([this] { q->gotoPreviousWordCamelCaseWithSelection(); })
|
|
.setScriptable(true);
|
|
ActionBuilder(this, GOTO_NEXT_WORD_CAMEL_CASE_WITH_SELECTION)
|
|
.setContext(m_editorContext)
|
|
.addOnTriggered([this] { q->gotoNextWordCamelCaseWithSelection(); })
|
|
.setScriptable(true);
|
|
|
|
// Collect additional modifying actions so we can check for them inside a readonly file
|
|
// and disable them
|
|
m_modifyingActions << m_autoIndentAction;
|
|
m_modifyingActions << m_autoFormatAction;
|
|
m_modifyingActions << m_unCommentSelectionAction;
|
|
|
|
updateOptionalActions();
|
|
}
|
|
|
|
void TextEditorWidgetPrivate::updateActions()
|
|
{
|
|
bool isWritable = !q->isReadOnly();
|
|
for (QAction *a : std::as_const(m_modifyingActions))
|
|
a->setEnabled(isWritable);
|
|
m_unCommentSelectionAction->setEnabled((m_optionalActionMask & OptionalActions::UnCommentSelection) && isWritable);
|
|
m_visualizeWhitespaceAction->setEnabled(q);
|
|
if (TextEditorSettings::fontSettings().relativeLineSpacing() == 100) {
|
|
m_textWrappingAction->setEnabled(q);
|
|
} else {
|
|
m_textWrappingAction->setEnabled(false);
|
|
m_textWrappingAction->setChecked(false);
|
|
}
|
|
m_visualizeWhitespaceAction->setChecked(m_displaySettings.m_visualizeWhitespace);
|
|
m_textWrappingAction->setChecked(m_displaySettings.m_textWrapping);
|
|
|
|
updateRedoAction();
|
|
updateUndoAction();
|
|
updateCopyAction(q->textCursor().hasSelection());
|
|
|
|
updateOptionalActions();
|
|
}
|
|
|
|
void TextEditorWidgetPrivate::updateOptionalActions()
|
|
{
|
|
using namespace OptionalActions;
|
|
m_followSymbolAction->setEnabled(m_optionalActionMask & FollowSymbolUnderCursor);
|
|
m_followSymbolInNextSplitAction->setEnabled(m_optionalActionMask & FollowSymbolUnderCursor);
|
|
m_followToTypeAction->setEnabled(m_optionalActionMask & FollowTypeUnderCursor);
|
|
m_followToTypeInNextSplitAction->setEnabled(m_optionalActionMask & FollowTypeUnderCursor);
|
|
m_findUsageAction->setEnabled(m_optionalActionMask & FindUsage);
|
|
m_jumpToFileAction->setEnabled(m_optionalActionMask & JumpToFileUnderCursor);
|
|
m_jumpToFileInNextSplitAction->setEnabled(m_optionalActionMask & JumpToFileUnderCursor);
|
|
m_unfoldAllAction->setEnabled(m_optionalActionMask & UnCollapseAll);
|
|
m_renameSymbolAction->setEnabled(m_optionalActionMask & RenameSymbol);
|
|
m_openCallHierarchyAction->setEnabled(m_optionalActionMask & CallHierarchy);
|
|
m_openTypeHierarchyAction->setEnabled(m_optionalActionMask & TypeHierarchy);
|
|
|
|
bool formatEnabled = (m_optionalActionMask & OptionalActions::Format)
|
|
&& !q->isReadOnly();
|
|
m_autoIndentAction->setEnabled(formatEnabled);
|
|
m_autoFormatAction->setEnabled(formatEnabled);
|
|
}
|
|
|
|
void TextEditorWidgetPrivate::updateRedoAction()
|
|
{
|
|
m_redoAction->setEnabled(q->isRedoAvailable());
|
|
}
|
|
|
|
void TextEditorWidgetPrivate::updateUndoAction()
|
|
{
|
|
m_undoAction->setEnabled(q->isUndoAvailable());
|
|
}
|
|
|
|
void TextEditorWidgetPrivate::updateCopyAction(bool hasCopyableText)
|
|
{
|
|
if (m_cutAction)
|
|
m_cutAction->setEnabled(hasCopyableText && !q->isReadOnly());
|
|
if (m_copyAction)
|
|
m_copyAction->setEnabled(hasCopyableText);
|
|
if (m_copyHtmlAction)
|
|
m_copyHtmlAction->setEnabled(hasCopyableText);
|
|
}
|
|
|
|
bool TextEditorWidget::codeFoldingVisible() const
|
|
{
|
|
return d->m_codeFoldingVisible;
|
|
}
|
|
|
|
/**
|
|
* Sets whether code folding is supported by the syntax highlighter. When not
|
|
* supported (the default), this makes sure the code folding is not shown.
|
|
*
|
|
* Needs to be called before calling setCodeFoldingVisible.
|
|
*/
|
|
void TextEditorWidget::setCodeFoldingSupported(bool b)
|
|
{
|
|
d->m_codeFoldingSupported = b;
|
|
d->updateCodeFoldingVisible();
|
|
}
|
|
|
|
bool TextEditorWidget::codeFoldingSupported() const
|
|
{
|
|
return d->m_codeFoldingSupported;
|
|
}
|
|
|
|
void TextEditorWidget::setMouseNavigationEnabled(bool b)
|
|
{
|
|
d->m_behaviorSettings.m_mouseNavigation = b;
|
|
}
|
|
|
|
bool TextEditorWidget::mouseNavigationEnabled() const
|
|
{
|
|
return d->m_behaviorSettings.m_mouseNavigation;
|
|
}
|
|
|
|
void TextEditorWidget::setMouseHidingEnabled(bool b)
|
|
{
|
|
d->m_behaviorSettings.m_mouseHiding = b;
|
|
}
|
|
|
|
bool TextEditorWidget::mouseHidingEnabled() const
|
|
{
|
|
return Utils::HostOsInfo::isMacHost() ? false : d->m_behaviorSettings.m_mouseHiding;
|
|
}
|
|
|
|
void TextEditorWidget::setScrollWheelZoomingEnabled(bool b)
|
|
{
|
|
d->m_behaviorSettings.m_scrollWheelZooming = b;
|
|
}
|
|
|
|
bool TextEditorWidget::scrollWheelZoomingEnabled() const
|
|
{
|
|
return d->m_behaviorSettings.m_scrollWheelZooming;
|
|
}
|
|
|
|
void TextEditorWidget::setConstrainTooltips(bool b)
|
|
{
|
|
d->m_behaviorSettings.m_constrainHoverTooltips = b;
|
|
}
|
|
|
|
bool TextEditorWidget::constrainTooltips() const
|
|
{
|
|
return d->m_behaviorSettings.m_constrainHoverTooltips;
|
|
}
|
|
|
|
void TextEditorWidget::setCamelCaseNavigationEnabled(bool b)
|
|
{
|
|
d->m_behaviorSettings.m_camelCaseNavigation = b;
|
|
}
|
|
|
|
bool TextEditorWidget::camelCaseNavigationEnabled() const
|
|
{
|
|
return d->m_behaviorSettings.m_camelCaseNavigation;
|
|
}
|
|
|
|
void TextEditorWidget::setRevisionsVisible(bool b)
|
|
{
|
|
d->m_revisionsVisible = b;
|
|
d->slotUpdateExtraAreaWidth();
|
|
}
|
|
|
|
bool TextEditorWidget::revisionsVisible() const
|
|
{
|
|
return d->m_revisionsVisible;
|
|
}
|
|
|
|
void TextEditorWidget::setVisibleWrapColumn(int column)
|
|
{
|
|
d->m_visibleWrapColumn = column;
|
|
viewport()->update();
|
|
}
|
|
|
|
int TextEditorWidget::visibleWrapColumn() const
|
|
{
|
|
return d->m_visibleWrapColumn;
|
|
}
|
|
|
|
void TextEditorWidget::setAutoCompleter(AutoCompleter *autoCompleter)
|
|
{
|
|
d->m_autoCompleter.reset(autoCompleter);
|
|
}
|
|
|
|
AutoCompleter *TextEditorWidget::autoCompleter() const
|
|
{
|
|
return d->m_autoCompleter.data();
|
|
}
|
|
|
|
//
|
|
// TextEditorWidgetPrivate
|
|
//
|
|
|
|
bool TextEditorWidgetPrivate::snippetCheckCursor(const QTextCursor &cursor)
|
|
{
|
|
if (!m_snippetOverlay->isVisible() || m_snippetOverlay->isEmpty())
|
|
return false;
|
|
|
|
QTextCursor start = cursor;
|
|
start.setPosition(cursor.selectionStart());
|
|
QTextCursor end = cursor;
|
|
end.setPosition(cursor.selectionEnd());
|
|
if (!m_snippetOverlay->hasCursorInSelection(start)
|
|
|| !m_snippetOverlay->hasCursorInSelection(end)
|
|
|| m_snippetOverlay->hasFirstSelectionBeginMoved()) {
|
|
m_snippetOverlay->accept();
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void TextEditorWidgetPrivate::snippetTabOrBacktab(bool forward)
|
|
{
|
|
if (!m_snippetOverlay->isVisible() || m_snippetOverlay->isEmpty())
|
|
return;
|
|
QTextCursor cursor = forward ? m_snippetOverlay->nextSelectionCursor(q->textCursor())
|
|
: m_snippetOverlay->previousSelectionCursor(q->textCursor());
|
|
q->setTextCursor(cursor);
|
|
if (m_snippetOverlay->isFinalSelection(cursor))
|
|
m_snippetOverlay->accept();
|
|
}
|
|
|
|
// Calculate global position for a tooltip considering the left extra area.
|
|
QPoint TextEditorWidget::toolTipPosition(const QTextCursor &c) const
|
|
{
|
|
const QPoint cursorPos = mapToGlobal(cursorRect(c).bottomRight() + QPoint(1,1));
|
|
return cursorPos + QPoint(d->m_extraArea->width(), HostOsInfo::isWindowsHost() ? -24 : -16);
|
|
}
|
|
|
|
void TextEditorWidget::showTextMarksToolTip(const QPoint &pos,
|
|
const TextMarks &marks,
|
|
const TextMark *mainTextMark) const
|
|
{
|
|
d->showTextMarksToolTip(pos, marks, mainTextMark);
|
|
}
|
|
|
|
void TextEditorWidgetPrivate::processTooltipRequest(const QTextCursor &c)
|
|
{
|
|
const QPoint toolTipPoint = q->toolTipPosition(c);
|
|
bool handled = false;
|
|
emit q->tooltipOverrideRequested(q, toolTipPoint, c.position(), &handled);
|
|
if (handled)
|
|
return;
|
|
|
|
const auto callback = [toolTipPoint](TextEditorWidget *widget, BaseHoverHandler *handler, int) {
|
|
handler->showToolTip(widget, toolTipPoint);
|
|
};
|
|
const auto fallback = [toolTipPoint, position = c.position()](TextEditorWidget *widget) {
|
|
emit widget->tooltipRequested(toolTipPoint, position);
|
|
};
|
|
m_hoverHandlerRunner.startChecking(c, callback, fallback);
|
|
}
|
|
|
|
bool TextEditorWidgetPrivate::processAnnotaionTooltipRequest(const QTextBlock &block,
|
|
const QPoint &pos) const
|
|
{
|
|
TextBlockUserData *blockUserData = TextDocumentLayout::textUserData(block);
|
|
if (!blockUserData)
|
|
return false;
|
|
|
|
for (const AnnotationRect &annotationRect : m_annotationRects[block.blockNumber()]) {
|
|
if (!annotationRect.rect.contains(pos))
|
|
continue;
|
|
showTextMarksToolTip(q->mapToGlobal(pos), blockUserData->marks(), annotationRect.mark);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool TextEditorWidget::viewportEvent(QEvent *event)
|
|
{
|
|
d->m_contentsChanged = false;
|
|
if (event->type() == QEvent::ToolTip) {
|
|
if (QApplication::keyboardModifiers() & Qt::ControlModifier
|
|
|| (!(QApplication::keyboardModifiers() & Qt::ShiftModifier)
|
|
&& d->m_behaviorSettings.m_constrainHoverTooltips)) {
|
|
// Tooltips should be eaten when either control is pressed (so they don't get in the
|
|
// way of code navigation) or if they are in constrained mode and shift is not pressed.
|
|
return true;
|
|
}
|
|
const QHelpEvent *he = static_cast<QHelpEvent*>(event);
|
|
const QPoint &pos = he->pos();
|
|
|
|
RefactorMarker refactorMarker = d->m_refactorOverlay->markerAt(pos);
|
|
if (refactorMarker.isValid() && !refactorMarker.tooltip.isEmpty()) {
|
|
ToolTip::show(he->globalPos(), refactorMarker.tooltip,
|
|
viewport(), {}, refactorMarker.rect);
|
|
return true;
|
|
}
|
|
|
|
QTextCursor tc = cursorForPosition(pos);
|
|
QTextBlock block = tc.block();
|
|
QTextLine line = block.layout()->lineForTextPosition(tc.positionInBlock());
|
|
QTC_CHECK(line.isValid());
|
|
// Only handle tool tip for text cursor if mouse is within the block for the text cursor,
|
|
// and not if the mouse is e.g. in the empty space behind a short line.
|
|
if (line.isValid()) {
|
|
const QRectF blockGeometry = blockBoundingGeometry(block);
|
|
const int width = block == d->m_suggestionBlock ? blockGeometry.width()
|
|
: line.naturalTextRect().right();
|
|
if (pos.x() <= blockGeometry.left() + width) {
|
|
d->processTooltipRequest(tc);
|
|
return true;
|
|
} else if (d->processAnnotaionTooltipRequest(block, pos)) {
|
|
return true;
|
|
}
|
|
ToolTip::hide();
|
|
}
|
|
}
|
|
return QPlainTextEdit::viewportEvent(event);
|
|
}
|
|
|
|
|
|
void TextEditorWidget::resizeEvent(QResizeEvent *e)
|
|
{
|
|
QPlainTextEdit::resizeEvent(e);
|
|
QRect cr = rect();
|
|
d->m_extraArea->setGeometry(
|
|
QStyle::visualRect(layoutDirection(), cr,
|
|
QRect(cr.left() + frameWidth(), cr.top() + frameWidth(),
|
|
extraAreaWidth(), cr.height() - 2 * frameWidth())));
|
|
d->adjustScrollBarRanges();
|
|
d->updateCurrentLineInScrollbar();
|
|
emit resized();
|
|
}
|
|
|
|
QRect TextEditorWidgetPrivate::foldBox()
|
|
{
|
|
if (m_highlightBlocksInfo.isEmpty() || extraAreaHighlightFoldedBlockNumber < 0)
|
|
return {};
|
|
|
|
QTextBlock begin = q->document()->findBlockByNumber(m_highlightBlocksInfo.open.last());
|
|
|
|
QTextBlock end = q->document()->findBlockByNumber(m_highlightBlocksInfo.close.first());
|
|
if (!begin.isValid() || !end.isValid())
|
|
return {};
|
|
QRectF br = q->blockBoundingGeometry(begin).translated(q->contentOffset());
|
|
QRectF er = q->blockBoundingGeometry(end).translated(q->contentOffset());
|
|
|
|
|
|
if (TextEditorSettings::fontSettings().relativeLineSpacing() == 100) {
|
|
return QRect(m_extraArea->width() - foldBoxWidth(q->fontMetrics()),
|
|
int(br.top()),
|
|
foldBoxWidth(q->fontMetrics()),
|
|
int(er.bottom() - br.top()));
|
|
}
|
|
|
|
return QRect(m_extraArea->width() - foldBoxWidth(),
|
|
int(br.top()),
|
|
foldBoxWidth(),
|
|
int(er.bottom() - br.top()));
|
|
}
|
|
|
|
QTextBlock TextEditorWidgetPrivate::foldedBlockAt(const QPoint &pos, QRect *box) const
|
|
{
|
|
QPointF offset = q->contentOffset();
|
|
QTextBlock block = q->firstVisibleBlock();
|
|
qreal top = q->blockBoundingGeometry(block).translated(offset).top();
|
|
qreal bottom = top + q->blockBoundingRect(block).height();
|
|
|
|
int viewportHeight = q->viewport()->height();
|
|
|
|
while (block.isValid() && top <= viewportHeight) {
|
|
QTextBlock nextBlock = block.next();
|
|
if (block.isVisible() && bottom >= 0 && q->replacementVisible(block.blockNumber())) {
|
|
if (nextBlock.isValid() && !nextBlock.isVisible()) {
|
|
QTextLayout *layout = block.layout();
|
|
QTextLine line = layout->lineAt(layout->lineCount()-1);
|
|
QRectF lineRect = line.naturalTextRect().translated(offset.x(), top);
|
|
lineRect.adjust(0, 0, -1, -1);
|
|
|
|
QString replacement = QLatin1String(" {") + q->foldReplacementText(block)
|
|
+ QLatin1String("}; ");
|
|
|
|
QRectF collapseRect(lineRect.right() + 12,
|
|
lineRect.top(),
|
|
q->fontMetrics().horizontalAdvance(replacement),
|
|
lineRect.height());
|
|
if (collapseRect.contains(pos)) {
|
|
QTextBlock result = block;
|
|
if (box)
|
|
*box = collapseRect.toAlignedRect();
|
|
return result;
|
|
} else {
|
|
block = nextBlock;
|
|
while (nextBlock.isValid() && !nextBlock.isVisible()) {
|
|
block = nextBlock;
|
|
nextBlock = block.next();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
block = nextBlock;
|
|
top = bottom;
|
|
bottom = top + q->blockBoundingRect(block).height();
|
|
}
|
|
return QTextBlock();
|
|
}
|
|
|
|
void TextEditorWidgetPrivate::highlightSearchResults(const QTextBlock &block, const PaintEventData &data) const
|
|
{
|
|
if (m_searchExpr.pattern().isEmpty())
|
|
return;
|
|
|
|
int blockPosition = block.position();
|
|
|
|
QTextCursor cursor = q->textCursor();
|
|
QString text = block.text();
|
|
text.replace(QChar::Nbsp, QLatin1Char(' '));
|
|
int idx = -1;
|
|
int l = 0;
|
|
|
|
const int left = data.viewportRect.left() - int(data.offset.x());
|
|
const int right = data.viewportRect.right() - int(data.offset.x());
|
|
const int top = data.viewportRect.top() - int(data.offset.y());
|
|
const int bottom = data.viewportRect.bottom() - int(data.offset.y());
|
|
const QColor &searchResultColor = m_document->fontSettings()
|
|
.toTextCharFormat(C_SEARCH_RESULT).background().color().darker(120);
|
|
|
|
while (idx < text.length()) {
|
|
const QRegularExpressionMatch match = m_searchExpr.match(text, idx + l + 1);
|
|
if (!match.hasMatch())
|
|
break;
|
|
idx = match.capturedStart();
|
|
l = match.capturedLength();
|
|
if (l == 0)
|
|
break;
|
|
if (m_findFlags & FindWholeWords) {
|
|
auto posAtWordSeparator = [](const QString &text, int idx) {
|
|
if (idx < 0)
|
|
return QTC_GUARD(idx == -1);
|
|
int textLength = text.length();
|
|
if (idx >= textLength)
|
|
return QTC_GUARD(idx == textLength);
|
|
const QChar c = text.at(idx);
|
|
return !c.isLetterOrNumber() && c != QLatin1Char('_');
|
|
};
|
|
if (!posAtWordSeparator(text, idx - 1) || !posAtWordSeparator(text, idx + l))
|
|
continue;
|
|
}
|
|
|
|
|
|
const int start = blockPosition + idx;
|
|
const int end = start + l;
|
|
if (!m_find->inScope(start, end))
|
|
continue;
|
|
|
|
// check if the result is inside the visible area for long blocks
|
|
const QTextLine &startLine = block.layout()->lineForTextPosition(idx);
|
|
const QTextLine &endLine = block.layout()->lineForTextPosition(idx + l);
|
|
|
|
if (startLine.isValid() && endLine.isValid()
|
|
&& startLine.lineNumber() == endLine.lineNumber()) {
|
|
const int lineY = int(endLine.y() + q->blockBoundingGeometry(block).y());
|
|
if (startLine.cursorToX(idx) > right) { // result is behind the visible area
|
|
if (endLine.lineNumber() >= block.lineCount() - 1)
|
|
break; // this is the last line in the block, nothing more to add
|
|
|
|
// skip to the start of the next line
|
|
idx = block.layout()->lineAt(endLine.lineNumber() + 1).textStart();
|
|
l = 0;
|
|
continue;
|
|
} else if (endLine.cursorToX(idx + l, QTextLine::Trailing) < left) { // result is in front of the visible area skip it
|
|
continue;
|
|
} else if (lineY + endLine.height() < top) {
|
|
if (endLine.lineNumber() >= block.lineCount() - 1)
|
|
break; // this is the last line in the block, nothing more to add
|
|
// before visible area, skip to the start of the next line
|
|
idx = block.layout()->lineAt(endLine.lineNumber() + 1).textStart();
|
|
l = 0;
|
|
continue;
|
|
} else if (lineY > bottom) {
|
|
break; // under the visible area, nothing more to add
|
|
}
|
|
}
|
|
|
|
const uint flag = (idx == cursor.selectionStart() - blockPosition
|
|
&& idx + l == cursor.selectionEnd() - blockPosition) ?
|
|
TextEditorOverlay::DropShadow : 0;
|
|
m_searchResultOverlay->addOverlaySelection(start, end, searchResultColor, QColor(), flag);
|
|
}
|
|
}
|
|
|
|
void TextEditorWidgetPrivate::highlightSelection(const QTextBlock &block) const
|
|
{
|
|
if (!m_displaySettings.m_highlightSelection || m_cursors.hasMultipleCursors())
|
|
return;
|
|
const QString selection = m_cursors.selectedText();
|
|
if (selection.trimmed().isEmpty())
|
|
return;
|
|
|
|
const int blockPosition = block.position();
|
|
|
|
QString text = block.text();
|
|
text.replace(QChar::Nbsp, QLatin1Char(' '));
|
|
const int l = selection.length();
|
|
|
|
for (int idx = text.indexOf(selection, 0, Qt::CaseInsensitive);
|
|
idx >= 0;
|
|
idx = text.indexOf(selection, idx + 1, Qt::CaseInsensitive)) {
|
|
const int start = blockPosition + idx;
|
|
const int end = start + l;
|
|
if (!Utils::contains(m_selectionHighlightOverlay->selections(),
|
|
[&](const OverlaySelection &selection) {
|
|
return selection.m_cursor_begin.position() == start
|
|
&& selection.m_cursor_end.position() == end;
|
|
})) {
|
|
m_selectionHighlightOverlay->addOverlaySelection(start, end, {}, {});
|
|
}
|
|
}
|
|
}
|
|
|
|
void TextEditorWidgetPrivate::startCursorFlashTimer()
|
|
{
|
|
const int flashTime = QApplication::cursorFlashTime();
|
|
if (flashTime > 0) {
|
|
m_cursorFlashTimer.stop();
|
|
m_cursorFlashTimer.start(flashTime / 2, q);
|
|
}
|
|
if (!m_cursorVisible) {
|
|
m_cursorVisible = true;
|
|
q->viewport()->update(cursorUpdateRect(m_cursors));
|
|
}
|
|
}
|
|
|
|
void TextEditorWidgetPrivate::resetCursorFlashTimer()
|
|
{
|
|
if (!m_cursorFlashTimer.isActive())
|
|
return;
|
|
const int flashTime = QApplication::cursorFlashTime();
|
|
if (flashTime > 0) {
|
|
m_cursorFlashTimer.stop();
|
|
m_cursorFlashTimer.start(flashTime / 2, q);
|
|
}
|
|
if (!m_cursorVisible) {
|
|
m_cursorVisible = true;
|
|
q->viewport()->update(cursorUpdateRect(m_cursors));
|
|
}
|
|
}
|
|
|
|
void TextEditorWidgetPrivate::updateCursorSelections()
|
|
{
|
|
const QTextCharFormat selectionFormat = TextEditorSettings::fontSettings().toTextCharFormat(
|
|
C_SELECTION);
|
|
QList<QTextEdit::ExtraSelection> selections;
|
|
for (const QTextCursor &cursor : m_cursors) {
|
|
if (cursor.hasSelection())
|
|
selections << QTextEdit::ExtraSelection{cursor, selectionFormat};
|
|
}
|
|
q->setExtraSelections(TextEditorWidget::CursorSelection, selections);
|
|
|
|
m_selectionHighlightOverlay->clear();
|
|
|
|
if (m_selectionHighlightFuture.isRunning())
|
|
m_selectionHighlightFuture.cancel();
|
|
|
|
m_selectionResults.clear();
|
|
if (!m_highlightScrollBarController)
|
|
return;
|
|
m_highlightScrollBarController->removeHighlights(Constants::SCROLL_BAR_SELECTION);
|
|
|
|
if (!m_displaySettings.m_highlightSelection || m_cursors.hasMultipleCursors())
|
|
return;
|
|
|
|
const QString txt = m_cursors.selectedText();
|
|
if (txt.trimmed().isEmpty())
|
|
return;
|
|
|
|
m_selectionHighlightFuture = Utils::asyncRun(Utils::searchInContents,
|
|
txt,
|
|
FindFlags{},
|
|
m_document->filePath(),
|
|
m_document->plainText());
|
|
|
|
Utils::onResultReady(m_selectionHighlightFuture,
|
|
this,
|
|
[this](const SearchResultItems &resultList) {
|
|
QList<SearchResult> results;
|
|
for (const SearchResultItem &item : resultList) {
|
|
int start = item.mainRange().begin.positionInDocument(
|
|
m_document->document());
|
|
int end = item.mainRange().end.positionInDocument(
|
|
m_document->document());
|
|
results << SearchResult{start, end - start};
|
|
}
|
|
m_selectionResults = results;
|
|
addSelectionHighlightToScrollBar(results);
|
|
});
|
|
}
|
|
|
|
void TextEditorWidgetPrivate::moveCursor(QTextCursor::MoveOperation operation,
|
|
QTextCursor::MoveMode mode)
|
|
{
|
|
MultiTextCursor cursor = m_cursors;
|
|
cursor.movePosition(operation, mode);
|
|
q->setMultiTextCursor(cursor);
|
|
}
|
|
|
|
QRect TextEditorWidgetPrivate::cursorUpdateRect(const MultiTextCursor &cursor)
|
|
{
|
|
QRect result(0, 0, 0, 0);
|
|
for (const QTextCursor &c : cursor)
|
|
result |= q->cursorRect(c);
|
|
return result;
|
|
}
|
|
|
|
void TextEditorWidgetPrivate::moveCursorVisible(bool ensureVisible)
|
|
{
|
|
QTextCursor cursor = q->textCursor();
|
|
if (!cursor.block().isVisible()) {
|
|
cursor.setVisualNavigation(true);
|
|
cursor.movePosition(QTextCursor::Up);
|
|
q->setTextCursor(cursor);
|
|
}
|
|
if (ensureVisible)
|
|
q->ensureCursorVisible();
|
|
}
|
|
|
|
static QColor blendColors(const QColor &a, const QColor &b, int alpha)
|
|
{
|
|
return QColor((a.red() * (256 - alpha) + b.red() * alpha) / 256,
|
|
(a.green() * (256 - alpha) + b.green() * alpha) / 256,
|
|
(a.blue() * (256 - alpha) + b.blue() * alpha) / 256);
|
|
}
|
|
|
|
static QColor calcBlendColor(const QColor &baseColor, int level, int count)
|
|
{
|
|
QColor color80;
|
|
QColor color90;
|
|
|
|
if (baseColor.value() > 128) {
|
|
const int f90 = 15;
|
|
const int f80 = 30;
|
|
color80.setRgb(qMax(0, baseColor.red() - f80),
|
|
qMax(0, baseColor.green() - f80),
|
|
qMax(0, baseColor.blue() - f80));
|
|
color90.setRgb(qMax(0, baseColor.red() - f90),
|
|
qMax(0, baseColor.green() - f90),
|
|
qMax(0, baseColor.blue() - f90));
|
|
} else {
|
|
const int f90 = 20;
|
|
const int f80 = 40;
|
|
color80.setRgb(qMin(255, baseColor.red() + f80),
|
|
qMin(255, baseColor.green() + f80),
|
|
qMin(255, baseColor.blue() + f80));
|
|
color90.setRgb(qMin(255, baseColor.red() + f90),
|
|
qMin(255, baseColor.green() + f90),
|
|
qMin(255, baseColor.blue() + f90));
|
|
}
|
|
|
|
if (level == count)
|
|
return baseColor;
|
|
if (level == 0)
|
|
return color80;
|
|
if (level == count - 1)
|
|
return color90;
|
|
|
|
const int blendFactor = level * (256 / (count - 1));
|
|
|
|
return blendColors(color80, color90, blendFactor);
|
|
}
|
|
|
|
static QTextLayout::FormatRange createBlockCursorCharFormatRange(int pos,
|
|
const QColor &textColor,
|
|
const QColor &baseColor)
|
|
{
|
|
QTextLayout::FormatRange o;
|
|
o.start = pos;
|
|
o.length = 1;
|
|
o.format.setForeground(baseColor);
|
|
o.format.setBackground(textColor);
|
|
return o;
|
|
}
|
|
|
|
static TextMarks availableMarks(const TextMarks &marks,
|
|
QRectF &boundingRect,
|
|
const QFontMetrics &fm,
|
|
const qreal itemOffset)
|
|
{
|
|
TextMarks ret;
|
|
bool first = true;
|
|
for (TextMark *mark : marks) {
|
|
const TextMark::AnnotationRects &rects = mark->annotationRects(
|
|
boundingRect, fm, first ? 0 : itemOffset, 0);
|
|
if (rects.annotationRect.isEmpty())
|
|
break;
|
|
boundingRect.setLeft(rects.fadeOutRect.right());
|
|
ret.append(mark);
|
|
if (boundingRect.isEmpty())
|
|
break;
|
|
first = false;
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
QRectF TextEditorWidgetPrivate::getLastLineLineRect(const QTextBlock &block)
|
|
{
|
|
QTextLayout *layout = nullptr;
|
|
if (TextSuggestion *suggestion = TextDocumentLayout::suggestion(block))
|
|
layout = suggestion->replacementDocument()->firstBlock().layout();
|
|
else
|
|
layout = block.layout();
|
|
|
|
QTC_ASSERT(layout, layout = block.layout());
|
|
const int lineCount = layout->lineCount();
|
|
if (lineCount < 1)
|
|
return {};
|
|
const QTextLine line = layout->lineAt(lineCount - 1);
|
|
const QPointF contentOffset = q->contentOffset();
|
|
const qreal top = q->blockBoundingGeometry(block).translated(contentOffset).top();
|
|
return line.naturalTextRect().translated(contentOffset.x(), top).adjusted(0, 0, -1, -1);
|
|
}
|
|
|
|
bool TextEditorWidgetPrivate::updateAnnotationBounds(TextBlockUserData *blockUserData,
|
|
TextDocumentLayout *layout,
|
|
bool annotationsVisible)
|
|
{
|
|
const bool additionalHeightNeeded = annotationsVisible
|
|
&& m_displaySettings.m_annotationAlignment == AnnotationAlignment::BetweenLines;
|
|
|
|
int additionalHeight = 0;
|
|
if (additionalHeightNeeded) {
|
|
if (TextEditorSettings::fontSettings().relativeLineSpacing() == 100)
|
|
additionalHeight = q->fontMetrics().lineSpacing();
|
|
else
|
|
TextEditorSettings::fontSettings().lineSpacing();
|
|
}
|
|
|
|
if (blockUserData->additionalAnnotationHeight() == additionalHeight)
|
|
return false;
|
|
blockUserData->setAdditionalAnnotationHeight(additionalHeight);
|
|
q->viewport()->update();
|
|
layout->emitDocumentSizeChanged();
|
|
return true;
|
|
}
|
|
|
|
void TextEditorWidgetPrivate::updateLineAnnotation(const PaintEventData &data,
|
|
const PaintEventBlockData &blockData,
|
|
QPainter &painter)
|
|
{
|
|
const QList<AnnotationRect> previousRects = m_annotationRects.take(data.block.blockNumber());
|
|
|
|
if (!m_displaySettings.m_displayAnnotations)
|
|
return;
|
|
|
|
TextBlockUserData *blockUserData = TextDocumentLayout::textUserData(data.block);
|
|
if (!blockUserData)
|
|
return;
|
|
|
|
TextMarks marks = Utils::filtered(blockUserData->marks(), [](const TextMark *mark) {
|
|
return !mark->lineAnnotation().isEmpty() && mark->isVisible()
|
|
&& !TextDocument::marksAnnotationHidden(mark->category().id);
|
|
});
|
|
|
|
const bool annotationsVisible = !marks.isEmpty();
|
|
|
|
if (updateAnnotationBounds(blockUserData, data.documentLayout, annotationsVisible)
|
|
|| !annotationsVisible) {
|
|
return;
|
|
}
|
|
|
|
const QRectF lineRect = getLastLineLineRect(data.block);
|
|
if (lineRect.isNull())
|
|
return;
|
|
|
|
Utils::sort(marks, [](const TextMark* mark1, const TextMark* mark2){
|
|
return mark1->priority() > mark2->priority();
|
|
});
|
|
|
|
qreal itemOffset = 0.0;
|
|
if (TextEditorSettings::fontSettings().relativeLineSpacing() == 100)
|
|
itemOffset = q->fontMetrics().lineSpacing();
|
|
else
|
|
itemOffset = blockData.boundingRect.height();
|
|
|
|
const qreal initialOffset = m_displaySettings.m_annotationAlignment == AnnotationAlignment::BetweenLines ? itemOffset / 2 : itemOffset * 2;
|
|
const qreal minimalContentWidth = charWidth() * m_displaySettings.m_minimalAnnotationContent;
|
|
qreal offset = initialOffset;
|
|
qreal x = 0;
|
|
if (marks.isEmpty())
|
|
return;
|
|
QRectF boundingRect;
|
|
if (m_displaySettings.m_annotationAlignment == AnnotationAlignment::BetweenLines) {
|
|
boundingRect = QRectF(lineRect.bottomLeft(), blockData.boundingRect.bottomRight());
|
|
} else {
|
|
boundingRect = QRectF(lineRect.topLeft().x(), lineRect.topLeft().y(),
|
|
q->viewport()->width() - lineRect.right(), lineRect.height());
|
|
x = lineRect.right();
|
|
if (m_displaySettings.m_annotationAlignment == AnnotationAlignment::NextToMargin
|
|
&& data.rightMargin > lineRect.right() + offset
|
|
&& q->viewport()->width() > data.rightMargin + minimalContentWidth) {
|
|
offset = data.rightMargin - lineRect.right();
|
|
} else if (m_displaySettings.m_annotationAlignment != AnnotationAlignment::NextToContent) {
|
|
marks = availableMarks(marks, boundingRect, q->fontMetrics(), itemOffset);
|
|
if (boundingRect.width() > 0)
|
|
offset = qMax(boundingRect.width(), initialOffset);
|
|
}
|
|
}
|
|
|
|
QList<AnnotationRect> newRects;
|
|
for (const TextMark *mark : std::as_const(marks)) {
|
|
boundingRect = QRectF(x, boundingRect.top(), q->viewport()->width() - x, boundingRect.height());
|
|
if (boundingRect.isEmpty())
|
|
break;
|
|
|
|
mark->paintAnnotation(painter,
|
|
data.eventRect,
|
|
&boundingRect,
|
|
offset,
|
|
itemOffset / 2,
|
|
q->contentOffset());
|
|
|
|
x = boundingRect.right();
|
|
offset = itemOffset / 2;
|
|
newRects.append({boundingRect, mark});
|
|
}
|
|
|
|
if (previousRects != newRects) {
|
|
for (const AnnotationRect &annotationRect : std::as_const(newRects))
|
|
q->viewport()->update(annotationRect.rect.toAlignedRect());
|
|
for (const AnnotationRect &annotationRect : previousRects)
|
|
q->viewport()->update(annotationRect.rect.toAlignedRect());
|
|
}
|
|
m_annotationRects[data.block.blockNumber()] = newRects;
|
|
QTC_ASSERT(data.lineSpacing != 0, return);
|
|
const int maxVisibleLines = data.viewportRect.height() / data.lineSpacing;
|
|
if (m_annotationRects.size() >= maxVisibleLines * 2)
|
|
scheduleCleanupAnnotationCache();
|
|
}
|
|
|
|
QColor blendRightMarginColor(const FontSettings &settings, bool areaColor)
|
|
{
|
|
const QColor baseColor = settings.toTextCharFormat(C_TEXT).background().color();
|
|
const QColor col = (baseColor.value() > 128) ? Qt::black : Qt::white;
|
|
return blendColors(baseColor, col, areaColor ? 16 : 32);
|
|
}
|
|
|
|
void TextEditorWidgetPrivate::paintRightMarginArea(PaintEventData &data, QPainter &painter) const
|
|
{
|
|
if (m_visibleWrapColumn <= 0)
|
|
return;
|
|
// Don't use QFontMetricsF::averageCharWidth here, due to it returning
|
|
// a fractional size even when this is not supported by the platform.
|
|
data.rightMargin = charWidth() * (m_visibleWrapColumn + m_visualIndentOffset)
|
|
+ data.offset.x() + 4;
|
|
if (m_marginSettings.m_tintMarginArea && data.rightMargin < data.viewportRect.width()) {
|
|
const QRectF behindMargin(data.rightMargin,
|
|
data.eventRect.top(),
|
|
data.viewportRect.width() - data.rightMargin,
|
|
data.eventRect.height());
|
|
painter.fillRect(behindMargin, blendRightMarginColor(m_document->fontSettings(), true));
|
|
}
|
|
}
|
|
|
|
void TextEditorWidgetPrivate::paintRightMarginLine(const PaintEventData &data,
|
|
QPainter &painter) const
|
|
{
|
|
if (m_visibleWrapColumn <= 0 || data.rightMargin >= data.viewportRect.width())
|
|
return;
|
|
|
|
const QPen pen = painter.pen();
|
|
painter.setPen(blendRightMarginColor(m_document->fontSettings(), false));
|
|
painter.drawLine(QPointF(data.rightMargin, data.eventRect.top()),
|
|
QPointF(data.rightMargin, data.eventRect.bottom()));
|
|
painter.setPen(pen);
|
|
}
|
|
|
|
static QTextBlock nextVisibleBlock(const QTextBlock &block,
|
|
const QTextDocument *doc)
|
|
{
|
|
QTextBlock nextVisibleBlock = block.next();
|
|
if (!nextVisibleBlock.isVisible()) {
|
|
// invisible blocks do have zero line count
|
|
nextVisibleBlock = doc->findBlockByLineNumber(nextVisibleBlock.firstLineNumber());
|
|
// paranoia in case our code somewhere did not set the line count
|
|
// of the invisible block to 0
|
|
while (nextVisibleBlock.isValid() && !nextVisibleBlock.isVisible())
|
|
nextVisibleBlock = nextVisibleBlock.next();
|
|
}
|
|
return nextVisibleBlock;
|
|
}
|
|
|
|
void TextEditorWidgetPrivate::paintBlockHighlight(const PaintEventData &data,
|
|
QPainter &painter) const
|
|
{
|
|
if (m_highlightBlocksInfo.isEmpty())
|
|
return;
|
|
|
|
const QColor baseColor = m_document->fontSettings().toTextCharFormat(C_TEXT).background().color();
|
|
|
|
// extra pass for the block highlight
|
|
|
|
const int margin = 5;
|
|
QTextBlock block = data.block;
|
|
QPointF offset = data.offset;
|
|
while (block.isValid()) {
|
|
QRectF blockBoundingRect = q->blockBoundingRect(block).translated(offset);
|
|
|
|
int n = block.blockNumber();
|
|
int depth = 0;
|
|
const QList<int> open = m_highlightBlocksInfo.open;
|
|
for (const int i : open)
|
|
if (n >= i)
|
|
++depth;
|
|
const QList<int> close = m_highlightBlocksInfo.close;
|
|
for (const int i : close)
|
|
if (n > i)
|
|
--depth;
|
|
|
|
int count = m_highlightBlocksInfo.count();
|
|
if (count) {
|
|
for (int i = 0; i <= depth; ++i) {
|
|
const QColor &blendedColor = calcBlendColor(baseColor, i, count);
|
|
int vi = i > 0 ? m_highlightBlocksInfo.visualIndent.at(i - 1) : 0;
|
|
QRectF oneRect = blockBoundingRect;
|
|
oneRect.setWidth(qMax(data.viewportRect.width(), data.documentWidth));
|
|
oneRect.adjust(vi, 0, 0, 0);
|
|
if (oneRect.left() >= oneRect.right())
|
|
continue;
|
|
if (data.rightMargin > 0 && oneRect.left() < data.rightMargin
|
|
&& oneRect.right() > data.rightMargin) {
|
|
QRectF otherRect = blockBoundingRect;
|
|
otherRect.setLeft(data.rightMargin + 1);
|
|
otherRect.setRight(oneRect.right());
|
|
oneRect.setRight(data.rightMargin - 1);
|
|
painter.fillRect(otherRect, blendedColor);
|
|
}
|
|
painter.fillRect(oneRect, blendedColor);
|
|
}
|
|
}
|
|
offset.ry() += blockBoundingRect.height();
|
|
|
|
if (offset.y() > data.viewportRect.height() + margin)
|
|
break;
|
|
|
|
block = TextEditor::nextVisibleBlock(block, data.doc);
|
|
}
|
|
|
|
}
|
|
|
|
void TextEditorWidgetPrivate::paintSearchResultOverlay(const PaintEventData &data,
|
|
QPainter &painter) const
|
|
{
|
|
m_searchResultOverlay->clear();
|
|
if (m_searchExpr.pattern().isEmpty() || !m_searchExpr.isValid())
|
|
return;
|
|
|
|
const int margin = 5;
|
|
QTextBlock block = data.block;
|
|
QPointF offset = data.offset;
|
|
while (block.isValid()) {
|
|
QRectF blockBoundingRect = q->blockBoundingRect(block).translated(offset);
|
|
|
|
if (blockBoundingRect.bottom() >= data.eventRect.top() - margin
|
|
&& blockBoundingRect.top() <= data.eventRect.bottom() + margin) {
|
|
highlightSearchResults(block, data);
|
|
}
|
|
offset.ry() += blockBoundingRect.height();
|
|
|
|
if (offset.y() > data.viewportRect.height() + margin)
|
|
break;
|
|
|
|
block = TextEditor::nextVisibleBlock(block, data.doc);
|
|
}
|
|
|
|
m_searchResultOverlay->fill(&painter,
|
|
data.searchResultFormat.background().color(),
|
|
data.eventRect);
|
|
}
|
|
|
|
void TextEditorWidgetPrivate::paintSelectionOverlay(const PaintEventData &data,
|
|
QPainter &painter) const
|
|
{
|
|
if (m_cursors.hasMultipleCursors())
|
|
return;
|
|
const QString expr = m_cursors.selectedText();
|
|
if (expr.isEmpty())
|
|
return;
|
|
|
|
const int margin = 5;
|
|
QTextBlock block = data.block;
|
|
QPointF offset = data.offset;
|
|
while (block.isValid()) {
|
|
QRectF blockBoundingRect = q->blockBoundingRect(block).translated(offset);
|
|
|
|
if (blockBoundingRect.bottom() >= data.eventRect.top() - margin
|
|
&& blockBoundingRect.top() <= data.eventRect.bottom() + margin) {
|
|
highlightSelection(block);
|
|
}
|
|
offset.ry() += blockBoundingRect.height();
|
|
|
|
if (offset.y() > data.viewportRect.height() + margin)
|
|
break;
|
|
|
|
block = TextEditor::nextVisibleBlock(block, data.doc);
|
|
}
|
|
|
|
QColor selection = m_document->fontSettings().toTextCharFormat(C_SELECTION).background().color();
|
|
const QColor text = m_document->fontSettings().toTextCharFormat(C_TEXT).background().color();
|
|
selection.setAlphaF(StyleHelper::luminance(text) > 0.5 ? 0.25 : 0.5);
|
|
|
|
m_selectionHighlightOverlay->fill(&painter, selection, data.eventRect);
|
|
}
|
|
|
|
void TextEditorWidgetPrivate::paintIfDefedOutBlocks(const PaintEventData &data,
|
|
QPainter &painter) const
|
|
{
|
|
QTextBlock block = data.block;
|
|
QPointF offset = data.offset;
|
|
while (block.isValid()) {
|
|
|
|
QRectF r = q->blockBoundingRect(block).translated(offset);
|
|
|
|
if (r.bottom() >= data.eventRect.top() && r.top() <= data.eventRect.bottom()) {
|
|
if (TextDocumentLayout::ifdefedOut(block)) {
|
|
QRectF rr = r;
|
|
rr.setRight(data.viewportRect.width() - offset.x());
|
|
if (data.rightMargin > 0)
|
|
rr.setRight(qMin(data.rightMargin, rr.right()));
|
|
painter.fillRect(rr, data.ifdefedOutFormat.background());
|
|
}
|
|
}
|
|
offset.ry() += r.height();
|
|
|
|
if (offset.y() > data.viewportRect.height())
|
|
break;
|
|
|
|
block = TextEditor::nextVisibleBlock(block, data.doc);
|
|
}
|
|
}
|
|
|
|
void TextEditorWidgetPrivate::paintFindScope(const PaintEventData &data, QPainter &painter) const
|
|
{
|
|
if (m_findScope.isNull())
|
|
return;
|
|
auto overlay = new TextEditorOverlay(q);
|
|
for (const QTextCursor &c : m_findScope) {
|
|
overlay->addOverlaySelection(c.selectionStart(),
|
|
c.selectionEnd(),
|
|
data.searchScopeFormat.foreground().color(),
|
|
data.searchScopeFormat.background().color(),
|
|
TextEditorOverlay::ExpandBegin);
|
|
}
|
|
overlay->setAlpha(false);
|
|
overlay->paint(&painter, data.eventRect);
|
|
delete overlay;
|
|
}
|
|
|
|
void TextEditorWidgetPrivate::paintCurrentLineHighlight(const PaintEventData &data,
|
|
QPainter &painter) const
|
|
{
|
|
if (!m_highlightCurrentLine)
|
|
return;
|
|
|
|
QList<QTextCursor> cursorsForBlock;
|
|
for (const QTextCursor &c : m_cursors) {
|
|
if (c.block() == data.block)
|
|
cursorsForBlock << c;
|
|
}
|
|
if (cursorsForBlock.isEmpty())
|
|
return;
|
|
|
|
const QRectF blockRect = q->blockBoundingRect(data.block).translated(data.offset);
|
|
QColor color = m_document->fontSettings().toTextCharFormat(C_CURRENT_LINE).background().color();
|
|
color.setAlpha(128);
|
|
QSet<int> seenLines;
|
|
for (const QTextCursor &cursor : cursorsForBlock) {
|
|
QTextLine line = data.block.layout()->lineForTextPosition(cursor.positionInBlock());
|
|
if (!Utils::insert(seenLines, line.lineNumber()))
|
|
continue;
|
|
QRectF lineRect = line.rect();
|
|
lineRect.moveTop(lineRect.top() + blockRect.top());
|
|
lineRect.setLeft(0);
|
|
lineRect.setRight(data.viewportRect.width());
|
|
painter.fillRect(lineRect, color);
|
|
}
|
|
}
|
|
|
|
QRectF TextEditorWidgetPrivate::cursorBlockRect(const QTextDocument *doc,
|
|
const QTextBlock &block,
|
|
int cursorPosition,
|
|
QRectF blockBoundingRect,
|
|
bool *doSelection) const
|
|
{
|
|
const qreal space = charWidth();
|
|
int relativePos = cursorPosition - block.position();
|
|
qobject_cast<TextDocumentLayout *>(m_document->document()->documentLayout())
|
|
->ensureBlockLayout(block);
|
|
QTextLine line = block.layout()->lineForTextPosition(relativePos);
|
|
QTC_ASSERT(line.isValid(), return {});
|
|
qreal x = line.cursorToX(relativePos);
|
|
qreal w = 0;
|
|
if (relativePos < line.textLength() - line.textStart()) {
|
|
w = line.cursorToX(relativePos + 1) - x;
|
|
if (doc->characterAt(cursorPosition) == QLatin1Char('\t')) {
|
|
if (doSelection)
|
|
*doSelection = false;
|
|
if (w > space) {
|
|
x += w - space;
|
|
w = space;
|
|
}
|
|
}
|
|
} else {
|
|
w = space; // in sync with QTextLine::draw()
|
|
}
|
|
|
|
if (blockBoundingRect.isEmpty())
|
|
blockBoundingRect = q->blockBoundingGeometry(block).translated(q->contentOffset());
|
|
|
|
QRectF cursorRect = line.rect();
|
|
cursorRect.moveTop(cursorRect.top() + blockBoundingRect.top());
|
|
cursorRect.moveLeft(blockBoundingRect.left() + x);
|
|
cursorRect.setWidth(w);
|
|
return cursorRect;
|
|
}
|
|
|
|
void TextEditorWidgetPrivate::paintCursorAsBlock(const PaintEventData &data,
|
|
QPainter &painter,
|
|
PaintEventBlockData &blockData,
|
|
int cursorPosition) const
|
|
{
|
|
bool doSelection = true;
|
|
const QRectF cursorRect = cursorBlockRect(data.doc,
|
|
data.block,
|
|
cursorPosition,
|
|
blockData.boundingRect,
|
|
&doSelection);
|
|
const QTextCharFormat textFormat = data.fontSettings.toTextCharFormat(C_TEXT);
|
|
painter.fillRect(cursorRect, textFormat.foreground());
|
|
int relativePos = cursorPosition - blockData.position;
|
|
if (doSelection) {
|
|
blockData.selections.append(
|
|
createBlockCursorCharFormatRange(relativePos,
|
|
textFormat.foreground().color(),
|
|
textFormat.background().color()));
|
|
}
|
|
}
|
|
|
|
void TextEditorWidgetPrivate::paintAdditionalVisualWhitespaces(PaintEventData &data,
|
|
QPainter &painter,
|
|
qreal top) const
|
|
{
|
|
if (!m_displaySettings.m_visualizeWhitespace)
|
|
return;
|
|
|
|
QTextLayout *layout = data.block.layout();
|
|
const bool nextBlockIsValid = data.block.next().isValid();
|
|
int lineCount = layout->lineCount();
|
|
if (lineCount >= 2 || !nextBlockIsValid) {
|
|
painter.save();
|
|
painter.setPen(data.visualWhitespaceFormat.foreground().color());
|
|
for (int i = 0; i < lineCount-1; ++i) { // paint line wrap indicator
|
|
QTextLine line = layout->lineAt(i);
|
|
QRectF lineRect = line.naturalTextRect().translated(data.offset.x(), top);
|
|
QChar visualArrow(ushort(0x21b5));
|
|
painter.drawText(QPointF(lineRect.right(), lineRect.top() + line.ascent()),
|
|
visualArrow);
|
|
}
|
|
if (!nextBlockIsValid) { // paint EOF symbol
|
|
if (TextSuggestion *suggestion = TextDocumentLayout::suggestion(data.block)) {
|
|
const QTextBlock lastReplacementBlock
|
|
= suggestion->replacementDocument()->lastBlock();
|
|
for (QTextBlock block = suggestion->replacementDocument()->firstBlock();
|
|
block != lastReplacementBlock && block.isValid();
|
|
block = block.next()) {
|
|
top += suggestion->replacementDocument()
|
|
->documentLayout()
|
|
->blockBoundingRect(block)
|
|
.height();
|
|
}
|
|
layout = lastReplacementBlock.layout();
|
|
lineCount = layout->lineCount();
|
|
}
|
|
QTextLine line = layout->lineAt(lineCount - 1);
|
|
QRectF lineRect = line.naturalTextRect().translated(data.offset.x(), top);
|
|
int h = 4;
|
|
lineRect.adjust(0, 0, -1, -1);
|
|
QPainterPath path;
|
|
QPointF pos(lineRect.topRight() + QPointF(h + 4, line.ascent()));
|
|
path.moveTo(pos);
|
|
path.lineTo(pos + QPointF(-h, -h));
|
|
path.lineTo(pos + QPointF(0, -2 * h));
|
|
path.lineTo(pos + QPointF(h, -h));
|
|
path.closeSubpath();
|
|
painter.setBrush(painter.pen().color());
|
|
painter.drawPath(path);
|
|
}
|
|
painter.restore();
|
|
}
|
|
}
|
|
|
|
int TextEditorWidgetPrivate::indentDepthForBlock(const QTextBlock &block, const PaintEventData &data)
|
|
{
|
|
const auto blockDepth = [&](const QTextBlock &block) {
|
|
int depth = m_visualIndentCache.value(block.blockNumber(), -1);
|
|
if (depth < 0) {
|
|
const QString text = block.text().mid(m_visualIndentOffset);
|
|
depth = text.simplified().isEmpty() ? -1 : data.tabSettings.indentationColumn(text);
|
|
}
|
|
return depth;
|
|
};
|
|
const auto ensureCacheSize = [&](const int size) {
|
|
if (m_visualIndentCache.size() < size)
|
|
m_visualIndentCache.resize(size, -1);
|
|
};
|
|
int depth = blockDepth(block);
|
|
if (depth < 0) {
|
|
// find previous non empty block and get the indent depth of this block
|
|
QTextBlock it = block.previous();
|
|
int prevDepth = -1;
|
|
while (it.isValid()) {
|
|
prevDepth = blockDepth(it);
|
|
if (prevDepth >= 0)
|
|
break;
|
|
it = it.previous();
|
|
}
|
|
const int startBlockNumber = it.isValid() ? it.blockNumber() + 1 : 0;
|
|
|
|
// find next non empty block and get the indent depth of this block
|
|
it = block.next();
|
|
int nextDepth = -1;
|
|
while (it.isValid()) {
|
|
nextDepth = blockDepth(it);
|
|
if (nextDepth >= 0)
|
|
break;
|
|
it = it.next();
|
|
}
|
|
const int endBlockNumber = it.isValid() ? it.blockNumber() : m_blockCount;
|
|
|
|
// get the depth for the whole range of empty blocks and fill the cache so we do not need to
|
|
// redo this for every paint event
|
|
depth = prevDepth > 0 && nextDepth > 0 ? qMin(prevDepth, nextDepth) : 0;
|
|
ensureCacheSize(endBlockNumber);
|
|
for (int i = startBlockNumber; i < endBlockNumber; ++i)
|
|
m_visualIndentCache[i] = depth;
|
|
}
|
|
return depth;
|
|
}
|
|
|
|
void TextEditorWidgetPrivate::paintIndentDepth(PaintEventData &data,
|
|
QPainter &painter,
|
|
const PaintEventBlockData &blockData)
|
|
{
|
|
if (!m_displaySettings.m_visualizeIndent)
|
|
return;
|
|
|
|
const int depth = indentDepthForBlock(data.block, data);
|
|
if (depth <= 0 || blockData.layout->lineCount() < 1)
|
|
return;
|
|
|
|
const qreal singleAdvance = charWidth();
|
|
const qreal indentAdvance = singleAdvance * data.tabSettings.m_indentSize;
|
|
|
|
painter.save();
|
|
|
|
const QTextLine textLine = blockData.layout->lineAt(0);
|
|
const QRectF rect = textLine.naturalTextRect();
|
|
qreal x = textLine.x() + data.offset.x() + qMax(0, q->cursorWidth() - 1)
|
|
+ singleAdvance * m_visualIndentOffset;
|
|
int paintColumn = 0;
|
|
|
|
QList<int> cursorPositions;
|
|
for (const QTextCursor & cursor : m_cursors) {
|
|
if (cursor.block() == data.block)
|
|
cursorPositions << cursor.positionInBlock();
|
|
}
|
|
|
|
const QColor normalColor = data.visualWhitespaceFormat.foreground().color();
|
|
QColor cursorColor = normalColor;
|
|
cursorColor.setAlpha(cursorColor.alpha() / 2);
|
|
|
|
const QString text = data.block.text().mid(m_visualIndentOffset);
|
|
while (paintColumn < depth) {
|
|
if (x >= 0) {
|
|
int paintPosition = data.tabSettings.positionAtColumn(text, paintColumn);
|
|
if (q->lineWrapMode() == QPlainTextEdit::WidgetWidth
|
|
&& blockData.layout->lineForTextPosition(paintPosition).lineNumber() != 0) {
|
|
break;
|
|
}
|
|
if (cursorPositions.contains(paintPosition))
|
|
painter.setPen(cursorColor);
|
|
else
|
|
painter.setPen(normalColor);
|
|
const QPointF top(x, blockData.boundingRect.top());
|
|
const QPointF bottom(x, blockData.boundingRect.top() + rect.height());
|
|
const QLineF line(top, bottom);
|
|
painter.drawLine(line);
|
|
}
|
|
x += indentAdvance;
|
|
paintColumn += data.tabSettings.m_indentSize;
|
|
}
|
|
painter.restore();
|
|
}
|
|
|
|
void TextEditorWidgetPrivate::paintReplacement(PaintEventData &data, QPainter &painter,
|
|
qreal top) const
|
|
{
|
|
QTextBlock nextBlock = data.block.next();
|
|
|
|
if (nextBlock.isValid() && !nextBlock.isVisible() && q->replacementVisible(data.block.blockNumber())) {
|
|
const bool selectThis = (data.textCursor.hasSelection()
|
|
&& nextBlock.position() >= data.textCursor.selectionStart()
|
|
&& nextBlock.position() < data.textCursor.selectionEnd());
|
|
|
|
|
|
const QTextCharFormat selectionFormat = data.fontSettings.toTextCharFormat(C_SELECTION);
|
|
|
|
painter.save();
|
|
if (selectThis) {
|
|
painter.setBrush(selectionFormat.background().style() != Qt::NoBrush
|
|
? selectionFormat.background()
|
|
: QApplication::palette().brush(QPalette::Highlight));
|
|
} else {
|
|
QColor rc = q->replacementPenColor(data.block.blockNumber());
|
|
if (rc.isValid())
|
|
painter.setPen(rc);
|
|
}
|
|
|
|
QTextLayout *layout = data.block.layout();
|
|
QTextLine line = layout->lineAt(layout->lineCount()-1);
|
|
QRectF lineRect = line.naturalTextRect().translated(data.offset.x(), top);
|
|
lineRect.adjust(0, 0, -1, -1);
|
|
|
|
QString replacement = q->foldReplacementText(data.block);
|
|
QString rectReplacement = QLatin1String(" {") + replacement + QLatin1String("}; ");
|
|
|
|
QRectF collapseRect(lineRect.right() + 12,
|
|
lineRect.top(),
|
|
q->fontMetrics().horizontalAdvance(rectReplacement),
|
|
lineRect.height());
|
|
painter.setRenderHint(QPainter::Antialiasing, true);
|
|
painter.translate(.5, .5);
|
|
painter.drawRoundedRect(collapseRect.adjusted(0, 0, 0, -1), 3, 3);
|
|
painter.setRenderHint(QPainter::Antialiasing, false);
|
|
painter.translate(-.5, -.5);
|
|
|
|
if (TextBlockUserData *nextBlockUserData = TextDocumentLayout::textUserData(nextBlock)) {
|
|
if (nextBlockUserData->foldingStartIncluded())
|
|
replacement.prepend(nextBlock.text().trimmed().at(0));
|
|
}
|
|
|
|
QTextBlock lastInvisibleBlock = TextEditor::nextVisibleBlock(data.block, data.doc).previous();
|
|
if (!lastInvisibleBlock.isValid())
|
|
lastInvisibleBlock = data.doc->lastBlock();
|
|
|
|
if (TextBlockUserData *blockUserData = TextDocumentLayout::textUserData(lastInvisibleBlock)) {
|
|
if (blockUserData->foldingEndIncluded()) {
|
|
QString right = lastInvisibleBlock.text().trimmed();
|
|
if (right.endsWith(QLatin1Char(';'))) {
|
|
right.chop(1);
|
|
right = right.trimmed();
|
|
replacement.append(right.right(right.endsWith('/') ? 2 : 1));
|
|
replacement.append(QLatin1Char(';'));
|
|
} else {
|
|
replacement.append(right.right(right.endsWith('/') ? 2 : 1));
|
|
}
|
|
}
|
|
}
|
|
|
|
if (selectThis)
|
|
painter.setPen(selectionFormat.foreground().color());
|
|
painter.drawText(collapseRect, Qt::AlignCenter, replacement);
|
|
painter.restore();
|
|
}
|
|
}
|
|
|
|
void TextEditorWidgetPrivate::paintWidgetBackground(const PaintEventData &data,
|
|
QPainter &painter) const
|
|
{
|
|
painter.fillRect(data.eventRect, data.fontSettings.toTextCharFormat(C_TEXT).background());
|
|
}
|
|
|
|
void TextEditorWidgetPrivate::paintOverlays(const PaintEventData &data, QPainter &painter) const
|
|
{
|
|
// draw the overlays, but only if we do not have a find scope, otherwise the
|
|
// view becomes too noisy.
|
|
if (m_findScope.isNull()) {
|
|
if (m_overlay->isVisible())
|
|
m_overlay->paint(&painter, data.eventRect);
|
|
|
|
if (m_snippetOverlay->isVisible())
|
|
m_snippetOverlay->paint(&painter, data.eventRect);
|
|
|
|
if (!m_refactorOverlay->isEmpty())
|
|
m_refactorOverlay->paint(&painter, data.eventRect);
|
|
}
|
|
|
|
if (!m_searchResultOverlay->isEmpty()) {
|
|
m_searchResultOverlay->paint(&painter, data.eventRect);
|
|
m_searchResultOverlay->clear();
|
|
}
|
|
}
|
|
|
|
void TextEditorWidgetPrivate::paintCursor(const PaintEventData &data, QPainter &painter) const
|
|
{
|
|
for (const CursorData &cursor : data.cursors) {
|
|
painter.setPen(cursor.pen);
|
|
cursor.layout->drawCursor(&painter, cursor.offset, cursor.pos, q->cursorWidth());
|
|
}
|
|
}
|
|
|
|
void TextEditorWidgetPrivate::setupBlockLayout(const PaintEventData &data,
|
|
QPainter &painter,
|
|
PaintEventBlockData &blockData) const
|
|
{
|
|
blockData.layout = data.block.layout();
|
|
|
|
QTextOption option = blockData.layout->textOption();
|
|
if (data.suppressSyntaxInIfdefedOutBlock
|
|
&& TextDocumentLayout::ifdefedOut(data.block)) {
|
|
option.setFlags(option.flags() | QTextOption::SuppressColors);
|
|
painter.setPen(data.ifdefedOutFormat.foreground().color());
|
|
} else {
|
|
option.setFlags(option.flags() & ~QTextOption::SuppressColors);
|
|
painter.setPen(data.context.palette.text().color());
|
|
}
|
|
blockData.layout->setTextOption(option);
|
|
blockData.layout->setFont(data.doc->defaultFont());
|
|
}
|
|
|
|
void TextEditorWidgetPrivate::setupSelections(const PaintEventData &data,
|
|
PaintEventBlockData &blockData) const
|
|
{
|
|
QVector<QTextLayout::FormatRange> prioritySelections;
|
|
|
|
int deltaPos = -1;
|
|
int delta = 0;
|
|
|
|
if (TextSuggestion *suggestion = TextDocumentLayout::suggestion(data.block)) {
|
|
deltaPos = suggestion->currentPosition() - data.block.position();
|
|
const QString trailingText = data.block.text().mid(deltaPos);
|
|
if (!trailingText.isEmpty()) {
|
|
const int trailingIndex = suggestion->replacementDocument()
|
|
->firstBlock()
|
|
.text()
|
|
.indexOf(trailingText, deltaPos);
|
|
if (trailingIndex >= 0)
|
|
delta = std::max(trailingIndex - deltaPos, 0);
|
|
}
|
|
}
|
|
|
|
for (int i = 0; i < data.context.selections.size(); ++i) {
|
|
const QAbstractTextDocumentLayout::Selection &range = data.context.selections.at(i);
|
|
const int selStart = range.cursor.selectionStart() - blockData.position;
|
|
const int selEnd = range.cursor.selectionEnd() - blockData.position;
|
|
if (selStart < blockData.length && selEnd >= 0
|
|
&& selEnd >= selStart) {
|
|
QTextLayout::FormatRange o;
|
|
o.start = selStart;
|
|
o.length = selEnd - selStart;
|
|
o.format = range.format;
|
|
QTextLayout::FormatRange rest;
|
|
rest.start = -1;
|
|
if (deltaPos >= 0 && delta != 0) {
|
|
if (o.start >= deltaPos) {
|
|
o.start += delta;
|
|
} else if (o.start + o.length > deltaPos) {
|
|
// the format range starts before and ends after the position so we need to
|
|
// split the format into before and after the suggestion format ranges
|
|
rest.start = deltaPos + delta;
|
|
rest.length = o.length - (deltaPos - o.start);
|
|
rest.format = o.format;
|
|
o.length = deltaPos - o.start;
|
|
}
|
|
}
|
|
|
|
o.format = range.format;
|
|
if (data.textCursor.hasSelection() && data.textCursor == range.cursor
|
|
&& data.textCursor.anchor() == range.cursor.anchor()) {
|
|
const QTextCharFormat selectionFormat = data.fontSettings.toTextCharFormat(C_SELECTION);
|
|
if (selectionFormat.background().style() != Qt::NoBrush)
|
|
o.format.setBackground(selectionFormat.background());
|
|
o.format.setForeground(selectionFormat.foreground());
|
|
}
|
|
if ((data.textCursor.hasSelection() && i == data.context.selections.size() - 1)
|
|
|| (o.format.foreground().style() == Qt::NoBrush
|
|
&& o.format.underlineStyle() != QTextCharFormat::NoUnderline
|
|
&& o.format.background() == Qt::NoBrush)) {
|
|
if (q->selectionVisible(data.block.blockNumber())) {
|
|
prioritySelections.append(o);
|
|
if (rest.start >= 0)
|
|
prioritySelections.append(rest);
|
|
}
|
|
} else {
|
|
blockData.selections.append(o);
|
|
if (rest.start >= 0)
|
|
blockData.selections.append(rest);
|
|
}
|
|
}
|
|
}
|
|
blockData.selections.append(prioritySelections);
|
|
}
|
|
|
|
static CursorData generateCursorData(const int cursorPos,
|
|
const PaintEventData &data,
|
|
const PaintEventBlockData &blockData,
|
|
QPainter &painter)
|
|
{
|
|
CursorData cursorData;
|
|
cursorData.layout = blockData.layout;
|
|
cursorData.offset = data.offset;
|
|
cursorData.pos = cursorPos;
|
|
cursorData.pen = painter.pen();
|
|
return cursorData;
|
|
}
|
|
|
|
static bool blockContainsCursor(const PaintEventBlockData &blockData, const QTextCursor &cursor)
|
|
{
|
|
const int pos = cursor.position();
|
|
return pos >= blockData.position && pos < blockData.position + blockData.length;
|
|
}
|
|
|
|
void TextEditorWidgetPrivate::addCursorsPosition(PaintEventData &data,
|
|
QPainter &painter,
|
|
const PaintEventBlockData &blockData) const
|
|
{
|
|
if (!m_dndCursor.isNull()) {
|
|
if (blockContainsCursor(blockData, m_dndCursor)) {
|
|
data.cursors.append(
|
|
generateCursorData(m_dndCursor.positionInBlock(), data, blockData, painter));
|
|
}
|
|
} else {
|
|
for (const QTextCursor &cursor : m_cursors) {
|
|
if (blockContainsCursor(blockData, cursor)) {
|
|
data.cursors.append(
|
|
generateCursorData(cursor.positionInBlock(), data, blockData, painter));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
QTextBlock TextEditorWidgetPrivate::nextVisibleBlock(const QTextBlock &block) const
|
|
{
|
|
return TextEditor::nextVisibleBlock(block, q->document());
|
|
}
|
|
|
|
void TextEditorWidgetPrivate::scheduleCleanupAnnotationCache()
|
|
{
|
|
if (cleanupAnnotationRectsScheduled)
|
|
return;
|
|
QMetaObject::invokeMethod(this,
|
|
&TextEditorWidgetPrivate::cleanupAnnotationCache,
|
|
Qt::QueuedConnection);
|
|
cleanupAnnotationRectsScheduled = true;
|
|
}
|
|
|
|
void TextEditorWidgetPrivate::cleanupAnnotationCache()
|
|
{
|
|
cleanupAnnotationRectsScheduled = false;
|
|
const int firstVisibleBlock = q->firstVisibleBlockNumber();
|
|
const int lastVisibleBlock = q->lastVisibleBlockNumber();
|
|
auto lineIsVisble = [&](int blockNumber){
|
|
auto behindFirstVisibleBlock = [&](){
|
|
return firstVisibleBlock >= 0 && blockNumber >= firstVisibleBlock;
|
|
};
|
|
auto beforeLastVisibleBlock = [&](){
|
|
return lastVisibleBlock < 0 || (lastVisibleBlock >= 0 && blockNumber <= lastVisibleBlock);
|
|
};
|
|
return behindFirstVisibleBlock() && beforeLastVisibleBlock();
|
|
};
|
|
auto it = m_annotationRects.begin();
|
|
auto end = m_annotationRects.end();
|
|
while (it != end) {
|
|
if (!lineIsVisble(it.key()))
|
|
it = m_annotationRects.erase(it);
|
|
else
|
|
++it;
|
|
}
|
|
}
|
|
|
|
void TextEditorWidget::paintEvent(QPaintEvent *e)
|
|
{
|
|
PaintEventData data(this, e, contentOffset());
|
|
QTC_ASSERT(data.documentLayout, return);
|
|
|
|
QPainter painter(viewport());
|
|
// Set a brush origin so that the WaveUnderline knows where the wave started
|
|
painter.setBrushOrigin(data.offset);
|
|
|
|
data.block = firstVisibleBlock();
|
|
data.context = getPaintContext();
|
|
const QTextCharFormat textFormat = textDocument()->fontSettings().toTextCharFormat(C_TEXT);
|
|
data.context.palette.setBrush(QPalette::Text, textFormat.foreground());
|
|
data.context.palette.setBrush(QPalette::Base, textFormat.background());
|
|
|
|
{ // paint background
|
|
d->paintWidgetBackground(data, painter);
|
|
// draw backgrond to the right of the wrap column before everything else
|
|
d->paintRightMarginArea(data, painter);
|
|
// paint a blended background color depending on scope depth
|
|
d->paintBlockHighlight(data, painter);
|
|
// paint background of if defed out blocks in bigger chunks
|
|
d->paintIfDefedOutBlocks(data, painter);
|
|
d->paintRightMarginLine(data, painter);
|
|
// paint find scope on top of ifdefed out blocks and right margin
|
|
d->paintFindScope(data, painter);
|
|
// paint search results on top of the find scope
|
|
d->paintSearchResultOverlay(data, painter);
|
|
// paint selection highlights
|
|
d->paintSelectionOverlay(data, painter);
|
|
}
|
|
|
|
while (data.block.isValid()) {
|
|
|
|
PaintEventBlockData blockData;
|
|
blockData.boundingRect = blockBoundingRect(data.block).translated(data.offset);
|
|
|
|
if (blockData.boundingRect.bottom() >= data.eventRect.top()
|
|
&& blockData.boundingRect.top() <= data.eventRect.bottom()) {
|
|
|
|
data.documentLayout->ensureBlockLayout(data.block);
|
|
d->setupBlockLayout(data, painter, blockData);
|
|
blockData.position = data.block.position();
|
|
blockData.length = data.block.length();
|
|
d->setupSelections(data, blockData);
|
|
|
|
d->paintCurrentLineHighlight(data, painter);
|
|
|
|
bool drawCursor = false;
|
|
bool drawCursorAsBlock = false;
|
|
if (d->m_dndCursor.isNull()) {
|
|
drawCursor = d->m_cursorVisible
|
|
&& Utils::anyOf(d->m_cursors, [&](const QTextCursor &cursor) {
|
|
return blockContainsCursor(blockData, cursor);
|
|
});
|
|
drawCursorAsBlock = drawCursor && overwriteMode();
|
|
} else {
|
|
drawCursor = blockContainsCursor(blockData, d->m_dndCursor);
|
|
}
|
|
|
|
if (drawCursorAsBlock) {
|
|
for (const QTextCursor &cursor : multiTextCursor()) {
|
|
if (blockContainsCursor(blockData, cursor))
|
|
d->paintCursorAsBlock(data, painter, blockData, cursor.position());
|
|
}
|
|
}
|
|
|
|
paintBlock(&painter, data.block, data.offset, blockData.selections, data.eventRect);
|
|
|
|
if (data.isEditable && !blockData.layout->preeditAreaText().isEmpty()) {
|
|
if (data.context.cursorPosition < -1) {
|
|
const int cursorPos = blockData.layout->preeditAreaPosition()
|
|
- (data.context.cursorPosition + 2);
|
|
data.cursors = {generateCursorData(cursorPos, data, blockData, painter)};
|
|
}
|
|
} else if (drawCursor && !drawCursorAsBlock) {
|
|
d->addCursorsPosition(data, painter, blockData);
|
|
}
|
|
d->paintIndentDepth(data, painter, blockData);
|
|
d->paintAdditionalVisualWhitespaces(data, painter, blockData.boundingRect.top());
|
|
d->paintReplacement(data, painter, blockData.boundingRect.top());
|
|
d->updateLineAnnotation(data, blockData, painter);
|
|
}
|
|
|
|
data.offset.ry() += blockData.boundingRect.height();
|
|
|
|
if (data.offset.y() > data.viewportRect.height())
|
|
break;
|
|
|
|
data.block = data.block.next();
|
|
|
|
if (!data.block.isVisible()) {
|
|
if (data.block.blockNumber() == d->visibleFoldedBlockNumber) {
|
|
data.visibleCollapsedBlock = data.block;
|
|
data.visibleCollapsedBlockOffset = data.offset;
|
|
}
|
|
|
|
// invisible blocks do have zero line count
|
|
data.block = data.doc->findBlockByLineNumber(data.block.firstLineNumber());
|
|
}
|
|
}
|
|
|
|
painter.setPen(data.context.palette.text().color());
|
|
|
|
d->updateAnimator(d->m_bracketsAnimator, painter);
|
|
d->updateAnimator(d->m_autocompleteAnimator, painter);
|
|
|
|
d->paintOverlays(data, painter);
|
|
|
|
// draw the cursor last, on top of everything
|
|
d->paintCursor(data, painter);
|
|
|
|
// paint a popup with the content of the collapsed block
|
|
drawCollapsedBlockPopup(painter, data.visibleCollapsedBlock,
|
|
data.visibleCollapsedBlockOffset, data.eventRect);
|
|
}
|
|
|
|
void TextEditorWidget::paintBlock(QPainter *painter,
|
|
const QTextBlock &block,
|
|
const QPointF &offset,
|
|
const QVector<QTextLayout::FormatRange> &selections,
|
|
const QRect &clipRect) const
|
|
{
|
|
if (TextSuggestion *suggestion = TextDocumentLayout::suggestion(block)) {
|
|
QTextBlock suggestionBlock = suggestion->replacementDocument()->firstBlock();
|
|
QPointF suggestionOffset = offset;
|
|
suggestionOffset.rx() += document()->documentMargin();
|
|
while (suggestionBlock.isValid()) {
|
|
const QVector<QTextLayout::FormatRange> blockSelections
|
|
= suggestionBlock.blockNumber() == 0 ? selections
|
|
: QVector<QTextLayout::FormatRange>{};
|
|
suggestionBlock.layout()->draw(painter,
|
|
suggestionOffset,
|
|
blockSelections,
|
|
clipRect);
|
|
suggestionOffset.ry() += suggestion->replacementDocument()
|
|
->documentLayout()
|
|
->blockBoundingRect(suggestionBlock)
|
|
.height();
|
|
suggestionBlock = suggestionBlock.next();
|
|
}
|
|
return;
|
|
}
|
|
|
|
block.layout()->draw(painter, offset, selections, clipRect);
|
|
}
|
|
|
|
int TextEditorWidget::visibleFoldedBlockNumber() const
|
|
{
|
|
return d->visibleFoldedBlockNumber;
|
|
}
|
|
|
|
void TextEditorWidget::drawCollapsedBlockPopup(QPainter &painter,
|
|
const QTextBlock &block,
|
|
QPointF offset,
|
|
const QRect &clip)
|
|
{
|
|
if (!block.isValid())
|
|
return;
|
|
|
|
int margin = int(block.document()->documentMargin());
|
|
qreal maxWidth = 0;
|
|
qreal blockHeight = 0;
|
|
QTextBlock b = block;
|
|
|
|
while (!b.isVisible()) {
|
|
b.setVisible(true); // make sure block bounding rect works
|
|
QRectF r = blockBoundingRect(b).translated(offset);
|
|
|
|
QTextLayout *layout = b.layout();
|
|
for (int i = layout->lineCount()-1; i >= 0; --i)
|
|
maxWidth = qMax(maxWidth, layout->lineAt(i).naturalTextWidth() + 2*margin);
|
|
|
|
blockHeight += r.height();
|
|
|
|
b.setVisible(false); // restore previous state
|
|
b.setLineCount(0); // restore 0 line count for invisible block
|
|
b = b.next();
|
|
}
|
|
|
|
painter.save();
|
|
painter.setRenderHint(QPainter::Antialiasing, true);
|
|
painter.translate(.5, .5);
|
|
QBrush brush = textDocument()->fontSettings().toTextCharFormat(C_TEXT).background();
|
|
const QTextCharFormat ifdefedOutFormat = textDocument()->fontSettings().toTextCharFormat(
|
|
C_DISABLED_CODE);
|
|
if (ifdefedOutFormat.hasProperty(QTextFormat::BackgroundBrush))
|
|
brush = ifdefedOutFormat.background();
|
|
painter.setBrush(brush);
|
|
painter.drawRoundedRect(QRectF(offset.x(),
|
|
offset.y(),
|
|
maxWidth, blockHeight).adjusted(0, 0, 0, 0), 3, 3);
|
|
painter.restore();
|
|
|
|
QTextBlock end = b;
|
|
b = block;
|
|
while (b != end) {
|
|
b.setVisible(true); // make sure block bounding rect works
|
|
QRectF r = blockBoundingRect(b).translated(offset);
|
|
QTextLayout *layout = b.layout();
|
|
QVector<QTextLayout::FormatRange> selections;
|
|
layout->draw(&painter, offset, selections, clip);
|
|
|
|
b.setVisible(false); // restore previous state
|
|
b.setLineCount(0); // restore 0 line count for invisible block
|
|
offset.ry() += r.height();
|
|
b = b.next();
|
|
}
|
|
}
|
|
|
|
QWidget *TextEditorWidget::extraArea() const
|
|
{
|
|
return d->m_extraArea;
|
|
}
|
|
|
|
int TextEditorWidget::extraAreaWidth(int *markWidthPtr) const
|
|
{
|
|
auto documentLayout = qobject_cast<TextDocumentLayout*>(document()->documentLayout());
|
|
if (!documentLayout)
|
|
return 0;
|
|
|
|
if (!d->m_marksVisible && documentLayout->hasMarks)
|
|
d->m_marksVisible = true;
|
|
|
|
if (!d->m_marksVisible && !d->m_lineNumbersVisible && !d->m_codeFoldingVisible)
|
|
return 0;
|
|
|
|
int space = 0;
|
|
const QFontMetrics fm(d->m_extraArea->fontMetrics());
|
|
|
|
if (d->m_lineNumbersVisible) {
|
|
QFont fnt = d->m_extraArea->font();
|
|
// this works under the assumption that bold or italic
|
|
// can only make a font wider
|
|
const QTextCharFormat currentLineNumberFormat
|
|
= textDocument()->fontSettings().toTextCharFormat(C_CURRENT_LINE_NUMBER);
|
|
fnt.setBold(currentLineNumberFormat.font().bold());
|
|
fnt.setItalic(currentLineNumberFormat.font().italic());
|
|
const QFontMetrics linefm(fnt);
|
|
|
|
space += linefm.horizontalAdvance(QLatin1Char('9')) * lineNumberDigits();
|
|
}
|
|
int markWidth = 0;
|
|
|
|
if (d->m_marksVisible) {
|
|
if (TextEditorSettings::fontSettings().relativeLineSpacing() == 100)
|
|
markWidth += fm.lineSpacing() + 2;
|
|
else
|
|
markWidth += TextEditorSettings::fontSettings().lineSpacing() + 2;
|
|
|
|
// if (documentLayout->doubleMarkCount)
|
|
// markWidth += fm.lineSpacing() / 3;
|
|
space += markWidth;
|
|
} else {
|
|
space += 2;
|
|
}
|
|
|
|
if (markWidthPtr)
|
|
*markWidthPtr = markWidth;
|
|
|
|
space += 4;
|
|
|
|
if (d->m_codeFoldingVisible) {
|
|
if (TextEditorSettings::fontSettings().relativeLineSpacing() == 100)
|
|
space += foldBoxWidth(fm);
|
|
else
|
|
space += foldBoxWidth();
|
|
}
|
|
|
|
if (viewportMargins() != QMargins{isLeftToRight() ? space : 0, 0, isLeftToRight() ? 0 : space, 0})
|
|
d->slotUpdateExtraAreaWidth(space);
|
|
|
|
return space;
|
|
}
|
|
|
|
void TextEditorWidgetPrivate::slotUpdateExtraAreaWidth(std::optional<int> width)
|
|
{
|
|
if (!width.has_value())
|
|
width = q->extraAreaWidth();
|
|
QMargins margins;
|
|
if (q->isLeftToRight())
|
|
margins = QMargins(*width, 0, 0, 0);
|
|
else
|
|
margins = QMargins(0, 0, *width, 0);
|
|
if (margins != q->viewportMargins())
|
|
q->setViewportMargins(margins);
|
|
}
|
|
|
|
struct Internal::ExtraAreaPaintEventData
|
|
{
|
|
ExtraAreaPaintEventData(const TextEditorWidget *editor, TextEditorWidgetPrivate *d)
|
|
: doc(editor->document())
|
|
, documentLayout(qobject_cast<TextDocumentLayout*>(doc->documentLayout()))
|
|
, selectionStart(editor->textCursor().selectionStart())
|
|
, selectionEnd(editor->textCursor().selectionEnd())
|
|
, fontMetrics(d->m_extraArea->font())
|
|
, lineSpacing(fontMetrics.lineSpacing())
|
|
, markWidth(d->m_marksVisible ? lineSpacing : 0)
|
|
, collapseColumnWidth(d->m_codeFoldingVisible ? foldBoxWidth(fontMetrics) : 0)
|
|
, extraAreaWidth(d->m_extraArea->width() - collapseColumnWidth)
|
|
, currentLineNumberFormat(
|
|
editor->textDocument()->fontSettings().toTextCharFormat(C_CURRENT_LINE_NUMBER))
|
|
, palette(d->m_extraArea->palette())
|
|
{
|
|
if (TextEditorSettings::fontSettings().relativeLineSpacing() != 100) {
|
|
lineSpacing = TextEditorSettings::fontSettings().lineSpacing();
|
|
collapseColumnWidth = d->m_codeFoldingVisible ? foldBoxWidth() : 0;
|
|
markWidth = d->m_marksVisible ? lineSpacing : 0;
|
|
}
|
|
palette.setCurrentColorGroup(QPalette::Active);
|
|
}
|
|
QTextBlock block;
|
|
const QTextDocument *doc;
|
|
const TextDocumentLayout *documentLayout;
|
|
const int selectionStart;
|
|
const int selectionEnd;
|
|
const QFontMetrics fontMetrics;
|
|
int lineSpacing;
|
|
int markWidth;
|
|
int collapseColumnWidth;
|
|
const int extraAreaWidth;
|
|
const QTextCharFormat currentLineNumberFormat;
|
|
QPalette palette;
|
|
};
|
|
|
|
void TextEditorWidgetPrivate::paintLineNumbers(QPainter &painter,
|
|
const ExtraAreaPaintEventData &data,
|
|
const QRectF &blockBoundingRect) const
|
|
{
|
|
if (!m_lineNumbersVisible)
|
|
return;
|
|
|
|
const QString &number = q->lineNumber(data.block.blockNumber());
|
|
const bool selected = (
|
|
(data.selectionStart < data.block.position() + data.block.length()
|
|
&& data.selectionEnd > data.block.position())
|
|
|| (data.selectionStart == data.selectionEnd && data.selectionEnd == data.block.position())
|
|
);
|
|
if (selected) {
|
|
painter.save();
|
|
QFont f = painter.font();
|
|
f.setBold(data.currentLineNumberFormat.font().bold());
|
|
f.setItalic(data.currentLineNumberFormat.font().italic());
|
|
painter.setFont(f);
|
|
painter.setPen(data.currentLineNumberFormat.foreground().color());
|
|
if (data.currentLineNumberFormat.background() != Qt::NoBrush) {
|
|
painter.fillRect(QRectF(0, blockBoundingRect.top(),
|
|
data.extraAreaWidth, blockBoundingRect.height()),
|
|
data.currentLineNumberFormat.background().color());
|
|
}
|
|
}
|
|
painter.drawText(QRectF(data.markWidth, blockBoundingRect.top(),
|
|
data.extraAreaWidth - data.markWidth - 4, blockBoundingRect.height()),
|
|
Qt::AlignRight,
|
|
number);
|
|
if (selected)
|
|
painter.restore();
|
|
}
|
|
|
|
void TextEditorWidgetPrivate::paintTextMarks(QPainter &painter, const ExtraAreaPaintEventData &data,
|
|
const QRectF &blockBoundingRect) const
|
|
{
|
|
auto userData = static_cast<TextBlockUserData*>(data.block.userData());
|
|
if (!userData || !m_marksVisible)
|
|
return;
|
|
TextMarks marks = userData->marks();
|
|
QList<QIcon> icons;
|
|
auto end = marks.crend();
|
|
int marksWithIconCount = 0;
|
|
QIcon overrideIcon;
|
|
for (auto it = marks.crbegin(); it != end; ++it) {
|
|
if ((*it)->isVisible()) {
|
|
const QIcon icon = (*it)->icon();
|
|
if (!icon.isNull()) {
|
|
if ((*it)->isLocationMarker()) {
|
|
overrideIcon = icon;
|
|
} else {
|
|
if (icons.size() < 3
|
|
&& !Utils::contains(icons, Utils::equal(&QIcon::cacheKey, icon.cacheKey()))) {
|
|
icons << icon;
|
|
}
|
|
++marksWithIconCount;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
int size = data.lineSpacing - 1;
|
|
int xoffset = 0;
|
|
int yoffset = blockBoundingRect.top();
|
|
|
|
painter.save();
|
|
const QScopeGuard cleanup([&painter, size, yoffset, xoffset, overrideIcon] {
|
|
if (!overrideIcon.isNull()) {
|
|
const QRect r(xoffset, yoffset, size, size);
|
|
overrideIcon.paint(&painter, r, Qt::AlignCenter);
|
|
}
|
|
painter.restore();
|
|
});
|
|
|
|
if (icons.isEmpty())
|
|
return;
|
|
|
|
if (icons.size() == 1) {
|
|
const QRect r(xoffset, yoffset, size, size);
|
|
icons.first().paint(&painter, r, Qt::AlignCenter);
|
|
return;
|
|
}
|
|
size = size / 2;
|
|
for (const QIcon &icon : std::as_const(icons)) {
|
|
const QRect r(xoffset, yoffset, size, size);
|
|
icon.paint(&painter, r, Qt::AlignCenter);
|
|
if (xoffset != 0) {
|
|
yoffset += size;
|
|
xoffset = 0;
|
|
} else {
|
|
xoffset = size;
|
|
}
|
|
}
|
|
QFont font = painter.font();
|
|
font.setPixelSize(size);
|
|
painter.setFont(font);
|
|
|
|
const QColor color = data.currentLineNumberFormat.foreground().color();
|
|
if (color.isValid())
|
|
painter.setPen(color);
|
|
|
|
const QRect r(size, blockBoundingRect.top() + size, size, size);
|
|
const QString detail = marksWithIconCount > 9 ? QString("+")
|
|
: QString::number(marksWithIconCount);
|
|
painter.drawText(r, Qt::AlignRight, detail);
|
|
}
|
|
|
|
static void drawRectBox(QPainter *painter, const QRect &rect, const QPalette &pal)
|
|
{
|
|
painter->save();
|
|
painter->setOpacity(0.5);
|
|
painter->fillRect(rect, pal.brush(QPalette::Highlight));
|
|
painter->restore();
|
|
}
|
|
|
|
void TextEditorWidgetPrivate::paintCodeFolding(QPainter &painter,
|
|
const ExtraAreaPaintEventData &data,
|
|
const QRectF &blockBoundingRect) const
|
|
{
|
|
if (!m_codeFoldingVisible)
|
|
return;
|
|
|
|
int extraAreaHighlightFoldBlockNumber = -1;
|
|
int extraAreaHighlightFoldEndBlockNumber = -1;
|
|
if (!m_highlightBlocksInfo.isEmpty()) {
|
|
extraAreaHighlightFoldBlockNumber = m_highlightBlocksInfo.open.last();
|
|
extraAreaHighlightFoldEndBlockNumber = m_highlightBlocksInfo.close.first();
|
|
}
|
|
|
|
const QTextBlock &nextBlock = data.block.next();
|
|
TextBlockUserData *nextBlockUserData = TextDocumentLayout::textUserData(nextBlock);
|
|
|
|
bool drawBox = nextBlockUserData
|
|
&& TextDocumentLayout::foldingIndent(data.block)
|
|
< nextBlockUserData->foldingIndent();
|
|
if (drawBox) {
|
|
qCDebug(foldingLog) << "need to paint folding marker";
|
|
qCDebug(foldingLog) << "folding indent for line" << (data.block.blockNumber() + 1) << "is"
|
|
<< TextDocumentLayout::foldingIndent(data.block);
|
|
qCDebug(foldingLog) << "folding indent for line" << (nextBlock.blockNumber() + 1) << "is"
|
|
<< nextBlockUserData->foldingIndent();
|
|
}
|
|
|
|
const int blockNumber = data.block.blockNumber();
|
|
bool active = blockNumber == extraAreaHighlightFoldBlockNumber;
|
|
bool hovered = blockNumber >= extraAreaHighlightFoldBlockNumber
|
|
&& blockNumber <= extraAreaHighlightFoldEndBlockNumber;
|
|
|
|
int boxWidth = 0;
|
|
if (TextEditorSettings::fontSettings().relativeLineSpacing() == 100)
|
|
boxWidth = foldBoxWidth(data.fontMetrics);
|
|
else
|
|
boxWidth = foldBoxWidth();
|
|
|
|
if (hovered) {
|
|
int itop = qRound(blockBoundingRect.top());
|
|
int ibottom = qRound(blockBoundingRect.bottom());
|
|
QRect box = QRect(data.extraAreaWidth + 1, itop, boxWidth - 2, ibottom - itop);
|
|
drawRectBox(&painter, box, data.palette);
|
|
}
|
|
|
|
if (drawBox) {
|
|
bool expanded = nextBlock.isVisible();
|
|
int size = boxWidth/4;
|
|
QRect box(data.extraAreaWidth + size, int(blockBoundingRect.top()) + size,
|
|
2 * (size) + 1, 2 * (size) + 1);
|
|
drawFoldingMarker(&painter, data.palette, box, expanded, active, hovered);
|
|
}
|
|
|
|
}
|
|
|
|
void TextEditorWidgetPrivate::paintRevisionMarker(QPainter &painter,
|
|
const ExtraAreaPaintEventData &data,
|
|
const QRectF &blockBoundingRect) const
|
|
{
|
|
if (m_revisionsVisible && data.block.revision() != data.documentLayout->lastSaveRevision) {
|
|
painter.save();
|
|
painter.setRenderHint(QPainter::Antialiasing, false);
|
|
if (data.block.revision() < 0)
|
|
painter.setPen(QPen(Qt::darkGreen, 2));
|
|
else
|
|
painter.setPen(QPen(Qt::red, 2));
|
|
painter.drawLine(data.extraAreaWidth - 1, int(blockBoundingRect.top()),
|
|
data.extraAreaWidth - 1, int(blockBoundingRect.bottom()) - 1);
|
|
painter.restore();
|
|
}
|
|
}
|
|
|
|
void TextEditorWidget::extraAreaPaintEvent(QPaintEvent *e)
|
|
{
|
|
ExtraAreaPaintEventData data(this, d.get());
|
|
QTC_ASSERT(data.documentLayout, return);
|
|
|
|
QPainter painter(d->m_extraArea);
|
|
|
|
painter.fillRect(e->rect(), data.palette.color(QPalette::Window));
|
|
|
|
data.block = firstVisibleBlock();
|
|
QPointF offset = contentOffset();
|
|
QRectF boundingRect = blockBoundingRect(data.block).translated(offset);
|
|
|
|
while (data.block.isValid() && boundingRect.top() <= e->rect().bottom()) {
|
|
if (boundingRect.bottom() >= e->rect().top()) {
|
|
|
|
painter.setPen(data.palette.color(QPalette::Dark));
|
|
|
|
d->paintLineNumbers(painter, data, boundingRect);
|
|
|
|
if (d->m_codeFoldingVisible || d->m_marksVisible) {
|
|
painter.save();
|
|
painter.setRenderHint(QPainter::Antialiasing, false);
|
|
|
|
d->paintTextMarks(painter, data, boundingRect);
|
|
d->paintCodeFolding(painter, data, boundingRect);
|
|
|
|
painter.restore();
|
|
}
|
|
|
|
d->paintRevisionMarker(painter, data, boundingRect);
|
|
}
|
|
|
|
offset.ry() += boundingRect.height();
|
|
data.block = d->nextVisibleBlock(data.block);
|
|
boundingRect = blockBoundingRect(data.block).translated(offset);
|
|
}
|
|
}
|
|
|
|
void TextEditorWidgetPrivate::drawFoldingMarker(QPainter *painter, const QPalette &pal,
|
|
const QRect &rect,
|
|
bool expanded,
|
|
bool active,
|
|
bool hovered) const
|
|
{
|
|
QStyle *s = q->style();
|
|
if (auto ms = qobject_cast<ManhattanStyle*>(s))
|
|
s = ms->baseStyle();
|
|
|
|
QStyleOptionViewItem 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());
|
|
|
|
const char *className = s->metaObject()->className();
|
|
|
|
// Do not use the windows folding marker since we cannot style them and the default hover color
|
|
// is a blue which does not guarantee an high contrast on all themes.
|
|
static QPointer<QStyle> fusionStyleOverwrite = nullptr;
|
|
if (!qstrcmp(className, "QWindowsVistaStyle")) {
|
|
if (fusionStyleOverwrite.isNull())
|
|
fusionStyleOverwrite = QStyleFactory::create("fusion");
|
|
if (!fusionStyleOverwrite.isNull()) {
|
|
s = fusionStyleOverwrite.data();
|
|
className = s->metaObject()->className();
|
|
}
|
|
}
|
|
|
|
if (!qstrcmp(className, "OxygenStyle")) {
|
|
const QStyle::PrimitiveElement direction = expanded ? QStyle::PE_IndicatorArrowDown
|
|
: QStyle::PE_IndicatorArrowRight;
|
|
StyleHelper::drawArrow(direction, painter, &opt);
|
|
} else {
|
|
// QGtkStyle needs a small correction to draw the marker in the right place
|
|
if (!qstrcmp(className, "QGtkStyle"))
|
|
opt.rect.translate(-2, 0);
|
|
else if (!qstrcmp(className, "QMacStyle"))
|
|
opt.rect.translate(-2, 0);
|
|
else if (!qstrcmp(className, "QFusionStyle"))
|
|
opt.rect.translate(0, -1);
|
|
|
|
s->drawPrimitive(QStyle::PE_IndicatorBranch, &opt, painter, q);
|
|
}
|
|
}
|
|
|
|
void TextEditorWidgetPrivate::slotUpdateRequest(const QRect &r, int dy)
|
|
{
|
|
if (dy) {
|
|
m_extraArea->scroll(0, dy);
|
|
} else if (r.width() > 4) { // wider than cursor width, not just cursor blinking
|
|
m_extraArea->update(0, r.y(), m_extraArea->width(), r.height());
|
|
if (!m_searchExpr.pattern().isEmpty()) {
|
|
const int m = m_searchResultOverlay->dropShadowWidth();
|
|
q->viewport()->update(r.adjusted(-m, -m, m, m));
|
|
}
|
|
}
|
|
|
|
if (r.contains(q->viewport()->rect()))
|
|
slotUpdateExtraAreaWidth();
|
|
}
|
|
|
|
void TextEditorWidgetPrivate::saveCurrentCursorPositionForNavigation()
|
|
{
|
|
m_lastCursorChangeWasInteresting = true;
|
|
emit q->saveCurrentStateForNavigationHistory();
|
|
}
|
|
|
|
void TextEditorWidgetPrivate::updateCurrentLineHighlight()
|
|
{
|
|
QList<QTextEdit::ExtraSelection> extraSelections;
|
|
|
|
if (m_highlightCurrentLine) {
|
|
for (const QTextCursor &c : m_cursors) {
|
|
QTextEdit::ExtraSelection sel;
|
|
sel.format.setBackground(
|
|
m_document->fontSettings().toTextCharFormat(C_CURRENT_LINE).background());
|
|
sel.format.setProperty(QTextFormat::FullWidthSelection, true);
|
|
sel.cursor = c;
|
|
sel.cursor.clearSelection();
|
|
extraSelections.append(sel);
|
|
}
|
|
}
|
|
updateCurrentLineInScrollbar();
|
|
|
|
q->setExtraSelections(TextEditorWidget::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.
|
|
const QPointF offset = q->contentOffset();
|
|
auto updateBlock = [&](const QTextBlock &block) {
|
|
if (block.isValid() && block.isVisible()) {
|
|
QRect updateRect = q->blockBoundingGeometry(block).translated(offset).toAlignedRect();
|
|
m_extraArea->update(updateRect);
|
|
updateRect.setLeft(0);
|
|
updateRect.setRight(q->viewport()->width());
|
|
q->viewport()->update(updateRect);
|
|
}
|
|
};
|
|
|
|
QSet<int> cursorBlockNumbers;
|
|
for (const QTextCursor &c : m_cursors)
|
|
cursorBlockNumbers.insert(c.blockNumber());
|
|
|
|
const QSet<int> updateBlockNumbers = (cursorBlockNumbers - m_cursorBlockNumbers)
|
|
+ (m_cursorBlockNumbers - cursorBlockNumbers);
|
|
|
|
for (const int blockNumber : updateBlockNumbers)
|
|
updateBlock(m_document->document()->findBlockByNumber(blockNumber));
|
|
|
|
m_cursorBlockNumbers = cursorBlockNumbers;
|
|
}
|
|
|
|
void TextEditorWidget::slotCursorPositionChanged()
|
|
{
|
|
#if 0
|
|
qDebug() << "block" << textCursor().blockNumber()+1
|
|
<< "brace depth:" << BaseTextDocumentLayout::braceDepth(textCursor().block())
|
|
<< "indent:" << BaseTextDocumentLayout::userData(textCursor().block())->foldingIndent();
|
|
#endif
|
|
if (!d->m_contentsChanged && d->m_lastCursorChangeWasInteresting) {
|
|
emit addSavedStateToNavigationHistory();
|
|
d->m_lastCursorChangeWasInteresting = false;
|
|
} else if (d->m_contentsChanged) {
|
|
d->saveCurrentCursorPositionForNavigation();
|
|
if (EditorManager::currentEditor() && EditorManager::currentEditor()->widget() == this)
|
|
EditorManager::setLastEditLocation(EditorManager::currentEditor());
|
|
}
|
|
MultiTextCursor cursor = multiTextCursor();
|
|
cursor.replaceMainCursor(textCursor());
|
|
setMultiTextCursor(cursor);
|
|
d->updateCursorSelections();
|
|
d->updateHighlights();
|
|
d->updateSuggestion();
|
|
}
|
|
|
|
void TextEditorWidgetPrivate::updateHighlights()
|
|
{
|
|
if (m_parenthesesMatchingEnabled && q->hasFocus()) {
|
|
// Delay update when no matching is displayed yet, to avoid flicker
|
|
if (q->extraSelections(TextEditorWidget::ParenthesesMatchingSelection).isEmpty()
|
|
&& m_bracketsAnimator == nullptr) {
|
|
m_parenthesesMatchingTimer.start();
|
|
} else {
|
|
// when we uncheck "highlight matching parentheses"
|
|
// we need clear current selection before viewport update
|
|
// otherwise we get sticky highlighted parentheses
|
|
if (!m_displaySettings.m_highlightMatchingParentheses)
|
|
q->setExtraSelections(TextEditorWidget::ParenthesesMatchingSelection, QList<QTextEdit::ExtraSelection>());
|
|
|
|
// use 0-timer, not direct call, to give the syntax highlighter a chance
|
|
// to update the parentheses information
|
|
m_parenthesesMatchingTimer.start(0);
|
|
}
|
|
}
|
|
|
|
if (m_highlightAutoComplete && !m_autoCompleteHighlightPos.isEmpty()) {
|
|
QMetaObject::invokeMethod(this, [this] {
|
|
const QTextCursor &cursor = q->textCursor();
|
|
auto popAutoCompletion = [&]() {
|
|
return !m_autoCompleteHighlightPos.isEmpty()
|
|
&& m_autoCompleteHighlightPos.last() != cursor;
|
|
};
|
|
if ((!m_keepAutoCompletionHighlight && !q->hasFocus()) || popAutoCompletion()) {
|
|
while (popAutoCompletion())
|
|
m_autoCompleteHighlightPos.pop_back();
|
|
updateAutoCompleteHighlight();
|
|
}
|
|
}, Qt::QueuedConnection);
|
|
}
|
|
|
|
updateCurrentLineHighlight();
|
|
|
|
if (m_displaySettings.m_highlightBlocks) {
|
|
QTextCursor cursor = q->textCursor();
|
|
extraAreaHighlightFoldedBlockNumber = cursor.blockNumber();
|
|
m_highlightBlocksTimer.start(100);
|
|
}
|
|
}
|
|
|
|
void TextEditorWidgetPrivate::updateCurrentLineInScrollbar()
|
|
{
|
|
if (m_highlightCurrentLine && m_highlightScrollBarController) {
|
|
m_highlightScrollBarController->removeHighlights(Constants::SCROLL_BAR_CURRENT_LINE);
|
|
for (const QTextCursor &tc : m_cursors) {
|
|
if (QTextLayout *layout = tc.block().layout()) {
|
|
const int pos = tc.block().firstLineNumber() +
|
|
layout->lineForTextPosition(tc.positionInBlock()).lineNumber();
|
|
m_highlightScrollBarController->addHighlight({Constants::SCROLL_BAR_CURRENT_LINE, pos,
|
|
Theme::TextEditor_CurrentLine_ScrollBarColor,
|
|
Highlight::HighestPriority});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void TextEditorWidgetPrivate::slotUpdateBlockNotify(const QTextBlock &block)
|
|
{
|
|
static bool blockRecursion = false;
|
|
if (blockRecursion)
|
|
return;
|
|
blockRecursion = true;
|
|
if (m_overlay->isVisible()) {
|
|
/* an overlay might draw outside the block bounderies, force
|
|
complete viewport update */
|
|
q->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 fold
|
|
box which now is invalid.*/
|
|
emit q->requestBlockUpdate(block.previous());
|
|
}
|
|
|
|
for (const QTextCursor &scope : m_findScope) {
|
|
QSet<int> updatedBlocks;
|
|
const bool blockContainsFindScope = block.position() < scope.selectionEnd()
|
|
&& block.position() + block.length()
|
|
>= scope.selectionStart();
|
|
if (blockContainsFindScope) {
|
|
QTextBlock b = block.document()->findBlock(scope.selectionStart());
|
|
do {
|
|
if (Utils::insert(updatedBlocks, b.blockNumber()))
|
|
emit q->requestBlockUpdate(b);
|
|
b = b.next();
|
|
} while (b.isValid() && b.position() < scope.selectionEnd());
|
|
}
|
|
}
|
|
}
|
|
blockRecursion = false;
|
|
}
|
|
|
|
void TextEditorWidget::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->foldedBlockTimer.timerId()) {
|
|
d->visibleFoldedBlockNumber = d->suggestedVisibleFoldedBlockNumber;
|
|
d->suggestedVisibleFoldedBlockNumber = -1;
|
|
d->foldedBlockTimer.stop();
|
|
viewport()->update();
|
|
} else if (e->timerId() == d->m_cursorFlashTimer.timerId()) {
|
|
d->m_cursorVisible = !d->m_cursorVisible;
|
|
viewport()->update(d->cursorUpdateRect(d->m_cursors));
|
|
}
|
|
QPlainTextEdit::timerEvent(e);
|
|
}
|
|
|
|
|
|
void TextEditorWidgetPrivate::clearVisibleFoldedBlock()
|
|
{
|
|
if (suggestedVisibleFoldedBlockNumber) {
|
|
suggestedVisibleFoldedBlockNumber = -1;
|
|
foldedBlockTimer.stop();
|
|
}
|
|
if (visibleFoldedBlockNumber >= 0) {
|
|
visibleFoldedBlockNumber = -1;
|
|
q->viewport()->update();
|
|
}
|
|
}
|
|
|
|
void TextEditorWidget::mouseMoveEvent(QMouseEvent *e)
|
|
{
|
|
d->requestUpdateLink(e);
|
|
|
|
bool onLink = false;
|
|
if (d->m_linkPressed && d->m_currentLink.hasValidTarget()) {
|
|
const int eventCursorPosition = cursorForPosition(e->pos()).position();
|
|
if (eventCursorPosition < d->m_currentLink.linkTextStart
|
|
|| eventCursorPosition > d->m_currentLink.linkTextEnd) {
|
|
d->m_linkPressed = false;
|
|
} else {
|
|
onLink = true;
|
|
}
|
|
}
|
|
|
|
static std::optional<MultiTextCursor> startMouseMoveCursor;
|
|
if (e->buttons() == Qt::LeftButton && e->modifiers() & Qt::AltModifier) {
|
|
if (!startMouseMoveCursor.has_value()) {
|
|
startMouseMoveCursor = multiTextCursor();
|
|
QTextCursor c = startMouseMoveCursor->takeMainCursor();
|
|
if (!startMouseMoveCursor->hasMultipleCursors()
|
|
&& !startMouseMoveCursor->hasSelection()) {
|
|
startMouseMoveCursor.emplace(MultiTextCursor());
|
|
}
|
|
c.setPosition(c.anchor());
|
|
startMouseMoveCursor->addCursor(c);
|
|
}
|
|
MultiTextCursor cursor = *startMouseMoveCursor;
|
|
const QTextCursor anchorCursor = cursor.takeMainCursor();
|
|
const QTextCursor eventCursor = cursorForPosition(e->pos());
|
|
|
|
const TabSettings tabSettings = d->m_document->tabSettings();
|
|
int eventColumn = tabSettings.columnAt(eventCursor.block().text(),
|
|
eventCursor.positionInBlock());
|
|
if (eventCursor.positionInBlock() == eventCursor.block().length() - 1) {
|
|
eventColumn += int((e->pos().x() - cursorRect(eventCursor).center().x())
|
|
/ d->charWidth());
|
|
}
|
|
|
|
int anchorColumn = tabSettings.columnAt(anchorCursor.block().text(),
|
|
anchorCursor.positionInBlock());
|
|
const TextEditorWidgetPrivate::BlockSelection blockSelection = {eventCursor.blockNumber(),
|
|
eventColumn,
|
|
anchorCursor.blockNumber(),
|
|
anchorColumn};
|
|
|
|
cursor.addCursors(d->generateCursorsForBlockSelection(blockSelection));
|
|
if (!cursor.isNull())
|
|
setMultiTextCursor(cursor);
|
|
} else {
|
|
if (startMouseMoveCursor.has_value())
|
|
startMouseMoveCursor.reset();
|
|
if (e->buttons() == Qt::NoButton) {
|
|
const QTextBlock collapsedBlock = d->foldedBlockAt(e->pos());
|
|
const int blockNumber = collapsedBlock.next().blockNumber();
|
|
if (blockNumber < 0) {
|
|
d->clearVisibleFoldedBlock();
|
|
} else if (blockNumber != d->visibleFoldedBlockNumber) {
|
|
d->suggestedVisibleFoldedBlockNumber = blockNumber;
|
|
d->foldedBlockTimer.start(40, this);
|
|
}
|
|
|
|
const RefactorMarker refactorMarker = d->m_refactorOverlay->markerAt(e->pos());
|
|
|
|
// Update the mouse cursor
|
|
if ((collapsedBlock.isValid() || refactorMarker.isValid())
|
|
&& !d->m_mouseOnFoldedMarker) {
|
|
d->m_mouseOnFoldedMarker = true;
|
|
viewport()->setCursor(Qt::PointingHandCursor);
|
|
} else if (!collapsedBlock.isValid() && !refactorMarker.isValid()
|
|
&& d->m_mouseOnFoldedMarker) {
|
|
d->m_mouseOnFoldedMarker = false;
|
|
viewport()->setCursor(Qt::IBeamCursor);
|
|
}
|
|
} else if (!onLink || e->buttons() != Qt::LeftButton
|
|
|| e->modifiers() != Qt::ControlModifier) {
|
|
QPlainTextEdit::mouseMoveEvent(e);
|
|
}
|
|
}
|
|
|
|
if (viewport()->cursor().shape() == Qt::BlankCursor)
|
|
viewport()->setCursor(Qt::IBeamCursor);
|
|
}
|
|
|
|
static bool handleForwardBackwardMouseButtons(QMouseEvent *e)
|
|
{
|
|
if (e->button() == Qt::XButton1) {
|
|
EditorManager::goBackInNavigationHistory();
|
|
return true;
|
|
}
|
|
if (e->button() == Qt::XButton2) {
|
|
EditorManager::goForwardInNavigationHistory();
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
void TextEditorWidget::mousePressEvent(QMouseEvent *e)
|
|
{
|
|
ICore::restartTrimmer();
|
|
|
|
if (e->button() == Qt::LeftButton) {
|
|
MultiTextCursor multiCursor = multiTextCursor();
|
|
const QTextCursor &cursor = cursorForPosition(e->pos());
|
|
if (e->modifiers() & Qt::AltModifier && !(e->modifiers() & Qt::ControlModifier)) {
|
|
if (e->modifiers() & Qt::ShiftModifier) {
|
|
const QTextCursor anchor = multiCursor.takeMainCursor();
|
|
|
|
const TabSettings tabSettings = d->m_document->tabSettings();
|
|
int eventColumn
|
|
= tabSettings.columnAt(cursor.block().text(), cursor.positionInBlock());
|
|
if (cursor.positionInBlock() == cursor.block().length() - 1) {
|
|
eventColumn += int(
|
|
(e->pos().x() - cursorRect(cursor).center().x()) / d->charWidth());
|
|
}
|
|
|
|
const int anchorColumn
|
|
= tabSettings.columnAt(anchor.block().text(), anchor.positionInBlock());
|
|
const TextEditorWidgetPrivate::BlockSelection blockSelection
|
|
= {cursor.blockNumber(), eventColumn, anchor.blockNumber(), anchorColumn};
|
|
|
|
multiCursor.addCursors(d->generateCursorsForBlockSelection(blockSelection));
|
|
setMultiTextCursor(multiCursor);
|
|
} else {
|
|
multiCursor.addCursor(cursor);
|
|
}
|
|
setMultiTextCursor(multiCursor);
|
|
return;
|
|
} else {
|
|
if (multiCursor.hasMultipleCursors())
|
|
setMultiTextCursor(MultiTextCursor({cursor}));
|
|
|
|
QTextBlock foldedBlock = d->foldedBlockAt(e->pos());
|
|
if (foldedBlock.isValid()) {
|
|
d->toggleBlockVisible(foldedBlock);
|
|
viewport()->setCursor(Qt::IBeamCursor);
|
|
}
|
|
|
|
RefactorMarker refactorMarker = d->m_refactorOverlay->markerAt(e->pos());
|
|
if (refactorMarker.isValid()) {
|
|
if (refactorMarker.callback) {
|
|
refactorMarker.callback(this);
|
|
return;
|
|
}
|
|
} else {
|
|
d->m_linkPressed = d->isMouseNavigationEvent(e);
|
|
}
|
|
}
|
|
} else if (e->button() == Qt::RightButton) {
|
|
int eventCursorPosition = cursorForPosition(e->pos()).position();
|
|
if (eventCursorPosition < textCursor().selectionStart()
|
|
|| eventCursorPosition > textCursor().selectionEnd()) {
|
|
setTextCursor(cursorForPosition(e->pos()));
|
|
}
|
|
}
|
|
|
|
if (HostOsInfo::isLinuxHost() && handleForwardBackwardMouseButtons(e))
|
|
return;
|
|
|
|
QPlainTextEdit::mousePressEvent(e);
|
|
}
|
|
|
|
void TextEditorWidget::mouseReleaseEvent(QMouseEvent *e)
|
|
{
|
|
const Qt::MouseButton button = e->button();
|
|
if (d->m_linkPressed && d->isMouseNavigationEvent(e) && button == Qt::LeftButton) {
|
|
bool inNextSplit = ((e->modifiers() & Qt::AltModifier) && !alwaysOpenLinksInNextSplit())
|
|
|| (alwaysOpenLinksInNextSplit() && !(e->modifiers() & Qt::AltModifier));
|
|
|
|
findLinkAt(textCursor(),
|
|
[inNextSplit, self = QPointer<TextEditorWidget>(this)](const Link &symbolLink) {
|
|
if (self && self->openLink(symbolLink, inNextSplit))
|
|
self->d->clearLink();
|
|
}, true, inNextSplit);
|
|
} else if (button == Qt::MiddleButton && !isReadOnly()
|
|
&& QGuiApplication::clipboard()->supportsSelection()) {
|
|
if (!(e->modifiers() & Qt::AltModifier))
|
|
doSetTextCursor(cursorForPosition(e->pos()));
|
|
if (const QMimeData *md = QGuiApplication::clipboard()->mimeData(QClipboard::Selection))
|
|
insertFromMimeData(md);
|
|
e->accept();
|
|
return;
|
|
}
|
|
|
|
if (!HostOsInfo::isLinuxHost() && handleForwardBackwardMouseButtons(e))
|
|
return;
|
|
|
|
// If the refactor marker was pressed then don't propagate release event to editor
|
|
RefactorMarker refactorMarker = d->m_refactorOverlay->markerAt(e->pos());
|
|
if (refactorMarker.isValid()) {
|
|
if (refactorMarker.callback) {
|
|
e->accept();
|
|
return;
|
|
}
|
|
}
|
|
|
|
QPlainTextEdit::mouseReleaseEvent(e);
|
|
|
|
d->setClipboardSelection();
|
|
const QTextCursor plainTextEditCursor = textCursor();
|
|
const QTextCursor multiMainCursor = multiTextCursor().mainCursor();
|
|
if (multiMainCursor.position() != plainTextEditCursor.position()
|
|
|| multiMainCursor.anchor() != plainTextEditCursor.anchor()) {
|
|
doSetTextCursor(plainTextEditCursor, true);
|
|
}
|
|
}
|
|
|
|
void TextEditorWidget::mouseDoubleClickEvent(QMouseEvent *e)
|
|
{
|
|
if (e->button() == Qt::LeftButton) {
|
|
QTextCursor cursor = textCursor();
|
|
const int position = cursor.position();
|
|
if (TextBlockUserData::findPreviousOpenParenthesis(&cursor, false, true)) {
|
|
if (position - cursor.position() == 1 && selectBlockUp())
|
|
return;
|
|
}
|
|
}
|
|
|
|
QTextCursor eventCursor = cursorForPosition(QPoint(e->pos().x(), e->pos().y()));
|
|
const int eventDocumentPosition = eventCursor.position();
|
|
|
|
QPlainTextEdit::mouseDoubleClickEvent(e);
|
|
|
|
// QPlainTextEdit::mouseDoubleClickEvent just selects the word under the text cursor. If the
|
|
// event is triggered on a position that is inbetween two whitespaces this event selects the
|
|
// previous word or nothing if the whitespaces are at the block start. Replace this behavior
|
|
// with selecting the whitespaces starting from the previous word end to the next word.
|
|
const QChar character = characterAt(eventDocumentPosition);
|
|
const QChar prevCharacter = characterAt(eventDocumentPosition - 1);
|
|
|
|
if (character.isSpace() && prevCharacter.isSpace()) {
|
|
if (prevCharacter != QChar::ParagraphSeparator) {
|
|
eventCursor.movePosition(QTextCursor::PreviousWord);
|
|
eventCursor.movePosition(QTextCursor::EndOfWord);
|
|
} else if (character == QChar::ParagraphSeparator) {
|
|
return; // no special handling for empty lines
|
|
}
|
|
eventCursor.movePosition(QTextCursor::NextWord, QTextCursor::KeepAnchor);
|
|
MultiTextCursor cursor = multiTextCursor();
|
|
cursor.replaceMainCursor(eventCursor);
|
|
setMultiTextCursor(cursor);
|
|
}
|
|
}
|
|
|
|
void TextEditorWidgetPrivate::setClipboardSelection()
|
|
{
|
|
QClipboard *clipboard = QGuiApplication::clipboard();
|
|
if (m_cursors.hasSelection() && clipboard->supportsSelection())
|
|
clipboard->setMimeData(q->createMimeDataFromSelection(), QClipboard::Selection);
|
|
}
|
|
|
|
void TextEditorWidget::leaveEvent(QEvent *e)
|
|
{
|
|
// Clear link emulation when the mouse leaves the editor
|
|
d->clearLink();
|
|
QPlainTextEdit::leaveEvent(e);
|
|
}
|
|
|
|
void TextEditorWidget::keyReleaseEvent(QKeyEvent *e)
|
|
{
|
|
if (e->key() == Qt::Key_Control) {
|
|
d->clearLink();
|
|
} else if (e->key() == Qt::Key_Shift && d->m_behaviorSettings.m_constrainHoverTooltips
|
|
&& ToolTip::isVisible()) {
|
|
ToolTip::hide();
|
|
} else if (e->key() == Qt::Key_Alt && d->m_maybeFakeTooltipEvent) {
|
|
d->m_maybeFakeTooltipEvent = false;
|
|
d->processTooltipRequest(textCursor());
|
|
}
|
|
|
|
QPlainTextEdit::keyReleaseEvent(e);
|
|
}
|
|
|
|
void TextEditorWidget::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);
|
|
}
|
|
|
|
static void appendMenuActionsFromContext(QMenu *menu, Id menuContextId)
|
|
{
|
|
ActionContainer *mcontext = ActionManager::actionContainer(menuContextId);
|
|
QMenu *contextMenu = mcontext->menu();
|
|
|
|
const QList<QAction *> actions = contextMenu->actions();
|
|
for (QAction *action : actions)
|
|
menu->addAction(action);
|
|
}
|
|
|
|
void TextEditorWidget::showDefaultContextMenu(QContextMenuEvent *e, Id menuContextId)
|
|
{
|
|
QMenu menu;
|
|
if (menuContextId.isValid())
|
|
appendMenuActionsFromContext(&menu, menuContextId);
|
|
appendStandardContextMenuActions(&menu);
|
|
menu.exec(e->globalPos());
|
|
}
|
|
|
|
void TextEditorWidget::addHoverHandler(BaseHoverHandler *handler)
|
|
{
|
|
if (!d->m_hoverHandlers.contains(handler))
|
|
d->m_hoverHandlers.append(handler);
|
|
}
|
|
|
|
void TextEditorWidget::removeHoverHandler(BaseHoverHandler *handler)
|
|
{
|
|
if (d->m_hoverHandlers.removeAll(handler) > 0)
|
|
d->m_hoverHandlerRunner.handlerRemoved(handler);
|
|
}
|
|
|
|
void TextEditorWidget::insertSuggestion(std::unique_ptr<TextSuggestion> &&suggestion)
|
|
{
|
|
d->insertSuggestion(std::move(suggestion));
|
|
}
|
|
|
|
void TextEditorWidget::clearSuggestion()
|
|
{
|
|
d->clearCurrentSuggestion();
|
|
}
|
|
|
|
TextSuggestion *TextEditorWidget::currentSuggestion() const
|
|
{
|
|
if (d->m_suggestionBlock.isValid())
|
|
return TextDocumentLayout::suggestion(d->m_suggestionBlock);
|
|
return nullptr;
|
|
}
|
|
|
|
bool TextEditorWidget::suggestionVisible() const
|
|
{
|
|
return currentSuggestion();
|
|
}
|
|
|
|
bool TextEditorWidget::suggestionsBlocked() const
|
|
{
|
|
return d->m_suggestionBlocker.use_count() > 1;
|
|
}
|
|
|
|
TextEditorWidget::SuggestionBlocker TextEditorWidget::blockSuggestions()
|
|
{
|
|
if (!suggestionsBlocked())
|
|
clearSuggestion();
|
|
return d->m_suggestionBlocker;
|
|
}
|
|
|
|
std::unique_ptr<EmbeddedWidgetInterface> TextEditorWidget::insertWidget(QWidget *widget, int line)
|
|
{
|
|
return d->insertWidget(widget, line);
|
|
}
|
|
|
|
QList<QTextCursor> TextEditorWidget::autoCompleteHighlightPositions() const
|
|
{
|
|
return d->m_autoCompleteHighlightPos;
|
|
}
|
|
|
|
#ifdef WITH_TESTS
|
|
void TextEditorWidget::processTooltipRequest(const QTextCursor &c)
|
|
{
|
|
d->processTooltipRequest(c);
|
|
}
|
|
#endif
|
|
|
|
void TextEditorWidget::extraAreaLeaveEvent(QEvent *)
|
|
{
|
|
d->extraAreaPreviousMarkTooltipRequestedLine = -1;
|
|
ToolTip::hide();
|
|
|
|
// fake missing mouse move event from Qt
|
|
QMouseEvent me(QEvent::MouseMove, QPoint(-1, -1), QCursor::pos(), Qt::NoButton, {}, {});
|
|
extraAreaMouseEvent(&me);
|
|
}
|
|
|
|
static bool xIsInsideFoldingRegion(int x, int extraAreaWidth, const QFontMetrics &fm)
|
|
{
|
|
int boxWidth = 0;
|
|
if (TextEditorSettings::fontSettings().relativeLineSpacing() == 100)
|
|
boxWidth = foldBoxWidth(fm);
|
|
else
|
|
boxWidth = foldBoxWidth();
|
|
|
|
return x > extraAreaWidth - boxWidth && x <= extraAreaWidth;
|
|
}
|
|
|
|
void TextEditorWidget::extraAreaContextMenuEvent(QContextMenuEvent *e)
|
|
{
|
|
if (d->m_codeFoldingVisible
|
|
&& xIsInsideFoldingRegion(e->pos().x(), extraArea()->width(), fontMetrics())) {
|
|
const QTextCursor cursor = cursorForPosition(QPoint(0, e->pos().y()));
|
|
const QTextBlock block = cursor.block();
|
|
auto menu = new QMenu(this);
|
|
|
|
menu->addAction(Tr::tr("Fold"), this, [&] { fold(block); });
|
|
menu->addAction(Tr::tr("Fold Recursively"), this, [&] { fold(block, true); });
|
|
menu->addAction(Tr::tr("Fold All"), this, [this] { unfoldAll(/* unfold = */ false); });
|
|
menu->addAction(Tr::tr("Unfold"), this, [&] { unfold(block); });
|
|
menu->addAction(Tr::tr("Unfold Recursively"), this, [&] { unfold(block, true); });
|
|
menu->addAction(Tr::tr("Unfold All"), this, [this] { unfoldAll(/* fold = */ true); });
|
|
menu->exec(e->globalPos());
|
|
|
|
delete menu;
|
|
e->accept();
|
|
return;
|
|
}
|
|
|
|
if (d->m_marksVisible) {
|
|
QTextCursor cursor = cursorForPosition(QPoint(0, e->pos().y()));
|
|
auto contextMenu = new QMenu(this);
|
|
bookmarkManager().requestContextMenu(textDocument()->filePath(),
|
|
cursor.blockNumber() + 1,
|
|
contextMenu);
|
|
emit markContextMenuRequested(this, cursor.blockNumber() + 1, contextMenu);
|
|
if (!contextMenu->isEmpty())
|
|
contextMenu->exec(e->globalPos());
|
|
delete contextMenu;
|
|
e->accept();
|
|
}
|
|
}
|
|
|
|
void TextEditorWidget::updateFoldingHighlight(const QPoint &pos)
|
|
{
|
|
if (!d->m_codeFoldingVisible)
|
|
return;
|
|
|
|
// Update which folder marker is highlighted
|
|
QTextCursor cursor;
|
|
if (xIsInsideFoldingRegion(pos.x(), extraArea()->width(), fontMetrics()))
|
|
cursor = cursorForPosition(QPoint(0, pos.y()));
|
|
else if (d->m_displaySettings.m_highlightBlocks)
|
|
cursor = textCursor();
|
|
|
|
updateFoldingHighlight(cursor);
|
|
}
|
|
|
|
void TextEditorWidget::updateFoldingHighlight(const QTextCursor &cursor)
|
|
{
|
|
const int highlightBlockNumber = d->extraAreaHighlightFoldedBlockNumber;
|
|
const bool curserIsNull = !cursor.isNull();
|
|
if (curserIsNull)
|
|
d->extraAreaHighlightFoldedBlockNumber = cursor.blockNumber();
|
|
else
|
|
d->extraAreaHighlightFoldedBlockNumber = -1;
|
|
|
|
if (curserIsNull || (highlightBlockNumber != d->extraAreaHighlightFoldedBlockNumber))
|
|
d->m_highlightBlocksTimer.start(d->m_highlightBlocksInfo.isEmpty() ? 120 : 0);
|
|
}
|
|
|
|
void TextEditorWidget::extraAreaToolTipEvent(QHelpEvent *e)
|
|
{
|
|
QTextCursor cursor = cursorForPosition(QPoint(0, e->pos().y()));
|
|
|
|
int markWidth = 0;
|
|
extraAreaWidth(&markWidth);
|
|
const bool inMarkArea = e->pos().x() <= markWidth && e->pos().x() >= 0;
|
|
if (!inMarkArea)
|
|
return;
|
|
int line = cursor.blockNumber() + 1;
|
|
if (d->extraAreaPreviousMarkTooltipRequestedLine != line) {
|
|
if (auto data = static_cast<TextBlockUserData *>(cursor.block().userData())) {
|
|
if (!data->marks().isEmpty())
|
|
d->showTextMarksToolTip(mapToGlobal(e->pos()), data->marks());
|
|
}
|
|
}
|
|
d->extraAreaPreviousMarkTooltipRequestedLine = line;
|
|
}
|
|
|
|
void TextEditorWidget::extraAreaMouseEvent(QMouseEvent *e)
|
|
{
|
|
QTextCursor cursor = cursorForPosition(QPoint(0, e->pos().y()));
|
|
|
|
int markWidth = 0;
|
|
extraAreaWidth(&markWidth);
|
|
const bool inMarkArea = e->pos().x() <= markWidth && e->pos().x() >= 0;
|
|
|
|
if (d->m_codeFoldingVisible
|
|
&& e->type() == QEvent::MouseMove && e->buttons() == 0) { // mouse tracking
|
|
updateFoldingHighlight(e->pos());
|
|
}
|
|
|
|
// Set whether the mouse cursor is a hand or normal arrow
|
|
if (e->type() == QEvent::MouseMove) {
|
|
if (inMarkArea) {
|
|
// tool tips are shown in extraAreaToolTipEvent
|
|
int line = cursor.blockNumber() + 1;
|
|
if (d->extraAreaPreviousMarkTooltipRequestedLine != line) {
|
|
if (auto data = static_cast<TextBlockUserData *>(cursor.block().userData())) {
|
|
if (data->marks().isEmpty())
|
|
ToolTip::hide();
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!d->m_markDragging && e->buttons() & Qt::LeftButton && !d->m_markDragStart.isNull()) {
|
|
int dist = (e->pos() - d->m_markDragStart).manhattanLength();
|
|
if (dist > QApplication::startDragDistance()) {
|
|
d->m_markDragging = true;
|
|
const int height = fontMetrics().lineSpacing() - 1;
|
|
d->m_markDragCursor = QCursor(d->m_dragMark->icon().pixmap({height, height}));
|
|
d->m_dragMark->setVisible(false);
|
|
QGuiApplication::setOverrideCursor(d->m_markDragCursor);
|
|
}
|
|
}
|
|
|
|
if (d->m_markDragging) {
|
|
QGuiApplication::changeOverrideCursor(inMarkArea ? d->m_markDragCursor
|
|
: QCursor(Qt::ForbiddenCursor));
|
|
} else if (inMarkArea != (d->m_extraArea->cursor().shape() == Qt::PointingHandCursor)) {
|
|
d->m_extraArea->setCursor(inMarkArea ? Qt::PointingHandCursor : Qt::ArrowCursor);
|
|
}
|
|
}
|
|
|
|
if (e->type() == QEvent::MouseButtonPress || e->type() == QEvent::MouseButtonDblClick) {
|
|
if (e->button() == Qt::LeftButton) {
|
|
int boxWidth = 0;
|
|
if (TextEditorSettings::fontSettings().relativeLineSpacing() == 100)
|
|
boxWidth = foldBoxWidth(fontMetrics());
|
|
else
|
|
boxWidth = foldBoxWidth();
|
|
if (d->m_codeFoldingVisible && e->pos().x() > extraArea()->width() - boxWidth) {
|
|
if (!cursor.block().next().isVisible()) {
|
|
d->toggleBlockVisible(cursor.block());
|
|
d->moveCursorVisible(false);
|
|
} else if (d->foldBox().contains(e->pos())) {
|
|
cursor.setPosition(
|
|
document()->findBlockByNumber(d->m_highlightBlocksInfo.open.last()).position()
|
|
);
|
|
QTextBlock c = cursor.block();
|
|
d->toggleBlockVisible(c);
|
|
d->moveCursorVisible(false);
|
|
}
|
|
} else if (d->m_lineNumbersVisible && !inMarkArea) {
|
|
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();
|
|
d->m_markDragging = false;
|
|
QTextBlock block = cursor.document()->findBlockByNumber(d->extraAreaToggleMarkBlockNumber);
|
|
if (auto data = static_cast<TextBlockUserData *>(block.userData())) {
|
|
TextMarks marks = data->marks();
|
|
for (int i = marks.size(); --i >= 0; ) {
|
|
TextMark *mark = marks.at(i);
|
|
if (mark->isDraggable()) {
|
|
d->m_markDragStart = e->pos();
|
|
d->m_dragMark = mark;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} 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;
|
|
const bool sameLine = cursor.blockNumber() == n;
|
|
const bool wasDragging = d->m_markDragging;
|
|
TextMark *dragMark = d->m_dragMark;
|
|
d->m_dragMark = nullptr;
|
|
d->m_markDragging = false;
|
|
d->m_markDragStart = QPoint();
|
|
if (dragMark)
|
|
dragMark->setVisible(true);
|
|
QGuiApplication::restoreOverrideCursor();
|
|
if (wasDragging && dragMark) {
|
|
dragMark->dragToLine(cursor.blockNumber() + 1);
|
|
return;
|
|
} else if (sameLine) {
|
|
QTextBlock block = cursor.document()->findBlockByNumber(n);
|
|
if (auto data = static_cast<TextBlockUserData *>(block.userData())) {
|
|
TextMarks marks = data->marks();
|
|
for (int i = marks.size(); --i >= 0; ) {
|
|
TextMark *mark = marks.at(i);
|
|
if (mark->isClickable()) {
|
|
mark->clicked();
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
int line = n + 1;
|
|
if (QApplication::keyboardModifiers() & Qt::ShiftModifier) {
|
|
if (!textDocument()->isTemporary())
|
|
bookmarkManager().toggleBookmark(textDocument()->filePath(), line);
|
|
} else {
|
|
emit markRequested(this, line, BreakpointRequest);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void TextEditorWidget::ensureCursorVisible()
|
|
{
|
|
ensureBlockIsUnfolded(textCursor().block());
|
|
QPlainTextEdit::ensureCursorVisible();
|
|
}
|
|
|
|
void TextEditorWidget::ensureBlockIsUnfolded(QTextBlock block)
|
|
{
|
|
if (singleShotAfterHighlightingDone([this, block] { ensureBlockIsUnfolded(block); }))
|
|
return;
|
|
|
|
if (!block.isVisible()) {
|
|
auto documentLayout = qobject_cast<TextDocumentLayout*>(document()->documentLayout());
|
|
QTC_ASSERT(documentLayout, return);
|
|
|
|
// Open all parent folds of current line.
|
|
int indent = TextDocumentLayout::foldingIndent(block);
|
|
block = block.previous();
|
|
while (block.isValid()) {
|
|
const int indent2 = TextDocumentLayout::foldingIndent(block);
|
|
if (TextDocumentLayout::canFold(block) && indent2 < indent) {
|
|
TextDocumentLayout::doFoldOrUnfold(block, /* unfold = */ true);
|
|
if (block.isVisible())
|
|
break;
|
|
indent = indent2;
|
|
}
|
|
block = block.previous();
|
|
}
|
|
|
|
documentLayout->requestUpdate();
|
|
documentLayout->emitDocumentSizeChanged();
|
|
}
|
|
}
|
|
|
|
void TextEditorWidgetPrivate::toggleBlockVisible(const QTextBlock &block)
|
|
{
|
|
if (q->singleShotAfterHighlightingDone([this, block] { toggleBlockVisible(block); }))
|
|
return;
|
|
|
|
auto documentLayout = qobject_cast<TextDocumentLayout*>(q->document()->documentLayout());
|
|
QTC_ASSERT(documentLayout, return);
|
|
|
|
TextDocumentLayout::doFoldOrUnfold(block, TextDocumentLayout::isFolded(block));
|
|
documentLayout->requestUpdate();
|
|
documentLayout->emitDocumentSizeChanged();
|
|
}
|
|
|
|
void TextEditorWidget::setLanguageSettingsId(Id settingsId)
|
|
{
|
|
d->m_tabSettingsId = settingsId;
|
|
if (auto doc = textDocument())
|
|
doc->setCodeStyle(TextEditorSettings::codeStyle(settingsId));
|
|
}
|
|
|
|
Id TextEditorWidget::languageSettingsId() const
|
|
{
|
|
return d->m_tabSettingsId;
|
|
}
|
|
|
|
const DisplaySettings &TextEditorWidget::displaySettings() const
|
|
{
|
|
return d->m_displaySettings;
|
|
}
|
|
|
|
const MarginSettings &TextEditorWidget::marginSettings() const
|
|
{
|
|
return d->m_marginSettings;
|
|
}
|
|
|
|
const BehaviorSettings &TextEditorWidget::behaviorSettings() const
|
|
{
|
|
return d->m_behaviorSettings;
|
|
}
|
|
|
|
void TextEditorWidgetPrivate::handleHomeKey(bool anchor, bool block)
|
|
{
|
|
const QTextCursor::MoveMode mode = anchor ? QTextCursor::KeepAnchor
|
|
: QTextCursor::MoveAnchor;
|
|
|
|
MultiTextCursor cursor = q->multiTextCursor();
|
|
for (QTextCursor &c : cursor) {
|
|
const int initpos = c.position();
|
|
int pos = c.block().position();
|
|
|
|
if (!block) {
|
|
// only go to the first non space if we are in the first line of the layout
|
|
if (QTextLayout *layout = c.block().layout();
|
|
layout->lineForTextPosition(initpos - pos).lineNumber() != 0) {
|
|
c.movePosition(QTextCursor::StartOfLine, mode);
|
|
continue;
|
|
}
|
|
}
|
|
|
|
QChar character = q->document()->characterAt(pos);
|
|
const QLatin1Char tab = QLatin1Char('\t');
|
|
|
|
while (character == tab || character.category() == QChar::Separator_Space) {
|
|
++pos;
|
|
if (pos == initpos)
|
|
break;
|
|
character = q->document()->characterAt(pos);
|
|
}
|
|
|
|
// Go to the start of the block when we're already at the start of the text
|
|
if (pos == initpos)
|
|
pos = c.block().position();
|
|
|
|
c.setPosition(pos, mode);
|
|
}
|
|
q->setMultiTextCursor(cursor);
|
|
}
|
|
|
|
void TextEditorWidgetPrivate::handleBackspaceKey()
|
|
{
|
|
QTC_ASSERT(!q->multiTextCursor().hasSelection(), return);
|
|
MultiTextCursor cursor = m_cursors;
|
|
cursor.beginEditBlock();
|
|
|
|
const TabSettings tabSettings = m_document->tabSettings();
|
|
const TypingSettings &typingSettings = m_document->typingSettings();
|
|
|
|
auto behavior = typingSettings.m_smartBackspaceBehavior;
|
|
if (cursor.hasMultipleCursors()) {
|
|
if (behavior == TypingSettings::BackspaceFollowsPreviousIndents) {
|
|
behavior = TypingSettings::BackspaceNeverIndents;
|
|
} else if (behavior == TypingSettings::BackspaceUnindents) {
|
|
for (QTextCursor &c : cursor) {
|
|
if (c.positionInBlock() == 0
|
|
|| c.positionInBlock() > TabSettings::firstNonSpace(c.block().text())) {
|
|
behavior = TypingSettings::BackspaceNeverIndents;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
for (QTextCursor &c : cursor) {
|
|
const int pos = c.position();
|
|
if (!pos)
|
|
continue;
|
|
|
|
bool cursorWithinSnippet = false;
|
|
if (m_snippetOverlay->isVisible()) {
|
|
QTextCursor snippetCursor = c;
|
|
snippetCursor.movePosition(QTextCursor::Left);
|
|
cursorWithinSnippet = snippetCheckCursor(snippetCursor);
|
|
}
|
|
|
|
if (typingSettings.m_autoIndent && !m_autoCompleteHighlightPos.isEmpty()
|
|
&& (m_autoCompleteHighlightPos.last() == c) && m_removeAutoCompletedText
|
|
&& m_autoCompleter->autoBackspace(c)) {
|
|
continue;
|
|
}
|
|
|
|
bool handled = false;
|
|
if (behavior == TypingSettings::BackspaceNeverIndents) {
|
|
if (cursorWithinSnippet)
|
|
c.beginEditBlock();
|
|
c.deletePreviousChar();
|
|
handled = true;
|
|
} else if (behavior
|
|
== TypingSettings::BackspaceFollowsPreviousIndents) {
|
|
QTextBlock currentBlock = c.block();
|
|
int positionInBlock = pos - currentBlock.position();
|
|
const QString blockText = currentBlock.text();
|
|
if (c.atBlockStart() || TabSettings::firstNonSpace(blockText) < positionInBlock) {
|
|
if (cursorWithinSnippet)
|
|
c.beginEditBlock();
|
|
c.deletePreviousChar();
|
|
handled = true;
|
|
} else {
|
|
if (cursorWithinSnippet)
|
|
m_snippetOverlay->accept();
|
|
cursorWithinSnippet = false;
|
|
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) {
|
|
c.beginEditBlock();
|
|
c.setPosition(currentBlock.position(), QTextCursor::KeepAnchor);
|
|
c.insertText(tabSettings.indentationString(previousNonEmptyBlockText));
|
|
c.endEditBlock();
|
|
handled = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
} else if (behavior == TypingSettings::BackspaceUnindents) {
|
|
if (c.positionInBlock() == 0
|
|
|| c.positionInBlock() > TabSettings::firstNonSpace(c.block().text())) {
|
|
if (cursorWithinSnippet)
|
|
c.beginEditBlock();
|
|
c.deletePreviousChar();
|
|
} else {
|
|
if (cursorWithinSnippet)
|
|
m_snippetOverlay->accept();
|
|
cursorWithinSnippet = false;
|
|
c = m_document->unindent(MultiTextCursor({c})).mainCursor();
|
|
}
|
|
handled = true;
|
|
}
|
|
|
|
if (!handled) {
|
|
if (cursorWithinSnippet)
|
|
c.beginEditBlock();
|
|
c.deletePreviousChar();
|
|
}
|
|
|
|
if (cursorWithinSnippet) {
|
|
c.endEditBlock();
|
|
m_snippetOverlay->updateEquivalentSelections(c);
|
|
}
|
|
}
|
|
cursor.endEditBlock();
|
|
q->setMultiTextCursor(cursor);
|
|
}
|
|
|
|
void TextEditorWidget::wheelEvent(QWheelEvent *e)
|
|
{
|
|
d->clearVisibleFoldedBlock();
|
|
if (e->modifiers() & Qt::ControlModifier) {
|
|
if (!scrollWheelZoomingEnabled()) {
|
|
// When the setting is disabled globally,
|
|
// we have to skip calling QPlainTextEdit::wheelEvent()
|
|
// that changes zoom in it.
|
|
return;
|
|
}
|
|
|
|
const int deltaY = e->angleDelta().y();
|
|
if (deltaY != 0)
|
|
zoomF(deltaY / 120.f);
|
|
return;
|
|
}
|
|
QPlainTextEdit::wheelEvent(e);
|
|
}
|
|
|
|
static void showZoomIndicator(QWidget *editor, const int newZoom)
|
|
{
|
|
Utils::FadingIndicator::showText(editor,
|
|
Tr::tr("Zoom: %1%").arg(newZoom),
|
|
Utils::FadingIndicator::SmallText);
|
|
}
|
|
|
|
void TextEditorWidget::increaseFontZoom()
|
|
{
|
|
d->clearVisibleFoldedBlock();
|
|
showZoomIndicator(this, TextEditorSettings::increaseFontZoom());
|
|
}
|
|
|
|
void TextEditorWidget::decreaseFontZoom()
|
|
{
|
|
d->clearVisibleFoldedBlock();
|
|
showZoomIndicator(this, TextEditorSettings::decreaseFontZoom());
|
|
}
|
|
|
|
void TextEditorWidget::zoomF(float delta)
|
|
{
|
|
d->clearVisibleFoldedBlock();
|
|
float step = 10.f * delta;
|
|
// Ensure we always zoom a minimal step in-case the resolution is more than 16x
|
|
if (step > 0 && step < 1)
|
|
step = 1;
|
|
else if (step < 0 && step > -1)
|
|
step = -1;
|
|
|
|
const int newZoom = TextEditorSettings::increaseFontZoom(int(step));
|
|
showZoomIndicator(this, newZoom);
|
|
}
|
|
|
|
void TextEditorWidget::zoomReset()
|
|
{
|
|
TextEditorSettings::resetFontZoom();
|
|
showZoomIndicator(this, 100);
|
|
}
|
|
|
|
void TextEditorWidget::findLinkAt(const QTextCursor &cursor,
|
|
const Utils::LinkHandler &callback,
|
|
bool resolveTarget,
|
|
bool inNextSplit)
|
|
{
|
|
emit requestLinkAt(cursor, callback, resolveTarget, inNextSplit);
|
|
}
|
|
|
|
void TextEditorWidget::findTypeAt(const QTextCursor &cursor,
|
|
const Utils::LinkHandler &callback,
|
|
bool resolveTarget,
|
|
bool inNextSplit)
|
|
{
|
|
emit requestTypeAt(cursor, callback, resolveTarget, inNextSplit);
|
|
}
|
|
|
|
bool TextEditorWidget::openLink(const Utils::Link &link, bool inNextSplit)
|
|
{
|
|
#ifdef WITH_TESTS
|
|
struct Signaller {
|
|
~Signaller() { emit EditorManager::instance()->linkOpened(); }
|
|
} s;
|
|
#endif
|
|
|
|
QString url = link.targetFilePath.toString();
|
|
if (url.startsWith(u"https://") || url.startsWith(u"http://")) {
|
|
QDesktopServices::openUrl(url);
|
|
return true;
|
|
}
|
|
|
|
if (!inNextSplit && textDocument()->filePath() == link.targetFilePath) {
|
|
emit addCurrentStateToNavigationHistory();
|
|
gotoLine(link.targetLine, link.targetColumn, true, true);
|
|
setFocus();
|
|
return true;
|
|
}
|
|
if (!link.hasValidTarget())
|
|
return false;
|
|
EditorManager::OpenEditorFlags flags;
|
|
if (inNextSplit)
|
|
flags |= EditorManager::OpenInOtherSplit;
|
|
|
|
return EditorManager::openEditorAt(link, Id(), flags);
|
|
}
|
|
|
|
bool TextEditorWidgetPrivate::isMouseNavigationEvent(QMouseEvent *e) const
|
|
{
|
|
return q->mouseNavigationEnabled() && e->modifiers() & Qt::ControlModifier
|
|
&& !(e->modifiers() & Qt::ShiftModifier);
|
|
}
|
|
|
|
void TextEditorWidgetPrivate::requestUpdateLink(QMouseEvent *e)
|
|
{
|
|
if (!isMouseNavigationEvent(e))
|
|
return;
|
|
// Link emulation behaviour for 'go to definition'
|
|
const QTextCursor cursor = q->cursorForPosition(e->pos());
|
|
|
|
// Avoid updating the link we already found
|
|
if (cursor.position() >= m_currentLink.linkTextStart
|
|
&& cursor.position() <= m_currentLink.linkTextEnd)
|
|
return;
|
|
|
|
// Check that the mouse was actually on the text somewhere
|
|
const int posX = e->position().x();
|
|
bool onText = q->cursorRect(cursor).right() >= posX;
|
|
if (!onText) {
|
|
QTextCursor nextPos = cursor;
|
|
nextPos.movePosition(QTextCursor::Right);
|
|
onText = q->cursorRect(nextPos).right() >= posX;
|
|
}
|
|
|
|
if (onText) {
|
|
m_pendingLinkUpdate = cursor;
|
|
QMetaObject::invokeMethod(this, &TextEditorWidgetPrivate::updateLink, Qt::QueuedConnection);
|
|
return;
|
|
}
|
|
|
|
clearLink();
|
|
}
|
|
|
|
void TextEditorWidgetPrivate::updateLink()
|
|
{
|
|
if (m_pendingLinkUpdate.isNull())
|
|
return;
|
|
if (m_pendingLinkUpdate == m_lastLinkUpdate)
|
|
return;
|
|
|
|
m_lastLinkUpdate = m_pendingLinkUpdate;
|
|
q->findLinkAt(m_pendingLinkUpdate,
|
|
[parent = QPointer<TextEditorWidget>(q), this](const Link &link) {
|
|
if (!parent)
|
|
return;
|
|
|
|
if (link.hasValidLinkText())
|
|
showLink(link);
|
|
else
|
|
clearLink();
|
|
}, false);
|
|
}
|
|
|
|
void TextEditorWidgetPrivate::showLink(const Utils::Link &link)
|
|
{
|
|
if (m_currentLink == link)
|
|
return;
|
|
|
|
QTextEdit::ExtraSelection sel;
|
|
sel.cursor = q->textCursor();
|
|
sel.cursor.setPosition(link.linkTextStart);
|
|
sel.cursor.setPosition(link.linkTextEnd, QTextCursor::KeepAnchor);
|
|
sel.format = m_document->fontSettings().toTextCharFormat(C_LINK);
|
|
sel.format.setFontUnderline(true);
|
|
q->setExtraSelections(TextEditorWidget::OtherSelection, QList<QTextEdit::ExtraSelection>() << sel);
|
|
q->viewport()->setCursor(Qt::PointingHandCursor);
|
|
m_currentLink = link;
|
|
}
|
|
|
|
void TextEditorWidgetPrivate::clearLink()
|
|
{
|
|
m_pendingLinkUpdate = QTextCursor();
|
|
m_lastLinkUpdate = QTextCursor();
|
|
if (!m_currentLink.hasValidLinkText())
|
|
return;
|
|
|
|
q->setExtraSelections(TextEditorWidget::OtherSelection, QList<QTextEdit::ExtraSelection>());
|
|
q->viewport()->setCursor(Qt::IBeamCursor);
|
|
m_currentLink = Utils::Link();
|
|
}
|
|
|
|
void TextEditorWidgetPrivate::highlightSearchResultsSlot(const QString &txt, FindFlags findFlags)
|
|
{
|
|
const QString pattern = (findFlags & FindRegularExpression) ? txt
|
|
: QRegularExpression::escape(txt);
|
|
const QRegularExpression::PatternOptions options
|
|
= (findFlags & FindCaseSensitively) ? QRegularExpression::NoPatternOption
|
|
: QRegularExpression::CaseInsensitiveOption;
|
|
if (m_searchExpr.pattern() == pattern && m_searchExpr.patternOptions() == options)
|
|
return;
|
|
m_searchExpr.setPattern(pattern);
|
|
m_searchExpr.setPatternOptions(options);
|
|
m_findText = txt;
|
|
m_findFlags = findFlags;
|
|
|
|
m_delayedUpdateTimer.start(50);
|
|
|
|
if (m_highlightScrollBarController)
|
|
m_scrollBarUpdateTimer.start(50);
|
|
}
|
|
|
|
void TextEditorWidgetPrivate::adjustScrollBarRanges()
|
|
{
|
|
if (!m_highlightScrollBarController)
|
|
return;
|
|
const double lineSpacing = TextEditorSettings::fontSettings().lineSpacing();
|
|
if (lineSpacing == 0)
|
|
return;
|
|
|
|
m_highlightScrollBarController->setLineHeight(lineSpacing);
|
|
m_highlightScrollBarController->setVisibleRange(q->viewport()->rect().height());
|
|
m_highlightScrollBarController->setMargin(q->textDocument()->document()->documentMargin());
|
|
}
|
|
|
|
void TextEditorWidgetPrivate::highlightSearchResultsInScrollBar()
|
|
{
|
|
if (!m_highlightScrollBarController)
|
|
return;
|
|
m_highlightScrollBarController->removeHighlights(Constants::SCROLL_BAR_SEARCH_RESULT);
|
|
m_searchResults.clear();
|
|
|
|
if (m_searchFuture.isRunning())
|
|
m_searchFuture.cancel();
|
|
|
|
const QString &txt = m_findText;
|
|
if (txt.isEmpty())
|
|
return;
|
|
|
|
adjustScrollBarRanges();
|
|
|
|
m_searchFuture = Utils::asyncRun(Utils::searchInContents,
|
|
txt,
|
|
m_findFlags,
|
|
m_document->filePath(),
|
|
m_document->plainText());
|
|
Utils::onResultReady(m_searchFuture, this, [this](const SearchResultItems &resultList) {
|
|
QList<SearchResult> results;
|
|
for (const SearchResultItem &result : resultList) {
|
|
int start = result.mainRange().begin.toPositionInDocument(m_document->document());
|
|
if (start < 0)
|
|
continue;
|
|
int end = result.mainRange().end.toPositionInDocument(m_document->document());
|
|
if (end < 0)
|
|
continue;
|
|
if (start > end)
|
|
std::swap(start, end);
|
|
if (m_find->inScope(start, end))
|
|
results << SearchResult{start, start - end};
|
|
}
|
|
m_searchResults << results;
|
|
addSearchResultsToScrollBar(results);
|
|
});
|
|
}
|
|
|
|
void TextEditorWidgetPrivate::scheduleUpdateHighlightScrollBar()
|
|
{
|
|
if (m_scrollBarUpdateScheduled)
|
|
return;
|
|
|
|
m_scrollBarUpdateScheduled = true;
|
|
QMetaObject::invokeMethod(this, &TextEditorWidgetPrivate::updateHighlightScrollBarNow,
|
|
Qt::QueuedConnection);
|
|
}
|
|
|
|
Highlight::Priority textMarkPrioToScrollBarPrio(const TextMark::Priority &prio)
|
|
{
|
|
switch (prio) {
|
|
case TextMark::LowPriority:
|
|
return Highlight::LowPriority;
|
|
case TextMark::NormalPriority:
|
|
return Highlight::NormalPriority;
|
|
case TextMark::HighPriority:
|
|
return Highlight::HighPriority;
|
|
default:
|
|
return Highlight::NormalPriority;
|
|
}
|
|
}
|
|
|
|
void TextEditorWidgetPrivate::addSearchResultsToScrollBar(const QVector<SearchResult> &results)
|
|
{
|
|
if (!m_highlightScrollBarController)
|
|
return;
|
|
for (const SearchResult &result : results) {
|
|
const QTextBlock &block = q->document()->findBlock(result.start);
|
|
if (block.isValid() && block.isVisible()) {
|
|
if (q->lineWrapMode() == QPlainTextEdit::WidgetWidth) {
|
|
const int firstLine = block.layout()->lineForTextPosition(result.start - block.position()).lineNumber();
|
|
const int lastLine = block.layout()->lineForTextPosition(result.start - block.position() + result.length).lineNumber();
|
|
for (int line = firstLine; line <= lastLine; ++line) {
|
|
m_highlightScrollBarController->addHighlight(
|
|
{Constants::SCROLL_BAR_SEARCH_RESULT, block.firstLineNumber() + line,
|
|
Theme::TextEditor_SearchResult_ScrollBarColor, Highlight::HighPriority});
|
|
}
|
|
} else {
|
|
m_highlightScrollBarController->addHighlight(
|
|
{Constants::SCROLL_BAR_SEARCH_RESULT,
|
|
block.blockNumber(),
|
|
Theme::TextEditor_SearchResult_ScrollBarColor,
|
|
Highlight::HighPriority});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void TextEditorWidgetPrivate::addSelectionHighlightToScrollBar(
|
|
const QVector<SearchResult> &selections)
|
|
{
|
|
if (!m_highlightScrollBarController)
|
|
return;
|
|
for (const SearchResult &result : selections) {
|
|
const QTextBlock &block = q->document()->findBlock(result.start);
|
|
if (block.isValid() && block.isVisible()) {
|
|
if (q->lineWrapMode() == QPlainTextEdit::WidgetWidth) {
|
|
const int firstLine = block.layout()->lineForTextPosition(result.start - block.position()).lineNumber();
|
|
const int lastLine = block.layout()->lineForTextPosition(result.start - block.position() + result.length).lineNumber();
|
|
for (int line = firstLine; line <= lastLine; ++line) {
|
|
m_highlightScrollBarController->addHighlight(
|
|
{Constants::SCROLL_BAR_SELECTION, block.firstLineNumber() + line,
|
|
Theme::TextEditor_Selection_ScrollBarColor, Highlight::NormalPriority});
|
|
}
|
|
} else {
|
|
m_highlightScrollBarController->addHighlight(
|
|
{Constants::SCROLL_BAR_SELECTION,
|
|
block.blockNumber(),
|
|
Theme::TextEditor_Selection_ScrollBarColor,
|
|
Highlight::NormalPriority});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Highlight markToHighlight(TextMark *mark, int lineNumber)
|
|
{
|
|
return Highlight(mark->category().id,
|
|
lineNumber,
|
|
mark->color().value_or(Utils::Theme::TextColorNormal),
|
|
textMarkPrioToScrollBarPrio(mark->priority()));
|
|
}
|
|
|
|
void TextEditorWidgetPrivate::updateHighlightScrollBarNow()
|
|
{
|
|
m_scrollBarUpdateScheduled = false;
|
|
if (!m_highlightScrollBarController)
|
|
return;
|
|
|
|
m_highlightScrollBarController->removeAllHighlights();
|
|
|
|
updateCurrentLineInScrollbar();
|
|
|
|
// update search results
|
|
addSearchResultsToScrollBar(m_searchResults);
|
|
|
|
// update search selection
|
|
addSelectionHighlightToScrollBar(m_selectionResults);
|
|
|
|
// update text marks
|
|
const TextMarks marks = m_document->marks();
|
|
for (TextMark *mark : marks) {
|
|
if (!mark->isVisible() || !mark->color().has_value())
|
|
continue;
|
|
const QTextBlock &block = q->document()->findBlockByNumber(mark->lineNumber() - 1);
|
|
if (block.isVisible())
|
|
m_highlightScrollBarController->addHighlight(markToHighlight(mark, block.firstLineNumber()));
|
|
}
|
|
}
|
|
|
|
MultiTextCursor TextEditorWidget::multiTextCursor() const
|
|
{
|
|
return d->m_cursors;
|
|
}
|
|
|
|
void TextEditorWidget::setMultiTextCursor(const Utils::MultiTextCursor &cursor)
|
|
{
|
|
if (cursor == d->m_cursors)
|
|
return;
|
|
|
|
const MultiTextCursor oldCursor = d->m_cursors;
|
|
const_cast<MultiTextCursor &>(d->m_cursors) = cursor;
|
|
doSetTextCursor(d->m_cursors.mainCursor(), /*keepMultiSelection*/ true);
|
|
QRect updateRect = d->cursorUpdateRect(oldCursor);
|
|
if (d->m_highlightCurrentLine)
|
|
updateRect = QRect(0, updateRect.y(), viewport()->rect().width(), updateRect.height());
|
|
updateRect |= d->cursorUpdateRect(d->m_cursors);
|
|
viewport()->update(updateRect);
|
|
emit cursorPositionChanged();
|
|
}
|
|
|
|
QRegion TextEditorWidget::translatedLineRegion(int lineStart, int lineEnd) const
|
|
{
|
|
QRegion region;
|
|
for (int i = lineStart ; i <= lineEnd; i++) {
|
|
QTextBlock block = document()->findBlockByNumber(i);
|
|
QPoint topLeft = blockBoundingGeometry(block).translated(contentOffset()).topLeft().toPoint();
|
|
|
|
if (block.isValid()) {
|
|
QTextLayout *layout = block.layout();
|
|
|
|
for (int i = 0; i < layout->lineCount();i++) {
|
|
QTextLine line = layout->lineAt(i);
|
|
region += line.naturalTextRect().translated(topLeft).toRect();
|
|
}
|
|
}
|
|
}
|
|
return region;
|
|
}
|
|
|
|
void TextEditorWidgetPrivate::setFindScope(const Utils::MultiTextCursor &scope)
|
|
{
|
|
if (m_findScope != scope) {
|
|
m_findScope = scope;
|
|
q->viewport()->update();
|
|
highlightSearchResultsInScrollBar();
|
|
}
|
|
}
|
|
|
|
void TextEditorWidgetPrivate::_q_animateUpdate(const QTextCursor &cursor,
|
|
QPointF lastPos, QRectF rect)
|
|
{
|
|
q->viewport()->update(QRectF(q->cursorRect(cursor).topLeft() + rect.topLeft(), rect.size()).toAlignedRect());
|
|
if (!lastPos.isNull())
|
|
q->viewport()->update(QRectF(lastPos + rect.topLeft(), rect.size()).toAlignedRect());
|
|
}
|
|
|
|
|
|
TextEditorAnimator::TextEditorAnimator(QObject *parent)
|
|
: QObject(parent), m_timeline(256)
|
|
{
|
|
m_value = 0;
|
|
m_timeline.setEasingCurve(QEasingCurve::SineCurve);
|
|
connect(&m_timeline, &QTimeLine::valueChanged, this, &TextEditorAnimator::step);
|
|
connect(&m_timeline, &QTimeLine::finished, this, &QObject::deleteLater);
|
|
m_timeline.start();
|
|
}
|
|
|
|
void TextEditorAnimator::init(const QTextCursor &cursor, const QFont &f, const QPalette &pal)
|
|
{
|
|
m_cursor = cursor;
|
|
m_font = f;
|
|
m_palette = pal;
|
|
m_text = cursor.selectedText();
|
|
QFontMetrics fm(m_font);
|
|
m_size = QSizeF(fm.horizontalAdvance(m_text), fm.height());
|
|
}
|
|
|
|
void TextEditorAnimator::draw(QPainter *p, const QPointF &pos)
|
|
{
|
|
m_lastDrawPos = pos;
|
|
p->setPen(m_palette.text().color());
|
|
QFont f = m_font;
|
|
f.setPointSizeF(f.pointSizeF() * (1.0 + m_value/2));
|
|
QFontMetrics fm(f);
|
|
int width = fm.horizontalAdvance(m_text);
|
|
QRectF r((m_size.width()-width)/2, (m_size.height() - fm.height())/2, width, fm.height());
|
|
r.translate(pos);
|
|
p->fillRect(r, m_palette.base());
|
|
p->setFont(f);
|
|
p->drawText(r, m_text);
|
|
}
|
|
|
|
bool TextEditorAnimator::isRunning() const
|
|
{
|
|
return m_timeline.state() == QTimeLine::Running;
|
|
}
|
|
|
|
QRectF TextEditorAnimator::rect() const
|
|
{
|
|
QFont f = m_font;
|
|
f.setPointSizeF(f.pointSizeF() * (1.0 + m_value/2));
|
|
QFontMetrics fm(f);
|
|
int width = fm.horizontalAdvance(m_text);
|
|
return QRectF((m_size.width()-width)/2, (m_size.height() - fm.height())/2, width, fm.height());
|
|
}
|
|
|
|
void TextEditorAnimator::step(qreal v)
|
|
{
|
|
QRectF before = rect();
|
|
m_value = v;
|
|
QRectF after = rect();
|
|
emit updateRequest(m_cursor, m_lastDrawPos, before.united(after));
|
|
}
|
|
|
|
void TextEditorAnimator::finish()
|
|
{
|
|
m_timeline.stop();
|
|
step(0);
|
|
deleteLater();
|
|
}
|
|
|
|
void TextEditorWidgetPrivate::_q_matchParentheses()
|
|
{
|
|
if (q->isReadOnly()
|
|
|| !(m_displaySettings.m_highlightMatchingParentheses
|
|
|| m_displaySettings.m_animateMatchingParentheses))
|
|
return;
|
|
|
|
QTextCursor backwardMatch = q->textCursor();
|
|
QTextCursor forwardMatch = q->textCursor();
|
|
if (q->overwriteMode())
|
|
backwardMatch.movePosition(QTextCursor::Right);
|
|
const TextBlockUserData::MatchType backwardMatchType = TextBlockUserData::matchCursorBackward(&backwardMatch);
|
|
const TextBlockUserData::MatchType forwardMatchType = TextBlockUserData::matchCursorForward(&forwardMatch);
|
|
|
|
QList<QTextEdit::ExtraSelection> extraSelections;
|
|
|
|
if (backwardMatchType == TextBlockUserData::NoMatch && forwardMatchType == TextBlockUserData::NoMatch) {
|
|
q->setExtraSelections(TextEditorWidget::ParenthesesMatchingSelection, extraSelections); // clear
|
|
return;
|
|
}
|
|
|
|
const QTextCharFormat matchFormat = m_document->fontSettings().toTextCharFormat(C_PARENTHESES);
|
|
const QTextCharFormat mismatchFormat = m_document->fontSettings().toTextCharFormat(
|
|
C_PARENTHESES_MISMATCH);
|
|
int animatePosition = -1;
|
|
if (backwardMatch.hasSelection()) {
|
|
QTextEdit::ExtraSelection sel;
|
|
if (backwardMatchType == TextBlockUserData::Mismatch) {
|
|
sel.cursor = backwardMatch;
|
|
sel.format = mismatchFormat;
|
|
extraSelections.append(sel);
|
|
} else {
|
|
|
|
sel.cursor = backwardMatch;
|
|
sel.format = matchFormat;
|
|
|
|
sel.cursor.setPosition(backwardMatch.selectionStart());
|
|
sel.cursor.setPosition(sel.cursor.position() + 1, QTextCursor::KeepAnchor);
|
|
extraSelections.append(sel);
|
|
|
|
if (m_displaySettings.m_animateMatchingParentheses && sel.cursor.block().isVisible())
|
|
animatePosition = backwardMatch.selectionStart();
|
|
|
|
sel.cursor.setPosition(backwardMatch.selectionEnd());
|
|
sel.cursor.setPosition(sel.cursor.position() - 1, QTextCursor::KeepAnchor);
|
|
extraSelections.append(sel);
|
|
}
|
|
}
|
|
|
|
if (forwardMatch.hasSelection()) {
|
|
QTextEdit::ExtraSelection sel;
|
|
if (forwardMatchType == TextBlockUserData::Mismatch) {
|
|
sel.cursor = forwardMatch;
|
|
sel.format = mismatchFormat;
|
|
extraSelections.append(sel);
|
|
} else {
|
|
|
|
sel.cursor = forwardMatch;
|
|
sel.format = matchFormat;
|
|
|
|
sel.cursor.setPosition(forwardMatch.selectionStart());
|
|
sel.cursor.setPosition(sel.cursor.position() + 1, QTextCursor::KeepAnchor);
|
|
extraSelections.append(sel);
|
|
|
|
sel.cursor.setPosition(forwardMatch.selectionEnd());
|
|
sel.cursor.setPosition(sel.cursor.position() - 1, QTextCursor::KeepAnchor);
|
|
extraSelections.append(sel);
|
|
|
|
if (m_displaySettings.m_animateMatchingParentheses && sel.cursor.block().isVisible())
|
|
animatePosition = forwardMatch.selectionEnd() - 1;
|
|
}
|
|
}
|
|
|
|
|
|
if (animatePosition >= 0) {
|
|
const QList<QTextEdit::ExtraSelection> selections = q->extraSelections(
|
|
TextEditorWidget::ParenthesesMatchingSelection);
|
|
for (const QTextEdit::ExtraSelection &sel : selections) {
|
|
if (sel.cursor.selectionStart() == animatePosition
|
|
|| sel.cursor.selectionEnd() - 1 == animatePosition) {
|
|
animatePosition = -1;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (animatePosition >= 0) {
|
|
cancelCurrentAnimations();// one animation is enough
|
|
QPalette pal;
|
|
pal.setBrush(QPalette::Text, matchFormat.foreground());
|
|
pal.setBrush(QPalette::Base, matchFormat.background());
|
|
QTextCursor cursor = q->textCursor();
|
|
cursor.setPosition(animatePosition + 1);
|
|
cursor.setPosition(animatePosition, QTextCursor::KeepAnchor);
|
|
m_bracketsAnimator = new TextEditorAnimator(this);
|
|
m_bracketsAnimator->init(cursor, q->font(), pal);
|
|
connect(m_bracketsAnimator.data(), &TextEditorAnimator::updateRequest,
|
|
this, &TextEditorWidgetPrivate::_q_animateUpdate);
|
|
}
|
|
if (m_displaySettings.m_highlightMatchingParentheses)
|
|
q->setExtraSelections(TextEditorWidget::ParenthesesMatchingSelection, extraSelections);
|
|
}
|
|
|
|
void TextEditorWidgetPrivate::_q_highlightBlocks()
|
|
{
|
|
TextEditorPrivateHighlightBlocks highlightBlocksInfo;
|
|
|
|
QTextBlock block;
|
|
if (extraAreaHighlightFoldedBlockNumber >= 0) {
|
|
block = q->document()->findBlockByNumber(extraAreaHighlightFoldedBlockNumber);
|
|
if (block.isValid()
|
|
&& block.next().isValid()
|
|
&& TextDocumentLayout::foldingIndent(block.next())
|
|
> TextDocumentLayout::foldingIndent(block))
|
|
block = block.next();
|
|
}
|
|
|
|
QTextBlock closeBlock = block;
|
|
while (block.isValid()) {
|
|
int foldingIndent = TextDocumentLayout::foldingIndent(block);
|
|
|
|
while (block.previous().isValid() && TextDocumentLayout::foldingIndent(block) >= foldingIndent)
|
|
block = block.previous();
|
|
int nextIndent = TextDocumentLayout::foldingIndent(block);
|
|
if (nextIndent == foldingIndent)
|
|
break;
|
|
highlightBlocksInfo.open.prepend(block.blockNumber());
|
|
while (closeBlock.next().isValid()
|
|
&& TextDocumentLayout::foldingIndent(closeBlock.next()) >= foldingIndent )
|
|
closeBlock = closeBlock.next();
|
|
highlightBlocksInfo.close.append(closeBlock.blockNumber());
|
|
int indent = qMin(visualIndent(block), visualIndent(closeBlock));
|
|
highlightBlocksInfo.visualIndent.prepend(indent);
|
|
}
|
|
|
|
#if 0
|
|
if (block.isValid()) {
|
|
QTextCursor cursor(block);
|
|
if (extraAreaHighlightCollapseColumn >= 0)
|
|
cursor.setPosition(cursor.position() + qMin(extraAreaHighlightCollapseColumn,
|
|
block.length()-1));
|
|
QTextCursor closeCursor;
|
|
bool firstRun = true;
|
|
while (TextBlockUserData::findPreviousBlockOpenParenthesis(&cursor, firstRun)) {
|
|
firstRun = false;
|
|
highlightBlocksInfo.open.prepend(cursor.blockNumber());
|
|
int visualIndent = visualIndent(cursor.block());
|
|
if (closeCursor.isNull())
|
|
closeCursor = cursor;
|
|
if (TextBlockUserData::findNextBlockClosingParenthesis(&closeCursor)) {
|
|
highlightBlocksInfo.close.append(closeCursor.blockNumber());
|
|
visualIndent = qMin(visualIndent, visualIndent(closeCursor.block()));
|
|
}
|
|
highlightBlocksInfo.visualIndent.prepend(visualIndent);
|
|
}
|
|
}
|
|
#endif
|
|
if (m_highlightBlocksInfo != highlightBlocksInfo) {
|
|
m_highlightBlocksInfo = highlightBlocksInfo;
|
|
q->viewport()->update();
|
|
m_extraArea->update();
|
|
}
|
|
}
|
|
|
|
void TextEditorWidgetPrivate::autocompleterHighlight(const QTextCursor &cursor)
|
|
{
|
|
if ((!m_animateAutoComplete && !m_highlightAutoComplete)
|
|
|| q->isReadOnly() || !cursor.hasSelection()) {
|
|
m_autoCompleteHighlightPos.clear();
|
|
} else if (m_highlightAutoComplete) {
|
|
m_autoCompleteHighlightPos.push_back(cursor);
|
|
}
|
|
if (m_animateAutoComplete) {
|
|
const QTextCharFormat matchFormat = m_document->fontSettings().toTextCharFormat(
|
|
C_AUTOCOMPLETE);
|
|
cancelCurrentAnimations();// one animation is enough
|
|
QPalette pal;
|
|
pal.setBrush(QPalette::Text, matchFormat.foreground());
|
|
pal.setBrush(QPalette::Base, matchFormat.background());
|
|
m_autocompleteAnimator = new TextEditorAnimator(this);
|
|
m_autocompleteAnimator->init(cursor, q->font(), pal);
|
|
connect(m_autocompleteAnimator.data(), &TextEditorAnimator::updateRequest,
|
|
this, &TextEditorWidgetPrivate::_q_animateUpdate);
|
|
}
|
|
updateAutoCompleteHighlight();
|
|
}
|
|
|
|
void TextEditorWidgetPrivate::updateAnimator(QPointer<TextEditorAnimator> animator,
|
|
QPainter &painter)
|
|
{
|
|
if (animator && animator->isRunning())
|
|
animator->draw(&painter, q->cursorRect(animator->cursor()).topLeft());
|
|
}
|
|
|
|
void TextEditorWidgetPrivate::cancelCurrentAnimations()
|
|
{
|
|
if (m_autocompleteAnimator)
|
|
m_autocompleteAnimator->finish();
|
|
if (m_bracketsAnimator)
|
|
m_bracketsAnimator->finish();
|
|
}
|
|
|
|
void TextEditorWidget::changeEvent(QEvent *e)
|
|
{
|
|
QPlainTextEdit::changeEvent(e);
|
|
if (e->type() == QEvent::ApplicationFontChange
|
|
|| e->type() == QEvent::FontChange) {
|
|
if (d->m_extraArea) {
|
|
QFont f = d->m_extraArea->font();
|
|
f.setPointSizeF(font().pointSizeF());
|
|
d->m_extraArea->setFont(f);
|
|
d->slotUpdateExtraAreaWidth();
|
|
d->m_extraArea->update();
|
|
}
|
|
} else if (e->type() == QEvent::PaletteChange) {
|
|
applyFontSettings();
|
|
}
|
|
}
|
|
|
|
void TextEditorWidget::focusInEvent(QFocusEvent *e)
|
|
{
|
|
QPlainTextEdit::focusInEvent(e);
|
|
d->startCursorFlashTimer();
|
|
d->updateHighlights();
|
|
}
|
|
|
|
void TextEditorWidget::focusOutEvent(QFocusEvent *e)
|
|
{
|
|
QPlainTextEdit::focusOutEvent(e);
|
|
d->m_hoverHandlerRunner.abortHandlers();
|
|
if (viewport()->cursor().shape() == Qt::BlankCursor)
|
|
viewport()->setCursor(Qt::IBeamCursor);
|
|
d->m_cursorFlashTimer.stop();
|
|
if (d->m_cursorVisible) {
|
|
d->m_cursorVisible = false;
|
|
viewport()->update(d->cursorUpdateRect(d->m_cursors));
|
|
}
|
|
d->updateHighlights();
|
|
if (!Utils::ToolTip::isVisible())
|
|
d->clearCurrentSuggestion();
|
|
}
|
|
|
|
void TextEditorWidgetPrivate::maybeSelectLine()
|
|
{
|
|
MultiTextCursor cursor = m_cursors;
|
|
if (cursor.hasSelection())
|
|
return;
|
|
for (QTextCursor &c : cursor) {
|
|
const QTextBlock &block = m_document->document()->findBlock(c.selectionStart());
|
|
const QTextBlock &end = m_document->document()->findBlock(c.selectionEnd()).next();
|
|
c.setPosition(block.position());
|
|
if (!end.isValid()) {
|
|
c.movePosition(QTextCursor::PreviousCharacter);
|
|
c.movePosition(QTextCursor::End, QTextCursor::KeepAnchor);
|
|
} else {
|
|
c.setPosition(end.position(), QTextCursor::KeepAnchor);
|
|
}
|
|
}
|
|
cursor.mergeCursors();
|
|
q->setMultiTextCursor(cursor);
|
|
}
|
|
|
|
// shift+del
|
|
void TextEditorWidget::cutLine()
|
|
{
|
|
d->maybeSelectLine();
|
|
cut();
|
|
}
|
|
|
|
// ctrl+ins
|
|
void TextEditorWidget::copyLine()
|
|
{
|
|
d->maybeSelectLine();
|
|
copy();
|
|
}
|
|
|
|
void TextEditorWidget::copyWithHtml()
|
|
{
|
|
if (!multiTextCursor().hasSelection())
|
|
return;
|
|
QGuiApplication::clipboard()->setMimeData(createMimeDataFromSelection(true));
|
|
}
|
|
|
|
void TextEditorWidgetPrivate::addCursorsToLineEnds()
|
|
{
|
|
MultiTextCursor multiCursor = q->multiTextCursor();
|
|
MultiTextCursor newMultiCursor;
|
|
const QList<QTextCursor> cursors = multiCursor.cursors();
|
|
|
|
if (multiCursor.cursorCount() == 0)
|
|
return;
|
|
|
|
QTextDocument *document = q->document();
|
|
|
|
for (const QTextCursor &cursor : cursors) {
|
|
if (!cursor.hasSelection())
|
|
continue;
|
|
|
|
QTextBlock block = document->findBlock(cursor.selectionStart());
|
|
|
|
while (block.isValid()) {
|
|
int blockEnd = block.position() + block.length() - 1;
|
|
if (blockEnd >= cursor.selectionEnd()) {
|
|
break;
|
|
}
|
|
|
|
QTextCursor newCursor(document);
|
|
newCursor.setPosition(blockEnd);
|
|
newMultiCursor.addCursor(newCursor);
|
|
|
|
block = block.next();
|
|
}
|
|
}
|
|
|
|
if (!newMultiCursor.isNull()) {
|
|
q->setMultiTextCursor(newMultiCursor);
|
|
}
|
|
}
|
|
|
|
void TextEditorWidgetPrivate::addSelectionNextFindMatch()
|
|
{
|
|
MultiTextCursor cursor = q->multiTextCursor();
|
|
const QList<QTextCursor> cursors = cursor.cursors();
|
|
|
|
if (cursor.cursorCount() == 0 || !cursors.first().hasSelection())
|
|
return;
|
|
|
|
const QTextCursor &firstCursor = cursors.first();
|
|
const QString selection = firstCursor.selectedText();
|
|
if (selection.contains(QChar::ParagraphSeparator))
|
|
return;
|
|
QTextDocument *document = firstCursor.document();
|
|
|
|
if (Utils::anyOf(cursors, [selection = selection.toCaseFolded()](const QTextCursor &c) {
|
|
return c.selectedText().toCaseFolded() != selection;
|
|
})) {
|
|
return;
|
|
}
|
|
|
|
const QTextDocument::FindFlags findFlags = Utils::textDocumentFlagsForFindFlags(m_findFlags);
|
|
|
|
int searchFrom = cursors.last().selectionEnd();
|
|
while (true) {
|
|
QTextCursor next = document->find(selection, searchFrom, findFlags);
|
|
if (next.isNull()) {
|
|
QTC_ASSERT(searchFrom != 0, return);
|
|
searchFrom = 0;
|
|
continue;
|
|
}
|
|
if (next.selectionStart() == firstCursor.selectionStart())
|
|
break;
|
|
cursor.addCursor(next);
|
|
q->setMultiTextCursor(cursor);
|
|
break;
|
|
}
|
|
}
|
|
|
|
void TextEditorWidgetPrivate::duplicateSelection(bool comment)
|
|
{
|
|
if (comment && !m_commentDefinition.hasMultiLineStyle())
|
|
return;
|
|
|
|
MultiTextCursor cursor = q->multiTextCursor();
|
|
cursor.beginEditBlock();
|
|
for (QTextCursor &c : cursor) {
|
|
if (c.hasSelection()) {
|
|
// Cannot "duplicate and comment" files without multi-line comment
|
|
|
|
QString dupText = c.selectedText().replace(QChar::ParagraphSeparator,
|
|
QLatin1Char('\n'));
|
|
if (comment) {
|
|
dupText = (m_commentDefinition.multiLineStart + dupText
|
|
+ m_commentDefinition.multiLineEnd);
|
|
}
|
|
const int selStart = c.selectionStart();
|
|
const int selEnd = c.selectionEnd();
|
|
const bool cursorAtStart = (c.position() == selStart);
|
|
c.setPosition(selEnd);
|
|
c.insertText(dupText);
|
|
c.setPosition(cursorAtStart ? selEnd : selStart);
|
|
c.setPosition(cursorAtStart ? selStart : selEnd, QTextCursor::KeepAnchor);
|
|
} else if (!m_cursors.hasMultipleCursors()) {
|
|
const int curPos = c.position();
|
|
const QTextBlock &block = c.block();
|
|
QString dupText = block.text() + QLatin1Char('\n');
|
|
if (comment && m_commentDefinition.hasSingleLineStyle())
|
|
dupText.append(m_commentDefinition.singleLine);
|
|
c.setPosition(block.position());
|
|
c.insertText(dupText);
|
|
c.setPosition(curPos);
|
|
}
|
|
}
|
|
cursor.endEditBlock();
|
|
q->setMultiTextCursor(cursor);
|
|
}
|
|
|
|
void TextEditorWidget::duplicateSelection()
|
|
{
|
|
d->duplicateSelection(false);
|
|
}
|
|
|
|
void TextEditorWidget::addCursorsToLineEnds()
|
|
{
|
|
d->addCursorsToLineEnds();
|
|
}
|
|
|
|
void TextEditorWidget::addSelectionNextFindMatch()
|
|
{
|
|
d->addSelectionNextFindMatch();
|
|
}
|
|
|
|
void TextEditorWidget::duplicateSelectionAndComment()
|
|
{
|
|
d->duplicateSelection(true);
|
|
}
|
|
|
|
void TextEditorWidget::deleteLine()
|
|
{
|
|
d->maybeSelectLine();
|
|
textCursor().removeSelectedText();
|
|
}
|
|
|
|
void TextEditorWidget::deleteEndOfLine()
|
|
{
|
|
d->moveCursor(QTextCursor::EndOfLine, QTextCursor::KeepAnchor);
|
|
MultiTextCursor cursor = multiTextCursor();
|
|
cursor.removeSelectedText();
|
|
setMultiTextCursor(cursor);
|
|
}
|
|
|
|
void TextEditorWidget::deleteEndOfWord()
|
|
{
|
|
d->moveCursor(QTextCursor::NextWord, QTextCursor::KeepAnchor);
|
|
MultiTextCursor cursor = multiTextCursor();
|
|
cursor.removeSelectedText();
|
|
setMultiTextCursor(cursor);
|
|
}
|
|
|
|
void TextEditorWidget::deleteEndOfWordCamelCase()
|
|
{
|
|
MultiTextCursor cursor = multiTextCursor();
|
|
CamelCaseCursor::right(&cursor, this, QTextCursor::KeepAnchor);
|
|
cursor.removeSelectedText();
|
|
setMultiTextCursor(cursor);
|
|
}
|
|
|
|
void TextEditorWidget::deleteStartOfLine()
|
|
{
|
|
d->moveCursor(QTextCursor::StartOfLine, QTextCursor::KeepAnchor);
|
|
MultiTextCursor cursor = multiTextCursor();
|
|
cursor.removeSelectedText();
|
|
setMultiTextCursor(cursor);
|
|
}
|
|
|
|
void TextEditorWidget::deleteStartOfWord()
|
|
{
|
|
d->moveCursor(QTextCursor::PreviousWord, QTextCursor::KeepAnchor);
|
|
MultiTextCursor cursor = multiTextCursor();
|
|
cursor.removeSelectedText();
|
|
setMultiTextCursor(cursor);
|
|
}
|
|
|
|
void TextEditorWidget::deleteStartOfWordCamelCase()
|
|
{
|
|
MultiTextCursor cursor = multiTextCursor();
|
|
CamelCaseCursor::left(&cursor, this, QTextCursor::KeepAnchor);
|
|
cursor.removeSelectedText();
|
|
setMultiTextCursor(cursor);
|
|
}
|
|
|
|
void TextEditorWidgetPrivate::setExtraSelections(Id kind, const QList<QTextEdit::ExtraSelection> &selections)
|
|
{
|
|
if (selections.isEmpty() && m_extraSelections[kind].isEmpty())
|
|
return;
|
|
m_extraSelections[kind] = selections;
|
|
|
|
if (kind == TextEditorWidget::CodeSemanticsSelection) {
|
|
m_overlay->clear();
|
|
for (const QTextEdit::ExtraSelection &selection : selections) {
|
|
m_overlay->addOverlaySelection(selection.cursor,
|
|
selection.format.background().color(),
|
|
selection.format.background().color(),
|
|
TextEditorOverlay::LockSize);
|
|
}
|
|
m_overlay->setVisible(!m_overlay->isEmpty());
|
|
} else {
|
|
QList<QTextEdit::ExtraSelection> all = m_extraSelections.value(
|
|
TextEditorWidget::OtherSelection);
|
|
|
|
for (auto i = m_extraSelections.constBegin(); i != m_extraSelections.constEnd(); ++i) {
|
|
if (i.key() == TextEditorWidget::CodeSemanticsSelection
|
|
|| i.key() == TextEditorWidget::SnippetPlaceholderSelection
|
|
|| i.key() == TextEditorWidget::OtherSelection)
|
|
continue;
|
|
all += i.value();
|
|
}
|
|
q->QPlainTextEdit::setExtraSelections(all);
|
|
}
|
|
}
|
|
|
|
void TextEditorWidget::setExtraSelections(Id kind, const QList<QTextEdit::ExtraSelection> &selections)
|
|
{
|
|
d->setExtraSelections(kind, selections);
|
|
}
|
|
|
|
QList<QTextEdit::ExtraSelection> TextEditorWidget::extraSelections(Id kind) const
|
|
{
|
|
return d->m_extraSelections.value(kind);
|
|
}
|
|
|
|
QString TextEditorWidget::extraSelectionTooltip(int pos) const
|
|
{
|
|
for (const QList<QTextEdit::ExtraSelection> &sel : std::as_const(d->m_extraSelections)) {
|
|
for (const QTextEdit::ExtraSelection &s : sel) {
|
|
if (s.cursor.selectionStart() <= pos
|
|
&& s.cursor.selectionEnd() >= pos
|
|
&& !s.format.toolTip().isEmpty())
|
|
return s.format.toolTip();
|
|
}
|
|
}
|
|
return QString();
|
|
}
|
|
|
|
void TextEditorWidget::autoIndent()
|
|
{
|
|
MultiTextCursor cursor = multiTextCursor();
|
|
cursor.beginEditBlock();
|
|
// The order is important, since some indenter refer to previous indent positions.
|
|
const QList<QTextCursor> cursors = Utils::sorted(cursor.cursors(),
|
|
[](const QTextCursor &lhs, const QTextCursor &rhs) {
|
|
return lhs.selectionStart() < rhs.selectionStart();
|
|
});
|
|
for (const QTextCursor &c : cursors)
|
|
d->m_document->autoFormatOrIndent(c);
|
|
cursor.mergeCursors();
|
|
cursor.endEditBlock();
|
|
setMultiTextCursor(cursor);
|
|
}
|
|
|
|
void TextEditorWidget::rewrapParagraph()
|
|
{
|
|
const int paragraphWidth = marginSettings().m_marginColumn;
|
|
static const QRegularExpression anyLettersOrNumbers("\\w");
|
|
const TabSettings ts = d->m_document->tabSettings();
|
|
|
|
QTextCursor cursor = textCursor();
|
|
cursor.beginEditBlock();
|
|
|
|
// Find start of paragraph.
|
|
|
|
while (cursor.movePosition(QTextCursor::PreviousBlock, QTextCursor::MoveAnchor)) {
|
|
QTextBlock block = cursor.block();
|
|
QString text = block.text();
|
|
|
|
// If this block is empty, move marker back to previous and terminate.
|
|
if (!text.contains(anyLettersOrNumbers)) {
|
|
cursor.movePosition(QTextCursor::NextBlock, QTextCursor::MoveAnchor);
|
|
break;
|
|
}
|
|
}
|
|
|
|
cursor.movePosition(QTextCursor::StartOfBlock, QTextCursor::MoveAnchor);
|
|
|
|
// Find indent level of current block.
|
|
const QString text = cursor.block().text();
|
|
int indentLevel = ts.indentationColumn(text);
|
|
|
|
// If there is a common prefix, it should be kept and expanded to all lines.
|
|
// this allows nice reflowing of doxygen style comments.
|
|
QTextCursor nextBlock = cursor;
|
|
QString commonPrefix;
|
|
|
|
const QString doxygenPrefix("^\\s*(?:///|/\\*\\*|/\\*\\!|\\*)?[ *]+");
|
|
if (nextBlock.movePosition(QTextCursor::NextBlock))
|
|
{
|
|
QString nText = nextBlock.block().text();
|
|
int maxLength = qMin(text.length(), nText.length());
|
|
|
|
const auto hasDoxygenPrefix = [&] {
|
|
static const QRegularExpression pattern(doxygenPrefix);
|
|
return pattern.match(commonPrefix).hasMatch();
|
|
};
|
|
|
|
for (int i = 0; i < maxLength; ++i) {
|
|
const QChar ch = text.at(i);
|
|
|
|
if (ch != nText[i] || ch.isLetterOrNumber()
|
|
|| ((ch == '@' || ch == '\\' ) && hasDoxygenPrefix())) {
|
|
break;
|
|
}
|
|
commonPrefix.append(ch);
|
|
}
|
|
}
|
|
|
|
// Find end of paragraph.
|
|
static const QRegularExpression immovableDoxygenCommand(doxygenPrefix + "[@\\\\][a-zA-Z]{2,}");
|
|
QTC_CHECK(immovableDoxygenCommand.isValid());
|
|
while (cursor.movePosition(QTextCursor::NextBlock, QTextCursor::KeepAnchor)) {
|
|
QString text = cursor.block().text();
|
|
|
|
if (!text.contains(anyLettersOrNumbers) || immovableDoxygenCommand.match(text).hasMatch())
|
|
break;
|
|
}
|
|
|
|
|
|
QString selectedText = cursor.selectedText();
|
|
|
|
// Preserve initial indent level.or common prefix.
|
|
QString spacing;
|
|
|
|
if (commonPrefix.isEmpty()) {
|
|
spacing = ts.indentationString(0, indentLevel, 0);
|
|
} else {
|
|
spacing = commonPrefix;
|
|
indentLevel = ts.columnCountForText(spacing);
|
|
}
|
|
|
|
int currentLength = indentLevel;
|
|
QString result;
|
|
result.append(spacing);
|
|
|
|
// Remove existing instances of any common prefix from paragraph to
|
|
// reflow.
|
|
selectedText.remove(0, commonPrefix.length());
|
|
commonPrefix.prepend(QChar::ParagraphSeparator);
|
|
selectedText.replace(commonPrefix, QLatin1String("\n"));
|
|
|
|
// remove any repeated spaces, trim lines to PARAGRAPH_WIDTH width and
|
|
// keep the same indentation level as first line in paragraph.
|
|
QString currentWord;
|
|
|
|
for (const QChar &ch : std::as_const(selectedText)) {
|
|
if (ch.isSpace() && ch != QChar::Nbsp) {
|
|
if (!currentWord.isEmpty()) {
|
|
currentLength += currentWord.length() + 1;
|
|
|
|
if (currentLength > paragraphWidth) {
|
|
currentLength = currentWord.length() + 1 + indentLevel;
|
|
result.chop(1); // remove trailing space
|
|
result.append(QChar::ParagraphSeparator);
|
|
result.append(spacing);
|
|
}
|
|
|
|
result.append(currentWord);
|
|
result.append(QLatin1Char(' '));
|
|
currentWord.clear();
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
currentWord.append(ch);
|
|
}
|
|
result.chop(1);
|
|
result.append(QChar::ParagraphSeparator);
|
|
|
|
cursor.insertText(result);
|
|
cursor.endEditBlock();
|
|
}
|
|
|
|
void TextEditorWidget::unCommentSelection()
|
|
{
|
|
const bool singleLine = d->m_document->typingSettings().m_preferSingleLineComments;
|
|
const MultiTextCursor cursor = Utils::unCommentSelection(multiTextCursor(),
|
|
d->m_commentDefinition,
|
|
singleLine);
|
|
setMultiTextCursor(cursor);
|
|
}
|
|
|
|
void TextEditorWidget::autoFormat()
|
|
{
|
|
QTextCursor cursor = textCursor();
|
|
cursor.beginEditBlock();
|
|
d->m_document->autoFormat(cursor);
|
|
cursor.endEditBlock();
|
|
}
|
|
|
|
void TextEditorWidget::encourageApply()
|
|
{
|
|
if (!d->m_snippetOverlay->isVisible() || d->m_snippetOverlay->isEmpty())
|
|
return;
|
|
d->m_snippetOverlay->updateEquivalentSelections(textCursor());
|
|
}
|
|
|
|
void TextEditorWidget::showEvent(QShowEvent* e)
|
|
{
|
|
triggerPendingUpdates();
|
|
// QPlainTextEdit::showEvent scrolls to make the cursor visible on first show
|
|
// which we don't want, since we restore previous states when
|
|
// opening editors, and when splitting/duplicating.
|
|
// So restore the previous state after that.
|
|
QByteArray state;
|
|
if (d->m_wasNotYetShown)
|
|
state = saveState();
|
|
QPlainTextEdit::showEvent(e);
|
|
if (d->m_wasNotYetShown) {
|
|
restoreState(state);
|
|
d->m_wasNotYetShown = false;
|
|
}
|
|
}
|
|
|
|
void TextEditorWidgetPrivate::applyFontSettingsDelayed()
|
|
{
|
|
m_fontSettingsNeedsApply = true;
|
|
if (q->isVisible())
|
|
q->triggerPendingUpdates();
|
|
updateActions();
|
|
}
|
|
|
|
void TextEditorWidgetPrivate::markRemoved(TextMark *mark)
|
|
{
|
|
if (m_dragMark == mark) {
|
|
m_dragMark = nullptr;
|
|
m_markDragging = false;
|
|
m_markDragStart = QPoint();
|
|
QGuiApplication::restoreOverrideCursor();
|
|
}
|
|
|
|
auto it = m_annotationRects.find(mark->lineNumber() - 1);
|
|
if (it == m_annotationRects.end())
|
|
return;
|
|
|
|
Utils::erase(it.value(), [mark](AnnotationRect rect) {
|
|
return rect.mark == mark;
|
|
});
|
|
}
|
|
|
|
void TextEditorWidget::triggerPendingUpdates()
|
|
{
|
|
if (d->m_fontSettingsNeedsApply)
|
|
applyFontSettings();
|
|
textDocument()->triggerPendingUpdates();
|
|
}
|
|
|
|
void TextEditorWidget::applyFontSettings()
|
|
{
|
|
d->m_fontSettingsNeedsApply = false;
|
|
const FontSettings &fs = textDocument()->fontSettings();
|
|
const QTextCharFormat textFormat = fs.toTextCharFormat(C_TEXT);
|
|
const QTextCharFormat lineNumberFormat = fs.toTextCharFormat(C_LINE_NUMBER);
|
|
QFont font(textFormat.font());
|
|
|
|
if (font != this->font()) {
|
|
setFont(font);
|
|
d->updateTabStops(); // update tab stops, they depend on the font
|
|
} else if (font != document()->defaultFont()) {
|
|
// When the editor already have the correct font configured it wont generate a font change
|
|
// signal. In turn the default font of the document wont get updated so we need to do that
|
|
// manually here
|
|
document()->setDefaultFont(font);
|
|
}
|
|
|
|
// Line numbers
|
|
QPalette ep;
|
|
ep.setColor(QPalette::Dark, lineNumberFormat.foreground().color());
|
|
ep.setColor(QPalette::Window, lineNumberFormat.background().style() != Qt::NoBrush ?
|
|
lineNumberFormat.background().color() : textFormat.background().color());
|
|
if (ep != d->m_extraArea->palette()) {
|
|
d->m_extraArea->setPalette(ep);
|
|
d->slotUpdateExtraAreaWidth(); // Adjust to new font width
|
|
}
|
|
|
|
d->updateHighlights();
|
|
}
|
|
|
|
void TextEditorWidget::setDisplaySettings(const DisplaySettings &ds)
|
|
{
|
|
const TextEditor::FontSettings &fs = TextEditorSettings::fontSettings();
|
|
if (fs.relativeLineSpacing() == 100)
|
|
setLineWrapMode(ds.m_textWrapping ? QPlainTextEdit::WidgetWidth : QPlainTextEdit::NoWrap);
|
|
else
|
|
setLineWrapMode(QPlainTextEdit::NoWrap);
|
|
|
|
QTC_ASSERT((fs.relativeLineSpacing() == 100) || (fs.relativeLineSpacing() != 100
|
|
&& lineWrapMode() == QPlainTextEdit::NoWrap), setLineWrapMode(QPlainTextEdit::NoWrap));
|
|
|
|
setLineNumbersVisible(ds.m_displayLineNumbers);
|
|
setHighlightCurrentLine(ds.m_highlightCurrentLine);
|
|
setRevisionsVisible(ds.m_markTextChanges);
|
|
setCenterOnScroll(ds.m_centerCursorOnScroll);
|
|
setParenthesesMatchingEnabled(ds.m_highlightMatchingParentheses);
|
|
d->m_fileEncodingLabelAction->setVisible(ds.m_displayFileEncoding);
|
|
|
|
const QTextOption::Flags currentOptionFlags = document()->defaultTextOption().flags();
|
|
QTextOption::Flags optionFlags = currentOptionFlags;
|
|
optionFlags.setFlag(QTextOption::AddSpaceForLineAndParagraphSeparators);
|
|
optionFlags.setFlag(QTextOption::ShowTabsAndSpaces, ds.m_visualizeWhitespace);
|
|
if (optionFlags != currentOptionFlags) {
|
|
if (SyntaxHighlighter *highlighter = textDocument()->syntaxHighlighter())
|
|
highlighter->rehighlight();
|
|
QTextOption option = document()->defaultTextOption();
|
|
option.setFlags(optionFlags);
|
|
document()->setDefaultTextOption(option);
|
|
}
|
|
|
|
d->m_displaySettings = ds;
|
|
if (!ds.m_highlightBlocks) {
|
|
d->extraAreaHighlightFoldedBlockNumber = -1;
|
|
d->m_highlightBlocksInfo = TextEditorPrivateHighlightBlocks();
|
|
}
|
|
|
|
d->updateCodeFoldingVisible();
|
|
d->updateFileLineEndingVisible();
|
|
d->updateTabSettingsButtonVisible();
|
|
d->updateHighlights();
|
|
d->setupScrollBar();
|
|
d->updateCursorSelections();
|
|
viewport()->update();
|
|
extraArea()->update();
|
|
}
|
|
|
|
void TextEditorWidget::setMarginSettings(const MarginSettings &ms)
|
|
{
|
|
d->m_marginSettings = ms;
|
|
updateVisualWrapColumn();
|
|
|
|
viewport()->update();
|
|
extraArea()->update();
|
|
}
|
|
|
|
void TextEditorWidget::setBehaviorSettings(const BehaviorSettings &bs)
|
|
{
|
|
d->m_behaviorSettings = bs;
|
|
}
|
|
|
|
void TextEditorWidget::setTypingSettings(const TypingSettings &typingSettings)
|
|
{
|
|
d->m_document->setTypingSettings(typingSettings);
|
|
d->setupFromDefinition(d->currentDefinition());
|
|
}
|
|
|
|
void TextEditorWidget::setStorageSettings(const StorageSettings &storageSettings)
|
|
{
|
|
d->m_document->setStorageSettings(storageSettings);
|
|
}
|
|
|
|
void TextEditorWidget::setCompletionSettings(const CompletionSettings &completionSettings)
|
|
{
|
|
d->m_autoCompleter->setAutoInsertBracketsEnabled(completionSettings.m_autoInsertBrackets);
|
|
d->m_autoCompleter->setSurroundWithBracketsEnabled(completionSettings.m_surroundingAutoBrackets);
|
|
d->m_autoCompleter->setAutoInsertQuotesEnabled(completionSettings.m_autoInsertQuotes);
|
|
d->m_autoCompleter->setSurroundWithQuotesEnabled(completionSettings.m_surroundingAutoQuotes);
|
|
d->m_autoCompleter->setOverwriteClosingCharsEnabled(completionSettings.m_overwriteClosingChars);
|
|
d->m_animateAutoComplete = completionSettings.m_animateAutoComplete;
|
|
d->m_highlightAutoComplete = completionSettings.m_highlightAutoComplete;
|
|
d->m_skipAutoCompletedText = completionSettings.m_skipAutoCompletedText;
|
|
d->m_removeAutoCompletedText = completionSettings.m_autoRemove;
|
|
}
|
|
|
|
void TextEditorWidget::setExtraEncodingSettings(const ExtraEncodingSettings &extraEncodingSettings)
|
|
{
|
|
d->m_document->setExtraEncodingSettings(extraEncodingSettings);
|
|
}
|
|
|
|
void TextEditorWidget::foldCurrentBlock()
|
|
{
|
|
fold(textCursor().block());
|
|
}
|
|
|
|
void TextEditorWidget::fold(const QTextBlock &block, bool recursive)
|
|
{
|
|
if (singleShotAfterHighlightingDone([this, block] { fold(block); }))
|
|
return;
|
|
|
|
QTextDocument *doc = document();
|
|
auto documentLayout = qobject_cast<TextDocumentLayout*>(doc->documentLayout());
|
|
QTC_ASSERT(documentLayout, return);
|
|
QTextBlock b = block;
|
|
if (!(TextDocumentLayout::canFold(b) && b.next().isVisible())) {
|
|
// find the closest previous block which can fold
|
|
int indent = TextDocumentLayout::foldingIndent(b);
|
|
while (b.isValid() && (TextDocumentLayout::foldingIndent(b) >= indent || !b.isVisible()))
|
|
b = b.previous();
|
|
}
|
|
if (b.isValid()) {
|
|
TextDocumentLayout::doFoldOrUnfold(b, false, recursive);
|
|
d->moveCursorVisible();
|
|
documentLayout->requestUpdate();
|
|
documentLayout->emitDocumentSizeChanged();
|
|
}
|
|
}
|
|
|
|
void TextEditorWidget::unfold(const QTextBlock &block, bool recursive)
|
|
{
|
|
if (singleShotAfterHighlightingDone([this, block] { unfold(block); }))
|
|
return;
|
|
|
|
QTextDocument *doc = document();
|
|
auto documentLayout = qobject_cast<TextDocumentLayout*>(doc->documentLayout());
|
|
QTC_ASSERT(documentLayout, return);
|
|
QTextBlock b = block;
|
|
while (b.isValid() && !b.isVisible())
|
|
b = b.previous();
|
|
TextDocumentLayout::doFoldOrUnfold(b, true, recursive);
|
|
d->moveCursorVisible();
|
|
documentLayout->requestUpdate();
|
|
documentLayout->emitDocumentSizeChanged();
|
|
}
|
|
|
|
void TextEditorWidget::unfoldCurrentBlock()
|
|
{
|
|
unfold(textCursor().block());
|
|
}
|
|
|
|
void TextEditorWidget::toggleFoldAll()
|
|
{
|
|
if (singleShotAfterHighlightingDone([this] { toggleFoldAll(); }))
|
|
return;
|
|
|
|
QTextDocument *doc = document();
|
|
QTextBlock block = doc->firstBlock();
|
|
|
|
bool makeVisible = true;
|
|
while (block.isValid()) {
|
|
if (block.isVisible() && TextDocumentLayout::canFold(block) && block.next().isVisible()) {
|
|
makeVisible = false;
|
|
break;
|
|
}
|
|
block = block.next();
|
|
}
|
|
|
|
unfoldAll(makeVisible);
|
|
}
|
|
|
|
void TextEditorWidget::unfoldAll(bool unfold)
|
|
{
|
|
if (singleShotAfterHighlightingDone([this, unfold] { unfoldAll(unfold); }))
|
|
return;
|
|
|
|
QTextDocument *doc = document();
|
|
auto documentLayout = qobject_cast<TextDocumentLayout*>(doc->documentLayout());
|
|
QTC_ASSERT(documentLayout, return);
|
|
|
|
QTextBlock block = doc->firstBlock();
|
|
|
|
while (block.isValid()) {
|
|
if (TextDocumentLayout::canFold(block))
|
|
TextDocumentLayout::doFoldOrUnfold(block, unfold);
|
|
block = block.next();
|
|
}
|
|
|
|
d->moveCursorVisible();
|
|
documentLayout->requestUpdate();
|
|
documentLayout->emitDocumentSizeChanged();
|
|
centerCursor();
|
|
}
|
|
|
|
void TextEditorWidget::cut()
|
|
{
|
|
copy();
|
|
MultiTextCursor cursor = multiTextCursor();
|
|
cursor.removeSelectedText();
|
|
setMultiTextCursor(cursor);
|
|
d->collectToCircularClipboard();
|
|
}
|
|
|
|
void TextEditorWidget::selectAll()
|
|
{
|
|
// Directly update the internal multi text cursor here to prevent calling setTextCursor.
|
|
// This would indirectly make sure the cursor is visible which is not desired for select all.
|
|
QTextCursor c = QPlainTextEdit::textCursor();
|
|
c.select(QTextCursor::Document);
|
|
const_cast<MultiTextCursor &>(d->m_cursors).setCursors({c});
|
|
QPlainTextEdit::selectAll();
|
|
}
|
|
|
|
void TextEditorWidget::copy()
|
|
{
|
|
QPlainTextEdit::copy();
|
|
d->collectToCircularClipboard();
|
|
}
|
|
|
|
void TextEditorWidget::paste()
|
|
{
|
|
QPlainTextEdit::paste();
|
|
encourageApply();
|
|
}
|
|
|
|
void TextEditorWidgetPrivate::collectToCircularClipboard()
|
|
{
|
|
const QMimeData *mimeData = QApplication::clipboard()->mimeData();
|
|
if (!mimeData)
|
|
return;
|
|
CircularClipboard *circularClipBoard = CircularClipboard::instance();
|
|
circularClipBoard->collect(TextEditorWidget::duplicateMimeData(mimeData));
|
|
// We want the latest copied content to be the first one to appear on circular paste.
|
|
circularClipBoard->toLastCollect();
|
|
}
|
|
|
|
void TextEditorWidget::circularPaste()
|
|
{
|
|
CircularClipboard *circularClipBoard = CircularClipboard::instance();
|
|
if (const QMimeData *clipboardData = QApplication::clipboard()->mimeData()) {
|
|
circularClipBoard->collect(TextEditorWidget::duplicateMimeData(clipboardData));
|
|
circularClipBoard->toLastCollect();
|
|
}
|
|
|
|
if (circularClipBoard->size() > 1) {
|
|
invokeAssist(QuickFix, &clipboardAssistProvider());
|
|
return;
|
|
}
|
|
|
|
if (const QMimeData *mimeData = circularClipBoard->next().get()) {
|
|
QApplication::clipboard()->setMimeData(TextEditorWidget::duplicateMimeData(mimeData));
|
|
paste();
|
|
}
|
|
}
|
|
|
|
void TextEditorWidget::pasteWithoutFormat()
|
|
{
|
|
d->m_skipFormatOnPaste = true;
|
|
paste();
|
|
d->m_skipFormatOnPaste = false;
|
|
}
|
|
|
|
void TextEditorWidget::switchUtf8bom()
|
|
{
|
|
textDocument()->switchUtf8Bom();
|
|
}
|
|
|
|
QMimeData *TextEditorWidget::createMimeDataFromSelection() const
|
|
{
|
|
return createMimeDataFromSelection(false);
|
|
}
|
|
|
|
QMimeData *TextEditorWidget::createMimeDataFromSelection(bool withHtml) const
|
|
{
|
|
if (multiTextCursor().hasSelection()) {
|
|
auto mimeData = new QMimeData;
|
|
|
|
QString text = plainTextFromSelection(multiTextCursor());
|
|
mimeData->setText(text);
|
|
|
|
// Copy the selected text as HTML
|
|
if (withHtml) {
|
|
// Create a new document from the selected text document fragment
|
|
auto tempDocument = new QTextDocument;
|
|
QTextCursor tempCursor(tempDocument);
|
|
for (const QTextCursor &cursor : multiTextCursor()) {
|
|
if (!cursor.hasSelection())
|
|
continue;
|
|
tempCursor.insertFragment(cursor.selection());
|
|
|
|
// Apply the additional formats set by the syntax highlighter
|
|
QTextBlock start = document()->findBlock(cursor.selectionStart());
|
|
QTextBlock last = document()->findBlock(cursor.selectionEnd());
|
|
QTextBlock end = last.next();
|
|
|
|
const int selectionStart = cursor.selectionStart();
|
|
const int endOfDocument = tempDocument->characterCount() - 1;
|
|
int removedCount = 0;
|
|
for (QTextBlock current = start; current.isValid() && current != end;
|
|
current = current.next()) {
|
|
if (selectionVisible(current.blockNumber())) {
|
|
const QTextLayout *layout = current.layout();
|
|
const QVector<QTextLayout::FormatRange> ranges = layout->formats();
|
|
for (const QTextLayout::FormatRange &range : ranges) {
|
|
const int startPosition = current.position() + range.start
|
|
- selectionStart - removedCount;
|
|
const int endPosition = startPosition + range.length;
|
|
if (endPosition <= 0 || startPosition >= endOfDocument - removedCount)
|
|
continue;
|
|
tempCursor.setPosition(qMax(startPosition, 0));
|
|
tempCursor.setPosition(qMin(endPosition, endOfDocument - removedCount),
|
|
QTextCursor::KeepAnchor);
|
|
tempCursor.setCharFormat(range.format);
|
|
}
|
|
} else {
|
|
const int startPosition = current.position() - selectionStart
|
|
- removedCount;
|
|
int endPosition = startPosition + current.text().size();
|
|
if (current != last)
|
|
endPosition++;
|
|
removedCount += endPosition - startPosition;
|
|
tempCursor.setPosition(startPosition);
|
|
tempCursor.setPosition(endPosition, QTextCursor::KeepAnchor);
|
|
tempCursor.deleteChar();
|
|
}
|
|
}
|
|
}
|
|
|
|
// Reset the user states since they are not interesting
|
|
for (QTextBlock block = tempDocument->begin(); block.isValid(); block = block.next())
|
|
block.setUserState(-1);
|
|
|
|
// Make sure the text appears pre-formatted
|
|
tempCursor.setPosition(0);
|
|
tempCursor.movePosition(QTextCursor::End, QTextCursor::KeepAnchor);
|
|
QTextBlockFormat blockFormat = tempCursor.blockFormat();
|
|
blockFormat.setNonBreakableLines(true);
|
|
tempCursor.setBlockFormat(blockFormat);
|
|
|
|
mimeData->setHtml(tempCursor.selection().toHtml());
|
|
delete tempDocument;
|
|
}
|
|
|
|
if (!multiTextCursor().hasMultipleCursors()) {
|
|
/*
|
|
Try to figure out whether we are copying an entire block, and store the
|
|
complete block including indentation in the qtcreator.blocktext mimetype.
|
|
*/
|
|
QTextCursor cursor = multiTextCursor().mainCursor();
|
|
QTextCursor selstart = cursor;
|
|
selstart.setPosition(cursor.selectionStart());
|
|
QTextCursor selend = cursor;
|
|
selend.setPosition(cursor.selectionEnd());
|
|
|
|
bool startOk = TabSettings::cursorIsAtBeginningOfLine(selstart);
|
|
bool multipleBlocks = (selend.block() != selstart.block());
|
|
|
|
if (startOk && multipleBlocks) {
|
|
selstart.movePosition(QTextCursor::StartOfBlock);
|
|
if (TabSettings::cursorIsAtBeginningOfLine(selend))
|
|
selend.movePosition(QTextCursor::StartOfBlock);
|
|
cursor.setPosition(selstart.position());
|
|
cursor.setPosition(selend.position(), QTextCursor::KeepAnchor);
|
|
text = plainTextFromSelection(cursor);
|
|
mimeData->setData(QLatin1String(kTextBlockMimeType), text.toUtf8());
|
|
}
|
|
}
|
|
return mimeData;
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
bool TextEditorWidget::canInsertFromMimeData(const QMimeData *source) const
|
|
{
|
|
return QPlainTextEdit::canInsertFromMimeData(source);
|
|
}
|
|
|
|
struct MappedText
|
|
{
|
|
MappedText(const QString text, MultiTextCursor &cursor)
|
|
: text(text)
|
|
{
|
|
if (cursor.hasMultipleCursors()) {
|
|
texts = text.split('\n');
|
|
if (texts.last().isEmpty())
|
|
texts.removeLast();
|
|
if (texts.count() != cursor.cursorCount())
|
|
texts.clear();
|
|
}
|
|
}
|
|
|
|
QString textAt(int i) const
|
|
{
|
|
return texts.value(i, text);
|
|
}
|
|
|
|
QStringList texts;
|
|
const QString text;
|
|
};
|
|
|
|
void TextEditorWidget::insertFromMimeData(const QMimeData *source)
|
|
{
|
|
if (!source || isReadOnly())
|
|
return;
|
|
|
|
QString text = source->text();
|
|
if (text.isEmpty())
|
|
return;
|
|
|
|
if (d->m_codeAssistant.hasContext())
|
|
d->m_codeAssistant.destroyContext();
|
|
|
|
if (d->m_snippetOverlay->isVisible() && (text.contains('\n') || text.contains('\t')))
|
|
d->m_snippetOverlay->accept();
|
|
|
|
const bool selectInsertedText = source->property(dropProperty).toBool();
|
|
const TypingSettings &tps = d->m_document->typingSettings();
|
|
MultiTextCursor cursor = multiTextCursor();
|
|
if (!tps.m_autoIndent) {
|
|
cursor.insertText(text, selectInsertedText);
|
|
setMultiTextCursor(cursor);
|
|
return;
|
|
}
|
|
|
|
if (source->hasFormat(QLatin1String(kTextBlockMimeType))) {
|
|
text = QString::fromUtf8(source->data(QLatin1String(kTextBlockMimeType)));
|
|
if (text.isEmpty())
|
|
return;
|
|
}
|
|
|
|
MappedText mappedText(text, cursor);
|
|
|
|
int index = 0;
|
|
cursor.beginEditBlock();
|
|
for (QTextCursor &cursor : cursor) {
|
|
const QString textForCursor = mappedText.textAt(index++);
|
|
|
|
cursor.removeSelectedText();
|
|
|
|
bool insertAtBeginningOfLine = TabSettings::cursorIsAtBeginningOfLine(cursor);
|
|
int reindentBlockStart = cursor.blockNumber() + (insertAtBeginningOfLine ? 0 : 1);
|
|
|
|
bool hasFinalNewline = (textForCursor.endsWith(QLatin1Char('\n'))
|
|
|| textForCursor.endsWith(QChar::ParagraphSeparator)
|
|
|| textForCursor.endsWith(QLatin1Char('\r')));
|
|
|
|
if (insertAtBeginningOfLine
|
|
&& hasFinalNewline) // since we'll add a final newline, preserve current line's indentation
|
|
cursor.setPosition(cursor.block().position());
|
|
|
|
int cursorPosition = cursor.position();
|
|
cursor.insertText(textForCursor);
|
|
const QTextCursor endCursor = cursor;
|
|
QTextCursor startCursor = endCursor;
|
|
startCursor.setPosition(cursorPosition);
|
|
|
|
int reindentBlockEnd = cursor.blockNumber() - (hasFinalNewline ? 1 : 0);
|
|
|
|
if (!d->m_skipFormatOnPaste
|
|
&& (reindentBlockStart < reindentBlockEnd
|
|
|| (reindentBlockStart == reindentBlockEnd
|
|
&& (!insertAtBeginningOfLine || hasFinalNewline)))) {
|
|
if (insertAtBeginningOfLine && !hasFinalNewline) {
|
|
QTextCursor unnecessaryWhitespace = cursor;
|
|
unnecessaryWhitespace.setPosition(cursorPosition);
|
|
unnecessaryWhitespace.movePosition(QTextCursor::StartOfBlock,
|
|
QTextCursor::KeepAnchor);
|
|
unnecessaryWhitespace.removeSelectedText();
|
|
}
|
|
QTextCursor c = cursor;
|
|
c.setPosition(cursor.document()->findBlockByNumber(reindentBlockStart).position());
|
|
c.setPosition(cursor.document()->findBlockByNumber(reindentBlockEnd).position(),
|
|
QTextCursor::KeepAnchor);
|
|
d->m_document->autoReindent(c);
|
|
}
|
|
|
|
if (selectInsertedText) {
|
|
cursor.setPosition(startCursor.position());
|
|
cursor.setPosition(endCursor.position(), QTextCursor::KeepAnchor);
|
|
}
|
|
}
|
|
cursor.endEditBlock();
|
|
setMultiTextCursor(cursor);
|
|
}
|
|
|
|
void TextEditorWidget::dragLeaveEvent(QDragLeaveEvent *)
|
|
{
|
|
const QRect rect = cursorRect(d->m_dndCursor);
|
|
d->m_dndCursor = QTextCursor();
|
|
if (!rect.isNull())
|
|
viewport()->update(rect);
|
|
}
|
|
|
|
void TextEditorWidget::dragMoveEvent(QDragMoveEvent *e)
|
|
{
|
|
const QRect rect = cursorRect(d->m_dndCursor);
|
|
d->m_dndCursor = cursorForPosition(e->position().toPoint());
|
|
if (!rect.isNull())
|
|
viewport()->update(rect);
|
|
viewport()->update(cursorRect(d->m_dndCursor));
|
|
}
|
|
|
|
void TextEditorWidget::dropEvent(QDropEvent *e)
|
|
{
|
|
const QRect rect = cursorRect(d->m_dndCursor);
|
|
d->m_dndCursor = QTextCursor();
|
|
if (!rect.isNull())
|
|
viewport()->update(rect);
|
|
const QMimeData *mime = e->mimeData();
|
|
if (!canInsertFromMimeData(mime))
|
|
return;
|
|
// Update multi text cursor before inserting data
|
|
MultiTextCursor cursor = multiTextCursor();
|
|
cursor.beginEditBlock();
|
|
const QTextCursor eventCursor = cursorForPosition(e->position().toPoint());
|
|
if (e->dropAction() == Qt::MoveAction && e->source() == viewport())
|
|
cursor.removeSelectedText();
|
|
cursor.setCursors({eventCursor});
|
|
setMultiTextCursor(cursor);
|
|
QMimeData *mimeOverwrite = nullptr;
|
|
if (mime && (mime->hasText() || mime->hasHtml())) {
|
|
mimeOverwrite = duplicateMimeData(mime);
|
|
mimeOverwrite->setProperty(dropProperty, true);
|
|
mime = mimeOverwrite;
|
|
}
|
|
insertFromMimeData(mime);
|
|
delete mimeOverwrite;
|
|
cursor.endEditBlock();
|
|
e->acceptProposedAction();
|
|
}
|
|
|
|
QMimeData *TextEditorWidget::duplicateMimeData(const QMimeData *source)
|
|
{
|
|
Q_ASSERT(source);
|
|
|
|
auto mimeData = new QMimeData;
|
|
mimeData->setText(source->text());
|
|
mimeData->setHtml(source->html());
|
|
if (source->hasFormat(QLatin1String(kTextBlockMimeType))) {
|
|
mimeData->setData(QLatin1String(kTextBlockMimeType),
|
|
source->data(QLatin1String(kTextBlockMimeType)));
|
|
}
|
|
|
|
return mimeData;
|
|
}
|
|
|
|
QString TextEditorWidget::lineNumber(int blockNumber) const
|
|
{
|
|
return QString::number(blockNumber + 1);
|
|
}
|
|
|
|
int TextEditorWidget::lineNumberDigits() const
|
|
{
|
|
int digits = 2;
|
|
int max = qMax(1, blockCount());
|
|
while (max >= 100) {
|
|
max /= 10;
|
|
++digits;
|
|
}
|
|
return digits;
|
|
}
|
|
|
|
bool TextEditorWidget::selectionVisible(int blockNumber) const
|
|
{
|
|
Q_UNUSED(blockNumber)
|
|
return true;
|
|
}
|
|
|
|
bool TextEditorWidget::replacementVisible(int blockNumber) const
|
|
{
|
|
Q_UNUSED(blockNumber)
|
|
return true;
|
|
}
|
|
|
|
QColor TextEditorWidget::replacementPenColor(int blockNumber) const
|
|
{
|
|
Q_UNUSED(blockNumber)
|
|
return {};
|
|
}
|
|
|
|
void TextEditorWidget::setupFallBackEditor(Id id)
|
|
{
|
|
TextDocumentPtr doc(new TextDocument(id));
|
|
doc->setFontSettings(TextEditorSettings::fontSettings());
|
|
setTextDocument(doc);
|
|
}
|
|
|
|
void TextEditorWidget::appendStandardContextMenuActions(QMenu *menu)
|
|
{
|
|
if (optionalActions() & OptionalActions::FollowSymbolUnderCursor) {
|
|
const auto action = ActionManager::command(Constants::FOLLOW_SYMBOL_UNDER_CURSOR)->action();
|
|
if (!menu->actions().contains(action))
|
|
menu->addAction(action);
|
|
}
|
|
if (optionalActions() & OptionalActions::FollowTypeUnderCursor) {
|
|
const auto action = ActionManager::command(Constants::FOLLOW_SYMBOL_TO_TYPE)->action();
|
|
if (!menu->actions().contains(action))
|
|
menu->addAction(action);
|
|
}
|
|
if (optionalActions() & OptionalActions::FindUsage) {
|
|
const auto action = ActionManager::command(Constants::FIND_USAGES)->action();
|
|
if (!menu->actions().contains(action))
|
|
menu->addAction(action);
|
|
}
|
|
if (optionalActions() & OptionalActions::RenameSymbol) {
|
|
const auto action = ActionManager::command(Constants::RENAME_SYMBOL)->action();
|
|
if (!menu->actions().contains(action))
|
|
menu->addAction(action);
|
|
}
|
|
if (optionalActions() & OptionalActions::CallHierarchy) {
|
|
const auto action = ActionManager::command(Constants::OPEN_CALL_HIERARCHY)->action();
|
|
if (!menu->actions().contains(action))
|
|
menu->addAction(action);
|
|
}
|
|
if (optionalActions() & OptionalActions::TypeHierarchy) {
|
|
const auto action = ActionManager::command(Constants::OPEN_TYPE_HIERARCHY)->action();
|
|
if (!menu->actions().contains(action))
|
|
menu->addAction(action);
|
|
}
|
|
|
|
menu->addSeparator();
|
|
appendMenuActionsFromContext(menu, Constants::M_STANDARDCONTEXTMENU);
|
|
|
|
if (Command *bomCmd = ActionManager::command(Constants::SWITCH_UTF8BOM)) {
|
|
QAction *a = bomCmd->action();
|
|
TextDocument *doc = textDocument();
|
|
if (doc->codec()->name() == QByteArray("UTF-8") && doc->supportsUtf8Bom()) {
|
|
a->setVisible(true);
|
|
a->setText(doc->format().hasUtf8Bom ? Tr::tr("Delete UTF-8 BOM on Save")
|
|
: Tr::tr("Add UTF-8 BOM on Save"));
|
|
} else {
|
|
a->setVisible(false);
|
|
}
|
|
}
|
|
}
|
|
|
|
uint TextEditorWidget::optionalActions()
|
|
{
|
|
return d->m_optionalActionMask;
|
|
}
|
|
|
|
void TextEditorWidget::setOptionalActions(uint optionalActionMask)
|
|
{
|
|
if (d->m_optionalActionMask == optionalActionMask)
|
|
return;
|
|
d->m_optionalActionMask = optionalActionMask;
|
|
d->updateOptionalActions();
|
|
}
|
|
|
|
void TextEditorWidget::addOptionalActions( uint optionalActionMask)
|
|
{
|
|
setOptionalActions(d->m_optionalActionMask | optionalActionMask);
|
|
}
|
|
|
|
BaseTextEditor::BaseTextEditor()
|
|
: d(new BaseTextEditorPrivate)
|
|
{
|
|
addContext(Constants::C_TEXTEDITOR);
|
|
setContextHelpProvider([this](const HelpCallback &callback) {
|
|
editorWidget()->contextHelpItem(callback);
|
|
});
|
|
}
|
|
|
|
BaseTextEditor::~BaseTextEditor()
|
|
{
|
|
delete m_widget;
|
|
delete d;
|
|
}
|
|
|
|
TextDocument *BaseTextEditor::textDocument() const
|
|
{
|
|
TextDocument *doc = editorWidget()->textDocument();
|
|
QTC_CHECK(doc);
|
|
return doc;
|
|
}
|
|
|
|
void BaseTextEditor::addContext(Id id)
|
|
{
|
|
m_context.add(id);
|
|
}
|
|
|
|
IDocument *BaseTextEditor::document() const
|
|
{
|
|
return textDocument();
|
|
}
|
|
|
|
QWidget *BaseTextEditor::toolBar()
|
|
{
|
|
return editorWidget()->toolBarWidget();
|
|
}
|
|
|
|
QAction * TextEditorWidget::insertExtraToolBarWidget(TextEditorWidget::Side side,
|
|
QWidget *widget)
|
|
{
|
|
if (widget->sizePolicy().horizontalPolicy() & QSizePolicy::ExpandFlag)
|
|
d->m_stretchAction->setVisible(false);
|
|
|
|
if (side == Left) {
|
|
auto findLeftMostAction = [this](QAction *action) {
|
|
if (d->m_toolbarOutlineAction && action == d->m_toolbarOutlineAction)
|
|
return false;
|
|
return d->m_toolBar->widgetForAction(action) != nullptr;
|
|
};
|
|
QAction *before = Utils::findOr(d->m_toolBar->actions(),
|
|
d->m_fileEncodingLabelAction,
|
|
findLeftMostAction);
|
|
return d->m_toolBar->insertWidget(before, widget);
|
|
} else {
|
|
return d->m_toolBar->insertWidget(d->m_fileLineEndingAction, widget);
|
|
}
|
|
}
|
|
|
|
void TextEditorWidget::setToolbarOutline(QWidget *widget)
|
|
{
|
|
if (d->m_toolbarOutlineAction) {
|
|
if (d->m_toolBar->widgetForAction(d->m_toolbarOutlineAction) == widget)
|
|
return;
|
|
d->m_toolBar->removeAction(d->m_toolbarOutlineAction);
|
|
delete d->m_toolbarOutlineAction;
|
|
d->m_toolbarOutlineAction = nullptr;
|
|
} else if (!widget) {
|
|
return;
|
|
}
|
|
|
|
if (widget) {
|
|
if (widget->sizePolicy().horizontalPolicy() & QSizePolicy::ExpandFlag)
|
|
d->m_stretchAction->setVisible(false);
|
|
|
|
d->m_toolbarOutlineAction = insertExtraToolBarWidget(Left, widget);
|
|
} else {
|
|
// check for a widget with an expanding size policy otherwise re-enable the stretcher
|
|
for (auto action : d->m_toolBar->actions()) {
|
|
if (QWidget *toolbarWidget = d->m_toolBar->widgetForAction(action)) {
|
|
if (toolbarWidget->isVisible()
|
|
&& toolbarWidget->sizePolicy().horizontalPolicy() & QSizePolicy::ExpandFlag) {
|
|
d->m_stretchAction->setVisible(false);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
d->m_stretchAction->setVisible(true);
|
|
}
|
|
|
|
emit toolbarOutlineChanged(widget);
|
|
}
|
|
|
|
const QWidget *TextEditorWidget::toolbarOutlineWidget()
|
|
{
|
|
return d->m_toolbarOutlineAction ? d->m_toolBar->widgetForAction(d->m_toolbarOutlineAction)
|
|
: nullptr;
|
|
}
|
|
|
|
void TextEditorWidget::keepAutoCompletionHighlight(bool keepHighlight)
|
|
{
|
|
d->m_keepAutoCompletionHighlight = keepHighlight;
|
|
}
|
|
|
|
void TextEditorWidget::setAutoCompleteSkipPosition(const QTextCursor &cursor)
|
|
{
|
|
QTextCursor tc = cursor;
|
|
// Create a selection of the next character but keep the current position, otherwise
|
|
// the cursor would be removed from the list of automatically inserted text positions
|
|
tc.movePosition(QTextCursor::NextCharacter);
|
|
tc.movePosition(QTextCursor::PreviousCharacter, QTextCursor::KeepAnchor);
|
|
d->autocompleterHighlight(tc);
|
|
}
|
|
|
|
int BaseTextEditor::currentLine() const
|
|
{
|
|
return editorWidget()->textCursor().blockNumber() + 1;
|
|
}
|
|
|
|
int BaseTextEditor::currentColumn() const
|
|
{
|
|
QTextCursor cursor = editorWidget()->textCursor();
|
|
return cursor.position() - cursor.block().position() + 1;
|
|
}
|
|
|
|
void BaseTextEditor::gotoLine(int line, int column, bool centerLine)
|
|
{
|
|
editorWidget()->gotoLine(line, column, centerLine);
|
|
}
|
|
|
|
int BaseTextEditor::columnCount() const
|
|
{
|
|
return editorWidget()->columnCount();
|
|
}
|
|
|
|
int BaseTextEditor::rowCount() const
|
|
{
|
|
return editorWidget()->rowCount();
|
|
}
|
|
|
|
int BaseTextEditor::position(TextPositionOperation posOp, int at) const
|
|
{
|
|
return editorWidget()->position(posOp, at);
|
|
}
|
|
|
|
void BaseTextEditor::convertPosition(int pos, int *line, int *column) const
|
|
{
|
|
editorWidget()->convertPosition(pos, line, column);
|
|
}
|
|
|
|
QString BaseTextEditor::selectedText() const
|
|
{
|
|
return editorWidget()->selectedText();
|
|
}
|
|
|
|
void BaseTextEditor::remove(int length)
|
|
{
|
|
editorWidget()->remove(length);
|
|
}
|
|
|
|
void TextEditorWidget::remove(int length)
|
|
{
|
|
QTextCursor tc = textCursor();
|
|
tc.setPosition(tc.position() + length, QTextCursor::KeepAnchor);
|
|
tc.removeSelectedText();
|
|
}
|
|
|
|
void BaseTextEditor::insert(const QString &string)
|
|
{
|
|
editorWidget()->insertPlainText(string);
|
|
}
|
|
|
|
void BaseTextEditor::replace(int length, const QString &string)
|
|
{
|
|
editorWidget()->replace(length, string);
|
|
}
|
|
|
|
void TextEditorWidget::replace(int length, const QString &string)
|
|
{
|
|
QTextCursor tc = textCursor();
|
|
tc.setPosition(tc.position() + length, QTextCursor::KeepAnchor);
|
|
tc.insertText(string);
|
|
}
|
|
|
|
void TextEditorWidget::replace(int pos, int length, const QString &string)
|
|
{
|
|
if (length == string.length()) {
|
|
bool different = false;
|
|
for (int i = 0; !different && i < length; ++i)
|
|
different = document()->characterAt(pos + i) != string.at(i);
|
|
if (!different)
|
|
return;
|
|
}
|
|
|
|
QTextCursor tc = textCursor();
|
|
tc.setPosition(pos);
|
|
tc.setPosition(pos + length, QTextCursor::KeepAnchor);
|
|
tc.insertText(string);
|
|
}
|
|
|
|
void BaseTextEditor::setCursorPosition(int pos)
|
|
{
|
|
editorWidget()->setCursorPosition(pos);
|
|
}
|
|
|
|
void TextEditorWidget::setCursorPosition(int pos)
|
|
{
|
|
QTextCursor tc = textCursor();
|
|
tc.setPosition(pos);
|
|
setTextCursor(tc);
|
|
}
|
|
|
|
QWidget *TextEditorWidget::toolBarWidget() const
|
|
{
|
|
return d->m_toolBarWidget;
|
|
}
|
|
|
|
QToolBar *TextEditorWidget::toolBar() const
|
|
{
|
|
return d->m_toolBar;
|
|
}
|
|
|
|
void BaseTextEditor::select(int toPos)
|
|
{
|
|
QTextCursor tc = editorWidget()->textCursor();
|
|
tc.setPosition(toPos, QTextCursor::KeepAnchor);
|
|
editorWidget()->setTextCursor(tc);
|
|
}
|
|
|
|
void BaseTextEditor::saveCurrentStateForNavigationHistory()
|
|
{
|
|
d->m_savedNavigationState = saveState();
|
|
}
|
|
|
|
void BaseTextEditor::addSavedStateToNavigationHistory()
|
|
{
|
|
if (EditorManager::currentEditor() == this)
|
|
EditorManager::addCurrentPositionToNavigationHistory(d->m_savedNavigationState);
|
|
}
|
|
|
|
void BaseTextEditor::addCurrentStateToNavigationHistory()
|
|
{
|
|
if (EditorManager::currentEditor() == this)
|
|
EditorManager::addCurrentPositionToNavigationHistory();
|
|
}
|
|
|
|
void TextEditorWidgetPrivate::updateCursorPosition()
|
|
{
|
|
m_contextHelpItem = HelpItem();
|
|
if (!q->textCursor().block().isVisible())
|
|
q->ensureCursorVisible();
|
|
}
|
|
|
|
void TextEditorWidget::contextHelpItem(const IContext::HelpCallback &callback)
|
|
{
|
|
if (!d->m_contextHelpItem.isEmpty()) {
|
|
callback(d->m_contextHelpItem);
|
|
return;
|
|
}
|
|
const QString fallbackWordUnderCursor = Text::wordUnderCursor(textCursor());
|
|
const auto hoverHandlerCallback = [fallbackWordUnderCursor, callback](
|
|
TextEditorWidget *widget, BaseHoverHandler *handler, int position) {
|
|
handler->contextHelpId(widget, position,
|
|
[fallbackWordUnderCursor, callback](const HelpItem &item) {
|
|
if (item.isEmpty())
|
|
callback(fallbackWordUnderCursor);
|
|
else
|
|
callback(item);
|
|
});
|
|
|
|
};
|
|
const auto fallback = [callback, fallbackWordUnderCursor](TextEditorWidget *) {
|
|
callback(fallbackWordUnderCursor);
|
|
};
|
|
|
|
d->m_hoverHandlerRunner.startChecking(textCursor(), hoverHandlerCallback, fallback);
|
|
}
|
|
|
|
void TextEditorWidget::setContextHelpItem(const HelpItem &item)
|
|
{
|
|
d->m_contextHelpItem = item;
|
|
}
|
|
|
|
RefactorMarkers TextEditorWidget::refactorMarkers() const
|
|
{
|
|
return d->m_refactorOverlay->markers();
|
|
}
|
|
|
|
void TextEditorWidget::setRefactorMarkers(const RefactorMarkers &markers)
|
|
{
|
|
const QList<RefactorMarker> oldMarkers = d->m_refactorOverlay->markers();
|
|
for (const RefactorMarker &marker : oldMarkers)
|
|
emit requestBlockUpdate(marker.cursor.block());
|
|
d->m_refactorOverlay->setMarkers(markers);
|
|
for (const RefactorMarker &marker : markers)
|
|
emit requestBlockUpdate(marker.cursor.block());
|
|
}
|
|
|
|
void TextEditorWidget::setRefactorMarkers(const RefactorMarkers &newMarkers, const Utils::Id &type)
|
|
{
|
|
RefactorMarkers markers = d->m_refactorOverlay->markers();
|
|
auto first = std::partition(markers.begin(),
|
|
markers.end(),
|
|
[type](const RefactorMarker &marker) {
|
|
return marker.type == type;
|
|
});
|
|
|
|
for (auto it = markers.begin(); it != first; ++it)
|
|
emit requestBlockUpdate(it->cursor.block());
|
|
markers.erase(markers.begin(), first);
|
|
markers.append(newMarkers);
|
|
d->m_refactorOverlay->setMarkers(markers);
|
|
for (const RefactorMarker &marker : newMarkers)
|
|
emit requestBlockUpdate(marker.cursor.block());
|
|
}
|
|
|
|
void TextEditorWidget::clearRefactorMarkers(const Utils::Id &type)
|
|
{
|
|
RefactorMarkers markers = d->m_refactorOverlay->markers();
|
|
for (auto it = markers.begin(); it != markers.end();) {
|
|
if (it->type == type) {
|
|
emit requestBlockUpdate(it->cursor.block());
|
|
it = markers.erase(it);
|
|
} else {
|
|
++it;
|
|
}
|
|
}
|
|
d->m_refactorOverlay->setMarkers(markers);
|
|
}
|
|
|
|
bool TextEditorWidget::inFindScope(const QTextCursor &cursor) const
|
|
{
|
|
return d->m_find->inScope(cursor);
|
|
}
|
|
|
|
void TextEditorWidget::updateVisualWrapColumn()
|
|
{
|
|
auto calcMargin = [this] {
|
|
const auto &ms = d->m_marginSettings;
|
|
|
|
if (!ms.m_showMargin) {
|
|
return 0;
|
|
}
|
|
if (ms.m_useIndenter) {
|
|
if (auto margin = d->m_document->indenter()->margin()) {
|
|
return *margin;
|
|
}
|
|
}
|
|
return ms.m_marginColumn;
|
|
};
|
|
setVisibleWrapColumn(calcMargin());
|
|
}
|
|
|
|
void TextEditorWidgetPrivate::updateTabStops()
|
|
{
|
|
// Although the tab stop is stored as qreal the API from QPlainTextEdit only allows it
|
|
// to be set as an int. A work around is to access directly the QTextOption.
|
|
QTextOption option = q->document()->defaultTextOption();
|
|
option.setTabStopDistance(charWidth() * m_document->tabSettings().m_tabSize);
|
|
q->document()->setDefaultTextOption(option);
|
|
if (TextSuggestion *suggestion = TextDocumentLayout::suggestion(m_suggestionBlock)) {
|
|
QTextOption option = suggestion->replacementDocument()->defaultTextOption();
|
|
option.setTabStopDistance(option.tabStopDistance());
|
|
suggestion->replacementDocument()->setDefaultTextOption(option);
|
|
}
|
|
}
|
|
|
|
void TextEditorWidgetPrivate::applyTabSettings()
|
|
{
|
|
updateTabStops();
|
|
m_autoCompleter->setTabSettings(m_document->tabSettings());
|
|
emit q->tabSettingsChanged();
|
|
}
|
|
|
|
int TextEditorWidget::columnCount() const
|
|
{
|
|
return int(viewport()->rect().width() / d->charWidth());
|
|
}
|
|
|
|
int TextEditorWidget::rowCount() const
|
|
{
|
|
int height = viewport()->rect().height();
|
|
int lineCount = 0;
|
|
QTextBlock block = firstVisibleBlock();
|
|
while (block.isValid()) {
|
|
height -= blockBoundingRect(block).height();
|
|
if (height < 0) {
|
|
const int blockLineCount = block.layout()->lineCount();
|
|
for (int i = 0; i < blockLineCount; ++i) {
|
|
++lineCount;
|
|
const QTextLine line = block.layout()->lineAt(i);
|
|
height += line.rect().height();
|
|
if (height >= 0)
|
|
break;
|
|
}
|
|
return lineCount;
|
|
}
|
|
lineCount += block.layout()->lineCount();
|
|
block = block.next();
|
|
}
|
|
return lineCount;
|
|
}
|
|
|
|
/**
|
|
Helper function to transform a selected text. If nothing is selected at the moment
|
|
the word under the cursor is used.
|
|
The type of the transformation is determined by the function pointer given.
|
|
|
|
@param method pointer to the QString function to use for the transformation
|
|
|
|
@see uppercaseSelection, lowercaseSelection
|
|
*/
|
|
void TextEditorWidgetPrivate::transformSelection(TransformationMethod method)
|
|
{
|
|
MultiTextCursor cursor = m_cursors;
|
|
cursor.beginEditBlock();
|
|
for (QTextCursor &c : cursor) {
|
|
int pos = c.position();
|
|
int anchor = c.anchor();
|
|
|
|
if (!c.hasSelection() && !m_cursors.hasMultipleCursors()) {
|
|
// if nothing is selected, select the word under the cursor
|
|
c.select(QTextCursor::WordUnderCursor);
|
|
}
|
|
|
|
QString text = c.selectedText();
|
|
QString transformedText = method(text);
|
|
|
|
if (transformedText == text)
|
|
continue;
|
|
|
|
c.insertText(transformedText);
|
|
|
|
// (re)select the changed text
|
|
// Note: this assumes the transformation did not change the length,
|
|
c.setPosition(anchor);
|
|
c.setPosition(pos, QTextCursor::KeepAnchor);
|
|
}
|
|
cursor.endEditBlock();
|
|
q->setMultiTextCursor(cursor);
|
|
}
|
|
|
|
void TextEditorWidget::inSnippetMode(bool *active)
|
|
{
|
|
*active = d->m_snippetOverlay->isVisible();
|
|
}
|
|
|
|
QTextBlock TextEditorWidget::blockForVisibleRow(int row) const
|
|
{
|
|
const int count = rowCount();
|
|
if (row < 0 && row >= count)
|
|
return QTextBlock();
|
|
|
|
QTextBlock block = firstVisibleBlock();
|
|
for (int i = 0; i < count;) {
|
|
if (!block.isValid() || i >= row)
|
|
return block;
|
|
|
|
i += block.lineCount();
|
|
block = d->nextVisibleBlock(block);
|
|
}
|
|
return QTextBlock();
|
|
|
|
}
|
|
|
|
QTextBlock TextEditorWidget::blockForVerticalOffset(int offset) const
|
|
{
|
|
QTextBlock block = firstVisibleBlock();
|
|
while (block.isValid()) {
|
|
offset -= blockBoundingRect(block).height();
|
|
if (offset < 0)
|
|
return block;
|
|
block = block.next();
|
|
}
|
|
return block;
|
|
}
|
|
|
|
void TextEditorWidget::invokeAssist(AssistKind kind, IAssistProvider *provider)
|
|
{
|
|
if (multiTextCursor().hasMultipleCursors())
|
|
return;
|
|
|
|
if (kind == QuickFix && d->m_snippetOverlay->isVisible())
|
|
d->m_snippetOverlay->accept();
|
|
|
|
bool previousMode = overwriteMode();
|
|
setOverwriteMode(false);
|
|
ensureCursorVisible();
|
|
d->m_codeAssistant.invoke(kind, provider);
|
|
setOverwriteMode(previousMode);
|
|
}
|
|
|
|
std::unique_ptr<AssistInterface> TextEditorWidget::createAssistInterface(AssistKind kind,
|
|
AssistReason reason) const
|
|
{
|
|
Q_UNUSED(kind)
|
|
return std::make_unique<AssistInterface>(textCursor(), d->m_document->filePath(), reason);
|
|
}
|
|
|
|
QString TextEditorWidget::foldReplacementText(const QTextBlock &) const
|
|
{
|
|
return QLatin1String("...");
|
|
}
|
|
|
|
QByteArray BaseTextEditor::saveState() const
|
|
{
|
|
return editorWidget()->saveState();
|
|
}
|
|
|
|
void BaseTextEditor::restoreState(const QByteArray &state)
|
|
{
|
|
editorWidget()->restoreState(state);
|
|
}
|
|
|
|
BaseTextEditor *BaseTextEditor::currentTextEditor()
|
|
{
|
|
return qobject_cast<BaseTextEditor *>(EditorManager::currentEditor());
|
|
}
|
|
|
|
QVector<BaseTextEditor *> BaseTextEditor::textEditorsForDocument(TextDocument *textDocument)
|
|
{
|
|
QVector<BaseTextEditor *> ret;
|
|
for (IEditor *editor : Core::DocumentModel::editorsForDocument(textDocument)) {
|
|
if (auto textEditor = qobject_cast<BaseTextEditor *>(editor))
|
|
ret << textEditor;
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
TextEditorWidget *BaseTextEditor::editorWidget() const
|
|
{
|
|
auto textEditorWidget = TextEditorWidget::fromEditor(this);
|
|
QTC_CHECK(textEditorWidget);
|
|
return textEditorWidget;
|
|
}
|
|
|
|
void BaseTextEditor::setTextCursor(const QTextCursor &cursor)
|
|
{
|
|
editorWidget()->setTextCursor(cursor);
|
|
}
|
|
|
|
QTextCursor BaseTextEditor::textCursor() const
|
|
{
|
|
return editorWidget()->textCursor();
|
|
}
|
|
|
|
QChar BaseTextEditor::characterAt(int pos) const
|
|
{
|
|
return textDocument()->characterAt(pos);
|
|
}
|
|
|
|
QString BaseTextEditor::textAt(int from, int to) const
|
|
{
|
|
return textDocument()->textAt(from, to);
|
|
}
|
|
|
|
QChar TextEditorWidget::characterAt(int pos) const
|
|
{
|
|
return textDocument()->characterAt(pos);
|
|
}
|
|
|
|
QString TextEditorWidget::textAt(int from, int to) const
|
|
{
|
|
return textDocument()->textAt(from, to);
|
|
}
|
|
|
|
void TextEditorWidget::configureGenericHighlighter()
|
|
{
|
|
const HighlighterHelper::Definitions definitions = HighlighterHelper::definitionsForDocument(textDocument());
|
|
d->configureGenericHighlighter(definitions.isEmpty() ? HighlighterHelper::Definition()
|
|
: definitions.first());
|
|
d->updateSyntaxInfoBar(definitions, textDocument()->filePath().fileName());
|
|
}
|
|
|
|
void TextEditorWidget::configureGenericHighlighter(const Utils::MimeType &mimeType)
|
|
{
|
|
const HighlighterHelper::Definitions definitions = HighlighterHelper::definitionsForMimeType(mimeType.name());
|
|
d->configureGenericHighlighter(definitions.isEmpty() ? HighlighterHelper::Definition()
|
|
: definitions.first());
|
|
d->removeSyntaxInfoBar();
|
|
}
|
|
|
|
expected_str<void> TextEditorWidget::configureGenericHighlighter(const QString &definitionName)
|
|
{
|
|
const HighlighterHelper::Definition definition = TextEditor::HighlighterHelper::definitionForName(definitionName);
|
|
if (!definition.isValid())
|
|
return make_unexpected(Tr::tr("Could not find definition."));
|
|
|
|
d->configureGenericHighlighter(definition);
|
|
d->removeSyntaxInfoBar();
|
|
return {};
|
|
}
|
|
|
|
int TextEditorWidget::blockNumberForVisibleRow(int row) const
|
|
{
|
|
QTextBlock block = blockForVisibleRow(row);
|
|
return block.isValid() ? block.blockNumber() : -1;
|
|
}
|
|
|
|
int TextEditorWidget::firstVisibleBlockNumber() const
|
|
{
|
|
return blockNumberForVisibleRow(0);
|
|
}
|
|
|
|
int TextEditorWidget::lastVisibleBlockNumber() const
|
|
{
|
|
QTextBlock block = blockForVerticalOffset(viewport()->height() - 1);
|
|
if (!block.isValid()) {
|
|
block = document()->lastBlock();
|
|
while (block.isValid() && !block.isVisible())
|
|
block = block.previous();
|
|
}
|
|
return block.isValid() ? block.blockNumber() : -1;
|
|
}
|
|
|
|
int TextEditorWidget::centerVisibleBlockNumber() const
|
|
{
|
|
QTextBlock block = blockForVerticalOffset(viewport()->height() / 2);
|
|
if (!block.isValid())
|
|
block.previous();
|
|
return block.isValid() ? block.blockNumber() : -1;
|
|
}
|
|
|
|
HighlightScrollBarController *TextEditorWidget::highlightScrollBarController() const
|
|
{
|
|
return d->m_highlightScrollBarController;
|
|
}
|
|
|
|
// The remnants of PlainTextEditor.
|
|
void TextEditorWidget::setupGenericHighlighter()
|
|
{
|
|
setLineSeparatorsAllowed(true);
|
|
|
|
connect(textDocument(), &IDocument::filePathChanged,
|
|
d.get(), &TextEditorWidgetPrivate::reconfigure);
|
|
}
|
|
|
|
//
|
|
// TextEditorLinkLabel
|
|
//
|
|
TextEditorLinkLabel::TextEditorLinkLabel(QWidget *parent)
|
|
: Utils::ElidingLabel(parent)
|
|
{
|
|
setElideMode(Qt::ElideMiddle);
|
|
}
|
|
|
|
void TextEditorLinkLabel::setLink(Utils::Link link)
|
|
{
|
|
m_link = link;
|
|
}
|
|
|
|
Utils::Link TextEditorLinkLabel::link() const
|
|
{
|
|
return m_link;
|
|
}
|
|
|
|
void TextEditorLinkLabel::mousePressEvent(QMouseEvent *event)
|
|
{
|
|
if (event->button() == Qt::LeftButton)
|
|
m_dragStartPosition = event->pos();
|
|
}
|
|
|
|
void TextEditorLinkLabel::mouseMoveEvent(QMouseEvent *event)
|
|
{
|
|
if (!(event->buttons() & Qt::LeftButton))
|
|
return;
|
|
if ((event->pos() - m_dragStartPosition).manhattanLength() < QApplication::startDragDistance())
|
|
return;
|
|
|
|
auto data = new DropMimeData;
|
|
data->addFile(m_link.targetFilePath, m_link.targetLine, m_link.targetColumn);
|
|
auto drag = new QDrag(this);
|
|
drag->setMimeData(data);
|
|
drag->exec(Qt::CopyAction);
|
|
}
|
|
|
|
void TextEditorLinkLabel::mouseReleaseEvent(QMouseEvent *event)
|
|
{
|
|
Q_UNUSED(event)
|
|
if (!m_link.hasValidTarget())
|
|
return;
|
|
|
|
EditorManager::openEditorAt(m_link);
|
|
}
|
|
|
|
//
|
|
// BaseTextEditorFactory
|
|
//
|
|
|
|
namespace Internal {
|
|
|
|
class TextEditorFactoryPrivate
|
|
{
|
|
public:
|
|
TextEditorFactoryPrivate(TextEditorFactory *parent)
|
|
: q(parent)
|
|
, m_widgetCreator([]() { return new TextEditorWidget; })
|
|
{}
|
|
|
|
BaseTextEditor *duplicateTextEditor(BaseTextEditor *other)
|
|
{
|
|
BaseTextEditor *editor = createEditorHelper(other->editorWidget()->textDocumentPtr());
|
|
editor->editorWidget()->finalizeInitializationAfterDuplication(other->editorWidget());
|
|
return editor;
|
|
}
|
|
|
|
BaseTextEditor *createEditorHelper(const TextDocumentPtr &doc);
|
|
|
|
TextEditorFactory *q;
|
|
TextEditorFactory::DocumentCreator m_documentCreator;
|
|
TextEditorFactory::EditorWidgetCreator m_widgetCreator;
|
|
TextEditorFactory::EditorCreator m_editorCreator;
|
|
TextEditorFactory::AutoCompleterCreator m_autoCompleterCreator;
|
|
TextEditorFactory::IndenterCreator m_indenterCreator;
|
|
TextEditorFactory::SyntaxHighLighterCreator m_syntaxHighlighterCreator;
|
|
CommentDefinition m_commentDefinition;
|
|
QList<BaseHoverHandler *> m_hoverHandlers; // owned
|
|
std::unique_ptr<CompletionAssistProvider> m_completionAssistProvider; // owned
|
|
int m_optionalActionMask = 0;
|
|
bool m_useGenericHighlighter = false;
|
|
bool m_duplicatedSupported = true;
|
|
bool m_codeFoldingSupported = false;
|
|
bool m_paranthesesMatchinEnabled = false;
|
|
bool m_marksVisible = true;
|
|
};
|
|
|
|
} /// namespace Internal
|
|
|
|
TextEditorFactory::TextEditorFactory()
|
|
: d(new TextEditorFactoryPrivate(this))
|
|
{
|
|
setEditorCreator([]() { return new BaseTextEditor; });
|
|
addHoverHandler(new SuggestionHoverHandler);
|
|
}
|
|
|
|
TextEditorFactory::~TextEditorFactory()
|
|
{
|
|
qDeleteAll(d->m_hoverHandlers);
|
|
delete d;
|
|
}
|
|
|
|
void TextEditorFactory::setDocumentCreator(const DocumentCreator &creator)
|
|
{
|
|
d->m_documentCreator = creator;
|
|
}
|
|
|
|
void TextEditorFactory::setEditorWidgetCreator(const EditorWidgetCreator &creator)
|
|
{
|
|
d->m_widgetCreator = creator;
|
|
}
|
|
|
|
void TextEditorFactory::setEditorCreator(const EditorCreator &creator)
|
|
{
|
|
d->m_editorCreator = creator;
|
|
IEditorFactory::setEditorCreator([this] {
|
|
static DocumentContentCompletionProvider basicSnippetProvider;
|
|
TextDocumentPtr doc(d->m_documentCreator());
|
|
|
|
if (d->m_indenterCreator)
|
|
doc->setIndenter(d->m_indenterCreator(doc->document()));
|
|
|
|
if (d->m_syntaxHighlighterCreator)
|
|
doc->resetSyntaxHighlighter(d->m_syntaxHighlighterCreator);
|
|
|
|
doc->setCompletionAssistProvider(d->m_completionAssistProvider
|
|
? d->m_completionAssistProvider.get()
|
|
: &basicSnippetProvider);
|
|
|
|
return d->createEditorHelper(doc);
|
|
});
|
|
}
|
|
|
|
void TextEditorFactory::setIndenterCreator(const IndenterCreator &creator)
|
|
{
|
|
d->m_indenterCreator = creator;
|
|
}
|
|
|
|
void TextEditorFactory::setSyntaxHighlighterCreator(const SyntaxHighLighterCreator &creator)
|
|
{
|
|
d->m_syntaxHighlighterCreator = creator;
|
|
}
|
|
|
|
void TextEditorFactory::setUseGenericHighlighter(bool enabled)
|
|
{
|
|
d->m_useGenericHighlighter = enabled;
|
|
}
|
|
|
|
void TextEditorFactory::setAutoCompleterCreator(const AutoCompleterCreator &creator)
|
|
{
|
|
d->m_autoCompleterCreator = creator;
|
|
}
|
|
|
|
void TextEditorFactory::setOptionalActionMask(int optionalActions)
|
|
{
|
|
d->m_optionalActionMask = optionalActions;
|
|
}
|
|
|
|
void TextEditorFactory::addHoverHandler(BaseHoverHandler *handler)
|
|
{
|
|
d->m_hoverHandlers.append(handler);
|
|
}
|
|
|
|
void TextEditorFactory::setCompletionAssistProvider(CompletionAssistProvider *provider)
|
|
{
|
|
d->m_completionAssistProvider.reset(provider);
|
|
}
|
|
|
|
void TextEditorFactory::setCommentDefinition(CommentDefinition definition)
|
|
{
|
|
d->m_commentDefinition = definition;
|
|
}
|
|
|
|
void TextEditorFactory::setDuplicatedSupported(bool on)
|
|
{
|
|
d->m_duplicatedSupported = on;
|
|
}
|
|
|
|
void TextEditorFactory::setMarksVisible(bool on)
|
|
{
|
|
d->m_marksVisible = on;
|
|
}
|
|
|
|
void TextEditorFactory::setCodeFoldingSupported(bool on)
|
|
{
|
|
d->m_codeFoldingSupported = on;
|
|
}
|
|
|
|
void TextEditorFactory::setParenthesesMatchingEnabled(bool on)
|
|
{
|
|
d->m_paranthesesMatchinEnabled = on;
|
|
}
|
|
|
|
BaseTextEditor *TextEditorFactoryPrivate::createEditorHelper(const TextDocumentPtr &document)
|
|
{
|
|
QWidget *widget = m_widgetCreator();
|
|
TextEditorWidget *textEditorWidget = Aggregation::query<TextEditorWidget>(widget);
|
|
QTC_ASSERT(textEditorWidget, return nullptr);
|
|
textEditorWidget->setMarksVisible(m_marksVisible);
|
|
textEditorWidget->setParenthesesMatchingEnabled(m_paranthesesMatchinEnabled);
|
|
textEditorWidget->setCodeFoldingSupported(m_codeFoldingSupported);
|
|
textEditorWidget->setOptionalActions(m_optionalActionMask);
|
|
|
|
BaseTextEditor *editor = m_editorCreator();
|
|
editor->setDuplicateSupported(m_duplicatedSupported);
|
|
editor->addContext(q->id());
|
|
editor->d->m_origin = this;
|
|
|
|
editor->m_widget = widget;
|
|
|
|
// Needs to go before setTextDocument as this copies the current settings.
|
|
if (m_autoCompleterCreator)
|
|
textEditorWidget->setAutoCompleter(m_autoCompleterCreator());
|
|
|
|
textEditorWidget->setTextDocument(document);
|
|
textEditorWidget->autoCompleter()->setTabSettings(document->tabSettings());
|
|
textEditorWidget->d->m_hoverHandlers = m_hoverHandlers;
|
|
|
|
textEditorWidget->d->m_commentDefinition = m_commentDefinition;
|
|
textEditorWidget->d->m_commentDefinition.isAfterWhitespace
|
|
= document->typingSettings().m_commentPosition != TypingSettings::StartOfLine;
|
|
|
|
QObject::connect(textEditorWidget,
|
|
&TextEditorWidget::activateEditor,
|
|
textEditorWidget,
|
|
[editor](EditorManager::OpenEditorFlags flags) {
|
|
EditorManager::activateEditor(editor, flags);
|
|
});
|
|
QObject::connect(
|
|
textEditorWidget,
|
|
&TextEditorWidget::saveCurrentStateForNavigationHistory,
|
|
editor,
|
|
&BaseTextEditor::saveCurrentStateForNavigationHistory);
|
|
QObject::connect(
|
|
textEditorWidget,
|
|
&TextEditorWidget::addSavedStateToNavigationHistory,
|
|
editor,
|
|
&BaseTextEditor::addSavedStateToNavigationHistory);
|
|
QObject::connect(
|
|
textEditorWidget,
|
|
&TextEditorWidget::addCurrentStateToNavigationHistory,
|
|
editor,
|
|
&BaseTextEditor::addCurrentStateToNavigationHistory);
|
|
|
|
if (m_useGenericHighlighter)
|
|
textEditorWidget->setupGenericHighlighter();
|
|
textEditorWidget->finalizeInitialization();
|
|
|
|
// Toolbar: Actions to show minimized info bars
|
|
document->minimizableInfoBars()->createShowInfoBarActions([textEditorWidget](QWidget *w) {
|
|
return textEditorWidget->insertExtraToolBarWidget(TextEditorWidget::Left, w);
|
|
});
|
|
|
|
editor->finalizeInitialization();
|
|
return editor;
|
|
}
|
|
|
|
BaseTextEditor *BaseTextEditor::duplicate()
|
|
{
|
|
// Use new standard setup if that's available.
|
|
if (d->m_origin) {
|
|
BaseTextEditor *dup = d->m_origin->duplicateTextEditor(this);
|
|
emit editorDuplicated(dup);
|
|
return dup;
|
|
}
|
|
|
|
// If neither is sufficient, you need to implement 'YourEditor::duplicate'.
|
|
QTC_CHECK(false);
|
|
return nullptr;
|
|
}
|
|
|
|
} // namespace TextEditor
|
|
|
|
QT_BEGIN_NAMESPACE
|
|
|
|
size_t qHash(const QColor &color)
|
|
{
|
|
return color.rgba();
|
|
}
|
|
|
|
QT_END_NAMESPACE
|
|
|
|
#include "texteditor.moc"
|