diff options
| author | J-P Nurmi <jpnurmi@digia.com> | 2014-05-15 18:08:34 +0200 |
|---|---|---|
| committer | J-P Nurmi <jpnurmi@digia.com> | 2014-06-16 18:35:40 +0200 |
| commit | 55e3a5d301e9382a66daf97718d2907d80c7c427 (patch) | |
| tree | 84826422123f5831f44919d1e7fbc8e876f19b04 /src | |
| parent | e7af888e86ddfd0f9b18b03f5ecbf8425fc5b0c0 (diff) | |
TextField & TextArea: add support for selection handles
Task-number: QTBUG-38934
Change-Id: Id581a6e56d3461b0df476c2b35c3e642fd505fc9
Reviewed-by: Caroline Chao <caroline.chao@digia.com>
Reviewed-by: Gabriel de Dietrich <gabriel.dedietrich@digia.com>
Diffstat (limited to 'src')
| -rw-r--r-- | src/controls/Private/TextHandle.qml | 91 | ||||
| -rw-r--r-- | src/controls/Private/private.pri | 1 | ||||
| -rw-r--r-- | src/controls/Private/qmldir | 1 | ||||
| -rw-r--r-- | src/controls/Styles/Base/TextAreaStyle.qml | 34 | ||||
| -rw-r--r-- | src/controls/Styles/Base/TextFieldStyle.qml | 34 | ||||
| -rw-r--r-- | src/controls/TextArea.qml | 133 | ||||
| -rw-r--r-- | src/controls/TextField.qml | 122 |
7 files changed, 394 insertions, 22 deletions
diff --git a/src/controls/Private/TextHandle.qml b/src/controls/Private/TextHandle.qml new file mode 100644 index 000000000..34daa4ab0 --- /dev/null +++ b/src/controls/Private/TextHandle.qml @@ -0,0 +1,91 @@ +/**************************************************************************** +** +** Copyright (C) 2014 Digia Plc and/or its subsidiary(-ies). +** Contact: http://www.qt-project.org/legal +** +** This file is part of the Qt Quick Controls module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:BSD$ +** You may use this file under the terms of the BSD license as follows: +** +** "Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are +** met: +** * Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** * Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in +** the documentation and/or other materials provided with the +** distribution. +** * Neither the name of Digia Plc and its Subsidiary(-ies) nor the names +** of its contributors may be used to endorse or promote products derived +** from this software without specific prior written permission. +** +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** +** $QT_END_LICENSE$ +** +****************************************************************************/ +import QtQuick 2.2 + +Loader { + id: handle + + property Item editor + property int minimum: -1 + property int maximum: -1 + property int position: -1 + property alias delegate: handle.sourceComponent + + readonly property alias pressed: mouse.pressed + + readonly property real handleX: x + (item ? item.x : 0) + readonly property real handleY: y + (item ? item.y : 0) + readonly property real handleWidth: item ? item.width : 0 + readonly property real handleHeight: item ? item.height : 0 + + property Item control + property QtObject styleData: QtObject { + readonly property alias pressed: mouse.pressed + readonly property alias position: handle.position + readonly property real lineHeight: position !== -1 ? editor.positionToRectangle(position).height + : editor.cursorRectangle.height + } + + MouseArea { + id: mouse + anchors.fill: item + property point offset + onPressed: offset = Qt.point(x + mouse.x, y + mouse.y) + onPositionChanged: { + var pt = mapToItem(editor, mouse.x - offset.x, mouse.y - offset.y) + + // limit vertically within mix/max coordinates or content bounds + var min = (minimum !== -1) ? minimum : 0 + var max = (maximum !== -1) ? maximum : editor.length - 1 + pt.y = Math.max(pt.y, editor.positionToRectangle(min).y) + pt.y = Math.min(pt.y, editor.positionToRectangle(max).y) + + var pos = editor.positionAt(pt.x, pt.y) + + // limit horizontally within min/max character positions + if (minimum !== -1) + pos = Math.max(pos, minimum) + if (maximum !== -1) + pos = Math.min(pos, maximum) + + handle.position = pos + } + } +} diff --git a/src/controls/Private/private.pri b/src/controls/Private/private.pri index f94468540..f2da1ec97 100644 --- a/src/controls/Private/private.pri +++ b/src/controls/Private/private.pri @@ -47,6 +47,7 @@ PRIVATE_QML_FILES += \ $$PWD/ScrollViewHelper.qml \ $$PWD/ScrollBar.qml \ $$PWD/TableViewSelection.qml \ + $$PWD/TextHandle.qml \ $$PWD/TextSingleton.qml \ $$PWD/FocusFrame.qml \ $$PWD/ColumnMenuContent.qml \ diff --git a/src/controls/Private/qmldir b/src/controls/Private/qmldir index d31483e31..589087bf4 100644 --- a/src/controls/Private/qmldir +++ b/src/controls/Private/qmldir @@ -25,3 +25,4 @@ ColumnMenuContent 1.0 ColumnMenuContent.qml ContentItem 1.0 ContentItem.qml HoverButton 1.0 HoverButton.qml singleton TextSingleton 1.0 TextSingleton.qml +TextHandle 1.0 TextHandle.qml diff --git a/src/controls/Styles/Base/TextAreaStyle.qml b/src/controls/Styles/Base/TextAreaStyle.qml index 7ad5686f9..4ffefe639 100644 --- a/src/controls/Styles/Base/TextAreaStyle.qml +++ b/src/controls/Styles/Base/TextAreaStyle.qml @@ -96,4 +96,38 @@ ScrollViewStyle { \sa Text::renderType */ property int renderType: Text.NativeRendering + + /*! The cursor handle. + \since QtQuick.Controls.Styles 1.3 + + The parent of the handle is positioned to the top left corner of + the cursor position. The interactive area is determined by the + geometry of the handle delegate. + + The following read-only properties are available within the scope + of the handle delegate: + \table + \row \li \b {styleData.pressed} : bool \li Whether the handle is pressed. + \row \li \b {styleData.position} : int \li The character position of the handle. + \row \li \b {styleData.lineHeight} : real \li The height of the line the handle is on. + \endtable + */ + property Component cursorHandle + + /*! The selection handle. + \since QtQuick.Controls.Styles 1.3 + + The parent of the handle is positioned to the top left corner of + the first selected character. The interactive area is determined + by the geometry of the handle delegate. + + The following read-only properties are available within the scope + of the handle delegate: + \table + \row \li \b {styleData.pressed} : bool \li Whether the handle is pressed. + \row \li \b {styleData.position} : int \li The character position of the handle. + \row \li \b {styleData.lineHeight} : real \li The height of the line the handle is on. + \endtable + */ + property Component selectionHandle } diff --git a/src/controls/Styles/Base/TextFieldStyle.qml b/src/controls/Styles/Base/TextFieldStyle.qml index d999551c0..dc25033aa 100644 --- a/src/controls/Styles/Base/TextFieldStyle.qml +++ b/src/controls/Styles/Base/TextFieldStyle.qml @@ -159,4 +159,38 @@ Style { anchors.fill: parent } } + + /*! The cursor handle. + \since QtQuick.Controls.Styles 1.3 + + The parent of the handle is positioned to the top left corner of + the cursor position. The interactive area is determined by the + geometry of the handle delegate. + + The following read-only properties are available within the scope + of the handle delegate: + \table + \row \li \b {styleData.pressed} : bool \li Whether the handle is pressed. + \row \li \b {styleData.position} : int \li The character position of the handle. + \row \li \b {styleData.lineHeight} : real \li The height of the line the handle is on. + \endtable + */ + property Component cursorHandle + + /*! The selection handle. + \since QtQuick.Controls.Styles 1.3 + + The parent of the handle is positioned to the top left corner of + the first selected character. The interactive area is determined + by the geometry of the handle delegate. + + The following read-only properties are available within the scope + of the handle delegate: + \table + \row \li \b {styleData.pressed} : bool \li Whether the handle is pressed. + \row \li \b {styleData.position} : int \li The character position of the handle. + \row \li \b {styleData.lineHeight} : real \li The height of the line the handle is on. + \endtable + */ + property Component selectionHandle } diff --git a/src/controls/TextArea.qml b/src/controls/TextArea.qml index 1ea265d69..b0c8882f6 100644 --- a/src/controls/TextArea.qml +++ b/src/controls/TextArea.qml @@ -683,7 +683,7 @@ ScrollView { Flickable { id: flickable - interactive: false + interactive: !edit.selectByMouse anchors.fill: parent TextEdit { @@ -739,7 +739,7 @@ ScrollView { wrapMode: TextEdit.WordWrap textMargin: 4 - selectByMouse: Qt.platform.os !== "android" // Workaround for QTBUG-36515 + selectByMouse: !cursorHandle.delegate || !selectionHandle.delegate readOnly: false Keys.forwardTo: area @@ -748,32 +748,139 @@ ScrollView { KeyNavigation.tab: area.tabChangesFocus ? area.KeyNavigation.tab : null KeyNavigation.backtab: area.tabChangesFocus ? area.KeyNavigation.backtab : null - // keep textcursor within scroll view + property bool blockRecursion: false + property bool hasSelection: selectionStart !== selectionEnd + readonly property int selectionPosition: selectionStart !== cursorPosition ? selectionStart : selectionEnd + + // force re-evaluation when contentWidth changes => text layout changes => selection moves + property rect selectionRectangle: contentWidth ? positionToRectangle(selectionPosition) + : positionToRectangle(selectionPosition) + + onSelectionStartChanged: { + if (!blockRecursion && selectionHandle.delegate) { + blockRecursion = true + selectionHandle.position = selectionPosition + blockRecursion = false + } + } + onCursorPositionChanged: { - if (cursorRectangle.y >= flickableItem.contentY + viewport.height - cursorRectangle.height - textMargin) { + if (!blockRecursion && cursorHandle.delegate) { + blockRecursion = true + cursorHandle.position = cursorPosition + blockRecursion = false + } + ensureVisible(cursorRectangle) + } + + function ensureVisible(rect) { + if (rect.y >= flickableItem.contentY + viewport.height - rect.height - textMargin) { // moving down - flickableItem.contentY = cursorRectangle.y - viewport.height + cursorRectangle.height + textMargin - } else if (cursorRectangle.y < flickableItem.contentY) { + flickableItem.contentY = rect.y - viewport.height + rect.height + textMargin + } else if (rect.y < flickableItem.contentY) { // moving up - flickableItem.contentY = cursorRectangle.y - textMargin + flickableItem.contentY = rect.y - textMargin } - if (cursorRectangle.x >= flickableItem.contentX + viewport.width - textMargin) { + if (rect.x >= flickableItem.contentX + viewport.width - textMargin) { // moving right - flickableItem.contentX = cursorRectangle.x - viewport.width + textMargin - } else if (cursorRectangle.x < flickableItem.contentX) { + flickableItem.contentX = rect.x - viewport.width + textMargin + } else if (rect.x < flickableItem.contentX) { // moving left - flickableItem.contentX = cursorRectangle.x - textMargin + flickableItem.contentX = rect.x - textMargin } } + onLinkActivated: area.linkActivated(link) onLinkHovered: area.linkHovered(link) + function activate() { + if (activeFocusOnPress) { + forceActiveFocus() + if (!readOnly) + Qt.inputMethod.show() + } + } + + function moveHandles(cursor, selection) { + blockRecursion = true + Qt.inputMethod.commit() + cursorPosition = cursor + if (selection === -1) { + selectWord() + selection = selectionStart + } + selectionHandle.position = selection + cursorHandle.position = cursorPosition + blockRecursion = false + } + MouseArea { - parent: area.viewport anchors.fill: parent cursorShape: edit.hoveredLink ? Qt.PointingHandCursor : Qt.IBeamCursor - acceptedButtons: Qt.NoButton + acceptedButtons: edit.selectByMouse ? Qt.NoButton : Qt.LeftButton + onClicked: { + var pos = edit.positionAt(mouse.x, mouse.y) + edit.moveHandles(pos, pos) + edit.activate() + } + onPressAndHold: { + var pos = edit.positionAt(mouse.x, mouse.y) + edit.moveHandles(pos, -1) + edit.activate() + } + } + + TextHandle { + id: selectionHandle + + editor: edit + control: area + z: 1 // above scrollbars + parent: __scroller // no clip + delegate: __style.selectionHandle + maximum: cursorHandle.position - 1 + x: edit.selectionRectangle.x - flickableItem.contentX + y: edit.selectionRectangle.y - flickableItem.contentY + visible: pressed || (edit.hasSelection && handleY + handleHeight >= -1 && handleY <= viewport.height + 1 + && handleX + handleWidth >= -1 && handleX <= viewport.width + 1) + + onPositionChanged: { + if (!edit.blockRecursion) { + edit.blockRecursion = true + edit.select(selectionHandle.position, cursorHandle.position) + if (pressed) + edit.ensureVisible(edit.selectionRectangle) + edit.blockRecursion = false + } + } + } + + TextHandle { + id: cursorHandle + + editor: edit + control: area + z: 1 // above scrollbars + parent: __scroller // no clip + delegate: __style.cursorHandle + minimum: edit.hasSelection ? selectionHandle.position + 1 : -1 + x: edit.cursorRectangle.x - flickableItem.contentX + y: edit.cursorRectangle.y - flickableItem.contentY + visible: pressed || ((edit.cursorVisible || edit.hasSelection) + && handleY + handleHeight >= -1 && handleY <= viewport.height + 1 + && handleX + handleWidth >= -1 && handleX <= viewport.width + 1) + + onPositionChanged: { + if (!edit.blockRecursion) { + edit.blockRecursion = true + if (!edit.hasSelection) + selectionHandle.position = cursorHandle.position + Qt.inputMethod.commit() + edit.select(selectionHandle.position, cursorHandle.position) + edit.blockRecursion = false + } + } } } } diff --git a/src/controls/TextField.qml b/src/controls/TextField.qml index a20a5c40c..24f5e5e7f 100644 --- a/src/controls/TextField.qml +++ b/src/controls/TextField.qml @@ -572,14 +572,6 @@ Control { Accessible.role: Accessible.EditableText Accessible.description: placeholderText - MouseArea { - id: mouseArea - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.IBeamCursor - onClicked: textfield.forceActiveFocus() - } - Text { id: placeholderTextComponent anchors.fill: textInput @@ -597,7 +589,7 @@ Control { TextInput { id: textInput focus: true - selectByMouse: Qt.platform.os !== "android" // Workaround for QTBUG-36515 + selectByMouse: !cursorHandle.delegate || !selectionHandle.delegate selectionColor: __panel ? __panel.selectionColor : "darkred" selectedTextColor: __panel ? __panel.selectedTextColor : "white" @@ -624,5 +616,117 @@ Control { } onEditingFinished: textfield.editingFinished() + + property bool blockRecursion: false + property bool hasSelection: selectionStart !== selectionEnd + readonly property int selectionPosition: selectionStart !== cursorPosition ? selectionStart : selectionEnd + + // force re-evaluation when selection moves: + // - cyrsorRectangle changes => content scrolled + // - contentWidth changes => text layout changed + property rect selectionRectangle: cursorRectangle.x && contentWidth ? positionToRectangle(selectionPosition) + : positionToRectangle(selectionPosition) + + onSelectionStartChanged: { + if (!blockRecursion && selectionHandle.delegate) { + blockRecursion = true + selectionHandle.position = selectionPosition + blockRecursion = false + } + } + + onCursorPositionChanged: { + if (!blockRecursion && cursorHandle.delegate) { + blockRecursion = true + cursorHandle.position = cursorPosition + blockRecursion = false + } + } + + function activate() { + if (activeFocusOnPress) { + forceActiveFocus() + if (!readOnly) + Qt.inputMethod.show() + } + } + + function moveHandles(cursor, selection) { + blockRecursion = true + Qt.inputMethod.commit() + cursorPosition = cursor + if (selection === -1) { + selectWord() + selection = selectionStart + } + selectionHandle.position = selection + cursorHandle.position = cursorPosition + blockRecursion = false + } + } + + MouseArea { + id: mouseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.IBeamCursor + acceptedButtons: textInput.selectByMouse ? Qt.NoButton : Qt.LeftButton + onClicked: { + var pos = textInput.positionAt(mouse.x, mouse.y) + textInput.moveHandles(pos, pos) + textInput.activate() + } + onPressAndHold: { + var pos = textInput.positionAt(mouse.x, mouse.y) + textInput.moveHandles(pos, -1) + textInput.activate() + } + } + + TextHandle { + id: selectionHandle + + editor: textInput + control: textfield + delegate: __style.selectionHandle + maximum: cursorHandle.position - 1 + readonly property real selectionX: textInput.selectionRectangle.x + x: textInput.x + (pressed ? Math.max(0, selectionX) : selectionX) + y: textInput.selectionRectangle.y + textInput.y + visible: pressed || (textInput.hasSelection && handleX + handleWidth >= -1 && handleX <= textfield.width + 1) + + onPositionChanged: { + if (!textInput.blockRecursion) { + textInput.blockRecursion = true + textInput.select(selectionHandle.position, cursorHandle.position) + if (pressed) + textInput.ensureVisible(position) + textInput.blockRecursion = false + } + } + } + + TextHandle { + id: cursorHandle + + editor: textInput + control: textfield + delegate: __style.cursorHandle + minimum: textInput.hasSelection ? selectionHandle.position + 1 : -1 + x: textInput.cursorRectangle.x + textInput.x + y: textInput.cursorRectangle.y + textInput.y + visible: pressed || ((textInput.cursorVisible || textInput.hasSelection) + && handleX + handleWidth >= -1 && handleX <= textfield.width + 1) + + onPositionChanged: { + if (!textInput.blockRecursion) { + textInput.blockRecursion = true + if (!textInput.hasSelection) + selectionHandle.position = cursorHandle.position + Qt.inputMethod.commit() + textInput.select(selectionHandle.position, cursorHandle.position) + textInput.blockRecursion = false + } + } } } |
