forked from qt-creator/qt-creator
It seems the keypad modifier is almost always set, but the alt modifier blocks usable shortcuts. Change-Id: I09e5d3a07bdc7ca2f81c3822097c412db2e212bd Reviewed-by: Marcus Tillmanns <marcus.tillmanns@qt.io>
495 lines
16 KiB
C++
495 lines
16 KiB
C++
// Copyright (C) 2021 The Qt Company Ltd.
|
|
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
|
|
|
|
#include "multitextcursor.h"
|
|
|
|
#include "algorithm.h"
|
|
#include "camelcasecursor.h"
|
|
#include "hostosinfo.h"
|
|
#include "qtcassert.h"
|
|
|
|
#include <QKeyEvent>
|
|
#include <QTextBlock>
|
|
|
|
namespace Utils {
|
|
|
|
MultiTextCursor::MultiTextCursor() {}
|
|
|
|
MultiTextCursor::MultiTextCursor(const QList<QTextCursor> &cursors)
|
|
{
|
|
setCursors(cursors);
|
|
}
|
|
|
|
void MultiTextCursor::fillMapWithList()
|
|
{
|
|
m_cursorMap.clear();
|
|
for (auto it = m_cursorList.begin(); it != m_cursorList.end(); ++it)
|
|
m_cursorMap[it->selectionStart()] = it;
|
|
}
|
|
|
|
MultiTextCursor& MultiTextCursor::operator=(const MultiTextCursor &multiCursor)
|
|
{
|
|
m_cursorList = multiCursor.m_cursorList;
|
|
fillMapWithList();
|
|
return *this;
|
|
}
|
|
|
|
MultiTextCursor::MultiTextCursor(const MultiTextCursor &multiCursor)
|
|
{
|
|
*this = multiCursor;
|
|
}
|
|
|
|
MultiTextCursor& MultiTextCursor::operator=(const MultiTextCursor &&multiCursor)
|
|
{
|
|
m_cursorList = std::move(multiCursor.m_cursorList);
|
|
fillMapWithList();
|
|
return *this;
|
|
}
|
|
|
|
MultiTextCursor::MultiTextCursor(const MultiTextCursor &&multiCursor)
|
|
{
|
|
*this = std::move(multiCursor);
|
|
}
|
|
|
|
MultiTextCursor::~MultiTextCursor() = default;
|
|
|
|
static bool cursorsOverlap(const QTextCursor &c1, const QTextCursor &c2)
|
|
{
|
|
if (c1.hasSelection()) {
|
|
if (c2.hasSelection()) {
|
|
return c2.selectionEnd() > c1.selectionStart()
|
|
&& c2.selectionStart() < c1.selectionEnd();
|
|
}
|
|
const int c2Pos = c2.position();
|
|
return c2Pos > c1.selectionStart() && c2Pos < c1.selectionEnd();
|
|
}
|
|
if (c2.hasSelection()) {
|
|
const int c1Pos = c1.position();
|
|
return c1Pos > c2.selectionStart() && c1Pos < c2.selectionEnd();
|
|
}
|
|
return c1 == c2;
|
|
};
|
|
|
|
static void mergeCursors(QTextCursor &c1, const QTextCursor &c2)
|
|
{
|
|
if (c1.position() == c2.position() && c1.anchor() == c2.anchor())
|
|
return;
|
|
if (c1.hasSelection()) {
|
|
if (!c2.hasSelection())
|
|
return;
|
|
int pos = c1.position();
|
|
int anchor = c1.anchor();
|
|
if (c1.selectionStart() > c2.selectionStart()) {
|
|
if (pos < anchor)
|
|
pos = c2.selectionStart();
|
|
else
|
|
anchor = c2.selectionStart();
|
|
}
|
|
if (c1.selectionEnd() < c2.selectionEnd()) {
|
|
if (pos < anchor)
|
|
anchor = c2.selectionEnd();
|
|
else
|
|
pos = c2.selectionEnd();
|
|
}
|
|
c1.setPosition(anchor);
|
|
c1.setPosition(pos, QTextCursor::KeepAnchor);
|
|
} else {
|
|
c1 = c2;
|
|
}
|
|
}
|
|
|
|
void MultiTextCursor::addCursor(const QTextCursor &cursor)
|
|
{
|
|
QTC_ASSERT(!cursor.isNull(), return);
|
|
|
|
QTextCursor c1 = cursor;
|
|
const int pos = c1.selectionStart();
|
|
|
|
auto found = m_cursorMap.lower_bound(pos);
|
|
if (found != m_cursorMap.begin())
|
|
--found;
|
|
|
|
for (; !m_cursorMap.empty() && found != m_cursorMap.end()
|
|
&& found->second->selectionStart() <= cursor.selectionEnd();) {
|
|
const QTextCursor &c2 = *found->second;
|
|
if (cursorsOverlap(c1, c2)) {
|
|
Utils::mergeCursors(c1, c2);
|
|
m_cursorList.erase(found->second);
|
|
found = m_cursorMap.erase(found);
|
|
continue;
|
|
}
|
|
++found;
|
|
}
|
|
|
|
m_cursorMap[pos] = m_cursorList.insert(m_cursorList.end(), c1);
|
|
}
|
|
|
|
void MultiTextCursor::addCursors(const QList<QTextCursor> &cursors)
|
|
{
|
|
for (const QTextCursor &c : cursors)
|
|
addCursor(c);
|
|
}
|
|
|
|
void MultiTextCursor::setCursors(const QList<QTextCursor> &cursors)
|
|
{
|
|
m_cursorList.clear();
|
|
m_cursorMap.clear();
|
|
addCursors(cursors);
|
|
}
|
|
|
|
const QList<QTextCursor> MultiTextCursor::cursors() const
|
|
{
|
|
return QList<QTextCursor>(m_cursorList.begin(), m_cursorList.end());
|
|
}
|
|
|
|
void MultiTextCursor::replaceMainCursor(const QTextCursor &cursor)
|
|
{
|
|
QTC_ASSERT(!cursor.isNull(), return);
|
|
takeMainCursor();
|
|
addCursor(cursor);
|
|
}
|
|
|
|
QTextCursor MultiTextCursor::mainCursor() const
|
|
{
|
|
if (m_cursorList.empty())
|
|
return {};
|
|
return m_cursorList.back();
|
|
}
|
|
|
|
QTextCursor MultiTextCursor::takeMainCursor()
|
|
{
|
|
if (m_cursorList.empty())
|
|
return {};
|
|
|
|
QTextCursor cursor = m_cursorList.back();
|
|
auto it = m_cursorList.end();
|
|
--it;
|
|
m_cursorMap.erase(it->selectionStart());
|
|
m_cursorList.erase(it);
|
|
|
|
return cursor;
|
|
}
|
|
|
|
void MultiTextCursor::beginEditBlock()
|
|
{
|
|
QTC_ASSERT(!m_cursorList.empty(), return);
|
|
m_cursorList.back().beginEditBlock();
|
|
}
|
|
|
|
void MultiTextCursor::endEditBlock()
|
|
{
|
|
QTC_ASSERT(!m_cursorList.empty(), return);
|
|
m_cursorList.back().endEditBlock();
|
|
}
|
|
|
|
bool MultiTextCursor::isNull() const
|
|
{
|
|
return m_cursorList.empty();
|
|
}
|
|
|
|
bool MultiTextCursor::hasMultipleCursors() const
|
|
{
|
|
return m_cursorList.size() > 1;
|
|
}
|
|
|
|
int MultiTextCursor::cursorCount() const
|
|
{
|
|
return static_cast<int>(m_cursorList.size());
|
|
}
|
|
|
|
void MultiTextCursor::movePosition(QTextCursor::MoveOperation operation,
|
|
QTextCursor::MoveMode mode,
|
|
int n)
|
|
{
|
|
for (auto &cursor : m_cursorList)
|
|
cursor.movePosition(operation, mode, n);
|
|
|
|
mergeCursors();
|
|
}
|
|
|
|
bool MultiTextCursor::hasSelection() const
|
|
{
|
|
return Utils::anyOf(m_cursorList, &QTextCursor::hasSelection);
|
|
}
|
|
|
|
QString MultiTextCursor::selectedText() const
|
|
{
|
|
QString text;
|
|
for (const auto &element : std::as_const(m_cursorMap)) {
|
|
const QTextCursor &cursor = *element.second;
|
|
const QString &cursorText = cursor.selectedText();
|
|
if (cursorText.isEmpty())
|
|
continue;
|
|
if (!text.isEmpty()) {
|
|
if (text.endsWith(QChar::ParagraphSeparator))
|
|
text.chop(1);
|
|
text.append('\n');
|
|
}
|
|
text.append(cursorText);
|
|
}
|
|
return text;
|
|
}
|
|
|
|
void MultiTextCursor::removeSelectedText()
|
|
{
|
|
beginEditBlock();
|
|
for (auto cursor = m_cursorList.begin(); cursor != m_cursorList.end(); ++cursor)
|
|
cursor->removeSelectedText();
|
|
endEditBlock();
|
|
mergeCursors();
|
|
}
|
|
|
|
void MultiTextCursor::clearSelection()
|
|
{
|
|
for (auto cursor = m_cursorList.begin(); cursor != m_cursorList.end(); ++cursor)
|
|
cursor->clearSelection();
|
|
}
|
|
|
|
static void insertAndSelect(QTextCursor &cursor, const QString &text, bool selectNewText)
|
|
{
|
|
if (selectNewText) {
|
|
const int anchor = cursor.position();
|
|
cursor.insertText(text);
|
|
const int pos = cursor.position();
|
|
cursor.setPosition(anchor);
|
|
cursor.setPosition(pos, QTextCursor::KeepAnchor);
|
|
} else {
|
|
cursor.insertText(text);
|
|
}
|
|
}
|
|
|
|
void MultiTextCursor::insertText(const QString &text, bool selectNewText)
|
|
{
|
|
if (m_cursorList.empty())
|
|
return;
|
|
|
|
m_cursorList.back().beginEditBlock();
|
|
if (hasMultipleCursors()) {
|
|
QStringList lines = text.split('\n');
|
|
if (!lines.isEmpty() && lines.last().isEmpty())
|
|
lines.pop_back();
|
|
int index = 0;
|
|
if (static_cast<long unsigned int>(lines.count()) == m_cursorList.size()) {
|
|
for (const auto &element : std::as_const(m_cursorMap)) {
|
|
QTextCursor &cursor = *element.second;
|
|
insertAndSelect(cursor, lines.at(index++), selectNewText);
|
|
}
|
|
m_cursorList.back().endEditBlock();
|
|
return;
|
|
}
|
|
}
|
|
for (auto cursor = m_cursorList.begin(); cursor != m_cursorList.end(); ++cursor)
|
|
insertAndSelect(*cursor, text, selectNewText);
|
|
m_cursorList.back().endEditBlock();
|
|
}
|
|
|
|
bool equalCursors(const QTextCursor &lhs, const QTextCursor &rhs)
|
|
{
|
|
return lhs == rhs && lhs.anchor() == rhs.anchor();
|
|
}
|
|
|
|
bool MultiTextCursor::operator==(const MultiTextCursor &other) const
|
|
{
|
|
if (m_cursorList.size() != other.m_cursorList.size())
|
|
return false;
|
|
if (m_cursorList.empty())
|
|
return true;
|
|
|
|
if (!equalCursors(m_cursorList.back(), other.m_cursorList.back()))
|
|
return false;
|
|
|
|
auto it = m_cursorMap.begin();
|
|
auto otherIt = other.m_cursorMap.begin();
|
|
for (;it != m_cursorMap.end() && otherIt != other.m_cursorMap.end(); ++it, ++otherIt) {
|
|
const QTextCursor &cursor = *it->second;
|
|
const QTextCursor &otherCursor = *otherIt->second;
|
|
if (it->first != otherIt->first || cursor != otherCursor
|
|
|| cursor.anchor() != otherCursor.anchor())
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool MultiTextCursor::operator!=(const MultiTextCursor &other) const
|
|
{
|
|
return !operator==(other);
|
|
}
|
|
|
|
void MultiTextCursor::mergeCursors()
|
|
{
|
|
QList<QTextCursor> cursors(m_cursorList.begin(), m_cursorList.end());
|
|
setCursors(cursors);
|
|
}
|
|
|
|
// could go into QTextCursor...
|
|
static QTextLine currentTextLine(const QTextCursor &cursor)
|
|
{
|
|
const QTextBlock block = cursor.block();
|
|
if (!block.isValid())
|
|
return {};
|
|
|
|
const QTextLayout *layout = block.layout();
|
|
if (!layout)
|
|
return {};
|
|
|
|
const int relativePos = cursor.position() - block.position();
|
|
return layout->lineForTextPosition(relativePos);
|
|
}
|
|
|
|
bool MultiTextCursor::multiCursorEvent(
|
|
QKeyEvent *e, QKeySequence::StandardKey matchKey, Qt::KeyboardModifiers filterModifiers)
|
|
{
|
|
filterModifiers |= (Utils::HostOsInfo::isMacHost() ? Qt::KeypadModifier : Qt::AltModifier);
|
|
uint searchkey = (e->modifiers() | e->key()) & ~filterModifiers;
|
|
|
|
const QList<QKeySequence> bindings = QKeySequence::keyBindings(matchKey);
|
|
return bindings.contains(QKeySequence(searchkey));
|
|
}
|
|
|
|
bool MultiTextCursor::handleMoveKeyEvent(QKeyEvent *e,
|
|
QPlainTextEdit *edit,
|
|
bool camelCaseNavigationEnabled)
|
|
{
|
|
if (e->modifiers() & Qt::AltModifier && !Utils::HostOsInfo::isMacHost()) {
|
|
QTextCursor::MoveOperation op = QTextCursor::NoMove;
|
|
if (multiCursorEvent(e, QKeySequence::MoveToNextWord)) {
|
|
op = QTextCursor::WordRight;
|
|
} else if (multiCursorEvent(e, QKeySequence::MoveToPreviousWord)) {
|
|
op = QTextCursor::WordLeft;
|
|
} else if (multiCursorEvent(e, QKeySequence::MoveToEndOfBlock)) {
|
|
op = QTextCursor::EndOfBlock;
|
|
} else if (multiCursorEvent(e, QKeySequence::MoveToStartOfBlock)) {
|
|
op = QTextCursor::StartOfBlock;
|
|
} else if (multiCursorEvent(e, QKeySequence::MoveToNextLine, Qt::ShiftModifier)) {
|
|
op = QTextCursor::Down;
|
|
} else if (multiCursorEvent(e, QKeySequence::MoveToPreviousLine, Qt::ShiftModifier)) {
|
|
op = QTextCursor::Up;
|
|
} else if (multiCursorEvent(e, QKeySequence::MoveToStartOfLine)) {
|
|
op = QTextCursor::StartOfLine;
|
|
} else if (multiCursorEvent(e, QKeySequence::MoveToEndOfLine)) {
|
|
op = QTextCursor::EndOfLine;
|
|
} else if (multiCursorEvent(e, QKeySequence::MoveToStartOfDocument)) {
|
|
op = QTextCursor::Start;
|
|
} else if (multiCursorEvent(e, QKeySequence::MoveToEndOfDocument)) {
|
|
op = QTextCursor::End;
|
|
}
|
|
|
|
if (op != QTextCursor::NoMove) {
|
|
const std::list<QTextCursor> cursors = m_cursorList;
|
|
for (QTextCursor cursor : cursors) {
|
|
if (camelCaseNavigationEnabled && op == QTextCursor::WordRight)
|
|
CamelCaseCursor::right(&cursor, edit, QTextCursor::MoveAnchor);
|
|
else if (camelCaseNavigationEnabled && op == QTextCursor::WordLeft)
|
|
CamelCaseCursor::left(&cursor, edit, QTextCursor::MoveAnchor);
|
|
else
|
|
cursor.movePosition(op, QTextCursor::MoveAnchor);
|
|
|
|
addCursor(cursor);
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
|
|
for (auto it = m_cursorList.begin(); it != m_cursorList.end(); ++it) {
|
|
QTextCursor &cursor = *it;
|
|
QTextCursor::MoveMode mode = QTextCursor::MoveAnchor;
|
|
QTextCursor::MoveOperation op = QTextCursor::NoMove;
|
|
|
|
if (e == QKeySequence::MoveToNextChar) {
|
|
op = QTextCursor::Right;
|
|
} else if (e == QKeySequence::MoveToPreviousChar) {
|
|
op = QTextCursor::Left;
|
|
} else if (multiCursorEvent(e, QKeySequence::SelectNextChar)) {
|
|
op = QTextCursor::Right;
|
|
mode = QTextCursor::KeepAnchor;
|
|
} else if (multiCursorEvent(e, QKeySequence::SelectPreviousChar)) {
|
|
op = QTextCursor::Left;
|
|
mode = QTextCursor::KeepAnchor;
|
|
} else if (multiCursorEvent(e, QKeySequence::SelectNextWord)) {
|
|
op = QTextCursor::WordRight;
|
|
mode = QTextCursor::KeepAnchor;
|
|
} else if (multiCursorEvent(e, QKeySequence::SelectPreviousWord)) {
|
|
op = QTextCursor::WordLeft;
|
|
mode = QTextCursor::KeepAnchor;
|
|
} else if (multiCursorEvent(e, QKeySequence::SelectStartOfLine)) {
|
|
op = QTextCursor::StartOfLine;
|
|
mode = QTextCursor::KeepAnchor;
|
|
} else if (multiCursorEvent(e, QKeySequence::SelectEndOfLine)) {
|
|
op = QTextCursor::EndOfLine;
|
|
mode = QTextCursor::KeepAnchor;
|
|
} else if (multiCursorEvent(e, QKeySequence::SelectStartOfBlock)) {
|
|
op = QTextCursor::StartOfBlock;
|
|
mode = QTextCursor::KeepAnchor;
|
|
} else if (multiCursorEvent(e, QKeySequence::SelectEndOfBlock)) {
|
|
op = QTextCursor::EndOfBlock;
|
|
mode = QTextCursor::KeepAnchor;
|
|
} else if (e == QKeySequence::SelectStartOfDocument) {
|
|
op = QTextCursor::Start;
|
|
mode = QTextCursor::KeepAnchor;
|
|
} else if (e == QKeySequence::SelectEndOfDocument) {
|
|
op = QTextCursor::End;
|
|
mode = QTextCursor::KeepAnchor;
|
|
} else if (e == QKeySequence::SelectPreviousLine) {
|
|
op = QTextCursor::Up;
|
|
mode = QTextCursor::KeepAnchor;
|
|
} else if (e == QKeySequence::SelectNextLine) {
|
|
op = QTextCursor::Down;
|
|
mode = QTextCursor::KeepAnchor;
|
|
{
|
|
QTextBlock block = cursor.block();
|
|
QTextLine line = currentTextLine(cursor);
|
|
if (!block.next().isValid() && line.isValid()
|
|
&& line.lineNumber() == block.layout()->lineCount() - 1)
|
|
op = QTextCursor::End;
|
|
}
|
|
} else if (e == QKeySequence::MoveToNextWord) {
|
|
op = QTextCursor::WordRight;
|
|
} else if (e == QKeySequence::MoveToPreviousWord) {
|
|
op = QTextCursor::WordLeft;
|
|
} else if (e == QKeySequence::MoveToEndOfBlock) {
|
|
op = QTextCursor::EndOfBlock;
|
|
} else if (e == QKeySequence::MoveToStartOfBlock) {
|
|
op = QTextCursor::StartOfBlock;
|
|
} else if (e == QKeySequence::MoveToNextLine) {
|
|
op = QTextCursor::Down;
|
|
} else if (e == QKeySequence::MoveToPreviousLine) {
|
|
op = QTextCursor::Up;
|
|
} else if (e == QKeySequence::MoveToStartOfLine) {
|
|
op = QTextCursor::StartOfLine;
|
|
} else if (e == QKeySequence::MoveToEndOfLine) {
|
|
op = QTextCursor::EndOfLine;
|
|
} else if (e == QKeySequence::MoveToStartOfDocument) {
|
|
op = QTextCursor::Start;
|
|
} else if (e == QKeySequence::MoveToEndOfDocument) {
|
|
op = QTextCursor::End;
|
|
} else {
|
|
return false;
|
|
}
|
|
|
|
// Except for pageup and pagedown, macOS has very different behavior, we don't do it all, but
|
|
// here's the breakdown:
|
|
// Shift still works as an anchor, but only one of the other keys can be down Ctrl (Command),
|
|
// Alt (Option), or Meta (Control).
|
|
// Command/Control + Left/Right -- Move to left or right of the line
|
|
// + Up/Down -- Move to top bottom of the file. (Control doesn't move the cursor)
|
|
// Option + Left/Right -- Move one word Left/right.
|
|
// + Up/Down -- Begin/End of Paragraph.
|
|
// Home/End Top/Bottom of file. (usually don't move the cursor, but will select)
|
|
|
|
bool visualNavigation = cursor.visualNavigation();
|
|
cursor.setVisualNavigation(true);
|
|
|
|
if (camelCaseNavigationEnabled && op == QTextCursor::WordRight)
|
|
CamelCaseCursor::right(&cursor, edit, mode);
|
|
else if (camelCaseNavigationEnabled && op == QTextCursor::WordLeft)
|
|
CamelCaseCursor::left(&cursor, edit, mode);
|
|
else if (!cursor.movePosition(op, mode) && mode == QTextCursor::MoveAnchor)
|
|
cursor.clearSelection();
|
|
cursor.setVisualNavigation(visualNavigation);
|
|
}
|
|
mergeCursors();
|
|
return true;
|
|
}
|
|
|
|
} // namespace Utils
|