Click \ flick through emptry area of a Flickable that partially overlaps another Flickable

1.1k views Asked by At

I have a scene "editor" flickable on the bottom, and docked at its right side, a scene "outliner" flickable on top of it, which shows a tree of the scene structure.

  Flickable {
    id: editor
    anchors.fill: parent
    contentWidth: 5000
    contentHeight: 5000
    Repeater {
      model: 500
      delegate: Rectangle {
        width: Math.random() * 200 + 50
        height: width
        x: Math.random() * editor.contentWidth
        y: Math.random() * editor.contentHeight
        color: Qt.rgba(Math.random(), Math.random(), Math.random(), 1)
        border.color: "black"
      }
    }
  }

  Flickable {
    id: outliner
    anchors.right: editor.right
    width: contentWidth
    height: parent.height
    contentWidth: contentItem.childrenRect.width
    contentHeight: contentItem.childrenRect.height
    Column {
      Repeater {
        model: 500
        delegate: Rectangle {
          width: Math.random() * 200 + 50
          x: outliner.contentWidth - width
          height: 50
          color: Qt.rgba(Math.random(), Math.random(), Math.random(), 1)
          border.color: "black"
        }
      }
    }
  }

Naturally, I want to be able to flick both to navigate both, since they both do get larger than the available screen real estate.

The problem is that the outliner flickable will simply block the editor flickable, even if I flick from a position that the outliner items do not occupy. I don't want that, what I want is to flick the outliner only if the flick happens on top of an outliner item, if not I want to skip through to the editor, so I can click and flick it from the area to the right.

I haven't been able to do it, due to the way Flickable works. It will come in first to check for a drag, and only if the click doesn't exceed the drag threshold will it let the click down to underlying elements. So I can't think of a way to flick only if there is an object in that position.

Is there any way I can get the event handling to do what I want it to?

2

There are 2 answers

0
dtech On BEST ANSWER

Mkay, I think I did it, the key was to disable the interactivity for the outliner, then intercept input via a MouseArea in the delegate, then carefully time and measure the manual dragging delta and if fast enough, schedule a manual flick on release.

Getting it to both stop an ongoing flick on press and subsequently launching another flick from the next drag turned out to be problematic after a flick has just been cancelled, but by delaying each flick to the next event loop cycle I got it working.

I also added wheel support and such.

If anyone experiences problems with it on any platform, do let me know in the comments.

// the editor code is the same from the OP
  Flickable {
    id: outliner
    anchors.right: editor.right
    width: contentWidth
    height: parent.height
    contentWidth: contentItem.childrenRect.width
    contentHeight: contentItem.childrenRect.height
    interactive: false
    property int ly: 0
    property real lt: 0
    property int dy: 0
    function go(yy) {
      dy = contentY - (contentY -= yy - ly)
      ly = yy
      lt = Date.now()
    }
    Timer { 
      id: trigger
      interval: 1
      onTriggered: outliner.flick(0, outliner.dy * 150)
    }
    Column {
      Repeater {
        model: 500
        delegate: Rectangle {
          width: Math.random() * 200 + 50
          x: outliner.contentWidth - width
          height: 50
          color: Qt.rgba(Math.random(), Math.random(), Math.random(), 1)
          border.color: "black"
          MouseArea {
            anchors.fill: parent
            Drag.active: drag.active
            drag.target: Item { id: dummy }
            onPressed: {
              dummy.y = 0
              if (outliner.flicking) outliner.cancelFlick()
            }
            onWheel: outliner.flick(0, wheel.angleDelta.y * 10)
            onPositionChanged: {
              if (drag.active) outliner.go(dummy.y)
            }
            onReleased: {
              if (Date.now() - outliner.lt < 50) trigger.start()
              outliner.ly = 0
              outliner.returnToBounds()
            }
          }
        }
      }
    }
  }
4
GrecKo On

You could do that by checking on each press in the scene if there is a outliner's delegate below the press. If not, mark the outliner as not interactive. Re enable it just after when there is movement in the editor.

Like so :

import QtQuick 2.7
import QtQuick.Window 2.2
import QtQuick.Controls 2.0

ApplicationWindow {
    id: app
    visible: true
    width: 600
    height: 480
    Item { // a common parent not containing the MouseArea to be able to call childAt on press
        id: flickablesParent
        anchors.fill: parent
        Flickable {
            id: editor
            anchors.fill: parent
            contentWidth: 5000
            contentHeight: 5000
            onMovementStarted: outliner.interactive = true //re-enable the outliner user interaction so it can be flicked again.
            Repeater {
                model: 500
                delegate: Rectangle {
                    width: Math.random() * 200 + 50
                    height: width
                    x: Math.random() * editor.contentWidth
                    y: Math.random() * editor.contentHeight
                    color: Qt.rgba(Math.random(), Math.random(), Math.random(), 1)
                    border.color: "black"
                }
            }
        }

        Flickable {
            id: outliner
            anchors.right: editor.right
            width: contentWidth
            height: parent.height
            contentWidth: contentItem.childrenRect.width
            contentHeight: contentItem.childrenRect.height

            Column {
                id: column // set an id so it can be referenced in the MouseArea
                Repeater {
                    model: 500
                    delegate: Rectangle {
                        width: Math.random() * 200 + 50
                        x: outliner.contentWidth - width
                        height: 50
                        color: Qt.rgba(Math.random(), Math.random(), Math.random(), 1)
                        border.color: "black"
                    }
                }
            }
        }
    }
    MouseArea {
        id: dispatcher
        anchors.fill: parent
        onPressed: {
            var flickable = flickablesParent.childAt(mouse.x, mouse.y); //find out on what flickable we pressed (the check could be ommited but I guess it's more performant with)
            if (flickable === outliner) {
                var mappedPos = mapToItem(column, mouse.x, mouse.y);
                var delegate = column.childAt(mappedPos.x, mappedPos.y);
                if (!delegate)
                    outliner.interactive = false; //if there's no delegate where we pressed in the column, we disable the interactions of the outliner flickable.
            }
            mouse.accepted = false; // let the underlying Flickable deal with this event
        }
    }
}

EDIT: You don't really need to reenable the outliner in onMovementStarted, you could do it in the MouseArea onPressed too.

like

onPressed: {
    var flickable = flickablesParent.childAt(mouse.x, mouse.y); //find out on what flickable we pressed (the check could be ommited but I guess it's more performant with)
    if (flickable === outliner) {
        var mappedPos = mapToItem(column, mouse.x, mouse.y);
        var delegate = column.childAt(mappedPos.x, mappedPos.y);
        outliner.interactive = delegate ? true : false;
    }
    mouse.accepted = false; // let the underlying Flickable deal with this event
}