Mobile: ensure input fields stay visible after keyboard opens

When the user taps on a TextField to enter text, usually the virtual
keyboard will pop up. This code tries to ensure that the keyboard
doesn't cover the entry field that the user was trying to work on.

In order to centralize these changes, this introduces a new
SsrfTextField type which we use to also remove a few redundant default
settings that we previously had for every field. The one TextArea for
the Notes field didn't seem worth creating yet another type for, so
there the changes are done directly in DiveDetailsEdit.

The awkward timer mechanism is necessary as the keyboard pops up
asynchronously and then triggers a change of height for the app, so we
need to wait a little bit before doing the adjustment.

Signed-off-by: Dirk Hohndel <dirk@hohndel.org>
This commit is contained in:
Dirk Hohndel 2019-10-13 14:43:38 -07:00
parent 85d810119b
commit 01f1bea995
3 changed files with 127 additions and 111 deletions

View file

@ -163,13 +163,10 @@ Item {
font.pointSize: subsurfaceTheme.smallPointSize font.pointSize: subsurfaceTheme.smallPointSize
color: subsurfaceTheme.textColor color: subsurfaceTheme.textColor
} }
Controls.TextField { SsrfTextField {
id: txtDate; id: txtDate;
Layout.fillWidth: true Layout.fillWidth: true
onEditingFinished: { flickable: detailsEditFlickable
focus = false
}
color: subsurfaceTheme.textColor
} }
Controls.Label { Controls.Label {
Layout.alignment: Qt.AlignRight Layout.alignment: Qt.AlignRight
@ -201,13 +198,10 @@ Item {
font.pointSize: subsurfaceTheme.smallPointSize font.pointSize: subsurfaceTheme.smallPointSize
color: subsurfaceTheme.textColor color: subsurfaceTheme.textColor
} }
Controls.TextField { SsrfTextField {
id: txtGps id: txtGps
Layout.fillWidth: true Layout.fillWidth: true
color: subsurfaceTheme.textColor flickable: detailsEditFlickable
onEditingFinished: {
focus = false
}
} }
Controls.Label { Controls.Label {
@ -238,14 +232,11 @@ Item {
font.pointSize: subsurfaceTheme.smallPointSize font.pointSize: subsurfaceTheme.smallPointSize
color: subsurfaceTheme.textColor color: subsurfaceTheme.textColor
} }
Controls.TextField { SsrfTextField {
id: txtDepth id: txtDepth
Layout.fillWidth: true Layout.fillWidth: true
color: subsurfaceTheme.textColor
validator: RegExpValidator { regExp: /[^-]*/ } validator: RegExpValidator { regExp: /[^-]*/ }
onEditingFinished: { flickable: detailsEditFlickable
focus = false
}
} }
Controls.Label { Controls.Label {
Layout.alignment: Qt.AlignRight Layout.alignment: Qt.AlignRight
@ -253,14 +244,11 @@ Item {
font.pointSize: subsurfaceTheme.smallPointSize font.pointSize: subsurfaceTheme.smallPointSize
color: subsurfaceTheme.textColor color: subsurfaceTheme.textColor
} }
Controls.TextField { SsrfTextField {
id: txtDuration id: txtDuration
Layout.fillWidth: true Layout.fillWidth: true
color: subsurfaceTheme.textColor
validator: RegExpValidator { regExp: /[^-]*/ } validator: RegExpValidator { regExp: /[^-]*/ }
onEditingFinished: { flickable: detailsEditFlickable
focus = false
}
} }
Controls.Label { Controls.Label {
@ -269,13 +257,10 @@ Item {
font.pointSize: subsurfaceTheme.smallPointSize font.pointSize: subsurfaceTheme.smallPointSize
color: subsurfaceTheme.textColor color: subsurfaceTheme.textColor
} }
Controls.TextField { SsrfTextField {
id: txtAirTemp id: txtAirTemp
Layout.fillWidth: true Layout.fillWidth: true
color: subsurfaceTheme.textColor flickable: detailsEditFlickable
onEditingFinished: {
focus = false
}
} }
Controls.Label { Controls.Label {
@ -284,13 +269,10 @@ Item {
font.pointSize: subsurfaceTheme.smallPointSize font.pointSize: subsurfaceTheme.smallPointSize
color: subsurfaceTheme.textColor color: subsurfaceTheme.textColor
} }
Controls.TextField { SsrfTextField {
id: txtWaterTemp id: txtWaterTemp
Layout.fillWidth: true Layout.fillWidth: true
color: subsurfaceTheme.textColor flickable: detailsEditFlickable
onEditingFinished: {
focus = false
}
} }
Controls.Label { Controls.Label {
@ -363,14 +345,11 @@ Item {
font.pointSize: subsurfaceTheme.smallPointSize font.pointSize: subsurfaceTheme.smallPointSize
color: subsurfaceTheme.textColor color: subsurfaceTheme.textColor
} }
Controls.TextField { SsrfTextField {
id: txtWeight id: txtWeight
readOnly: text === "cannot edit multiple weight systems" readOnly: text === "cannot edit multiple weight systems"
Layout.fillWidth: true Layout.fillWidth: true
color: subsurfaceTheme.textColor flickable: detailsEditFlickable
onEditingFinished: {
focus = false
}
} }
// all cylinder info should be able to become dynamic instead of this blob of code. // all cylinder info should be able to become dynamic instead of this blob of code.
// first cylinder // first cylinder
@ -395,15 +374,12 @@ Item {
font.pointSize: subsurfaceTheme.smallPointSize font.pointSize: subsurfaceTheme.smallPointSize
color: subsurfaceTheme.textColor color: subsurfaceTheme.textColor
} }
Controls.TextField { SsrfTextField {
id: txtGasMix0 id: txtGasMix0
text: usedGas[0] != null ? usedGas[0] : null text: usedGas[0] != null ? usedGas[0] : null
Layout.fillWidth: true Layout.fillWidth: true
color: subsurfaceTheme.textColor
validator: RegExpValidator { regExp: /(EAN100|EAN\d\d|AIR|100|\d{1,2}|\d{1,2}\/\d{1,2})/i } validator: RegExpValidator { regExp: /(EAN100|EAN\d\d|AIR|100|\d{1,2}|\d{1,2}\/\d{1,2})/i }
onEditingFinished: { flickable: detailsEditFlickable
focus = false
}
} }
Controls.Label { Controls.Label {
@ -412,14 +388,11 @@ Item {
font.pointSize: subsurfaceTheme.smallPointSize font.pointSize: subsurfaceTheme.smallPointSize
color: subsurfaceTheme.textColor color: subsurfaceTheme.textColor
} }
Controls.TextField { SsrfTextField {
id: txtStartPressure0 id: txtStartPressure0
text: startpressure[0] != null ? startpressure[0] : null text: startpressure[0] != null ? startpressure[0] : null
Layout.fillWidth: true Layout.fillWidth: true
color: subsurfaceTheme.textColor flickable: detailsEditFlickable
onEditingFinished: {
focus = false
}
} }
Controls.Label { Controls.Label {
@ -428,14 +401,11 @@ Item {
font.pointSize: subsurfaceTheme.smallPointSize font.pointSize: subsurfaceTheme.smallPointSize
color: subsurfaceTheme.textColor color: subsurfaceTheme.textColor
} }
Controls.TextField { SsrfTextField {
id: txtEndPressure0 id: txtEndPressure0
text: endpressure[0] != null ? endpressure[0] : null text: endpressure[0] != null ? endpressure[0] : null
Layout.fillWidth: true Layout.fillWidth: true
color: subsurfaceTheme.textColor flickable: detailsEditFlickable
onEditingFinished: {
focus = false
}
} }
//second cylinder //second cylinder
Controls.Label { Controls.Label {
@ -462,16 +432,13 @@ Item {
font.pointSize: subsurfaceTheme.smallPointSize font.pointSize: subsurfaceTheme.smallPointSize
color: subsurfaceTheme.textColor color: subsurfaceTheme.textColor
} }
Controls.TextField { SsrfTextField {
visible: usedCyl[1] != null ? true : false visible: usedCyl[1] != null ? true : false
id: txtGasMix1 id: txtGasMix1
text: usedGas[1] != null ? usedGas[1] : null text: usedGas[1] != null ? usedGas[1] : null
Layout.fillWidth: true Layout.fillWidth: true
color: subsurfaceTheme.textColor
validator: RegExpValidator { regExp: /(EAN100|EAN\d\d|AIR|100|\d{1,2}|\d{1,2}\/\d{1,2})/i } validator: RegExpValidator { regExp: /(EAN100|EAN\d\d|AIR|100|\d{1,2}|\d{1,2}\/\d{1,2})/i }
onEditingFinished: { flickable: detailsEditFlickable
focus = false
}
} }
Controls.Label { Controls.Label {
@ -481,15 +448,12 @@ Item {
color: subsurfaceTheme.textColor color: subsurfaceTheme.textColor
font.pointSize: subsurfaceTheme.smallPointSize font.pointSize: subsurfaceTheme.smallPointSize
} }
Controls.TextField { SsrfTextField {
visible: usedCyl[1] != null ? true : false visible: usedCyl[1] != null ? true : false
id: txtStartPressure1 id: txtStartPressure1
text: startpressure[1] != null ? startpressure[1] : null text: startpressure[1] != null ? startpressure[1] : null
Layout.fillWidth: true Layout.fillWidth: true
color: subsurfaceTheme.textColor flickable: detailsEditFlickable
onEditingFinished: {
focus = false
}
} }
Controls.Label { Controls.Label {
@ -499,15 +463,12 @@ Item {
color: subsurfaceTheme.textColor color: subsurfaceTheme.textColor
font.pointSize: subsurfaceTheme.smallPointSize font.pointSize: subsurfaceTheme.smallPointSize
} }
Controls.TextField { SsrfTextField {
visible: usedCyl[1] != null ? true : false visible: usedCyl[1] != null ? true : false
id: txtEndPressure1 id: txtEndPressure1
text: endpressure[1] != null ? endpressure[1] : null text: endpressure[1] != null ? endpressure[1] : null
Layout.fillWidth: true Layout.fillWidth: true
color: subsurfaceTheme.textColor flickable: detailsEditFlickable
onEditingFinished: {
focus = false
}
} }
// third cylinder // third cylinder
Controls.Label { Controls.Label {
@ -535,16 +496,12 @@ Item {
font.pointSize: subsurfaceTheme.smallPointSize font.pointSize: subsurfaceTheme.smallPointSize
color: subsurfaceTheme.textColor color: subsurfaceTheme.textColor
} }
Controls.TextField { SsrfTextField {
visible: usedCyl[2] != null ? true : false visible: usedCyl[2] != null ? true : false
id: txtGasMix2 id: txtGasMix2
text: usedGas[2] != null ? usedGas[2] : null text: usedGas[2] != null ? usedGas[2] : null
Layout.fillWidth: true Layout.fillWidth: true
color: subsurfaceTheme.textColor
validator: RegExpValidator { regExp: /(EAN100|EAN\d\d|AIR|100|\d{1,2}|\d{1,2}\/\d{1,2})/i } validator: RegExpValidator { regExp: /(EAN100|EAN\d\d|AIR|100|\d{1,2}|\d{1,2}\/\d{1,2})/i }
onEditingFinished: {
focus = false
}
} }
Controls.Label { Controls.Label {
@ -554,15 +511,12 @@ Item {
font.pointSize: subsurfaceTheme.smallPointSize font.pointSize: subsurfaceTheme.smallPointSize
color: subsurfaceTheme.textColor color: subsurfaceTheme.textColor
} }
Controls.TextField { SsrfTextField {
visible: usedCyl[2] != null ? true : false visible: usedCyl[2] != null ? true : false
id: txtStartPressure2 id: txtStartPressure2
text: startpressure[2] != null ? startpressure[2] : null text: startpressure[2] != null ? startpressure[2] : null
Layout.fillWidth: true Layout.fillWidth: true
color: subsurfaceTheme.textColor flickable: detailsEditFlickable
onEditingFinished: {
focus = false
}
} }
Controls.Label { Controls.Label {
@ -572,15 +526,12 @@ Item {
font.pointSize: subsurfaceTheme.smallPointSize font.pointSize: subsurfaceTheme.smallPointSize
color: subsurfaceTheme.textColor color: subsurfaceTheme.textColor
} }
Controls.TextField { SsrfTextField {
visible: usedCyl[2] != null ? true : false visible: usedCyl[2] != null ? true : false
id: txtEndPressure2 id: txtEndPressure2
text: endpressure[2] != null ? endpressure[2] : null text: endpressure[2] != null ? endpressure[2] : null
Layout.fillWidth: true Layout.fillWidth: true
color: subsurfaceTheme.textColor flickable: detailsEditFlickable
onEditingFinished: {
focus = false
}
} }
// fourth cylinder // fourth cylinder
Controls.Label { Controls.Label {
@ -608,16 +559,13 @@ Item {
font.pointSize: subsurfaceTheme.smallPointSize font.pointSize: subsurfaceTheme.smallPointSize
color: subsurfaceTheme.textColor color: subsurfaceTheme.textColor
} }
Controls.TextField { SsrfTextField {
visible: usedCyl[3] != null ? true : false visible: usedCyl[3] != null ? true : false
id: txtGasMix3 id: txtGasMix3
text: usedGas[3] != null ? usedGas[3] : null text: usedGas[3] != null ? usedGas[3] : null
Layout.fillWidth: true Layout.fillWidth: true
validator: RegExpValidator { regExp: /(EAN100|EAN\d\d|AIR|100|\d{1,2}|\d{1,2}\/\d{1,2})/i } validator: RegExpValidator { regExp: /(EAN100|EAN\d\d|AIR|100|\d{1,2}|\d{1,2}\/\d{1,2})/i }
color: subsurfaceTheme.textColor flickable: detailsEditFlickable
onEditingFinished: {
focus = false
}
} }
Controls.Label { Controls.Label {
@ -627,15 +575,12 @@ Item {
font.pointSize: subsurfaceTheme.smallPointSize font.pointSize: subsurfaceTheme.smallPointSize
color: subsurfaceTheme.textColor color: subsurfaceTheme.textColor
} }
Controls.TextField { SsrfTextField {
visible: usedCyl[3] != null ? true : false visible: usedCyl[3] != null ? true : false
id: txtStartPressure3 id: txtStartPressure3
text: startpressure[3] != null ? startpressure[3] : null text: startpressure[3] != null ? startpressure[3] : null
Layout.fillWidth: true Layout.fillWidth: true
color: subsurfaceTheme.textColor flickable: detailsEditFlickable
onEditingFinished: {
focus = false
}
} }
Controls.Label { Controls.Label {
@ -645,15 +590,12 @@ Item {
font.pointSize: subsurfaceTheme.smallPointSize font.pointSize: subsurfaceTheme.smallPointSize
color: subsurfaceTheme.textColor color: subsurfaceTheme.textColor
} }
Controls.TextField { SsrfTextField {
visible: usedCyl[3] != null ? true : false visible: usedCyl[3] != null ? true : false
id: txtEndPressure3 id: txtEndPressure3
text: endpressure[3] != null ? endpressure[3] : null text: endpressure[3] != null ? endpressure[3] : null
Layout.fillWidth: true Layout.fillWidth: true
color: subsurfaceTheme.textColor flickable: detailsEditFlickable
onEditingFinished: {
focus = false
}
} }
// fifth cylinder // fifth cylinder
Controls.Label { Controls.Label {
@ -681,16 +623,13 @@ Item {
font.pointSize: subsurfaceTheme.smallPointSize font.pointSize: subsurfaceTheme.smallPointSize
color: subsurfaceTheme.textColor color: subsurfaceTheme.textColor
} }
Controls.TextField { SsrfTextField {
visible: usedCyl[4] != null ? true : false visible: usedCyl[4] != null ? true : false
id: txtGasMix4 id: txtGasMix4
text: usedGas[4] != null ? usedGas[4] : null text: usedGas[4] != null ? usedGas[4] : null
Layout.fillWidth: true Layout.fillWidth: true
color: subsurfaceTheme.textColor
validator: RegExpValidator { regExp: /(EAN100|EAN\d\d|AIR|100|\d{1,2}|\d{1,2}\/\d{1,2})/i } validator: RegExpValidator { regExp: /(EAN100|EAN\d\d|AIR|100|\d{1,2}|\d{1,2}\/\d{1,2})/i }
onEditingFinished: { flickable: detailsEditFlickable
focus = false
}
} }
Controls.Label { Controls.Label {
@ -700,15 +639,12 @@ Item {
font.pointSize: subsurfaceTheme.smallPointSize font.pointSize: subsurfaceTheme.smallPointSize
color: subsurfaceTheme.textColor color: subsurfaceTheme.textColor
} }
Controls.TextField { SsrfTextField {
visible: usedCyl[4] != null ? true : false visible: usedCyl[4] != null ? true : false
id: txtStartPressure4 id: txtStartPressure4
text: startpressure[4] != null ? startpressure[4] : null text: startpressure[4] != null ? startpressure[4] : null
Layout.fillWidth: true Layout.fillWidth: true
color: subsurfaceTheme.textColor flickable: detailsEditFlickable
onEditingFinished: {
focus = false
}
} }
Controls.Label { Controls.Label {
@ -718,15 +654,12 @@ Item {
font.pointSize: subsurfaceTheme.smallPointSize font.pointSize: subsurfaceTheme.smallPointSize
color: subsurfaceTheme.textColor color: subsurfaceTheme.textColor
} }
Controls.TextField { SsrfTextField {
visible: usedCyl[4] != null ? true : false visible: usedCyl[4] != null ? true : false
id: txtEndPressure4 id: txtEndPressure4
text: endpressure[4] != null ? endpressure[4] : null text: endpressure[4] != null ? endpressure[4] : null
Layout.fillWidth: true Layout.fillWidth: true
color: subsurfaceTheme.textColor flickable: detailsEditFlickable
onEditingFinished: {
focus = false
}
} }
Controls.Label { Controls.Label {
@ -775,6 +708,31 @@ Item {
Layout.minimumHeight: Kirigami.Units.gridUnit * 6 Layout.minimumHeight: Kirigami.Units.gridUnit * 6
selectByMouse: true selectByMouse: true
wrapMode: TextEdit.WrapAtWordBoundaryOrAnywhere wrapMode: TextEdit.WrapAtWordBoundaryOrAnywhere
property bool firstTime: true
onPressed: waitForKeyboard.start()
// we repeat the Timer / Function from the SsrfTextField here (no point really in creating a matching customized TextArea)
function ensureVisible(yDest) {
detailsEditFlickable.contentY = yDest
}
// give the OS enough time to actually resize the flickable
Timer {
id: waitForKeyboard
interval: 300 // 300ms seems like FOREVER
onTriggered: {
if (!Qt.inputMethod.visible) {
if (firstTime) {
firstTime = false
restart()
}
return
}
// make sure at least half the Notes field is visible
if (txtNotes.y + txtNotes.height / 2 > detailsEditFlickable.contentY + detailsEditFlickable.height - 3 * Kirigami.Units.gridUnit || txtNotes.y < detailsEditFlickable.contentY)
txtNotes.ensureVisible(Math.max(0, 3 * Kirigami.Units.gridUnit + txtNotes.y + txtNotes.height / 2 - detailsEditFlickable.height))
}
}
} }
} }
Item { Item {

View file

@ -0,0 +1,57 @@
// SPDX-License-Identifier: GPL-2.0
import QtQuick 2.2
import QtQuick.Controls 2.2 as Controls
import org.kde.kirigami 2.4 as Kirigami
Controls.TextField {
/**
* set the flickable property to the Flickable this TextField is part of and
* when the user starts editing the text this should stay visible
*/
property var flickable
property bool firstTime: true
id: stf
// while we are at it, let's put some common settings here into the shared element
color: subsurfaceTheme.textColor
onEditingFinished: {
focus = false
firstTime = true
}
// that's when a user taps on the field to start entering text
onPressed: {
if (flickable !== undefined) {
waitForKeyboard.start()
} else {
console.log("flickable is undefined")
}
}
// give the OS enough time to actually resize the flickable
Timer {
id: waitForKeyboard
interval: 300 // 300ms seems like FOREVER, but even that sometimes isn't long enough on Android
onTriggered: {
if (!Qt.inputMethod.visible) {
if (firstTime) {
firstTime = false
restart()
}
return
}
// make sure there's enough space for the input field above the keyboard and action button (and that it's not too far up, either)
if (stf.y + stf.height > flickable.contentY + flickable.height - 3 * Kirigami.Units.gridUnit || y < flickable.contentY)
ensureVisible(Math.max(0, 3 * Kirigami.Units.gridUnit + stf.y + stf.height - flickable.height))
}
}
// scroll the flickable to the desired position if the keyboard has shown up
// this didn't work when setting it from within the Timer, but calling this function works.
// go figure.
function ensureVisible(yDest) {
flickable.contentY = yDest
}
}

View file

@ -21,6 +21,7 @@
<file>SsrfButton.qml</file> <file>SsrfButton.qml</file>
<file>SsrfCheckBox.qml</file> <file>SsrfCheckBox.qml</file>
<file>SsrfSwitch.qml</file> <file>SsrfSwitch.qml</file>
<file>SsrfTextField.qml</file>
<!-- ********** pictures ********** --> <!-- ********** pictures ********** -->
<file>icons/dive.jpg</file> <file>icons/dive.jpg</file>