Commit b5c51d5f authored by Noah Davis's avatar Noah Davis 🌵
Browse files

Add mobile text actions menu and text selection handles

parent 38e970c4
/* SPDX-FileCopyrightText: 2018 Marco Martin <mart@kde.org>
* SPDX-FileCopyrightText: 2021 Noah Davis <noahadvs@gmail.com>
* SPDX-License-Identifier: LGPL-2.0-or-later
*/
import QtQuick 2.15
import QtQuick.Window 2.15
import QtQuick.Templates 2.15
import org.kde.kirigami 2.14 as Kirigami
Item {
id: root
property alias target: root.parent
Rectangle {
id: cursorLine
property real previousX: 0
property real previousY: 0
parent: target
implicitWidth: target.cursorRectangle.width
implicitHeight: target.cursorRectangle.height
x: Math.floor(target.cursorRectangle.x)
y: Math.floor(target.cursorRectangle.y)
color: target.color
SequentialAnimation {
id: blinkAnimation
running: root.visible && Qt.styleHints.cursorFlashTime != 0 && target.selectionStart === target.selectionEnd
PropertyAction {
target: cursorLine
property: "opacity"
value: 1
}
PauseAnimation {
duration: Qt.styleHints.cursorFlashTime/2
}
SequentialAnimation {
loops: Animation.Infinite
OpacityAnimator {
target: cursorLine
from: 1
to: 0
duration: Qt.styleHints.cursorFlashTime/2
easing.type: Easing.OutCubic
}
OpacityAnimator {
target: cursorLine
from: 0
to: 1
duration: Qt.styleHints.cursorFlashTime/2
easing.type: Easing.OutCubic
}
}
}
// NumberAnimations/SmoothedAnimations appear smoother than X/Y Animators for some reason
/*Behavior on x {
SmoothedAnimation {
velocity: 200
reversingMode: SmoothedAnimation.Immediate
duration: Kirigami.Settings.tabletMode ? Kirigami.Units.shortDuration : 0//Kirigami.Units.veryShortDuration
}
}
Behavior on y {
SmoothedAnimation {
velocity: 200
reversingMode: SmoothedAnimation.Immediate
duration: Kirigami.Settings.tabletMode ? Kirigami.Units.shortDuration : 0//Kirigami.Units.veryShortDuration
}
}
*/
}
Connections {
target: root.target
function onCursorPositionChanged() {
blinkAnimation.restart()
}
}
}
/* SPDX-FileCopyrightText: 2018 Marco Martin <mart@kde.org>
* SPDX-FileCopyrightText: 2021 Noah Davis <noahadvs@gmail.com>
* SPDX-License-Identifier: LGPL-2.0-or-later
*/
import QtQuick 2.15
import QtQuick.Window 2.15
import QtQuick.Templates 2.15
import org.kde.kirigami 2.14 as Kirigami
Loader {
id: root
property Item target
property bool isSelectionEnd: false
visible: Kirigami.Settings.tabletMode && target.activeFocus && (isSelectionEnd ? target.selectionStart !== target.selectionEnd : true)
active: visible
sourceComponent: Kirigami.ShadowedRectangle {
id: handle
property real selectionStartX: Math.floor(Qt.inputMethod.anchorRectangle.x + (Qt.inputMethod.cursorRectangle.width - width)/2)
property real selectionStartY: Math.floor(Qt.inputMethod.anchorRectangle.y + Qt.inputMethod.cursorRectangle.height + pointyBitVerticalOffset)
property real selectionEndX: Math.floor(Qt.inputMethod.cursorRectangle.x + (Qt.inputMethod.cursorRectangle.width - width)/2)
property real selectionEndY: Math.floor(Qt.inputMethod.cursorRectangle.y + Qt.inputMethod.cursorRectangle.height + pointyBitVerticalOffset)
property real pointyBitVerticalOffset: Math.abs(pointyBit.y*2)
parent: Overlay.overlay
x: isSelectionEnd ? selectionEndX : selectionStartX
y: isSelectionEnd ? selectionEndY : selectionStartY
//opacity: target.activeFocus ? 1 : 0
implicitHeight: {
let h = Kirigami.Units.gridUnit
return h - (h % 2 == 0 ? 1 : 0)
}
implicitWidth: implicitHeight
radius: width/2
color: target.selectionColor
shadow {
color: Qt.rgba(0,0,0,0.2)
size: 3
yOffset: 1
}
Rectangle {
id: pointyBit
x: (parent.width - width)/2
y: -height/4 + 0.2 // magic number to get it to line up with the edge of the circle
implicitHeight: parent.implicitHeight/2
implicitWidth: implicitHeight
antialiasing: true
rotation: 45
color: parent.color
}
Kirigami.ShadowedRectangle {
id: inner
visible: target.selectionStart !== target.selectionEnd && (handle.y < selectionStartY || handle.y < selectionEndY)
anchors.fill: parent
anchors.margins: Kirigami.Units.smallBorder
color: target.selectedTextColor
radius: height/2
Rectangle {
id: innerPointyBit
x: (parent.width - width)/2
y: -height/4 + 0.8 // magic number to get it to line up with the edge of the circle
implicitHeight: pointyBit.implicitHeight
implicitWidth: implicitHeight
antialiasing: true
rotation: 45
color: parent.color
}
}
MouseArea {
enabled: handle.visible
anchors.fill: parent
// preventStealing: true
onPositionChanged: {
let pos = mapToItem(root.target, mouse.x, mouse.y);
pos = root.target.positionAt(pos.x, pos.y - handle.height - handle.pointyBitVerticalOffset);
if (target.selectionStart !== target.selectionEnd) {
if (!isSelectionEnd) {
root.target.select(Math.min(pos, root.target.selectionEnd - 1), root.target.selectionEnd);
} else {
root.target.select(root.target.selectionStart, Math.max(pos, root.target.selectionStart + 1));
}
} else {
root.target.cursorPosition = pos;
}
}
}
// NumberAnimations/SmoothedAnimations appear smoother than X/Y Animators for some reason.
// The animations feel a bit janky when moving handles while text is selected.
/*Behavior on x {
enabled: enableXYAnimations && target.selectionStart === target.selectionEnd
SmoothedAnimation {
velocity: 200
reversingMode: SmoothedAnimation.Immediate
duration: Kirigami.Units.shortDuration
}
}
Behavior on y {
enabled: enableXYAnimations && target.selectionStart === target.selectionEnd
SmoothedAnimation {
velocity: 200
reversingMode: SmoothedAnimation.Immediate
duration: Kirigami.Units.shortDuration
}
}
//HACK
property bool enableXYAnimations: false
Timer {
id: animationHackTimer
running: true
interval: 1
onTriggered: handle.enableXYAnimations = true
}
*/
}
}
/*
SPDX-FileCopyrightText: 2018 Marco Martin <mart@kde.org>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
import QtQuick 2.1
import org.kde.kirigami 2.5 as Kirigami
Item {
id: root
width: 1 //<-important that this is actually a single device pixel
height: Kirigami.Units.gridUnit
property Item target
property bool selectionStartHandle: false
visible: Kirigami.Settings.tabletMode && ((target.activeFocus && !selectionStartHandle) || target.selectedText.length > 0)
Rectangle {
width: Math.round(Kirigami.Units.devicePixelRatio * 3)
anchors {
horizontalCenter: parent.horizontalCenter
top: parent.top
bottom: parent.bottom
}
color: Qt.tint(Kirigami.Theme.highlightColor, Qt.rgba(1,1,1,0.4))
radius: width
Rectangle {
width: Math.round(Kirigami.Units.gridUnit/1.5)
height: width
visible: MobileTextActionsToolBar.shouldBeVisible
anchors {
horizontalCenter: parent.horizontalCenter
verticalCenter: parent.bottom
}
radius: width
color: Qt.tint(Kirigami.Theme.highlightColor, Qt.rgba(1,1,1,0.4))
}
MouseArea {
anchors {
fill: parent
margins: -Kirigami.Units.gridUnit
}
preventStealing: true
onPositionChanged: {
var pos = mapToItem(target, mouse.x, mouse.y);
pos = target.positionAt(pos.x, pos.y);
if (target.selectedText.length > 0) {
if (selectionStartHandle) {
target.select(Math.min(pos, target.selectionEnd - 1), target.selectionEnd);
} else {
target.select(target.selectionStart, Math.max(pos, target.selectionStart + 1));
}
} else {
target.cursorPosition = pos;
}
}
}
}
}
/*
SPDX-FileCopyrightText: 2018 Marco Martin <mart@kde.org>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
pragma Singleton
import QtQuick 2.1
import QtQuick.Layouts 1.2
import QtQuick.Window 2.2
import QtQuick.Controls 2.15
import org.kde.kirigami 2.5 as Kirigami
Popup {
id: root
property Item controlRoot
parent: controlRoot ? controlRoot.Window.contentItem : undefined
modal: false
focus: false
closePolicy: Popup.NoAutoClose
property bool shouldBeVisible: false
x: {
if (!controlRoot || !controlRoot.Window.contentItem) {
return 0;
}
return Math.min(Math.max(0, controlRoot.mapToItem(root.parent, controlRoot.positionToRectangle(controlRoot.selectionStart).x, 0).x - root.width/2), controlRoot.Window.contentItem.width - root.width);
}
y: {
if (!controlRoot || !controlRoot.Window.contentItem) {
return 0;
}
var desiredY = controlRoot.mapToItem(root.parent, 0, controlRoot.positionToRectangle(controlRoot.selectionStart).y).y - root.height;
if (desiredY >= 0) {
return Math.min(desiredY, controlRoot.Window.contentItem.height - root.height);
} else {
return Math.min(Math.max(0, controlRoot.mapToItem(root.parent, 0, controlRoot.positionToRectangle(controlRoot.selectionEnd).y + Math.round(Kirigami.Units.gridUnit*1.5)).y), controlRoot.Window.contentItem.height - root.height);
}
}
visible: controlRoot ? shouldBeVisible && Kirigami.Settings.tabletMode && (controlRoot.selectedText.length > 0 || controlRoot.canPaste) : false
width: contentItem.implicitWidth + leftPadding + rightPadding
contentItem: RowLayout {
ToolButton {
focusPolicy: Qt.NoFocus
icon.name: "edit-cut"
visible: controlRoot && controlRoot.selectedText.length > 0 && (!controlRoot.hasOwnProperty("echoMode") || controlRoot.echoMode === TextInput.Normal)
onClicked: {
controlRoot.cut();
}
}
ToolButton {
focusPolicy: Qt.NoFocus
icon.name: "edit-copy"
visible: controlRoot && controlRoot.selectedText.length > 0 && (!controlRoot.hasOwnProperty("echoMode") || controlRoot.echoMode === TextInput.Normal)
onClicked: {
controlRoot.copy();
}
}
ToolButton {
focusPolicy: Qt.NoFocus
icon.name: "edit-paste"
visible: controlRoot && controlRoot.canPaste
onClicked: {
controlRoot.paste();
}
}
}
}
/* SPDX-FileCopyrightText: 2018 Marco Martin <mart@kde.org>
* SPDX-FileCopyrightText: 2021 Noah Davis <noahadvs@gmail.com>
* SPDX-License-Identifier: LGPL-2.0-or-later
*/
import QtQuick 2.15
import QtQuick.Layouts 1.15
import QtQuick.Window 2.15
import QtQuick.Controls 2.15
import org.kde.kirigami 2.14 as Kirigami
Loader {
id: root
property Item target
visible: Kirigami.Settings.tabletMode && target.selectedText.length > 0
active: visible
sourceComponent: Popup {
id: popup
property real xAlignHCenter: Math.round(Qt.inputMethod.anchorRectangle.x + (Qt.inputMethod.cursorRectangle.x - Qt.inputMethod.anchorRectangle.x - width)/2)
property real yAlignOver: Math.round(Qt.inputMethod.anchorRectangle.y - height - fontMetrics.descent)
visible: false
parent: Overlay.overlay
modal: false
focus: false
margins: Kirigami.Units.verySmallSpacing
x: xAlignHCenter
y: yAlignOver
contentItem: RowLayout {
spacing: 0
ToolButton {
focusPolicy: Qt.NoFocus
icon.name: "edit-cut"
visible: target && target.selectedText.length > 0 && (!target.hasOwnProperty("echoMode") || target.echoMode === TextInput.Normal)
onClicked: {
target.cut();
}
}
ToolButton {
focusPolicy: Qt.NoFocus
icon.name: "edit-copy"
visible: target && target.selectedText.length > 0 && (!target.hasOwnProperty("echoMode") || target.echoMode === TextInput.Normal)
onClicked: {
target.copy();
}
}
ToolButton {
focusPolicy: Qt.NoFocus
icon.name: "edit-paste"
visible: target && target.canPaste
onClicked: {
target.paste();
}
}
}
FontMetrics {
id: fontMetrics
font: target.font
}
}
}
......@@ -5,7 +5,6 @@
import QtQuick 2.15
import QtQuick.Controls 2.15 as Controls
import QtQuick.Controls.impl 2.15
import QtQuick.Templates 2.15 as T
import org.kde.kirigami 2.14 as Kirigami
import "impl"
......@@ -19,27 +18,51 @@ T.TextArea {
implicitHeight: Math.max(contentHeight + topPadding + bottomPadding,
implicitBackgroundHeight + topInset + bottomInset,
placeholder.implicitHeight + topPadding + bottomPadding)
property bool visualFocus: control.activeFocus && (
control.focusReason == Qt.TabFocusReason ||
control.focusReason == Qt.BacktabFocusReason ||
control.focusReason == Qt.ShortcutFocusReason
)
padding: Kirigami.Units.mediumSpacing
property real horizontalPadding: Kirigami.Units.mediumHorizontalPadding
property real verticalPadding: padding
leftPadding: horizontalPadding
rightPadding: horizontalPadding
topPadding: verticalPadding
bottomPadding: verticalPadding
Kirigami.Theme.colorSet: Kirigami.Theme.View
Kirigami.Theme.inherit: background == null
color: Kirigami.Theme.textColor
selectionColor: Kirigami.Theme.highlightColor
selectedTextColor: Kirigami.Theme.highlightedTextColor
placeholderTextColor: Kirigami.Theme.disabledTextColor
PlaceholderText {
selectByMouse: !(Kirigami.Settings.hasTransientTouchInput && Kirigami.Settings.tabletMode)
cursorDelegate: Loader {
visible: control.activeFocus && !control.readOnly && control.selectionStart === control.selectionEnd
active: visible
sourceComponent: CursorDelegate { target: control }
}
Controls.Label {
id: placeholder
x: control.leftPadding
y: control.topPadding
width: control.width - (control.leftPadding + control.rightPadding)
height: control.height - (control.topPadding + control.bottomPadding)
anchors {
fill: parent
leftMargin: control.leftPadding
rightMargin: control.rightPadding
topMargin: control.topPadding
bottomMargin: control.bottomPadding
}
text: control.placeholderText
font: control.font
color: control.placeholderTextColor
horizontalAlignment: control.horizontalAlignment
verticalAlignment: control.verticalAlignment
visible: !control.length && !control.preeditText && (!control.activeFocus || control.horizontalAlignment !== Qt.AlignHCenter)
elide: Text.ElideRight
......@@ -49,5 +72,45 @@ T.TextArea {
background: TextEditBackground {
control: control
implicitWidth: 200
visualFocus: control.visualFocus
}
CursorHandle {
id: selectionStartHandle
target: control
}
CursorHandle {
id: selectionEndHandle
target: control
isSelectionEnd: true
}
MobileTextActionsToolBar {
id: mobileTextActionsToolBar
target: control
}
onActiveFocusChanged: {
if (!activeFocus) {
mobileTextActionsToolBar.visible = false
} else if (Kirigami.Settings.tabletMode) {
mobileTextActionsToolBar.visible = true
}
}
onSelectedTextChanged: {
if (Kirigami.Settings.tabletMode && selectedText.length > 0) {
mobileTextActionsToolBar.item.open()
}
}
onPressAndHold: {
if (Kirigami.Settings.tabletMode) {
forceActiveFocus();
cursorPosition = positionAt(event.x, event.y);
selectWord();
mobileTextActionsToolBar.item.open()
}
}
}
......@@ -5,7 +5,6 @@
import QtQuick 2.15
import QtQuick.Controls 2.15 as Controls
import QtQuick.Controls.impl 2.15
import QtQuick.Templates 2.15 as T
import org.kde.kirigami 2.14 as Kirigami
import "impl"
......@@ -13,6 +12,12 @@ import "impl"
T.TextField {
id: control
property bool visualFocus: control.activeFocus && (
control.focusReason == Qt.TabFocusReason ||
control.focusReason == Qt.BacktabFocusReason ||
control.focusReason == Qt.ShortcutFocusReason
)
implicitWidth: implicitBackgroundWidth + leftInset + rightInset
|| Math.max(contentWidth, placeholder.implicitWidth) + leftPadding + rightPadding
implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset,
......@@ -26,61 +31,37 @@ T.TextField {
palette: Kirigami.Theme.palette
Kirigami.Theme.colorSet: Kirigami.Theme.View
Kirigami.Theme.inherit: false
Kirigami.Theme.inherit: background == null
color: Kirigami.Theme.textColor
selectionColor: Kirigami.Theme.highlightColor
selectedTextColor: Kirigami.Theme.highlightedTextColor
placeholderTextColor: Kirigami.Theme.disabledTextColor
verticalAlignment: TextInput.AlignVCenter
horizontalAlignment: TextInput.AlignLeft
selectByMouse: !Kirigami.Settings.tabletMode
cursorDelegate: Kirigami.Settings.tabletMode ? mobileCursor : null
Component {
id: mobileCursor
MobileCursor {
target: control
}
}
/*onFocusChanged: {
if (control.activeFocus) {
MobileTextActionsToolBar.control = control;
}
}
TapHandler {
acceptedDevices: PointerDevice.Mouse | PointerDevice.Stylus
acceptedButtons: Qt.LeftButton | Qt.RightButton
// unfortunately, taphandler's pressed event only triggers when the press is lifted
// we need to use the longpress signal since it triggers when the button is first pressed
longPressThreshold: 0
onLongPressed: TextFieldContextMenu.targetClick(point, control);
}
Keys.onPressed: {
// trigger if context menu button is pressed
TextFieldContextMenu.targetKeyPressed(event, control)
}
selectByMouse: !(Kirigami.Settings.hasTransientTouchInput && Kirigami.Settings.tabletMode)
onPressAndHold: {
if (!Kirigami.Settings.tabletMode) {
return;
}
forceActiveFocus();
cursorPosition = positionAt(event.x, event.y);
selectWord();
}*/
cursorDelegate: Loader {
visible: control.activeFocus && !control.readOnly && control.selectionStart === control.selectionEnd
active: visible
sourceComponent: CursorDelegate { target: control }
}
PlaceholderText {
Controls.Label {