I've been learning QtQuick for about a week and I'm facing a weird behaviour on what I'm trying to achieve. I would like to make a vertical ListView with a Keyboard navigation so that when I press UP or DOWN, the items move up or down and if an item goes in or out of the "viewport", its opacity property will change smoothly to 0 or 1.
Here is my current QML code:
import QtQuick 2.4
Rectangle {
width:200
height:400
ListView {
property int activePosition:1
property int itemDisplayed:3
width:parent.width-50
height:parent.height-50
anchors.centerIn:parent
model:10
snapMode:ListView.SnapToItem
focus:true
cacheBuffer:2000
Component.onCompleted: {
console.log(count+' != '+contentItem.children.length+' ???')
}
Keys.onPressed: {
var i = 0
console.log('pos='+activePosition)
console.log(count+' != '+contentItem.children.length+' ???')
if (event.key === Qt.Key_Up) {
if (activePosition == 1 && currentIndex > 0) {
i = currentIndex+itemDisplayed-1
if (i < contentItem.children.length - 2/* why -2 instead of -1 ??? */) {
console.log('out='+i)
contentItem.children[i].state = 'out'
}
}
activePosition = activePosition > 1 ? activePosition - 1 : activePosition
}
if (event.key === Qt.Key_Down) {
if (activePosition == itemDisplayed && currentIndex < contentItem.children.length - 2) {
i = currentIndex-itemDisplayed+1
if (i >= 0) {
console.log('out='+i)
contentItem.children[i].state = 'out'
}
}
activePosition = activePosition < itemDisplayed ? activePosition + 1 : activePosition
}
}
delegate: Rectangle {
id:rect
state:index < ListView.view.itemDisplayed ? 'in' : 'out'
opacity:1.0
width:ListView.view.width
height:ListView.view.height/ListView.view.itemDisplayed
border.color:'white'
border.width:1
color:activeFocus ? 'red': 'gray'
onActiveFocusChanged: {
if (activeFocus) {
state = 'in'
console.log('in='+index)
}
}
states: [
State { name:'in'; PropertyChanges { target:rect; opacity:1.0 } },
State { name:'out'; PropertyChanges { target:rect; opacity:0.0 } }
]
transitions: [
Transition {
to:'in'
NumberAnimation { property:'opacity'; duration:250 }
},
Transition {
to:'out'
NumberAnimation { property:'opacity'; duration:250 }
}
]
Text {
text:index
anchors.centerIn:parent
}
}
}
}
First question : model=10, why model.count is not equal to contentItem.children.length? onCompleted gives 5 vs 11 and during navigation 10 vs 11
Second question: If I press UP or DOWN, it works fine until I reach index=4, why?
As I'm a beginner on QtQuick so maybe it's not the right approach. I tried to use the visible property but every item has visible = true even if they are outside. I tried also indexAt() with no success.
Any help would be great :-)
Now I know better about the ListView behavior. My previous code can be fixed by removing the Keys.onPressed event which is no longer useful, and by using the itemAt() method directly into the onActiveFocusChanged handler.
Related
I have a ListView, where the currently displayed modelData changes as a button cycles through several department options. If one of these departments has no data, my delegate continues showing the previous list data until it reaches a new section of modelData with data.
What I want to do, is when the model is 'empty' (being undefined, which may happen when the key it's looking for is yet to be created in my Firebase Database, or no items are currently visible), text/image is shown instead; i.e, "Move along now, nothing to see here".
My model is drawn from JSON, an example is below. and my calendarUserItems is the root node of multiple children within my Firebase Database, the aim of my AppButton.groupCycle was too add a further direction to each child node, filtering the data by this to view and edit within the page.
A sample of my code is:
Page {
id: adminPage
property var departments: [1,2,3,4]
property int currGroupIndex: 0
AppButton {
id: groupCycle
text: "Viewing: " + departments[currGroupIndex]
onClicked: {
if (currGroupIndex == departments.length - 1)
currGroupIndex = 0;
else
currGroupIndex++;
}
}
ListView {
model: Object.keys(dataModel.calendarUserItems[departments[currGroupIndex]])
delegate: modelData.visible ? currentGroupList : emptyHol
Component {
id: emptyHol
AppText {
text: "nothing to see here move along now!"
}
}
Component {
id: currentGroupList
SimpleRow {
id: container
readonly property var calendarUserItem: dataModel.calendarUserItems[departments[currGroupIndex]][modelData] || {}
visible: container.calendarUserItem.status === "pending" ? true : false
// only pending items visible
// remaining code for simple row
}
}
}
}
an example of JSON within my dataModel.calendarUserItems is:
"groupName": [
{ "department1":
{ "1555111624727" : {
"creationDate" : 1555111624727,
"date" : "2019-03-15T12:00:00.000",
"name" : "Edward Lawrence",
"status": "pending"
},
//several of these entries within department1
},
},
{ "department2":
{ "1555111624727" : {
"creationDate" : 1555111624456,
"date" : "2019-05-1T12:00:00.000",
"name" : "Katie P",
"status": 1
},
//several of these entries within department2
},
}
//departments 3 & 4 as the same
]
If departments 2 and 3 have modelData, yet 1 and 4 do not, I want the text to display instead, and the ListView emptied, instead of showing the previous modelData.
I have tried playing with the image/text visibility but the issue lays more with clearing the modelData and I'm unsure where to begin?
Any help is greatly appreciated!
I have achieved the display by using the following as my delegate:
delegate: {
if (!(departments[currGroupIndex] in dataModel.calendarUserItems) ) {
return emptyHol;
}
var subgroups = Object.keys(dataModel.calendarUserItems[departments[currGroupIndex]]);
for (var i in subgroups) {
var subgroup = dataModel.calendarUserItems[departments[currGroupIndex]][subgroups[i]];
modelArr.push(subgroup);
}
var modelObect = modelArr.find( function(obj) { return obj.status === "pending"; } );
if (modelObect === undefined) {
return emptyHol;
}
return currentGroupList;
}
Then when my AppButton.groupCycle is pressed, I have added modelArr = [] to clear the array on each press, this works as intended.
Thanks!
I am trying to implement a keyClick with a shift modifier but it doesn't work. Below is a basic setup of what I am trying to do. The first test_case1 can perform what I am doing but I'd like the second test_case2 to work as well but with using the Qt.ShiftModifier.
../MyTextBox.qml
Page {
id: page1
objectName: "page1"
TextField {
id: lastNameField
objectName: "lastNameField"
text: qsTr("")
}
}
tst_page.qml
import "../"
Item {
width: 800
height: 600
MyTextBox {
id: page1
}
TestCase {
id: "txtBox"
when: windowShown
function test_case1 () {
//var qmlObj = findChild(page1, "lastNameField")
var qmlObj = page1.lastNameField
// Bring to focus
mouseClick(qmlObj, Qt.LeftButton, Qt.NoModifier)
// Keypress
keyPress("Y")
keyPress("e")
keyPress("s")
tryCompare(qmlObj, "text", "Yes") // pass
}
function test_case2 () {
var qmlObj = page1.lastNameField
// Bring to focus
mouseClick(qmlObj, Qt.LeftButton, Qt.NoModifier)
// Keypress
keyClick(QT.Key_Y, Qt.ShiftModifier)
keyClick(QT.Key_E)
keyClick(QT.Key_S)
tryCompare(qmlObj, "text", "Yes") // fail
}
}
}
test output
PASS : test_case1()
FAIL! : test_case2()
Actual (): yes
Expected (): Yes
Edit: Added a simple project to github for testing.
I'm using this example: QML Flipable Example.
I created rectangles with GridLayout. I just added my new state the example:
states: [
State {
name: 'back'
PropertyChanges { target: rotation; angle: 180 }
when: flipable.flipped
},
State {
name: 'remove'
PropertyChanges {
target: card
visible: false
}
}
]
I wanted when I click the rectangles, check rectangles, they open and if they are same or not. My algorithm for this job:
property int card1: -1
property int card2: -1
property int remaining: 20
function cardClicked(index) {
var card = repeater.itemAt(index); // Selected card
if (!card.flipped) { // If selected card is not opened
card.flipped = true; // Open card
if (card1 === -1) { // If card is first selected card
card1 = index; // Set first selected card
} else { // If selected card is not first card, I mean that is second card because first card is already selected
card2 = index; // Set second card
area.enabled = false; // Disabled GridLayout (area)
delayTimer.start(); // Start `Timer` QML component
}
} else { // If card is opened so, close that card, because that card is opened and player clicked its
card.flipped = false; // Close that card
card1 = -1; // first card is not selected
}
}
function validateCards() {
var state = ''; // Default state: Close cards
if (imageIndexes[card1] === imageIndexes[card2]) { // If first card and second card are equal, you found same cards :)
state = 'remove'; // You found cards so, remove cards
--remaining; // If this equals 0, you found all cards
}
// If cards are not same, close cards but if they are same remove them
repeater.itemAt(card1).state = state;
repeater.itemAt(card2).state = state;
card1 = -1; // first card is not selected
card2 = -1; // second card is not selected
area.enabled = true; // Enabled area (GridLayout)
if (remaining === 0) { // If you found all cards, game over
console.log('Game Over!');
}
}
I added MouseArea into Rectangles:
MouseArea {
anchors.fill: parent
onClicked: cardClicked(index) // This index belongs to my `Repeater`.
}
I put a Timer for animation to work correctly and check cards:
Timer {
id: delayTimer
interval: 1000
onTriggered: validateCards()
}
This example and animations are running nice but sometimes they aren't running correctly:
How can I solve this animation bug?
UPDATE!
You can find all source code on here.
I think you are complicating the application by using the timer since you do not know if the item finished changing its position, it would be appropriate to execute that task when it finishes turning the letter over.
Another error that I see in your code is that you are assigning a state = "".
I have modified in the following parts:
GameArea.qml
GridLayout {
[...]
property variant imageIndexes: GameUtils.generateCardIndexes(
imageCount, repeatCount)
property int lastIndex : -1
Repeater {
id: repeater
model: 40
Card {
id: card
backImageSource: 'qrc:/images/img_' + area.imageIndexes[index] + '.jpg'
onFinished: verify(index)
}
}
function verify(index){
if(lastIndex == -1){
lastIndex = index
return
}
area.enabled = false
var lastItem = repeater.itemAt(lastIndex)
var currentItem = repeater.itemAt(index)
if(lastItem.backImageSource === currentItem.backImageSource){
lastItem.state = "remove"
currentItem.state = "remove"
}
else{
lastItem.flipped = false
currentItem.flipped = false
}
if(repeater.model === 0){
console.log("Winning")
}
lastIndex = -1
area.enabled = true
}
}
Card.qml
Item {
[...]
property alias state: flipable.state
signal finished()
Flipable {
[...]
transitions: Transition {
NumberAnimation { target: rotation; property: 'angle'; duration: 400 }
onRunningChanged: {
if ((state == "back") && (!running))
finished()
}
}
MouseArea {
anchors.fill: parent
onClicked: card.flipped = true
}
}
}
The complete code can be found at the following link
Is there a way to hide the language selection key from the virtual keyboard without use a custom layout?
I was able to hide the language key with a workaround:
property var keyboardLayout: inputPanel.keyboard.layout
function findChildByProperty(parent, propertyName, propertyValue, compareCb) {
var obj = null
if (parent === null)
return null
var children = parent.children
for (var i = 0; i < children.length; i++) {
obj = children[i]
if (obj.hasOwnProperty(propertyName)) {
if (compareCb !== null) {
if (compareCb(obj[propertyName], propertyValue))
break
} else if (obj[propertyName] === propertyValue) {
break
}
}
obj = findChildByProperty(obj, propertyName, propertyValue, compareCb)
if (obj)
break
}
return obj
}
onKeyboardLayoutChanged: {
if(keyboardLayout!=""){
var ChangeLanguageKey= findChildByProperty(inputPanel.keyboard, "objectName", "changeLanguageKey", null)
if(ChangeLanguageKey){
ChangeLanguageKey.visible=false
}
}
}
InputPanel {
id: inputPanel
z: 99
y: parent.height
anchors.left: parent.left
anchors.right: parent.right
states: State {
name: "visible"
when: inputPanel.active
PropertyChanges {
target: inputPanel
y: parent.height - inputPanel.height
}
}
transitions: Transition {
from: ""
to: "visible"
reversible: true
ParallelAnimation {
NumberAnimation {
properties: "y"
duration: 400
easing.type: Easing.InOutBack
}
}
}
CustomComponents.AutoScroller {
id:autoscroller
panelY: inputPanel.y
}
}
This only works in version 5.9 where the objectname property is defined with "changeLanguageKey", for previous versions set the property in the source code and recompile.
No, not without using a custom layout.
You can always modify the layouts that come with the keyboard though.
I was able to hide the hideKeyboard key with this trick. I basically tried to get the reference of the emoji key and thereby was able to disable the next key which is hideKeyboard key.
function disableKey(parent, objectText)
{
var obj = null
if (parent === null)
return null
var children = parent.children
for (var i = 0; i < children.length; i++) {
obj = children[i]
if (obj.text === objectText && obj.toString().substring(0, 7) === "BaseKey") {
console.log("Disabling symbols. " + obj.text)
obj.enabled = false
}
else if(obj.displayText === "HWR"){
console.log("Disabling Handwriting mode button." + obj.displayText + " " + objectText)
obj.visible = false
}
else if(obj.text === ":-)" && obj.toString().substring(0, 7) === "BaseKey"){
console.log("Disabling hidekeyboard key." + obj.text)
children[i+1].visible = false
}
obj = disableKey(obj, objectText)
if (obj)
break
}
return obj
}
I have a treeview from which I drag items to a drop area by creating draggable items dynamically. Everything works except I cannot pass mimedata
function createItem(parentItem)
{
if (itemComponent.status === Component.Ready && draggedItem == null)
{
draggedItem = itemComponent.createObject(
parentItem,
{
"x": positionInParent.x,
"y": positionInParent.y,
"baseColor":Qt.lighter("red", 1.5),
"copyable":false,
visible: true
}
);
draggedItem.Drag.mimeData = { "text/plain": "Hello!" };
draggedItem.Drag.supportedActions = Qt.CopyAction;
draggedItem.Drag.dragType = Drag.Automatic;
draggedItem.Drag.start();
}
else if (itemComponent.status === Component.Error) {
draggedItem = null;
console.log("error creating component");
console.log(itemComponent.errorString());
}
}
when I try to get the mime data from drop event (drop.text field) it is empty.
DropArea
{
height:parent.height
width:parent.width
onDropped: {
drop.acceptProposedAction()
var txt = drop.text; // Empty!!!
console.log("dropped into dest")
}
onEntered: {
console.log("entered dest")
}
}
Resolved it myself! Just to add
draggedItem.Drag.active = true;