diff --git a/src/libs/utils/CMakeLists.txt b/src/libs/utils/CMakeLists.txt index d898f228be4..830b036aed2 100644 --- a/src/libs/utils/CMakeLists.txt +++ b/src/libs/utils/CMakeLists.txt @@ -24,6 +24,7 @@ add_qtc_library(Utils basetreeview.cpp basetreeview.h benchmarker.cpp benchmarker.h buildablehelperlibrary.cpp buildablehelperlibrary.h + camelcasecursor.cpp camelcasecursor.h categorysortfiltermodel.cpp categorysortfiltermodel.h changeset.cpp changeset.h checkablemessagebox.cpp checkablemessagebox.h diff --git a/src/libs/utils/camelcasecursor.cpp b/src/libs/utils/camelcasecursor.cpp new file mode 100644 index 00000000000..65e69bd70b1 --- /dev/null +++ b/src/libs/utils/camelcasecursor.cpp @@ -0,0 +1,341 @@ +/************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Copyright (C) 2019 Andre Hartmann +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#include "camelcasecursor.h" + +#include +#include + +template +bool moveCursor(C *cursor, E *edit, QTextCursor::MoveOperation direction, QTextCursor::MoveMode mode); + +template<> +bool moveCursor(QTextCursor *cursor, QPlainTextEdit *, QTextCursor::MoveOperation direction, + QTextCursor::MoveMode mode) +{ + return cursor->movePosition(direction, mode); +} + +template +bool moveCursor(C *, QLineEdit *edit, QTextCursor::MoveOperation direction, QTextCursor::MoveMode mode) +{ + bool mark = (mode == QTextCursor::KeepAnchor); + switch (direction) { + case QTextCursor::Left: + edit->cursorBackward(mark); + break; + case QTextCursor::WordLeft: + edit->cursorWordBackward(mark); + break; + case QTextCursor::Right: + edit->cursorForward(mark); + break; + case QTextCursor::WordRight: + edit->cursorWordForward(mark); + break; + default: + return false; + } + return edit->cursorPosition() > 0 && edit->cursorPosition() < edit->text().size(); +} + +template +QChar charUnderCursor(C *cursor, E *edit); + +template<> +QChar charUnderCursor(QTextCursor *cursor, QPlainTextEdit *edit) +{ + return edit->document()->characterAt(cursor->position()); +} + +template +QChar charUnderCursor(C *, QLineEdit *edit) +{ + const int pos = edit->cursorPosition(); + if (pos < 0 || pos >= edit->text().length()) + return QChar::Null; + + return edit->text().at(pos); +}; + +template +int position(C *cursor, E *edit); + +template<> +int position(QTextCursor *cursor, QPlainTextEdit *) +{ + return cursor->position(); +} + +template +int position(C *, QLineEdit *edit) +{ + return edit->cursorPosition(); +} + +enum class Input { + Upper, + Lower, + Underscore, + Space, + Other +}; + +template +bool camelCaseLeft(C *cursor, E *edit, QTextCursor::MoveMode mode) +{ + int state = 0; + + if (!moveCursor(cursor, edit, QTextCursor::Left, mode)) + return false; + + for (;;) { + QChar c = charUnderCursor(cursor, edit); + Input input = Input::Other; + if (c.isUpper()) + input = Input::Upper; + else if (c.isLower() || c.isDigit()) + input = Input::Lower; + else if (c == '_') + input = Input::Underscore; + else if (c.isSpace() && c != QChar::ParagraphSeparator) + input = Input::Space; + else + input = Input::Other; + + switch (state) { + case 0: + switch (input) { + case Input::Upper: + state = 1; + break; + case Input::Lower: + state = 2; + break; + case Input::Underscore: + state = 3; + break; + case Input::Space: + state = 4; + break; + default: + moveCursor(cursor, edit, QTextCursor::Right, mode); + return moveCursor(cursor, edit, QTextCursor::WordLeft, mode); + } + break; + case 1: + switch (input) { + case Input::Upper: + break; + default: + return moveCursor(cursor, edit, QTextCursor::Right, mode); + return true; + } + break; + case 2: + switch (input) { + case Input::Upper: + return true; + case Input::Lower: + break; + default: + return moveCursor(cursor, edit, QTextCursor::Right, mode); + return true; + } + break; + case 3: + switch (input) { + case Input::Underscore: + break; + case Input::Upper: + state = 1; + break; + case Input::Lower: + state = 2; + break; + default: + moveCursor(cursor, edit, QTextCursor::Right, mode); + return true; + } + break; + case 4: + switch (input) { + case Input::Space: + break; + case Input::Upper: + state = 1; + break; + case Input::Lower: + state = 2; + break; + case Input::Underscore: + state = 3; + break; + default: + return moveCursor(cursor, edit, QTextCursor::Right, mode); + if (position(cursor, edit) == 0) + return true; + return moveCursor(cursor, edit, QTextCursor::WordLeft, mode); + } + } + + if (!moveCursor(cursor, edit, QTextCursor::Left, mode)) + return true; + } +} + +template +bool camelCaseRight(C *cursor, E *edit, QTextCursor::MoveMode mark) +{ + int state = 0; + + for (;;) { + QChar c = charUnderCursor(cursor, edit); + Input input = Input::Other; + if (c.isUpper()) + input = Input::Upper; + else if (c.isLower() || c.isDigit()) + input = Input::Lower; + else if (c == '_') + input = Input::Underscore; + else if (c.isSpace() && c != QChar::ParagraphSeparator) + input = Input::Space; + else + input = Input::Other; + + switch (state) { + case 0: + switch (input) { + case Input::Upper: + state = 4; + break; + case Input::Lower: + state = 1; + break; + case Input::Underscore: + state = 6; + break; + default: + return moveCursor(cursor, edit, QTextCursor::WordRight, mark); + } + break; + case 1: + switch (input) { + case Input::Upper: + return true; + case Input::Lower: + break; + case Input::Underscore: + state = 6; + break; + case Input::Space: + state = 7; + break; + default: + return true; + } + break; + case 2: + switch (input) { + case Input::Upper: + break; + case Input::Lower: + moveCursor(cursor, edit, QTextCursor::Left, mark); + return true; + case Input::Underscore: + state = 6; + break; + case Input::Space: + state = 7; + break; + default: + return true; + } + break; + case 4: + switch (input) { + case Input::Upper: + state = 2; + break; + case Input::Lower: + state = 1; + break; + case Input::Underscore: + state = 6; + break; + case Input::Space: + state = 7; + break; + default: + return true; + } + break; + case 6: + switch (input) { + case Input::Underscore: + break; + case Input::Space: + state = 7; + break; + default: + return true; + } + break; + case 7: + switch (input) { + case Input::Space: + break; + default: + return true; + } + break; + } + if (!moveCursor(cursor, edit, QTextCursor::Right, mark)) + return false; + } +} + +bool CamelCaseCursor::left(QTextCursor *cursor, QPlainTextEdit *edit, QTextCursor::MoveMode mode) +{ + return camelCaseLeft(cursor, edit, mode); +} + +bool CamelCaseCursor::left(QLineEdit *edit, QTextCursor::MoveMode mode) +{ + QTextCursor temp; + return camelCaseLeft(&temp, edit, mode); +} + +bool CamelCaseCursor::right(QTextCursor *cursor, QPlainTextEdit *edit, QTextCursor::MoveMode mode) +{ + return camelCaseRight(cursor, edit, mode); +} + +bool CamelCaseCursor::right(QLineEdit *edit, QTextCursor::MoveMode mode) +{ + QTextCursor temp; + return camelCaseRight(&temp, edit, mode); +} diff --git a/src/libs/utils/camelcasecursor.h b/src/libs/utils/camelcasecursor.h new file mode 100644 index 00000000000..59675457370 --- /dev/null +++ b/src/libs/utils/camelcasecursor.h @@ -0,0 +1,45 @@ +/************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Copyright (C) 2019 Andre Hartmann +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#pragma once + +#include "utils_global.h" + +#include + +QT_BEGIN_NAMESPACE +class QLineEdit; +class QPlainTextEdit; +QT_END_NAMESPACE + +class QTCREATOR_UTILS_EXPORT CamelCaseCursor +{ +public: + static bool left(QTextCursor *cursor, QPlainTextEdit *edit, QTextCursor::MoveMode mode); + static bool left(QLineEdit *edit, QTextCursor::MoveMode mode); + static bool right(QTextCursor *cursor, QPlainTextEdit *edit, QTextCursor::MoveMode mode); + static bool right(QLineEdit *edit, QTextCursor::MoveMode mode); +}; diff --git a/src/libs/utils/fancylineedit.cpp b/src/libs/utils/fancylineedit.cpp index 205de0dcc9d..29178ce02fb 100644 --- a/src/libs/utils/fancylineedit.cpp +++ b/src/libs/utils/fancylineedit.cpp @@ -23,6 +23,7 @@ ** ****************************************************************************/ +#include "camelcasecursor.h" #include "execmenu.h" #include "fancylineedit.h" #include "historycompleter.h" @@ -80,6 +81,8 @@ enum { margin = 6 }; namespace Utils { +static bool camelCaseNavigation = false; + // --------- FancyLineEditPrivate class FancyLineEditPrivate : public QObject { @@ -328,6 +331,24 @@ void FancyLineEdit::onEditingFinished() d->m_historyCompleter->addEntry(text()); } +void FancyLineEdit::keyPressEvent(QKeyEvent *event) +{ + const QTextCursor::MoveMode mode = (event->modifiers() & Qt::ShiftModifier) + ? QTextCursor::KeepAnchor : QTextCursor::MoveAnchor; + + if (camelCaseNavigation && event == QKeySequence::MoveToPreviousWord) + CamelCaseCursor::left(this, mode); + else if (camelCaseNavigation && event == QKeySequence::MoveToNextWord) + CamelCaseCursor::right(this, mode); + else + QLineEdit::keyPressEvent(event); +} + +void FancyLineEdit::setCamelCaseNavigationEnabled(bool enabled) +{ + camelCaseNavigation = enabled; +} + void FancyLineEdit::setSpecialCompleter(QCompleter *completer) { QTC_ASSERT(!d->m_historyCompleter, return); diff --git a/src/libs/utils/fancylineedit.h b/src/libs/utils/fancylineedit.h index fd0b121d88d..986f061338f 100644 --- a/src/libs/utils/fancylineedit.h +++ b/src/libs/utils/fancylineedit.h @@ -141,9 +141,12 @@ public: void validate(); void onEditingFinished(); + static void setCamelCaseNavigationEnabled(bool enabled); + protected: // Custom behaviour can be added here. virtual void handleChanged(const QString &) {} + void keyPressEvent(QKeyEvent *event); signals: void buttonClicked(Utils::FancyLineEdit::Side side); @@ -170,6 +173,8 @@ private: void updateMargins(); void updateButtonPositions(); + bool camelCaseBackward(bool mark); + bool camelCaseForward(bool mark); friend class FancyLineEditPrivate; FancyLineEditPrivate *d; diff --git a/src/libs/utils/utils-lib.pri b/src/libs/utils/utils-lib.pri index 793658a1eca..2b1c767d9f1 100644 --- a/src/libs/utils/utils-lib.pri +++ b/src/libs/utils/utils-lib.pri @@ -130,7 +130,8 @@ SOURCES += \ $$PWD/removefiledialog.cpp \ $$PWD/differ.cpp \ $$PWD/jsontreeitem.cpp \ - $$PWD/namevaluevalidator.cpp + $$PWD/namevaluevalidator.cpp \ + $$PWD/camelcasecursor.cpp HEADERS += \ $$PWD/environmentfwd.h \ @@ -275,7 +276,8 @@ HEADERS += \ $$PWD/cpplanguage_details.h \ $$PWD/jsontreeitem.h \ $$PWD/listmodel.h \ - $$PWD/namevaluevalidator.h + $$PWD/namevaluevalidator.h \ + $$PWD/camelcasecursor.h FORMS += $$PWD/filewizardpage.ui \ $$PWD/newclasswidget.ui \ diff --git a/src/libs/utils/utils.qbs b/src/libs/utils/utils.qbs index c3adefd0603..a1263a3a421 100644 --- a/src/libs/utils/utils.qbs +++ b/src/libs/utils/utils.qbs @@ -50,6 +50,8 @@ Project { "benchmarker.h", "buildablehelperlibrary.cpp", "buildablehelperlibrary.h", + "camelcasecursor.cpp", + "camelcasecursor.h", "categorysortfiltermodel.cpp", "categorysortfiltermodel.h", "changeset.cpp", diff --git a/src/plugins/texteditor/texteditor.cpp b/src/plugins/texteditor/texteditor.cpp index 98ff2359e10..570773be248 100644 --- a/src/plugins/texteditor/texteditor.cpp +++ b/src/plugins/texteditor/texteditor.cpp @@ -71,6 +71,7 @@ #include #include #include +#include #include #include #include @@ -559,8 +560,6 @@ public: void universalHelper(); // test function for development bool cursorMoveKeyEvent(QKeyEvent *e); - bool camelCaseRight(QTextCursor &cursor, QTextCursor::MoveMode mode); - bool camelCaseLeft(QTextCursor &cursor, QTextCursor::MoveMode mode); void processTooltipRequest(const QTextCursor &c); bool processAnnotaionTooltipRequest(const QTextBlock &block, const QPoint &pos) const; @@ -1628,28 +1627,28 @@ void TextEditorWidget::gotoNextWordWithSelection() void TextEditorWidget::gotoPreviousWordCamelCase() { QTextCursor c = textCursor(); - d->camelCaseLeft(c, QTextCursor::MoveAnchor); + CamelCaseCursor::left(&c, this, QTextCursor::MoveAnchor); setTextCursor(c); } void TextEditorWidget::gotoPreviousWordCamelCaseWithSelection() { QTextCursor c = textCursor(); - d->camelCaseLeft(c, QTextCursor::KeepAnchor); + CamelCaseCursor::left(&c, this, QTextCursor::KeepAnchor); setTextCursor(c); } void TextEditorWidget::gotoNextWordCamelCase() { QTextCursor c = textCursor(); - d->camelCaseRight(c, QTextCursor::MoveAnchor); + CamelCaseCursor::right(&c, this, QTextCursor::MoveAnchor); setTextCursor(c); } void TextEditorWidget::gotoNextWordCamelCaseWithSelection() { QTextCursor c = textCursor(); - d->camelCaseRight(c, QTextCursor::KeepAnchor); + CamelCaseCursor::right(&c, this, QTextCursor::KeepAnchor); setTextCursor(c); } @@ -2061,232 +2060,6 @@ static QTextLine currentTextLine(const QTextCursor &cursor) return layout->lineForTextPosition(relativePos); } -bool TextEditorWidgetPrivate::camelCaseLeft(QTextCursor &cursor, QTextCursor::MoveMode mode) -{ - int state = 0; - enum Input { - Input_U, - Input_l, - Input_underscore, - Input_space, - Input_other - }; - - if (!cursor.movePosition(QTextCursor::Left, mode)) - return false; - - forever { - QChar c = q->document()->characterAt(cursor.position()); - Input input = Input_other; - if (c.isUpper()) - input = Input_U; - else if (c.isLower() || c.isDigit()) - input = Input_l; - else if (c == QLatin1Char('_')) - input = Input_underscore; - else if (c.isSpace() && c != QChar::ParagraphSeparator) - input = Input_space; - else - input = Input_other; - - switch (state) { - case 0: - switch (input) { - case Input_U: - state = 1; - break; - case Input_l: - state = 2; - break; - case Input_underscore: - state = 3; - break; - case Input_space: - state = 4; - break; - default: - cursor.movePosition(QTextCursor::Right, mode); - return cursor.movePosition(QTextCursor::WordLeft, mode); - } - break; - case 1: - switch (input) { - case Input_U: - break; - default: - cursor.movePosition(QTextCursor::Right, mode); - return true; - } - break; - - case 2: - switch (input) { - case Input_U: - return true; - case Input_l: - break; - default: - cursor.movePosition(QTextCursor::Right, mode); - return true; - } - break; - case 3: - switch (input) { - case Input_underscore: - break; - case Input_U: - state = 1; - break; - case Input_l: - state = 2; - break; - default: - cursor.movePosition(QTextCursor::Right, mode); - return true; - } - break; - case 4: - switch (input) { - case Input_space: - break; - case Input_U: - state = 1; - break; - case Input_l: - state = 2; - break; - case Input_underscore: - state = 3; - break; - default: - cursor.movePosition(QTextCursor::Right, mode); - if (cursor.positionInBlock() == 0) - return true; - return cursor.movePosition(QTextCursor::WordLeft, mode); - } - } - - if (!cursor.movePosition(QTextCursor::Left, mode)) - return true; - } -} - -bool TextEditorWidgetPrivate::camelCaseRight(QTextCursor &cursor, QTextCursor::MoveMode mode) -{ - int state = 0; - enum Input { - Input_U, - Input_l, - Input_underscore, - Input_space, - Input_other - }; - - forever { - QChar c = q->document()->characterAt(cursor.position()); - Input input = Input_other; - if (c.isUpper()) - input = Input_U; - else if (c.isLower() || c.isDigit()) - input = Input_l; - else if (c == QLatin1Char('_')) - input = Input_underscore; - else if (c.isSpace() && c != QChar::ParagraphSeparator) - input = Input_space; - else - input = Input_other; - - switch (state) { - case 0: - switch (input) { - case Input_U: - state = 4; - break; - case Input_l: - state = 1; - break; - case Input_underscore: - state = 6; - break; - default: - return cursor.movePosition(QTextCursor::WordRight, mode); - } - break; - case 1: - switch (input) { - case Input_U: - return true; - case Input_l: - break; - case Input_underscore: - state = 6; - break; - case Input_space: - state = 7; - break; - default: - return true; - } - break; - case 2: - switch (input) { - case Input_U: - break; - case Input_l: - cursor.movePosition(QTextCursor::Left, mode); - return true; - case Input_underscore: - state = 6; - break; - case Input_space: - state = 7; - break; - default: - return true; - } - break; - case 4: - switch (input) { - case Input_U: - state = 2; - break; - case Input_l: - state = 1; - break; - case Input_underscore: - state = 6; - break; - case Input_space: - state = 7; - break; - default: - return true; - } - break; - case 6: - switch (input) { - case Input_underscore: - break; - case Input_space: - state = 7; - break; - default: - return true; - } - break; - case 7: - switch (input) { - case Input_space: - break; - default: - return true; - } - break; - } - cursor.movePosition(QTextCursor::Right, mode); - } -} - bool TextEditorWidgetPrivate::cursorMoveKeyEvent(QKeyEvent *e) { QTextCursor cursor = q->textCursor(); @@ -2381,9 +2154,9 @@ bool TextEditorWidgetPrivate::cursorMoveKeyEvent(QKeyEvent *e) cursor.setVisualNavigation(true); if (q->camelCaseNavigationEnabled() && op == QTextCursor::WordRight) - camelCaseRight(cursor, mode); + CamelCaseCursor::right(&cursor, q, mode); else if (q->camelCaseNavigationEnabled() && op == QTextCursor::WordLeft) - camelCaseLeft(cursor, mode); + CamelCaseCursor::left(&cursor, q, mode); else if (!cursor.movePosition(op, mode) && mode == QTextCursor::MoveAnchor) cursor.clearSelection(); cursor.setVisualNavigation(visualNavigation); @@ -2586,7 +2359,7 @@ void TextEditorWidget::keyPressEvent(QKeyEvent *e) QTextCursor c = textCursor(); int pos = c.position(); if (camelCaseNavigationEnabled()) - d->camelCaseLeft(c, QTextCursor::MoveAnchor); + CamelCaseCursor::left(&c, this, QTextCursor::MoveAnchor); else c.movePosition(QTextCursor::StartOfWord, QTextCursor::MoveAnchor); int targetpos = c.position(); @@ -2602,7 +2375,7 @@ void TextEditorWidget::keyPressEvent(QKeyEvent *e) e->accept(); QTextCursor c = textCursor(); if (camelCaseNavigationEnabled()) - d->camelCaseLeft(c, QTextCursor::KeepAnchor); + CamelCaseCursor::left(&c, this, QTextCursor::KeepAnchor); else c.movePosition(QTextCursor::StartOfWord, QTextCursor::KeepAnchor); c.removeSelectedText(); @@ -2611,7 +2384,7 @@ void TextEditorWidget::keyPressEvent(QKeyEvent *e) e->accept(); QTextCursor c = textCursor(); if (camelCaseNavigationEnabled()) - d->camelCaseRight(c, QTextCursor::KeepAnchor); + CamelCaseCursor::right(&c, this, QTextCursor::KeepAnchor); else c.movePosition(QTextCursor::EndOfWord, QTextCursor::KeepAnchor); c.removeSelectedText(); @@ -7111,7 +6884,7 @@ void TextEditorWidget::deleteEndOfWord() void TextEditorWidget::deleteEndOfWordCamelCase() { QTextCursor c = textCursor(); - d->camelCaseRight(c, QTextCursor::KeepAnchor); + CamelCaseCursor::right(&c, this, QTextCursor::KeepAnchor); c.removeSelectedText(); setTextCursor(c); } @@ -7133,7 +6906,7 @@ void TextEditorWidget::deleteStartOfWord() void TextEditorWidget::deleteStartOfWordCamelCase() { QTextCursor c = textCursor(); - d->camelCaseLeft(c, QTextCursor::KeepAnchor); + CamelCaseCursor::left(&c, this, QTextCursor::KeepAnchor); c.removeSelectedText(); setTextCursor(c); } diff --git a/src/plugins/texteditor/texteditorsettings.cpp b/src/plugins/texteditor/texteditorsettings.cpp index 3d66f387e4f..650959c73dc 100644 --- a/src/plugins/texteditor/texteditorsettings.cpp +++ b/src/plugins/texteditor/texteditorsettings.cpp @@ -48,6 +48,7 @@ #include #include #include +#include #include #include @@ -399,6 +400,13 @@ TextEditorSettings::TextEditorSettings() this, &TextEditorSettings::completionSettingsChanged); connect(d->m_completionSettingsPage, &CompletionSettingsPage::commentsSettingsChanged, this, &TextEditorSettings::commentsSettingsChanged); + + auto updateCamelCaseNavigation = [] { + Utils::FancyLineEdit::setCamelCaseNavigationEnabled(behaviorSettings().m_camelCaseNavigation); + }; + connect(d->m_behaviorSettingsPage, &BehaviorSettingsPage::behaviorSettingsChanged, + this, updateCamelCaseNavigation); + updateCamelCaseNavigation(); } TextEditorSettings::~TextEditorSettings()