WebEngineView + virtual keyboard - maintain scroll position after resizing (focused input) - qt

I have a very simple browser app based on WebEngineView and virtual keyboard made in Qt Quick.
Everything works fine - the keyboard is shown perfectly each time I click on an input in the webview, but what bothers me is that if I click on an input that is at the bottom, the keyboard covers it after opening and I cannot see what I'm typing.
I tried solving it by resizing the WebEngineView element to accomodate for the keyboard height, like most mobile apps work. It works, I can scroll the page under the keyboard but the keyboard still covers the input and I need to scroll manually.
Is there any way I could adjust the web view scroll position so the keyboard doesn't cover the focused input from QML?
I cannot do it at a single website because I allow navigation to any website user wants, so I need some universal method.
Here is my qml code:
import QtQuick 2.12
import QtQuick.Window 2.12
import FreeVirtualKeyboard 1.0
import QtWebEngine 1.8
Window {
id: appContainer;
visible: true
width: 1280
height: 600
title: qsTr("WebEngineView")
property string pathUrl: "https://www.w3schools.com/html/html_forms.asp"
WebEngineView {
id: webview
width: appContainer.width
url: appContainer.pathUrl
height: appContainer.height
}
/*
Virtual keyboard
*/
InputPanel {
id: inputPanel
z: 99
y: appContainer.height
anchors.left: parent.left
anchors.right: parent.right
states: State {
name: "visible"
when: Qt.inputMethod.visible
PropertyChanges {
target: inputPanel
y: appContainer.height - inputPanel.height
}
}
transitions: Transition {
from: ""
to: "visible"
reversible: true
ParallelAnimation {
NumberAnimation {
properties: "y"
duration: 150
easing.type: Easing.InOutQuad
}
}
onRunningChanged: {
if(!running && inputPanel.state == "visible") {
// finished showing keyboard
webview.height = appContainer.height - inputPanel.height
console.log('Keyboard shown')
} else if(running && inputPanel.state != "visible") {
// begins to hide keyboard
webview.height = appContainer.height
console.log('Keyboard starts to hide');
}
}
}
}
}
So far the resizing part works okay - I do it in onRunningChanged so the webview resizes before the transition starts and after it ends - this prevents ugly empty space showing during transition.
Update
I have achieved the effect I wanted using webview.runJavaScript together with scrollIntoView after showing the keyboard:
webview.runJavaScript("document.activeElement.scrollIntoView({block: 'nearest', inline: 'nearest', behavior: 'smooth'})");
However I'm not sure if this is solution is the best, as I don't like the fact of involving javascript evaluation into the process. I'd like to know if there's any more "native" way of doing this.

Resize WebEngineView, scroll into view
The problem with resizing the WebEngineView is that HTML will see that your device screen suddenly shrunk and may decide to present a vastly different layout, for example move menu from top to side of the screen.
Even if this has not happened, layout has changed. The position on the new "screen" does not correspond to the position on the old one, there is no 1:1 relation, which is why it scrolls to a seemingly random spot in the first place.
We can tell webpage to scroll a focused element into view of new viewport:
If it was already onscreen than nothing happens.
If not, webpage scrolls so that the element fits on the screen if possible. scrollIntoView has parameters to scroll to the top/bottom of the screen as desired
So when onscreen keyboard is opened:
Save original scrollPosition
Resize WebEngineView
Optionally assign scrollPosition to saved value - although it probably won't do you any good
Use runJavaScript to determine activeElement and make it scrollIntoView
Repeat same steps when onscreen keyboard is dismissed.
Do not resize, scroll manually
Another approach would be to not resize the "screen" and just scroll the element into view if it got covered.
This would require Qt to change VisualViewport while leaving LayoutViewport intact (see this and this for more information) but it seems that Qt cannot do that, at least not through QML alone.
That leaves us with having to do it manually: determine position with getBoundingClientRect, calculate how much space does keyboard cover, and if it is not inside our calculated uncovered view area - scrollTo it.
(you will still need runJavaScript to get inside the webpage)
Perhaps this and this SO questions can help
Other options
#Hazelnutek reported some success with document.activeElement.scrollIntoViewIfNeeded()
Please see discussion in comments to this answer below:

Related

Qt QML Put element above a Drawer

I'm struggling with a very basic question..
Using QT 5.15.2:
We have a simple application with one main window and 2-3 sub-window (1 level down from main). The main window consists of a content item, a header and some menu-flaps distributed across the main window. So Far the sub-pages were opened with a drawer element.
However, the drawer overlays the flaps and header once opened and we need to re-instanciate the flaps and header within the drawer to have it visible. This is not really nice. Is there any way to define the z-level on which the drawer is opened? (apparently setting z doesn't work).
Item{
id: id_mainWindow
z: 0
Drawer{
id: id_subMenu1
anchors.fill: parent
z: 1
/* Not so nice workaround */
Button{
id: id_subClose
z: 100
onClicked{
id_subMenu1.close()
}
}
}
/* Unfortunately, this one gets hidden once, the drawer is open */
Button{
id: id_subOpenClose
z: 100
onClicked{
if( id_subMenu1.open ){
id_subMenu1.close()
} else {
id_subMenu1.open()
}
}
}
}
I would suggest that a Drawer is not the right component for this job as it is technically a Popup. It might be worth checking out the TabBar component instead.
Nevertheless here's a re-write of your code so that your Drawer opens without covering your id_subOpenClose button.
import QtQuick
import QtQuick.Controls
import QtQuick.Controls.Material
Rectangle {
id: id_mainWindow
anchors.fill: parent
Drawer {
id: id_subMenu1
/*
Set the Drawer's height and y position so that it does not cover your button
*/
y: id_subOpenClose.height
height: id_mainWindow.height - id_subOpenClose.height
width: id_mainWindow.width
// Do not dim background
dim: false
// Set this to zero if you want no shadow
Material.elevation: 2
edge: Qt.RightEdge
Label {
text: 'Hello World'
anchors.centerIn: parent
}
}
/*
This is your header button that was getting hidden
Here it stays as if it were part of a global header and does not get hidden by
the Drawer.
*/
Button{
id: id_subOpenClose
text: id_subMenu1.visible? 'close': 'open'
onClicked: id_subMenu1.visible? id_subMenu1.close(): id_subMenu1.open()
}
}
For an interactive WASM example of the above see here.

How to remove bounds of the Flickable QML component?

I want to display a large amount of content, for example, a grid of multiple images inside a window that is smaller than the content, similar to a geographical map but instead of a map, I want my own components as the "map". For this minimal working example, let's take for the content a grid of images with a total size of 1000x1000 with a window into this content of only 300x300.
I have tried 2 different approaches, but I will only go into detail of the first approach as that is the one that got me closest to my desired result:
I have tried the Flickable component but the content cannot be moved outside the predefined bounds, making the user unable to move the view in order to display all the parts of the content. So the simplest solution that I'm thinking about now is if I could remove these bounds from the Flickable component, but how?
I have also tried the Map component but it requires a "plugin" and I was unable to figure out how to use this component with my own content of an image grid.
The content that I want to show is something this
Grid {
columns: 5
spacing: 2
Repeater {
model: ListModel {
ListElement {
path: 'test1'
}
ListElement {
path: 'test2'
}
// ...
ListElement {
path: 'test25'
}
}
Rectangle {
width: 200
height: 200
Image {
anchors.fill: parent
source: 'file:' + path
}
}
}
}
I tried, putting this inside the Flickable like this
Flickable {
anchors.centerIn: parent
width: 300
height: 300
contentWidth: 1000
contentHeight: 1000
clip: true
// INSERT CUSTOM GRID COMPONENT HERE
}
This results in a 300x300 view inside the content as expected, but once you start to flick through the content to view different parts of it, you are stopped by the bounds preventing you from seeing anything outside these bounds. You can see it while dragging but once you release the view of the content is reset to these bounds.
So how do I remove these bounds? (Or is there a different component more suitable for my application?)
Here is a gif that shows how the content can be dragged passed the bounds, but once released it will only go up to the bounds and not further
I found the issue, I set the contentWidth and contentHeight of the Flickable incorrectly, this example works fine. The contentWidth and contentHeight determine the bounds in which you can flick.

Qt - QML Screen Mirroring in a dual monitor setup

I've developed an application, with the UI done using QML. What I was asked to do now is to make it so that when a second monitor is used, the second monitor shows everything that the program is doing. At first I thought of just telling the client to configure Windows to clone its screens. However when the applications uses some of its functionalities I need for the cloned screen to display certain indicators in the cloned screens but not on the original screen.
So my question is, How can I accomplish this. How can mirror what is happening in one screen, while maintaing enough control to draw in one and not in the other.
My only idea is to use a timer to take as screen shot at regular intervals and show that image in the second screen.
Is this doable?
Taking periodic screenshots, although perfectly doable, is undesired because of the impact to performance. Instead you should make use the main window's onFrameSwapped() signal, to grab images only when a new frame is generated.
Ideally, you'd want to make use of Layer or ShaderEffectSource, as suggested by #dtech, to read and re-render the frame straight from the GPU. Unfortunately, due to limitations in Qt Quick's Scene Graph, it is not possible to accomplish this across separate windows without destabilizing the source window's scene graph.
Update: Nevertheless, I've found a way to emulate the desired features of ShaderEffectSource using ShaderEffect instead. Your other options are to either copy frames through the CPU using Item's grabToImage function, or to re-implement QQuickView (your QML window) in C++ to grab images from the QML Engine when a new frame is generated.
Working Solution 1: Use ShaderEffect (Update: New Solution)
The new approach I've found consists of using ShaderEffect on a child Window and forcing it to update by calling that Window's update() method, which is inherited from QWindow. By using ShaderEffect, you guarantee the source image will be accessed only through the GPU, increasing performance by orders of magnitude over solution #2.
import QtQuick 2.15
import QtQuick.Window 2.15
Window {
id: mainWindow
title: qsTr("Main Window")
visible: true
width: 400
height: 200
color: "#0F0"
Rectangle {
id: viewport
color: "#00F"
width: parent.width/2
height: parent.height/2
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
// Draw QML Item into an off screen layer
layer.enabled: true
// Performance tweaks
layer.smooth: false
layer.mipmap: false
}
onFrameSwapped: {
// Update mirror window manually with each new frame
projectionWindow.update()
}
Window {
id: projectionWindow
property int screenID: 1
transientParent: mainWindow
visible: true
x: Qt.application.screens[screenID].virtualX
y: Qt.application.screens[screenID].virtualY
width: Qt.application.screens[screenID].desktopAvailableWidth
height: Qt.application.screens[screenID].desktopAvailableHeight
flags: Qt.FramelessWindowHint
color: "#000"
visibility: Window.Maximized
ShaderEffect {
id: img
// Set source to copy visuals from
property variant source: viewport
// Setting shader to the same resolution as the source may improve performance
width: viewport.width
height: viewport.height
// Performance tweak
blending: false
}
}
}
Triggering update() manually has the side effect of preventing the child from updating on its own. Calling the update method on the source window from the child Window once whenever needed works around this limitation.
It is worth noting that this solution only works with Qt's basic renderer, which is single threaded. Qt prevents the texture from being accessed from a diferent rendering threads, giving the following error message and returning a null pointer:
QQuickShaderEffectSource::textureProvider: can only be queried on the rendering thread of an exposed window
The performance gained from this method is far greater than the performance loss of using the single threaded renderer. You can force use of the single threaded renderer by setting an environment variable at the start of your application. This can be done from inside the app by adding the following code at the very start of your main function:
#if defined(Q_OS_WINDOWS)
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
putenv("QSG_RENDER_LOOP=windows");
#else
putenv("QSG_RENDER_LOOP=basic");
#endif
#elif defined(Q_OS_MACOS) || defined(Q_OS_LINUX)
setenv("QSG_RENDER_LOOP", "basic", 1);
#endif
Qt's documentation states that using the basic renderer reduces portability, but I can attest this approach works fine on Windows, macOS, and Linux and that it does not work on Haiku OS.
I've switched to this solution on the most recent versions of a project I develop. You can study the full implementation at:
https://github.com/Cuperino/QPrompt/blob/072f0a7808f3a361cb93bca8961e884ac9d8bb56/src/kirigami_ui/PrompterPage.qml#L760
https://github.com/Cuperino/QPrompt/blob/072f0a7808f3a361cb93bca8961e884ac9d8bb56/src/kirigami_ui/main.qml#L847
https://github.com/Cuperino/QPrompt/blob/072f0a7808f3a361cb93bca8961e884ac9d8bb56/src/prompter/ProjectionsManager.qml#L261
Working Solution 2: Use Item's grabToImage (original answer)
Another easy approach is to grab frames from QML, using an Item's grabToImage() function. Since Window is not an item in itself, you'd have to grab the image from one of its elements. In this example, I grab the image from an item called viewport each time a frame is swapped on mainWindow, using the onFrameSwapped() signal. Then the path to that image in memory is set as the source for the image in the second window, named projectionWindow. The second window will open on the screen set by the screenID variable; it is also set to be a frame-less window with its visibility is set to either Maximized or FullScreen, such that it is the only window seen on the second screen.
import QtQuick 2.15
import QtQuick.Window 2.15
Window {
id: mainWindow
title: qsTr("Main Window")
visible: true
width: 400
height: 200
color: "#0F0"
Rectangle {
id: viewport
color: "#00F"
width: parent.width/2
height: parent.height/2
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
}
onFrameSwapped: {
viewport.grabToImage(function(result) {
projectionWindow.frame = String(result.url);
});
}
Window {
id: projectionWindow
property int screenID: 1
property alias frame: img.source
transientParent: mainWindow
visible: true
x: Qt.application.screens[screenID].virtualX
y: Qt.application.screens[screenID].virtualY
width: Qt.application.screens[screenID].desktopAvailableWidth
height: Qt.application.screens[screenID].desktopAvailableHeight
flags: Qt.FramelessWindowHint
color: "#000"
visibility: Window.Maximized
Image {
id: img
anchors.fill: parent
fillMode: Image.PreserveAspectFit
// Performance tweaks
asynchronous: true
cache: false
}
}
}
I used to employed this solution on a project I develop. You can study the full implementation at:
https://github.com/Cuperino/QPrompt/blob/a873a2ec9b0619ec1bfdbde22fe90850e76969a5/src/kirigami_ui/main.qml#L828
https://github.com/Cuperino/QPrompt/blob/a873a2ec9b0619ec1bfdbde22fe90850e76969a5/src/prompter/ProjectionsManager.qml#L260
Alternate, incomplete, solution
The third, more manual, and unfinished solution consists of the following: You'd inherit from QQuickView or QQuickWindow, and then send the image to a second window on either the QQuickWindow::afterRendering() or the QQuickWindow::frameSwapped() signal. You would use a QQuickFramebufferObject or some other rendering pipeline such as DirectX, Metal or Vulkan, to render and grab the frame off screen. Using OpenGL as your renderer, means negating all the performance advantages that come from using the native rendering pipelines supported by Qt 6. You may need to implement the frame grab once per pipeline to get all performance benefits.
https://doc.qt.io/qt-6/qtquick-visualcanvas-scenegraph.html
The following talk by Giuseppe D’Angelo shows how some of this is setup in Qt 5. It doesn't show how to copy the contents to another window/screen but it could aid in this regard.
https://www.youtube.com/watch?v=V_idc9BBRuI
https://www.youtube.com/watch?v=D-7fVGIBz6k
You can use particular QML elements as texture sources and easily duplicate them via trivial fragment shaders.
You definitely do not want to take screenshots and draw that image back, it is wildly inefficient.
Keep in mind that it will be just a visual duplicate, it will not take user input. If you want it to be interactive in both windows, then you should simply use a single data object and connect it to two individual GUI elements.
OK, here is the code, but unfortunately, it evidently uncovers a bug in QML, as the implementation doesn't seem to work across different windows:
Window {
id: mainw
visible: true
width: 640
height: 480
title: qsTr("main window")
Row {
spacing: 5
Rectangle {
id: source
width: 100
height: 100
color: ma.containsPress ? "red" : "cyan"
Text {
text: "adoy"
anchors.centerIn: parent
}
MouseArea {
id: ma
anchors.fill: parent
}
}
ShaderEffectSource {
width: source.width
height: source.height
live: true
sourceItem: source
}
}
Window {
visible: true
width: 640
height: 480
title: qsTr("another window")
x: mainw.x + width + 10
y: mainw.y
Row {
ShaderEffectSource {
width: source.width
height: source.height
live: true
sourceItem: source
}
Rectangle {
width: 100
height: 100
color: "blue"
}
}
}
}

Setting activeFocus by clicking anywhere within an Item

I want to set the activeFocus for a FocusScope by clicking anywhere within an Item.
Is there a way to achieve this without having a MouseArea over the entire Item? Because it would have to overlay all elements within the Item, making them unclickable.
I'm pretty new to QtQuick/QML and have troubles understanding how to properly implement FocusScopes. I've read about propagating click signals, but couldn't get it to work.
Assuming I have something like this (no FocusScopes for readability):
Rectangle
{
id: outerRectangle
width: 1000
height: 1000
// various controls in here
Rectangle
{
id: innerRectangle
anchors.centerIn: parent
width: 200
height: 200
// even more controls in here
}
}
I want the outerRectangle to get the activeFocus when I click anywhere on the outerRectangle and vice-versa for the innerRectangle. But all controls on both Rectangles still have to work properly.
How can I achieve this?
Surround your Item with FocusScope:
FocusScope {
Item {
focus: true
}
}
See Qt Doc

Ubuntu SDK: How do I implement a “onClick” for an image in QML?

I have a background image for my page, and I want to implement a sort of page refresh when the background is clicked. However, I didn't find any actions for the image element in QT quick.
What's the right way to implement this?
You need to use MouseArea to handle click events.
Image {
source: "myimage.png"
MouseArea {
anchors.fill: parent
onClicked: {
// do what you want here
}
}
}

Resources