From e66fe7ea65b218dd320cb553bf6669d2a2021657 Mon Sep 17 00:00:00 2001 From: Warwick Allison Date: Fri, 28 May 2010 11:55:11 +1000 Subject: Add selection methods to TextEdit Sufficient to allow different selection look and feel (see whacky example) Task-number: QTBUG-10968 Reviewed-by: Michael Brasser Reviewed-by: Alan Alpert --- examples/declarative/text/edit/edit.qml | 248 +++++++++++++++++++++ examples/declarative/text/edit/pics/endHandle.png | Bin 0 -> 185 bytes examples/declarative/text/edit/pics/endHandle.sci | 5 + .../declarative/text/edit/pics/startHandle.png | Bin 0 -> 178 bytes .../declarative/text/edit/pics/startHandle.sci | 5 + src/declarative/QmlChanges.txt | 2 + .../graphicsitems/qdeclarativetextedit.cpp | 116 ++++++++++ .../graphicsitems/qdeclarativetextedit_p.h | 8 + .../graphicsitems/qdeclarativetextinput.cpp | 4 +- .../graphicsitems/qdeclarativetextinput_p.h | 2 +- .../qdeclarativetextedit/MultilineEdit.qml | 74 ++++++ .../qdeclarativetextedit/usingMultilineEdit.qml | 13 ++ .../qmlvisual/qdeclarativetextinput/LineEdit.qml | 4 +- 13 files changed, 476 insertions(+), 5 deletions(-) create mode 100644 examples/declarative/text/edit/edit.qml create mode 100644 examples/declarative/text/edit/pics/endHandle.png create mode 100644 examples/declarative/text/edit/pics/endHandle.sci create mode 100644 examples/declarative/text/edit/pics/startHandle.png create mode 100644 examples/declarative/text/edit/pics/startHandle.sci create mode 100644 tests/auto/declarative/qmlvisual/qdeclarativetextedit/MultilineEdit.qml create mode 100644 tests/auto/declarative/qmlvisual/qdeclarativetextedit/usingMultilineEdit.qml diff --git a/examples/declarative/text/edit/edit.qml b/examples/declarative/text/edit/edit.qml new file mode 100644 index 0000000..2774739 --- /dev/null +++ b/examples/declarative/text/edit/edit.qml @@ -0,0 +1,248 @@ +/**************************************************************************** +** +** Copyright (C) 2010 Nokia Corporation and/or its subsidiary(-ies). +** All rights reserved. +** Contact: Nokia Corporation (qt-info@nokia.com) +** +** This file is part of the QtDeclarative module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** No Commercial Usage +** This file contains pre-release code and may not be distributed. +** You may use this file in accordance with the terms and conditions +** contained in the Technology Preview License Agreement accompanying +** this package. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Nokia gives you certain additional +** rights. These rights are described in the Nokia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** If you have questions regarding the use of this file, please contact +** Nokia at qt-info@nokia.com. +** +** +** +** +** +** +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ +import Qt 4.7 + +Rectangle { + id: editor + color: "lightGrey" + width: 640; height: 480 + + Rectangle { + color: "white" + anchors.fill: parent + anchors.margins: 20 + + BorderImage { + id: startHandle + source: "pics/startHandle.sci" + opacity: 0.0 + width: 10 + x: edit.positionToRectangle(edit.selectionStart).x-flick.contentX-width + y: edit.positionToRectangle(edit.selectionStart).y-flick.contentY + height: edit.positionToRectangle(edit.selectionStart).height + } + + BorderImage { + id: endHandle + source: "pics/endHandle.sci" + opacity: 0.0 + width: 10 + x: edit.positionToRectangle(edit.selectionEnd).x-flick.contentX + y: edit.positionToRectangle(edit.selectionEnd).y-flick.contentY + height: edit.positionToRectangle(edit.selectionEnd).height + } + + Flickable { + id: flick + + anchors.fill: parent + contentWidth: edit.paintedWidth + contentHeight: edit.paintedHeight + interactive: true + clip: true + + function ensureVisible(r) + { + if (contentX >= r.x) + contentX = r.x; + else if (contentX+width <= r.x+r.width) + contentX = r.x+r.width-width; + if (contentY >= r.y) + contentY = r.y; + else if (contentY+height <= r.y+r.height) + contentY = r.y+r.height-height; + } + + TextEdit { + id: edit + width: flick.width + height: flick.height + focus: true + wrapMode: TextEdit.Wrap + onCursorRectangleChanged: flick.ensureVisible(cursorRectangle) + text: "

Text Selection

" + +"

This example is a whacky text selection mechanisms, showing how these can be implemented in the TextEdit element, to cater for whatever style is appropriate for the target platform." + +"

Press-and-hold to select a word, then drag the selection handles." + +"

Drag outside the selection to scroll the text." + +"

Click inside the selection to cut/copy/paste/cancel selection." + +"

It's too whacky to let you paste if there is no current selection." + MouseArea { + x: -startHandle.width + y: 0 + width: parent.width+startHandle.width+endHandle.width + height: parent.height + property string drag: ""; + property int pressPos; + onPressAndHold: { + if (editor.state == "") { + edit.cursorPosition = edit.positionAt(mouse.x+x,mouse.y+y); + edit.selectWord(); + editor.state = "selection" + } + } + onClicked: { + if (editor.state == "") { + edit.cursorPosition = edit.positionAt(mouse.x+x,mouse.y+y); + } + } + function hitHandle(h,x,y) { return x>=h.x+flick.contentX && x=h.y+flick.contentY && y= edit.selectionStart && pos <= edit.selectionEnd) { + drag = "selection" + flick.interactive = false + } else { + drag = "" + flick.interactive = true + } + } + } + } + onReleased: { + if (editor.state == "selection") { + if (drag == "selection") { + editor.state = "menu" + } + drag = "" + } + flick.interactive = true + } + onPositionChanged: { + if (editor.state == "selection" && drag != "") { + if (drag == "start") { + var pos = edit.positionAt(mouse.x+x+startHandle.width/2,mouse.y+y); + if (edit.selectionEnd < pos) + edit.selectionEnd = pos; + edit.selectionStart = pos; + } else if (drag == "end") { + var pos = edit.positionAt(mouse.x+x-endHandle.width/2,mouse.y+y); + if (edit.selectionStart > pos) + edit.selectionStart = pos; + edit.selectionEnd = pos; + } + } + } + } + } + } + + Item { + id: menu + opacity: 0.0 + width: 100 + height: 120 + anchors.centerIn: parent + Rectangle { + border.width: 1 + border.color: "darkBlue" + radius: 15 + color: "#806080FF" + anchors.fill: parent + } + Column { + anchors.centerIn: parent + spacing: 8 + Rectangle { + border.width: 1 + border.color: "darkBlue" + color: "#ff7090FF" + width: 60 + height: 16 + Text { anchors.centerIn: parent; text: "Cut" } + MouseArea { anchors.fill: parent; + onClicked: { edit.cut(); editor.state = "" } } + } + Rectangle { + border.width: 1 + border.color: "darkBlue" + color: "#ff7090FF" + width: 60 + height: 16 + Text { anchors.centerIn: parent; text: "Copy" } + MouseArea { anchors.fill: parent; + onClicked: { edit.copy(); editor.state = "selection" } } + } + Rectangle { + border.width: 1 + border.color: "darkBlue" + color: "#ff7090FF" + width: 60 + height: 16 + Text { anchors.centerIn: parent; text: "Paste" } + MouseArea { anchors.fill: parent; + onClicked: { edit.paste(); edit.cursorPosition = edit.selectionEnd; editor.state = "" } } + } + Rectangle { + border.width: 1 + border.color: "darkBlue" + color: "#ff7090FF" + width: 60 + height: 16 + Text { anchors.centerIn: parent; text: "Deselect" } + MouseArea { anchors.fill: parent; + onClicked: { edit.cursorPosition = edit.selectionEnd; edit.selectionStart = edit.selectionEnd; editor.state = "" } } + } + } + } + } + + states: [ + State { + name: "selection" + PropertyChanges { target: startHandle; opacity: 1.0 } + PropertyChanges { target: endHandle; opacity: 1.0 } + }, + State { + name: "menu" + PropertyChanges { target: startHandle; opacity: 0.5 } + PropertyChanges { target: endHandle; opacity: 0.5 } + PropertyChanges { target: menu; opacity: 1.0 } + } + ] +} diff --git a/examples/declarative/text/edit/pics/endHandle.png b/examples/declarative/text/edit/pics/endHandle.png new file mode 100644 index 0000000..1a4bc5d Binary files /dev/null and b/examples/declarative/text/edit/pics/endHandle.png differ diff --git a/examples/declarative/text/edit/pics/endHandle.sci b/examples/declarative/text/edit/pics/endHandle.sci new file mode 100644 index 0000000..4f51f24 --- /dev/null +++ b/examples/declarative/text/edit/pics/endHandle.sci @@ -0,0 +1,5 @@ +border.left: 0 +border.top: 6 +border.bottom: 6 +border.right: 6 +source: endHandle.png diff --git a/examples/declarative/text/edit/pics/startHandle.png b/examples/declarative/text/edit/pics/startHandle.png new file mode 100644 index 0000000..deedcd5 Binary files /dev/null and b/examples/declarative/text/edit/pics/startHandle.png differ diff --git a/examples/declarative/text/edit/pics/startHandle.sci b/examples/declarative/text/edit/pics/startHandle.sci new file mode 100644 index 0000000..f9eae20 --- /dev/null +++ b/examples/declarative/text/edit/pics/startHandle.sci @@ -0,0 +1,5 @@ +border.left: 6 +border.top: 6 +border.bottom: 6 +border.right: 0 +source: startHandle.png diff --git a/src/declarative/QmlChanges.txt b/src/declarative/QmlChanges.txt index 142920c..0df5f10 100644 --- a/src/declarative/QmlChanges.txt +++ b/src/declarative/QmlChanges.txt @@ -14,6 +14,8 @@ Component: status property instead - errorsString() renamed to errorString() +TextInput xToPosition -> positionAt (to match TextEdit.positionAt) + QList models no longer provide properties in model object. The properties are now updated when the object changes. An object's property "foo" may now be accessed as "foo", modelData.foo" or model.modelData.foo" diff --git a/src/declarative/graphicsitems/qdeclarativetextedit.cpp b/src/declarative/graphicsitems/qdeclarativetextedit.cpp index 9ccb8d2..f8876f2 100644 --- a/src/declarative/graphicsitems/qdeclarativetextedit.cpp +++ b/src/declarative/graphicsitems/qdeclarativetextedit.cpp @@ -89,6 +89,13 @@ TextEdit { A particular look-and-feel might use smooth scrolling (eg. using SmoothedFollow), might have a visible scrollbar, or a scrollbar that fades in to show location, etc. + Clipboard support is provided by the cut(), copy(), and paste() functions, and the selection can + be handled in a traditional "mouse" mechanism by setting selectByMouse, or handled completely + from QML by manipulating selectionStart and selectionEnd, or using selectAll() or selectWord(). + + You can translate between cursor positions (characters from the start of the document) and pixel + points using positionAt() and positionToRectangle(). + \sa Text */ @@ -531,6 +538,70 @@ qreal QDeclarativeTextEdit::paintedHeight() const return implicitHeight(); } +/*! + \qmlmethod rectangle TextEdit::positionToRectangle(position) + + Returns the rectangle at the given \a position in the text. The x, y, + and height properties correspond to the cursor that would describe + that position. +*/ +QRectF QDeclarativeTextEdit::positionToRectangle(int pos) const +{ + Q_D(const QDeclarativeTextEdit); + QTextCursor c(d->document); + c.setPosition(pos); + return d->control->cursorRect(c); + +} + +/*! + \qmlmethod int TextEdit::positionAt(x,y) + + Returns the text position closest to pixel position (\a x,\a y). + + Position 0 is before the first character, position 1 is after the first character + but before the second, and so on until position text.length, which is after all characters. +*/ +int QDeclarativeTextEdit::positionAt(int x, int y) const +{ + Q_D(const QDeclarativeTextEdit); + int r = d->document->documentLayout()->hitTest(QPoint(x,y-d->yoff), Qt::FuzzyHit); + return r; +} + +/*! + \qmlmethod int TextEdit::moveCursorSeletion(int pos) + + Moves the cursor to \a position and updates the selection accordingly. + (To only move the cursor, set the \l cursorPosition property.) + + When this method is called it additionally sets either the + selectionStart or the selectionEnd (whichever was at the previous cursor position) + to the specified position. This allows you to easily extend and contract the selected + text range. + + For example, take this sequence of calls: + + \code + cursorPosition = 5 + moveCursorSelection(9) + moveCursorSelection(7) + \endcode + + This moves the cursor to position 5, extend the selection end from 5 to 9 + and then retract the selection end from 9 to 7, leaving the text from position 5 to 7 + selected (the 6th and 7th characters). +*/ +void QDeclarativeTextEdit::moveCursorSelection(int pos) +{ + //Note that this is the same as setCursorPosition but with the KeepAnchor flag set + Q_D(QDeclarativeTextEdit); + QTextCursor cursor = d->control->textCursor(); + if (cursor.position() == pos) + return; + cursor.setPosition(pos, QTextCursor::KeepAnchor); + d->control->setTextCursor(cursor); +} /*! \qmlproperty bool TextEdit::cursorVisible @@ -956,6 +1027,51 @@ void QDeclarativeTextEdit::selectAll() } /*! + Causes the word closest to the current cursor position to be selected. +*/ +void QDeclarativeTextEdit::selectWord() +{ + Q_D(QDeclarativeTextEdit); + QTextCursor c = d->control->textCursor(); + c.select(QTextCursor::WordUnderCursor); + d->control->setTextCursor(c); +} + +/*! + \qmlmethod TextEdit::cut() + + Moves the currently selected text to the system clipboard. +*/ +void QDeclarativeTextEdit::cut() +{ + Q_D(QDeclarativeTextEdit); + d->control->cut(); +} + +/*! + \qmlmethod TextEdit::copy() + + Copies the currently selected text to the system clipboard. +*/ +void QDeclarativeTextEdit::copy() +{ + Q_D(QDeclarativeTextEdit); + d->control->copy(); +} + +/*! + \qmlmethod TextEdit::paste() + + Relaces the currently selected text by the contents of the system clipboard. +*/ +void QDeclarativeTextEdit::paste() +{ + Q_D(QDeclarativeTextEdit); + d->control->paste(); +} + + +/*! \overload Handles the given mouse \a event. */ diff --git a/src/declarative/graphicsitems/qdeclarativetextedit_p.h b/src/declarative/graphicsitems/qdeclarativetextedit_p.h index 47174f4..a83b3db 100644 --- a/src/declarative/graphicsitems/qdeclarativetextedit_p.h +++ b/src/declarative/graphicsitems/qdeclarativetextedit_p.h @@ -198,6 +198,10 @@ public: qreal paintedWidth() const; qreal paintedHeight() const; + Q_INVOKABLE QRectF positionToRectangle(int) const; + Q_INVOKABLE int positionAt(int x, int y) const; + Q_INVOKABLE void moveCursorSelection(int pos); + Q_SIGNALS: void textChanged(const QString &); void paintedSizeChanged(); @@ -225,6 +229,10 @@ Q_SIGNALS: public Q_SLOTS: void selectAll(); + void selectWord(); + void cut(); + void copy(); + void paste(); private Q_SLOTS: void updateImgCache(const QRectF &rect); diff --git a/src/declarative/graphicsitems/qdeclarativetextinput.cpp b/src/declarative/graphicsitems/qdeclarativetextinput.cpp index 9c70ea9..9a6a070 100644 --- a/src/declarative/graphicsitems/qdeclarativetextinput.cpp +++ b/src/declarative/graphicsitems/qdeclarativetextinput.cpp @@ -833,7 +833,7 @@ void QDeclarativeTextInput::moveCursor() } /*! - \qmlmethod int TextInput::xToPosition(int x) + \qmlmethod int TextInput::positionAt(int x) This function returns the character position at x pixels from the left of the textInput. Position 0 is before the @@ -843,7 +843,7 @@ void QDeclarativeTextInput::moveCursor() This means that for all x values before the first character this function returns 0, and for all x values after the last character this function returns text.length. */ -int QDeclarativeTextInput::xToPosition(int x) +int QDeclarativeTextInput::positionAt(int x) { Q_D(const QDeclarativeTextInput); return d->control->xToPos(x - d->hscroll); diff --git a/src/declarative/graphicsitems/qdeclarativetextinput_p.h b/src/declarative/graphicsitems/qdeclarativetextinput_p.h index 438293a..6bb94c2 100644 --- a/src/declarative/graphicsitems/qdeclarativetextinput_p.h +++ b/src/declarative/graphicsitems/qdeclarativetextinput_p.h @@ -110,7 +110,7 @@ public: }; //Auxilliary functions needed to control the TextInput from QML - Q_INVOKABLE int xToPosition(int x); + Q_INVOKABLE int positionAt(int x); Q_INVOKABLE void moveCursorSelection(int pos); Q_INVOKABLE void openSoftwareInputPanel(); diff --git a/tests/auto/declarative/qmlvisual/qdeclarativetextedit/MultilineEdit.qml b/tests/auto/declarative/qmlvisual/qdeclarativetextedit/MultilineEdit.qml new file mode 100644 index 0000000..0273282 --- /dev/null +++ b/tests/auto/declarative/qmlvisual/qdeclarativetextedit/MultilineEdit.qml @@ -0,0 +1,74 @@ +import Qt 4.7 + +Item { + id:lineedit + property alias text: textEdit.text + + width: 240 + 11 //Should be set manually in most cases + height: textEdit.height + 11 + + Rectangle{ + color: 'lightsteelblue' + anchors.fill: parent + } + clip: true + Component.onCompleted: textEdit.cursorPosition = 0; + TextEdit{ + id:textEdit + cursorDelegate: Item{ + Rectangle{ + visible: parent.parent.focus + color: "#009BCE" + height: 13 + width: 2 + y: 1 + } + } + property int leftMargin: 6 + property int topMargin: 6 + property int rightMargin: 6 + property int bottomMargin: 6 + x: leftMargin + width: parent.width - leftMargin - rightMargin; + y: 5 + //Below function implements all scrolling logic + onCursorPositionChanged: { + if(cursorRectangle.y < topMargin - textEdit.y){//Cursor went off the front + textEdit.y = topMargin - Math.max(0, cursorRectangle.y); + }else if(cursorRectangle.y > parent.height - topMargin - bottomMargin - textEdit.y){//Cursor went off the end + textEdit.y = topMargin - Math.max(0, cursorRectangle.y - (parent.height - topMargin - bottomMargin)) - cursorRectangle.height; + } + } + + text:"" + horizontalAlignment: TextInput.AlignLeft + wrapMode: TextEdit.WordWrap + font.pixelSize:15 + } + MouseArea{ + //Implements all line edit mouse handling + id: mainMouseArea + anchors.fill: parent; + function translateY(y){ + return y - textEdit.y + } + function translateX(x){ + return x - textEdit.x + } + onPressed: { + textEdit.focus = true; + textEdit.cursorPosition = textEdit.positionAt(translateX(mouse.x), translateY(mouse.y)); + } + onPositionChanged: { + textEdit.moveCursorSelection(textEdit.positionAt(translateX(mouse.x), translateY(mouse.y))); + } + onReleased: { + } + onDoubleClicked: { + textEdit.selectionStart=0; + textEdit.selectionEnd=textEdit.text.length; + } + z: textEdit.z + 1 + } + +} diff --git a/tests/auto/declarative/qmlvisual/qdeclarativetextedit/usingMultilineEdit.qml b/tests/auto/declarative/qmlvisual/qdeclarativetextedit/usingMultilineEdit.qml new file mode 100644 index 0000000..47b48d8 --- /dev/null +++ b/tests/auto/declarative/qmlvisual/qdeclarativetextedit/usingMultilineEdit.qml @@ -0,0 +1,13 @@ +import Qt 4.7 + +Rectangle{ + width: 600 + height: 200 + Column{ + MultilineEdit{ + text: 'I am the very model of a modern major general. I\'ve information vegetable, animal and mineral. I know the kings of england and I quote the fights historical - from Marathon to Waterloo in order categorical.'; + width: 182; height: 60; + } + MultilineEdit{text: 'Hello world'} + } +} diff --git a/tests/auto/declarative/qmlvisual/qdeclarativetextinput/LineEdit.qml b/tests/auto/declarative/qmlvisual/qdeclarativetextinput/LineEdit.qml index 31f24ec..cc0ad3c 100644 --- a/tests/auto/declarative/qmlvisual/qdeclarativetextinput/LineEdit.qml +++ b/tests/auto/declarative/qmlvisual/qdeclarativetextinput/LineEdit.qml @@ -50,10 +50,10 @@ Item { } onPressed: { textInp.focus = true; - textInp.cursorPosition = textInp.xToPosition(translateX(mouse.x)); + textInp.cursorPosition = textInp.positionAt(translateX(mouse.x)); } onPositionChanged: { - textInp.moveCursorSelection(textInp.xToPosition(translateX(mouse.x))); + textInp.moveCursorSelection(textInp.positionAt(translateX(mouse.x))); } onReleased: { } -- cgit v0.12