Question

I create a tree of objects, the tree is drawn using very simple logic:

  • each object is as big as its own representation plus that of its children (and their children and so on...)
  • if the object is the first child of its parent it is drawn indented under its visual representation
  • every next object is drawn directly under its previous sibling

However, this simple algorithm fails when inserting new objects in some cases. For example:

enter image description here

In this case, node 1 and 2 are successfully inserted into node 0, with the full height of node 0 (the black line left of every node) being the sum of its own UI plus both its children.

But when node 3 is inserted into node 1, the layout breaks - node 2 is not pushed down, as a result it overlaps with the newly inserted node, making its black line invisible.

When an additional refresh cycle is triggered, the tree returns to normal, so the glitch must be cause by some temporary "or of sync" properties. Note that one extra refresh does not always put the tree in order, depending on where the insert is made the bug can sometimes take a few refresh cycles to "cascade out" off from disturbing the alignment.

Any ideas what might cause this erratic behavior and/or how to fix it? What causes the code to occasionally break, and why even though it ultimately works it require one to multiple refreshes to get itself in order?

EDIT: I've been able to isolate and pinpoint when does the alignment break - when inserting to a node that is not ending up the last node on its level. All bindings propagate properly as long as each new node is inserted in a node that is last and "open" e.g. there is no next node on that level, but inserting in any previous node breaks the bindings. So additional refresh cycles are needed to phase out the bug through all the levels it affects. But I still don't know what causes it and how to fix it, only when it occurs.

EDIT: Investigating a little more, it appears that after the new child is inserted into the parent item and its children are re-aligned based on their order, the childrenRect property of the parent does not reflect the changes in size right away. So when the next sibling of the object inserted into is being positioned, it still uses the old outdated value for the object height, which does not affect the newly inserted object since it doesn't use it. So I guess the next big question is how to fix it so that so bindings take place as they should so that the algorithm can work as expected with the right data? I tried using a timer to delay the operation but it doesn't seem that it is an issue of time but about a logical order, e.g. the count of refreshes it currently requires to eliminate the bug equal to the dept it occurred at.

EDIT: I figured how to "solve it" but... not really, I still don't know what causes it and how to avoid it, but a little more "economical" solution than updating the entire tree until it is fixed just go down the parents and update only the current branch until root is reached. This does save a lot but I would still prefer to have the right values right away, as bindings are supposed to work, at least IMO.

EDIT: Here is the ordering code:

for (int i = 0; i < _children.size(); ++i) {
        UI * ui = _children.at(i)->_ui;
        if (ui) {
            if (i) {
                UI * prev = _children.at(i-1)->_ui;
                ui->setX(prev->x());
                ui->setY(prev->height() + prev->y());
            } else {
                ui->setX(Object::_helper->nodeSize());
                ui->setY(_ui->childOffset());
            }
        }
    }

And an example how the QML is set up:

UI {
    id: main
    width: childrenRect.width
    height: childrenRect.height    

    Item { 
        height: childrenRect.height

        Rectangle {
            width: Helper.nodeSize
            height: Helper.nodeSize
            color: "red"

            Text {
                anchors.centerIn: parent
                text: main.id
            }

            MouseArea {
                anchors.fill: parent   
                acceptedButtons: Qt.LeftButton | Qt.RightButton                 
                onClicked: {
                    if (mouse.button == Qt.RightButton)
                        Helper.gotoXY(main.absolutePosition())
                    else {
                        Helper.createObject(1, main)
                        Helper.updateRoot()
                    }
                }

                Rectangle {
                    height: main.height
                    width: 10
                    x: -10
                    color: "black"
                }
            }
        }
    }
}
Was it helpful?

Solution

When you think about it, that behavior makes perfect sense, there is nothing wrong with the bindings, just with your expectations of what they are capable of.

The reason your "cascading update" works is very simple and points out to why your code does not work and how to fix it.

Basically, when you insert a new node, it pushes everything down "a node", which is why your code does not break as long as you insert a "last" node. But if it is not at the end, it will push everything down a bit, but this change will not reflect until its parent updates the positions. If you call update from the root and the insert is anywhere deeper, your first pass will trigger only the first update of the binding for height, so the next update cycle its next sibling can be properly positioned. If the insert was deeper, there would be more bindings which need to take place.

The reason the new node is inserted correctly is it doesn't rely on the property that its own insertion will not change until the next update which will use the still non-updated value and then evaluate the binding to make it kick in the next cycle.

I think the fastest way to do this will be to start at the level of the insert and propagate the update "outwards" and "downwards" e.g. update every next object, then the next siblings of the parent to the next parent's siblings until you hit your root object. This will only reposition objects that are "under" and affected by the insert, which should shave off a lot of cycles compared to your current best solution.

OTHER TIPS

I played around with QML and JS a little bit and it tuned out that I have no graphical UI problems.

My demo project is able to add child objects as well as siblings to a tree (by left and right clicks on the red boxes). The logic may not be complete but the basics run very smoothly.

main.qml

import QtQuick 2.1
import QtQuick.Controls 1.0

ApplicationWindow {
    id: app
    width: 800
    height: 600

    property int lastNodeId: 0
    function getId() { return lastNodeId++; }

    Level { }
}

Level.qml

import QtQuick 2.0

Item {
    id: frame
    width: childrenRect.width
    height: childrenRect.height

    Rectangle {
        width: 10
        color: "#000000"
        height: parent.height
        x: 0
        y: 0
    }

    Column {
        x: 10
        id: content

        Position { }
    }
}

The red box and a surrounding item to place siblings Position.qml

import QtQuick 2.0
import "component_creation.js" as CC

Item {
    id: position0
    width: childrenRect.width
    height: childrenRect.height

    Rectangle {
        color: "red"
        width: 80
        height: 30

        Text {
            anchors.fill: parent
            horizontalAlignment: Text.AlignHCenter
            verticalAlignment: Text.AlignVCenter

            Component.onCompleted: text = app.getId() // Set once, avoid bindung

            MouseArea {
                anchors.fill: parent
                acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
                onClicked: {
                    if (mouse.button == Qt.LeftButton)
                    {
                        CC.createChild(position0);
                    }
                    else if (mouse.button == Qt.RightButton)
                    {
                        CC.createSiblingAfter(position0)
                    }
                    else if (mouse.button == Qt.MiddleButton)
                    {
                        // no clue how to implement
                        // CC.createSiblingBefore(position0)
                    }
                }
            }
        }
    }
}

And some JavaScript: component_creation.js

function createChild(parent_item) {
    var component = Qt.createComponent("Level.qml");
    if (component.status == Component.Ready)
    {
        var sprite = component.createObject(parent_item, {"x": 70, "y": 30});
        if (sprite == null) console.log("Error creating Level object");
    }
    else
    {
        console.log("Error loading component:", component.errorString());
    }
}

function createSiblingAfter(sibling_position)
{
    var component = Qt.createComponent("Position.qml");
    if (component.status == Component.Ready)
    {
        var sprite = component.createObject(sibling_position.parent);
        if (sprite == null) console.log("Error creating Position object");
    }
    else
    {
        console.log("Error loading component:", component.errorString());
    }
}

Hope that helps.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top