Friday, February 10, 2012

Help! Virtual keyboard is hiding the input on Symbian

Modern GUIs often place labels, input fields and other controls on a scrollable pane - think of a Settings page for example. Implementing this in QML is easy: just lay out the controls in a Column, and place the Column in a Flickable.

For example:

Page {
  Flickable {
    anchors.fill: parent
    Column {
      Label {text: "Input 1:"}
      TextField {id: input1}
      Label {text: "Input 2:"}
      TextField {id: input2}
      // More controls
    }
  }
}


This works fine on Meego, but on Symbian it has a flaw: the virtual keyboard (VKB) can hide the input fields towards the bottom of the page. This is a serious shortcoming without a trivial solution, so I had to come up with a non-trivial one.

Basic idea: Whenever the virtual keyboard is displayed, the input field with the focus should be moved up within the Flickable, if it was originally covered by the VKB.

Here is the implementation:

Page {
  Flickable {
    id: flickable
    anchors.fill: parent
    Column {
      Label {text: "Input 1:"}
      TextField {id: input1}
      // More controls
    }

    // (1)
    Timer {
      id: adjuster
      interval: 200
      onTriggered: flickable.adjust()
    }

    // (2)
    Component.onCompleted: {
      inputContext.visibleChanged.connect(adjuster.restart)
    }

    // (3)
    function adjust() {
      if (!inputContext.visible) {
        return
      }

      var focusChild = null
      function findFocusChild(p) {
        if (p["activeFocus"] === true) {
          focusChild = p
        } else {
          for (var i = 0; i < p["children"].length; i++) {
            findFocusChild(p["children"][i])
            if (focusChild !== null) {
              break
            }
          }
        }
      }
      findFocusChild(flickable)

      if (focusChild === null) {
        return
      }
      var focusChildY = focusChild["y"]
      var focusChildHeight = focusChild["height"]
      if ((flickable.contentY + flickable.height) < (focusChildY + focusChildHeight)) {
        flickable.contentY = focusChildY + focusChildHeight - flickable.height
      }
    }
  }
}

Some explanations:

(1) We need a timer to adjust the focus item's position. We can't adjust it immediately after the virtual keyboard appears, because the the Flickable component will get resized with a nice animation, which takes time.

(2) So when the VKB gets visible, we just start a timer, that will adjust the focus item later

(3) Shifting the focus item (and all the others of course in the Flickable) happens in the adjust() function. The function tries to find a child item with active focus. If there is such an item, and it is currently hidden, the whole Flickable content will be elevated by changing it's contentY property.

Oh one more important remark: The PageStack component that owns the Page should have its platformSoftwareInputPanel property set to true. Otherwise the Page (and with it the Flickable) won't get resized when the VKB is shown.

Finally: QML is changing fast, so I'm sure one day this bug will be fixed by Nokia. The problem was found on, and the solution was written for the following environment:

  • Qt 4.7.4 on Symbian Anna or Belle
  • QtQuick 1.1
  • com.nokia.symbian 1.1

No comments:

Post a Comment