QML several items with active focus / keyboard shortcut control - qt

I'm trying to implement a keyboard shortcut control for my qml application. I know there's the possibility to do that with an Action element, but I don't want menus and toolbars which are then mandatory to use.
That's why I'm approaching this topic with keyboard events. For this, I need to have the element performing the action to be in focus. But my goal is a global shortcut control, so theoratically I'd need to have all the elements in question in focus.
I found the FocusScope type in the documentation, but I'm not sure if this is what I need.
Does it mean that the focus of nested FocusScopes 'slides' through to the last element that's not a FocusScope and acquiring focus manually with focus: true thus only this last element holding focus? Or do all the elements on the way down the slide that acquire focus have the activeFocus property set?
Is this the right approach or would I need something else?

Focus in Qt Quick is a mess in my opinion. It always confuses me and I end up hacking around it with forceActiveFocus(). I'd recommend the new Shortcut type:
Shortcut {
sequence: StandardKey.Quit
context: Qt.ApplicationShortcut
onActivated: Qt.quit()
}
With the context property, you can choose whether you want the shortcut to apply to the current window or the entire application.
The motivation for this type can be seen in the comments of patch set 5:
Shortcut aims to supersede Action. I want to kill the latter in the future because...
compare the actual user code: http://cutebin.fi/prwznhkbo
look at the amount of "action" related expressions all around BasicButton.qml
IMHO the whole concept doesn't quite fit mobile/embedded or QML
Action was a frequently requested feature. Now that they have it, the frequent questions are "how to use a different icon/text" or "how to know the source that triggered an action". Both are contradicting the sole purpose of Action, and neither "problem" would exist if they just wrote simpler QML code in the first place, as illustrated by the example snippet. :)
Evidently the most usable part of Action is the shortcut feature. Those who need shortcuts are not happy that they need to use Action, because "what's up with all this other stuff, I just want a shortcut".

Maybe there are different ways of achieving this, but the way I know is the following one.
The idea is to have an Item which controls the key events you need to handle.
I'll explain myself with an example. As you will see, if we have input widgets (i.e. TextInput) we have to implement a mechanism to return the input to our Item in order to process again the keyboard events. In this example, the Qt.Key_Escape key will be used to set the focus back.
import QtQuick 2.4
import QtQuick.Controls 1.3
ApplicationWindow {
id: mainwindow
title: qsTr("Hello")
width: 640
height: 480
visible: true
Item {
anchors.fill: parent
focus: true
Keys.onPressed: {
if ( (event.key === Qt.Key_Q) && (event.modifiers & Qt.ShiftModifier) ) {
rect.blue()
} else if ( (event.key === Qt.Key_W) && (event.modifiers & Qt.AltModifier) ) {
rect.red()
} else if ( (event.key === Qt.Key_E) && (event.modifiers & Qt.AltModifier) ) {
text.text = 'Key Alt+E was pressed'
}
}
Rectangle{
id: rect
width: 100
height: 100
color: "black"
function blue() {color = "blue"}
function red() {color = "red"}
}
Text {
id: text
anchors.centerIn: parent
font.pointSize: 20
}
TextInput {
id: textinput
anchors.top: text.bottom
text: "sample text"
Keys.onPressed: {
if (event.key === Qt.Key_Escape) {
console.log('Key Escape was pressed');
parent.focus = true;
}
}
}
}
}
Edit #1: #Mitch suggested to use the Shortcut QML Type. If you can use it (it's available since Qt 5.5), the code will be slightly different. Anyway, you need also to set the focus to the main app in some cases depending on the shortcut sequences implemented. For example, if we're typing text, Shift+Q doesn't have effect in this example. We need to press Escape first.
import QtQuick 2.5
import QtQuick.Controls 1.3
ApplicationWindow {
id: mainwindow
title: qsTr("Hello")
width: 640
height: 480
visible: true
Shortcut {
sequence: "Shift+Q"
onActivated: rect.blue()
context: Qt.ApplicationShortcut
}
Shortcut {
sequence: "Alt+W"
onActivated: rect.red()
context: Qt.ApplicationShortcut
}
Shortcut {
sequence: "Alt+E"
onActivated: text.text = 'Key Alt+E was pressed'
context: Qt.ApplicationShortcut
}
Item {
anchors.fill: parent
Rectangle{
id: rect
width: 100
height: 100
color: "black"
function blue() {color = "blue"}
function red() {color = "red"}
}
Text {
id: text
anchors.centerIn: parent
font.pointSize: 20
}
TextInput {
id: textinput
anchors.top: text.bottom
text: "sample text"
Keys.onPressed: {
if (event.key === Qt.Key_Escape) {
console.log('Key Escape was pressed');
parent.focus = true;
}
}
}
}
}

Much like Mitch, I found focus to be a mess in QML, much like many other aspects of it.
I ended up implementing my own "active focus / selection" scheme. Basically I keep a list of item pointers as my "active selection", I have the keyboard focus fixed at a single item acting as an event dispatcher, and it redirects keyboard events to all items in the active selection list. I still use QML's MouseArea to manage the selected items.

Related

Qt5-QML: How to handle change of color and text of a button after click event

I am writing a small application that is working as follows:
1) I launch the application and I select a robot to which I will connect. See print screen below of the small app:
2) That will lead me to another page where I can actually choose the robot to connect to as shown in the print screen below:
3) Finally after selecting the robot the application brings me back to the initial screen that will show me an additional Button showing the chosen robot.
The problem: I have is that after I choose the robot and I am back to the initial screen and I push the button the color of the button should turn into a (for example) green color and changing the text into (for example) Connecting...
The code I am using is the following for which I am only putting the related part:
import QtQuick 2.12
import QtQuick.Controls 2.12
import QtQuick.Layouts 1.12
import QtQuick.Controls.Styles 1.4
Page {
property int dialogId: -1
signal selectDialog()
ColumnLayout {
anchors.fill: parent
spacing: 5
Button {
id: button1
text: "Select Robot"
onClicked: selectDialog()
Layout.fillWidth: true
font.pointSize: 20
}
Button {
id: dialogA
text: "FreddieMercury: Connect";
visible: dialogId === 1
Layout.fillWidth: true
font.pointSize: 20
function buttonClick()
{
console.log("Button "+ dialogA.text +" is clicked!")
}
Rectangle {
id: button
color: "red"
width: 96; height: 24; anchors.centerIn: parent
MouseArea {
id: region
anchors.fill: parent;
onClicked: console.log("clicked()")
onPressed: dialogA.color = "green"
onReleased: dialogA.color = "red"
}
Text {
id: st_text
anchors.centerIn: parent
text: "Connecting..."
font.bold: true
font.pointSize: 20
color: "green"
}
}
}
// Other Buttons
}
}
What I tried so far
I went through this source and also this post which I followed. As you can see from the point 3) I am close to the good functioning but there is clearly something I am not doing right.
Also this was useful and in fact I used the MouseArea option exactly from that post.
However I still don't see the whole color extended into the button.
Finally the text changed after the click event happened I included it in the Button as shown and thought that the property text: "Connecting..." was enough to overwrite the existing text but without success.
Please advise on what I am missing that is keeping me from a full working example.
I think the base issue is that you're trying to use examples for QtQuick Controls 1 with QtQuick Controls 2. They're completely different animals and you cannot style the v2 controls using QtQuick.Controls.Styles.
For customizing Controls 2 styles, like Button, see here. I also find it useful to look at the source code for the included controls (they're in your Qt library install folder inside /qml/QtQuick/Controls2/ directory). Though personally I find needing to re-create a whole new Button (or whatever) just to change a color or font is a bit much, especially if I want it to work across all the included QtQuick Controls2 Styles.
An alternative is to "hack" the properties of the built-in Control styles. This certainly has some drawbacks like if you want to be able to reset the control style back to default bindings, you'd have to save the original bindings and re-create them to reset the style. OTOH it beats creating customized controls for each style. YMMV.
Anyway here's an example of what i think you're looking for. This is based on our previous exercise with the buttons. :) Specifically, I just modified the Page1.qml code and the other 2 files are exactly the same as before. In this page I added buttonClick() handler and the Button::onClicked calls to trigger it from each button (and the button texts of course :).
Page1.qml
import QtQuick 2.12
import QtQuick.Controls 2.12
import QtQuick.Controls.impl 2.12 // for IconLabel
import QtQuick.Layouts 1.12
Page {
property int dialogId: -1;
signal selectDialog()
function buttonClick(button)
{
button.text = qsTr("Connecting to %1...").arg(button.text);
button.enabled = false; // prevent repeat clicks
// If Button has a background Rectangle object then we can set properties on it.
// note: `instanceof` was added in Qt 5.10
if (button.background && button.background instanceof Rectangle) {
button.background.color = "red"; // override style color
button.background.gradient = null; // some styles use a gradient
button.background.visible = true; // some styles may hide it in some situations
}
// Similar with the label element, IconLabel is used by all included QML styles.
if (button.contentItem && button.contentItem instanceof IconLabel) {
button.contentItem.color = "blue"; // override style color
button.contentItem.font.bold = true;
button.contentItem.font.pointSize = 20;
}
}
ColumnLayout {
anchors.fill: parent
spacing: 5
Button {
id: button1
text: "Select"
onClicked: selectDialog()
Layout.fillWidth: true
}
// These buttons should appear only after the user selects the choices on `Page2`
Button {
id: dialogA
text: "Freddie Mercury"
visible: dialogId === 1
Layout.fillWidth: true
onClicked: buttonClick(this)
}
Button {
id: dialogB
text: "David Gilmour"
visible: dialogId === 2
Layout.fillWidth: true
onClicked: buttonClick(this)
}
Button {
id: dialogC
text: "Mick Jagger"
visible: dialogId === 3
Layout.fillWidth: true
onClicked: buttonClick(this)
}
}
}
If you had a customized Button (like in the Qt docs example) then you could still do basically the same thing in buttonClick() but probably w/out worrying about the if (button.background ...) stuff (since you'd be sure your button has valid background/contentItem Items).
A better implementation of a "default" (Style-specific) Button but with custom colors/text properties would involve a subclass which uses Binding and/or Connections QML elements to control the properties and be able to reset them back to the current QtQuick Style defaults.

finding click event in Qml TableView Header when Column is clicked

I have 2 static ListModel's in this example, in reality I use LocalStorage to fill the ListModel, but to keep it simple, I added 2 buttons to change the Models, but I want to tie it to the TableView's Header Column click event, and can not figure out how to do that from other examples of trying to sort, I do not know if it is possible to have a sort using ListModel, I could not find any example, so can someone explain this or show an example, of how to replace the buttons with column click events, I can then use this to pass the sort by argument to my LocalStorage sql statement to update the ListModel.
Update: I forgot to mention I was looking for a Qml / Qml JavaScript solution, for some reason I thought if I left off the C++ tag, I would avoid this issue, I will use this method as a last resort, since I decided to write this App using only Qml, with no C++ back end, but I do have that version now, since I had issues with how I was importing JavaScript written for the Web, as opposed to Qml JavaScript, which is not the same.
To be clear, I am trying to change Models and not Sort the rows, those are not the same question, the difference is in how the click event is used, and all I want to do is change the name of a query in the back end, which is Qml JavaScript, the reason I do not want C++ solutions is because I am doing this in Felgo, but this is not a Felgo quesiton, it works fine with C++, but you have to set up Live to work with it, and in reality this is going to be source I open up to github, and want it to be able to work without C++, and it seems there should be a way to hook this, and Mouse did work for me, capturing it in the even keep the header from loading, since it hooks at the beginning and waits for input, but if I have to, I am sure your solution will work, then I will accept it, sorry about that confusion, I get confused about what tags to use, so originally I only included qml, and qt was added to it, which I though was a great idea, because this really is a Qt question, that relates to Qml only, and not C++, that is another tag, this is a trend that Felgo is pushing, and they have good reasons, its easier for JavaScript or C/C++ Programmers to use, and Live Debugging works faster when used without a C++ back end, so now I gave more information, when Originally I though this was a simple question, that only related to Qml, if not then the answer has been given for C++, unless there is a better way, seeing how all I want to do is click on the header column the same way I would like on the button, can I embed the button into the column? If so how? I could not find an example of this, only ones that would effect text properties, and would sort the rows, which is not what I was trying to do, only update the model.
import QtQuick 2.11
import QtQuick.Controls 1.3
import QtQuick.Layouts 1.3
import QtQuick.Window 2.11
Window {
visible: true
width: 640
height: 480
title: qsTr("TableView Sort")
Column {
id: column
spacing: 9
anchors.fill: parent
TableView {
id: tableView
anchors.left: column.left
anchors.leftMargin: 6
anchors.right: column.right
anchors.rightMargin: 273
highlightOnFocus: true
model: myListModel1
sortIndicatorVisible: true
TableViewColumn {
role: "title"
title: "Column 1"
//width: 133
}
TableViewColumn {
role: "description"
title: "Column 2"
//width: 166
}
}
Button {
id: button1
text: qsTr("Model 1")
anchors.left: column.left
anchors.leftMargin: 6
onClicked: {
tableView.model = myListModel1
}
}
Button {
id: button2
text: qsTr("Model 2")
anchors.left: column.left
anchors.leftMargin: 6
onClicked: {
tableView.model = myListModel2
}
}
}
ListModel {
id: myListModel1
ListElement {
title: "Orange"
description: "Orange is Orange"
}
ListElement {
title: "Banana"
description: "Yellow"
}
ListElement {
title: "Apple"
description: "Red"
}
}
ListModel {
id: myListModel2
ListElement {
title: "Apple"
description: "Red"
}
ListElement {
title: "Banana"
description: "Yellow"
}
ListElement {
title: "Orange"
description: "Orange is Orange"
}
}
}
Update: This worked
onSortIndicatorColumnChanged: tableView.model = (sortIndicatorColumn == 0) ? myListModel1 : myListModel2
onSortIndicatorOrderChanged: tableView.model = (sortIndicatorColumn == 0) ? myListModel1 : myListModel2
Thanks for any help.
You could use a proxy model to sort your model. But, there is no QML component and you have to use QSortFilterProxyModel.
It's quite easy to do. But, QSortFilterProxyModel is not made to be used with a QML tableview (your table uses role names to display the columns and the proxy model will attempt to sort on index).
A quick example:
class SortProxyModel : public QSortFilterProxyModel
{
Q_OBJECT
public:
SortProxyModel(): QSortFilterProxyModel ()
{
}
// Define the way you want to sort data
bool lessThan(const QModelIndex& left, const QModelIndex& right) const
{
int role = sourceModel()->roleNames().key(roleName.toLocal8Bit(), 0);
return left.data(role) < right.data(role);
}
Q_INVOKABLE void setSortRole(QString const& roleName) // Used to select the sort role
{
this->roleName = roleName;
}
Q_INVOKABLE virtual void sort(int /*column*/, Qt::SortOrder order = Qt::AscendingOrder)
{
QSortFilterProxyModel::sort(0, order); // Always use the first column.
}
private:
QString roleName; // Role used to sort the model
};
// main.cpp
// Declare your type to use it in QML
qmlRegisterType<SortProxyModel>("SortProxyModel", 0, 1, "SortProxyModel");
// Main.qml
import SortFilterProxyModel 0.1;
TableView {
id: tableView
model: proxy // Use the proxy model rather than the model itself
sortIndicatorVisible: true
onSortIndicatorColumnChanged: { // Called when you click on the header
if (sortIndicatorColumn == 0) // Set the role used to sort data
model.setSortRole("title");
else
model.setSortRole("description");
model.sort(sortIndicatorColumn, sortIndicatorOrder)
}
onSortIndicatorOrderChanged: { // Called when you click on the header
if (sortIndicatorColumn == 0) // Set the role used to sort data
model.setSortRole("title");
else
model.setSortRole("description");
model.sort(sortIndicatorColumn, sortIndicatorOrder)
}
SortProxyModel {
id: proxy
objectName: "proxy"
sourceModel: myListModel1
}
It's just a quick example and you should improve the code. But, I think it will be a good start...

Can I select text by mouse from a number of delegates in QML?

Lets imaging project for desktop which contains only one QML file:
import QtQuick 2.4
import QtQuick.Window 2.2
Window {
visible: true
width: 500
height: 500
ListModel {
id: myModel
ListElement {
color: "red"
text: "some interesting information"
}
ListElement {
color: "blue"
text: "not so interesting information"
}
ListElement {
color: "green"
text: "and some more information"
}
}
ListView {
anchors.fill: parent
interactive: false
model: myModel
delegate: Rectangle {
width: parent.width
height: 30
color: model.color
TextEdit {
anchors.centerIn: parent
text: model.text
selectByMouse: true
}
}
}
}
With the selectByMouse property of TextEdit set to true I can select text in it. But how can I select text in multiple delegates at the same time? In multiple TextEdits? Is it even possible?
Since the other answers seem incomplete or don't answer what I believe VALOD9 was asking: "can you select text across multiple delegates as though their TextEdits are one element?"
This is not inherently possible, but can be crafted in QML with a lot of manual tracking of mouse presses and movement.
It could be accomplished by placing MouseArea over your ListView and delegates that each contain DropAreas. To track your text selection clicks/drags across your delegates, you could use an invisible MouseArea.drag.target that triggers the delegate DropAreas' onEntered and onPositionChanged events. Based on all this data, you can use TextEdit.positionAt() with your mouse coordinate results to get where your selections start and end, and use TextEdit.select() to programmatically select the text in each delegate. Since you are programmatically selecting text, your TextEdits would need to have selectByMouse: false.
You will need to store any necessary selection data in your model since you shouldn't store state in delegates in case they are removed from the ListView from automatic caching. You would then use this data to recreate the selection when they are re-loaded from cache using Component.OnCompleted. To do selection operations like copy, you could iterate over your model and pick up the saved selection data (especially if you save the selected text to the model using TextEdit.selectedText).
This would allow many TextEdit-based delegates to act as though they are one when selecting text across any of them.
You can set persistentSelection to true and each of your TextEdit will keep the text selected (http://doc.qt.io/qt-5/qml-qtquick-textedit.html#persistentSelection-prop)

QML ListView method positionViewAtEnd() does exactly the opposite

I'm going crazy. I have a ListView inside a ScrollView, hooked up to a model that inherits QAbstractListModel. When objects are added to the model, the ListView shows them using a delegate. So far, so good.
But I really want the view to stay scrolled to the bottom (like a chat window), and I'm having a very difficult time making that happen. Here is the relevant QML code:
Rectangle {
ScrollView {
[anchor stuff]
ListView {
id: messageList
model: textMessageFiltered
delegate: messageDelegate
}
}
TextField {
id: messageEditor
[anchor stuff]
onAccepted: {
controller.sendTextMessage(text)
text = ""
/* This works. */
//messageList.positionViewAtEnd();
}
}
Component {
id: messageDelegate
Rectangle {
anchors.left: parent.left
anchors.right: parent.right
color: "white"
height: nameText.height + 4
Text {
id: nameText
wrapMode: Text.Wrap
text: "<b>" + authorName + " (" + authorId + ")</b> " + message
[anchor stuff]
}
ListView.onAdd: {
console.log("This prints just fine!")
messageList.positionViewAtEnd()
}
}
}
}
The really strange thing, is that messageList.positionViewAtEnd() (at the end of the file) actually jumps it to the beginning. Without the call, the view stays where it is, even as new entries appear in the list. And indeed, if you look at the Qt documentation for the ListView.positionViewAtEnd(), it says:
Positions the view at the beginning or end, taking into account ...
Is that a silly error in the documentation, or what? I've tried everything I can think of to make this work, particularly the positionViewAtIndex() method and using highlighters to force the scroll to happen. But nothing works. Note the /* This works. */ comment in the source code above. When that is enabled, it works totally fine! (except of course, it jumps to the ListView.count()-2 index, instead of the end of the list)
Does anyone have any idea what might be wrong here? Any examples I could try to prove that there's a terrible, terrible bug in QML?
I'm using Qt 5.3.1 with QtQuick 2.0 (or 2.1 or 2.2 fail too). I've tried many, many other configurations and code as well, so please ask if you need more info. I've completely exhausted my google-fu.
Thanks!
Edit 1
While the accepted answer does solve the above problem, it involves adding the Component.onCompleted to the delegate. This seems to cause problems when you scroll the list, because (I believe) the delegates are added to the view when you scroll up, causing the onCompleted trigger to be called even if the model item isn't new. This is highly undesirable. In fact, the application is freezing when I try to scroll up and then add new elements to the list.
It seems like I need a model.onAdd() signal instead of using the existence of a delegate instance to trigger the scroll. Any ideas?
Edit 2
And how does this NOT work?
ListView {
id: messageList
model: textMessageFiltered
delegate: messageDelegate
onCountChanged: {
console.log("This prints properly.")
messageList.positionViewAtEnd()
}
}
The text "This prints properly" prints, so why doesn't it position? In fact, it appears to reset the position to the top. So I tried positionViewAtBeginning(), but that did the same thing.
I'm totally stumped. It feels like a bug.
You need to set the currentIndex as well.
testme.qml
import QtQuick 2.2
import QtQuick.Controls 1.1
import QtQuick.Window 2.0
ApplicationWindow {
title: qsTr("Hello World")
width: 300
height: 240
ScrollView {
anchors.fill: parent
ListView {
anchors.fill: parent
id: messageList
model: messageModel
delegate: Text { text: mytextrole }
highlight: Rectangle { color: "red" }
highlightMoveDuration: 0
onCountChanged: {
var newIndex = count - 1 // last index
positionViewAtEnd()
currentIndex = newIndex
}
}
}
ListModel {
id: messageModel
ListElement { mytextrole: "Dog"; }
ListElement { mytextrole: "Cat"; }
}
Timer {
property int counter: 0
running: true
interval: 500
repeat: true
onTriggered: {
messageModel.append({"mytextrole": "Line" + (counter++)})
}
}
}
There is still some jumping to the first element and jumping back down for a fraction of a second.
There is a note in documentation:
Note: methods should only be called after the Component has completed. To position the view at startup, this method should be called by Component.onCompleted.
Change your ListView.onAdd: to
Component.onCompleted: {
console.log("This prints just fine!")
messageList.positionViewAtEnd()
}
And it works well.
In your case, the ListView emits add signal before the new delegate is created and completed. The ListView is still working on something behind the scene, so positionViewAtEnd cannot work as expected. And /* This works. */ because it is called after the new delegate is completed. However, don't assume this always works. Simply follow the note, call positionViewAtEnd in Component.onCompleted, in documentation.

QML Keys.onEnterPressed issue

I have a QtQuick project for Desktop. It is very simple:
// import QtQuick 1.0 // to target S60 5th Edition or Maemo 5
import QtQuick 1.1
Rectangle {
width: 360
height: 360
Grid
{
id: xGrid
width: parent.width
height: parent.height
columns: 2
spacing: 1
Rectangle
{
height: parent.height
width: 10
color: "#ff0000"
Text {
id: xText
text: qsTr("t\na\ns")
}
}
TextEdit
{
id: xTextEdit
height: parent.height
width: 350
Keys.onEnterPressed: {
console.log(event.key)
xText.text = (qsTr("A"))
}
}
}
}
My code does not run like I want. The Keys.onEnterPressed seem never be captured, so I try Keys.onPressed it work but not sure why when I press Enter, the even.key returns 16777220.
Any one get this issue? How can I solve it?
Thanks for your answer!
I got the same problem with a TextInput item. I tried
onPressed
onEnterPressed
onReturnPressed
Only the latter one worked (onReturnPressed). I guess, the underlying implementation of the TextInput captures the 'Enter' key so it doesn't get processed by the onPressed signal in a regular way.
By the way: the key code is correct. It's an abstraction on the platform specific key codes.
A better way to handle users entering a text value is to use TextInput.onAccepted
Here's an example:
TextInput {
onAccepted: processText()
}
When the user presses Enter, the processText() method will be called.
This approach is simpler and should improve cross-platform portability.
TextArea {
id: messageField
Layout.fillWidth: true
placeholderText: qsTr("Message")
wrapMode: TextArea.Wrap
inputMethodHints: Qt.ImhNoPredictiveText
function _onEnterPressed(event)
{
if ((event.modifiers & Qt.ControlModifier))
{
sendMessage()
}
else
{
event.accepted = false;
}
}
Keys.onReturnPressed: { _onEnterPressed(event) }
Keys.onEnterPressed: { _onEnterPressed(event) }
}
I'd say use onReturnPressed as well. Otherwise you can also check the key value in onPressed() and react there. onReturn/EnterPressed are just convenience functions.
Potentially relevant context taken from the docs:
[...] the order of key event
processing is:
Items specified in forwardTo
specific key handlers, e.g. onReturnPressed
onPressed, onReleased handlers
Item specific key handling, e.g. TextInput key handling
parent item

Resources