6

I am creating a QML ListView that supports multiple selection, and will be instantiating multiple of these in my application. I need to give keyboard focus to a specific list, handle key presses for that list, and draw highlighting for selected row(s) depending on the focus.

However, giving focus=true to either a ListView delegate or the ListView itself does not cause activeFocus to be present on the ListView, and does not cause key press signals to be triggered.

I have created a simple sample app showing my problem:

enter image description here

import QtQuick 2.7
import QtQuick.Window 2.0

Window {
  id:window; visible:true; width:400; height:160; color:'darkgray'

  Component {
    id: row
    Rectangle {
      id: root
      property int  i: index
      property bool current: ListView.isCurrentItem
      property bool focused: ListView.view.activeFocus
      width:parent.width; height:20
      color: current ? ( focused ? 'pink' : 'lightblue' ) : 'lightgray'
      MouseArea {
        anchors.fill: parent
        onClicked: {
          root.ListView.view.currentIndex = i;
          root.ListView.view.focus = true;
        }
      }
      Text { anchors.fill:parent; text:modelData }
    }
  }

  Component {
    id: myList
    ListView {
      delegate: row
      width:window.width/2-10; height:window.height; y:5
      Keys.onUpPressed:   decrementCurrentIndex()
      Keys.onDownPressed: incrementCurrentIndex()
    }
  }

  Loader {
    sourceComponent:myList; x:5
    onLoaded: item.model = ['a','b','c','d']
  }
  Loader {
    sourceComponent:myList; x:window.width/2
    onLoaded: item.model = ['1','2','3','4','5']
  }
}

Clicking on either list does not turn the selected row pink, and pressing key up/down does not adjust the selection.

How can I pass focus to a particular ListView such that (a) only one ListView holds it at a time, and (b) I can detect when a ListView holds this focus, and (c) it allows keyboard signals to work for that ListView only while focused?

I am using Qt 5.7, in case it matters.


Things I've tried, unsuccessfully:

  • Setting focus=true on the Rectangle instead of the ListView.
  • Setting the focused property to watch for ListView.view.focus instead of activeFocus: this allows both lists to turn pink simultaneously.
  • Reading through Keyboard Focus in Qt Quick and wrapping either the Rectangle or the ListView in a FocusScope. (What a pain that is, forwarding all interfaces along.)

    • Re-reading the page in the light of day and wrapping the two Loader in a single FocusScope. Cursing as both ListView are apparently allowed to have simultaneous focus, violating my reading of this section of the documentation:

      Within each focus scope one object may have Item::focus set to true. If more than one Item has the focus property set, the last type to set the focus will have the focus and the others are unset, similar to when there are no focus scopes.

  • Wrapping the ListView in an Item and putting the Keys handlers on that item, and attempting to focus that item.

  • Setting interactive:false on the ListView.
  • Setting keyNavigationEnabled:false on the ListView. (This oddly yields "ListView.keyNavigationEnabled" is not available in QtQuick 2.7, despite the Help stating that it was introduced in 5.7, and suggesting that QtQuick 2.7 is the proper import statement.)
  • Putting focus:true on just about every object I can find, just in case.

Note: I realize that a standard ListView uses a highlight to show selection, and keyboard navigation to adjust the currentItem. However, because my needs require multiple concurrently-selected items, I must manage the highlight and keyboard specially.


Edit: here's a simpler test case that's not behaving as I would expect:

import QtQuick 2.7
import QtQuick.Window 2.0

Window {
  id:window; visible:true; width:400; height:160; color:'darkgray'

  FocusScope {
    Rectangle {
      width: window.width/2-6; height:window.height-8; x:4; y:4
      color: focus ? 'red' : 'gray'
      MouseArea { anchors.fill:parent; onClicked:parent.focus=true }
      Text { text:'focused'; visible:parent.activeFocus }
      Keys.onSpacePressed: console.log('space left')
    }
    Rectangle {
      width: window.width/2-6; height:window.height-8; x:window.width/2+2; y:4
      color: focus ? 'red' : 'gray'
      MouseArea { anchors.fill:parent; onClicked:parent.focus=true }
      Text { text:'focused'; visible:parent.activeFocus }
      Keys.onSpacePressed: console.log('space right')
    }
  }
}

Clicking on each Rectangle makes it turn red, and only one is allowed to be red at any given time. (That's good.) However, the text "focused" does not show up, because the Rectangle does not have activeFocus. Further, pressing space never logs a message when either is focused.

Edit 2: Whoa. If I delete the FocusScope it works as expected: the focus is still exclusive, activeFocus is granted, and the space works correctly. Perhaps my problems above are because ListView is a FocusScope (according to the docs).

Edit 3: Per Mitch's comment below, setting focus:true on the FocusScope also allows everything to work correctly with the FocusScope. However, per my answer below (and Mitch's comment), the FocusScope is not necessary to get either of my simplified sample apps working correctly.

Phrogz
  • 296,393
  • 112
  • 651
  • 745
  • Using this [example](http://doc.qt.io/qt-5/qml-qtquick-listview.html#example-usage), you get the keyboard focus by default. I don't know if you could set a `Rectangle` as parent of `ListView`. – Tarod Sep 05 '16 at 12:40
  • Editorial rant: why is `focus:true` allowed for more than one concurrent element? What bizarre concept of focus is this that has Qt allowing multiple items to be "focused" at the same time? Why are `FocusScope`s a solution to this? – Phrogz Sep 05 '16 at 14:48
  • `focus` is a request more than a command; `activeFocus` indicates whether the item is actually focused. The focus stuff can be a bit of a nightmare though, I agree. I don't know where you're seeing two different items with `focus: true`, but it can be necessary. – Mitch Sep 05 '16 at 15:06
  • @Mitch Please see my edit at bottom. Should those Rectangle be getting `activeFocus`? Should Keys be getting triggered? Am I misunderstanding focus? – Phrogz Sep 05 '16 at 15:23
  • 1
    You're missing `focus: true` from the `FocusScope` and the colour assignment should be: `color: activeFocus ? 'red' : 'gray'`. I'm not really sure why a `FocusScope` is necessary here, though. In [this](http://doc.qt.io/qt-5/qtquick-input-focus.html#acquiring-focus-and-focus-scopes) example, it talks about two components that both set focus, but you're not doing that, so I would think it should work (your original example). – Mitch Sep 05 '16 at 15:26
  • @Mitch Is there a difference between two components setting `focus:true` and two components having `focus=true` set on them? I'm doing the latter, and so expected that a FocusScope was needed. As you say (per my "Edit 2") everything works correctly without the FocusScope. – Phrogz Sep 05 '16 at 15:29
  • @Mitch Thank you (again) for your help. Per my answer below, it seems that my test case was flawed in that I used a `Loader` (to make my sample code smaller), and it has some problem (bug?) with `activeFocus`. – Phrogz Sep 05 '16 at 15:54

2 Answers2

5

If you print out the active focus item in your original example

onActiveFocusItemChanged: print(activeFocusItem)

you can see that none of the loaders ever get focus; it's always the root item of the window.

If you set focus: true on one of the loaders, then it will have focus:

import QtQuick 2.7
import QtQuick.Window 2.2

Window {
    id: window
    visible: true
    width: 400
    height: 160
    color: 'darkgray'

    onActiveFocusItemChanged: print(activeFocusItem)

    Component {
        id: row
        Rectangle {
            id: root
            property int i: index
            property bool current: ListView.isCurrentItem
            property bool focused: ListView.view.activeFocus
            width: parent.width
            height: 20
            color: current ? (focused ? 'pink' : 'lightblue') : 'lightgray'

            MouseArea {
                anchors.fill: parent
                onClicked: {
                    root.ListView.view.currentIndex = i
                    root.ListView.view.focus = true
                }
            }
            Text {
                anchors.fill: parent
                text: modelData
            }
        }
    }

    Component {
        id: myList
        ListView {
            delegate: row
            width: window.width / 2 - 10
            height: window.height
            y: 5
            Keys.onUpPressed: decrementCurrentIndex()
            Keys.onDownPressed: incrementCurrentIndex()
        }
    }

    Loader {
        objectName: "loader1"
        sourceComponent: myList
        focus: true
        x: 5
        onLoaded: item.model = ['a', 'b', 'c', 'd']
    }
    Loader {
        objectName: "loader2"
        sourceComponent: myList
        x: window.width / 2
        onLoaded: item.model = ['1', '2', '3', '4', '5']
    }
}

However, now the second view doesn't have focus when you click on its delegates. If we declared them as both having focus, we lose control over which one ends up with it. In fact, even if we do declare them as both having focus, clicking on the second view's delegates doesn't give that view active focus, because the loader that it was in lost focus when the first loader got it. In other words, for it to work, you'd have to give the loader focus as well, which defeats the whole purpose of having nice little bundled up delegate components:

import QtQuick 2.7
import QtQuick.Window 2.2

Window {
    id: window
    visible: true
    width: 400
    height: 160
    color: 'darkgray'

    onActiveFocusItemChanged: print(activeFocusItem)

    Component {
        id: row
        Rectangle {
            id: root
            property int i: index
            property bool current: ListView.isCurrentItem
            property bool focused: ListView.view.activeFocus
            width: parent.width
            height: 20
            color: current ? (focused ? 'pink' : 'lightblue') : 'lightgray'

            MouseArea {
                anchors.fill: parent
                onClicked: {
                    root.ListView.view.currentIndex = i
                    root.ListView.view.parent.focus = true
                    root.ListView.view.focus = true
                }
            }
            Text {
                anchors.fill: parent
                text: modelData
            }
        }
    }

    Component {
        id: myList
        ListView {
            delegate: row
            width: window.width / 2 - 10
            height: window.height
            y: 5
            Keys.onUpPressed: decrementCurrentIndex()
            Keys.onDownPressed: incrementCurrentIndex()
        }
    }

    Loader {
        objectName: "loader1"
        sourceComponent: myList
        x: 5
        onLoaded: item.model = ['a', 'b', 'c', 'd']
    }
    Loader {
        objectName: "loader2"
        sourceComponent: myList
        x: window.width / 2
        onLoaded: item.model = ['1', '2', '3', '4', '5']
    }
}

At this point I would just give up and force active focus:

import QtQuick 2.7
import QtQuick.Window 2.2

Window {
    id: window
    visible: true
    width: 400
    height: 160
    color: 'darkgray'

    onActiveFocusItemChanged: print(activeFocusItem)

    Component {
        id: row
        Rectangle {
            id: root
            objectName: ListView.view.objectName + "Rectangle" + index
            property int i: index
            property bool current: ListView.isCurrentItem
            property bool focused: ListView.view.activeFocus
            width: parent.width
            height: 20
            color: current ? (focused ? 'pink' : 'lightblue') : 'lightgray'
            MouseArea {
                anchors.fill: parent
                onClicked: {
                    root.ListView.view.currentIndex = i
                    root.ListView.view.forceActiveFocus()
                }
            }
            Text {
                anchors.fill: parent
                text: modelData
            }
        }
    }

    Component {
        id: myList
        ListView {
            objectName: parent.objectName + "ListView"
            delegate: row
            width: window.width / 2 - 10
            height: window.height
            y: 5
            Keys.onUpPressed: decrementCurrentIndex()
            Keys.onDownPressed: incrementCurrentIndex()
        }
    }

    Loader {
        id: loader1
        objectName: "loader1"
        sourceComponent: myList
        x: 5
        onLoaded: item.model = ['a', 'b', 'c', 'd']
    }
    Loader {
        objectName: "loader2"
        sourceComponent: myList
        x: window.width / 2
        onLoaded: item.model = ['1', '2', '3', '4', '5']
    }
}

I really dislike the focus system. I'm not saying I could come up with something better, but it's just not easy to use.

Mitch
  • 23,716
  • 9
  • 83
  • 122
  • Interesting. I think what I'm learning from you here is that for a child item to get focus, its ancestors need to have focus as well. If true, this seems upside-down to me. I assumed that giving focus to a child item would **cause** its ancestor chain to all receive focus. – Phrogz Sep 05 '16 at 19:14
  • 3
    Thank you so much, BTW, for [`forceActiveFocus()`](http://doc.qt.io/qt-5/qml-qtquick-item.html#forceActiveFocus-method). That is working like a charm. – Phrogz Sep 05 '16 at 20:15
0

The problem in my original sample app appears to be related to my use of a Loader. Ignoring the Window and delegate component code, replacing my Loader code with explicit lists works:

ListView {
  width:window.width/2-6; height:window.height-8; x:4; y:4
  delegate: row
  model:    ['a','b','c','d']
  Keys.onUpPressed:   decrementCurrentIndex()
  Keys.onDownPressed: incrementCurrentIndex()
}
ListView {
  width:window.width/2-6; height:window.height-8; x:window.width/2+2; y:4
  delegate: row
  model:    ['1','2','3','4','5']
  Keys.onUpPressed:   decrementCurrentIndex()
  Keys.onDownPressed: incrementCurrentIndex()
}

...and it also works if I use a file-based component:

MyList.qml

import QtQuick 2.7
ListView {
    delegate: row
    width:window.width/2-6; height:window.height-8; y:4
    Keys.onUpPressed:   decrementCurrentIndex()
    Keys.onDownPressed: incrementCurrentIndex()
}

main.qml

// ...Window and row component as in the question above..
MyList { x:4;                model:['a','b','c','d']      }
MyList { x:window.width/2+2; model: ['1','2','3','4','5'] }

...but this original code does not work, because the ListView is never given activeFocus:

Component {
  id: myList
  ListView {
    delegate: row
    width:window.width/2-6; height:window.height-8; y:4
    Keys.onUpPressed:   decrementCurrentIndex()
    Keys.onDownPressed: incrementCurrentIndex()
  }
}
Loader {
  sourceComponent:myList; x:4
  onLoaded: item.model = ['a','b','c','d']
}
Loader {
  sourceComponent:myList; x:window.width/2+2
  onLoaded: item.model = ['1','2','3','4','5']
}
Phrogz
  • 296,393
  • 112
  • 651
  • 745
  • Unfortunately, my real-world app is not using a Loader, so I'll have to hunt further for differences between it and this pared-down sample code to figure out where focus is going. – Phrogz Sep 05 '16 at 15:53