FancyLineEdit: Add camel case navigation

Use it for search and replace functions as well as for Locator.

The camel case navigation can be switched on / off with
the existing menu Options > Text Editor > Behavior >
Enable Build-in camel case navigation.

Fixes: QTCREATORBUG-21140
Change-Id: I3f2dcafff231366b3c8f08c14514dd8940cca2a0
Reviewed-by: Eike Ziller <eike.ziller@qt.io>
This commit is contained in:
Andre Hartmann
2017-07-30 21:39:50 +02:00
committed by André Hartmann
parent 71e9e3832a
commit 839f45faa9
9 changed files with 439 additions and 241 deletions

View File

@@ -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

View File

@@ -0,0 +1,341 @@
/**************************************************************************
**
** Copyright (C) 2019 The Qt Company Ltd.
** Copyright (C) 2019 Andre Hartmann <aha_1980@gmx.de>
** 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 <QLineEdit>
#include <QPlainTextEdit>
template<typename C, typename E>
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<typename C>
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<typename C, typename E>
QChar charUnderCursor(C *cursor, E *edit);
template<>
QChar charUnderCursor(QTextCursor *cursor, QPlainTextEdit *edit)
{
return edit->document()->characterAt(cursor->position());
}
template<typename C>
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<typename C, typename E>
int position(C *cursor, E *edit);
template<>
int position(QTextCursor *cursor, QPlainTextEdit *)
{
return cursor->position();
}
template<typename C>
int position(C *, QLineEdit *edit)
{
return edit->cursorPosition();
}
enum class Input {
Upper,
Lower,
Underscore,
Space,
Other
};
template<typename C, typename E>
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<typename C, typename E>
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);
}

View File

@@ -0,0 +1,45 @@
/**************************************************************************
**
** Copyright (C) 2019 The Qt Company Ltd.
** Copyright (C) 2019 Andre Hartmann <aha_1980@gmx.de>
** 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 <QTextCursor>
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);
};

View File

@@ -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);

View File

@@ -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;

View File

@@ -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 \

View File

@@ -50,6 +50,8 @@ Project {
"benchmarker.h",
"buildablehelperlibrary.cpp",
"buildablehelperlibrary.h",
"camelcasecursor.cpp",
"camelcasecursor.h",
"categorysortfiltermodel.cpp",
"categorysortfiltermodel.h",
"changeset.cpp",

View File

@@ -71,6 +71,7 @@
#include <coreplugin/find/highlightscrollbarcontroller.h>
#include <utils/algorithm.h>
#include <utils/textutils.h>
#include <utils/camelcasecursor.h>
#include <utils/fixedsizeclicklabel.h>
#include <utils/fileutils.h>
#include <utils/dropsupport.h>
@@ -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);
}

View File

@@ -48,6 +48,7 @@
#include <extensionsystem/pluginmanager.h>
#include <coreplugin/icore.h>
#include <coreplugin/messagemanager.h>
#include <utils/fancylineedit.h>
#include <utils/qtcassert.h>
#include <QApplication>
@@ -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()