Finding child objects by type and property values

Last edited on

The simple case

Usually it is possible to search for objects by some of their properties. Here is an example that uses real names instead of symbolic names to be a bit simpler:

function main() {
    ...

    o = waitForObject({"type": "ButtonClass", "text": "Click me!"});
}

To find the second object with the text “Click me!” one could use this real name:

{"type": "ButtonClass", "text": "Click me!", "occurrence": 1}

The harder case - Objects in containers

When the above example does not find objects that are located in containers, for example cell objects inside a table, then you must probably specify the table object as the “container” of the cell, for example:

function main() {
    ...

    o = waitForObject({"container": {"type": "TableClass"}, "type": "TableCellClass", "text": "Hello"});
}
test.js

To find the second cell with the text “Hello” one could use this real name:

{"container": {"type": "TableClass"}, "type": "TableCellClass", "text": "Hello", "occurrence": 1}

Unfortunately, this approach probably won’t work for tree containers.

Alternative approach - Using object.children()

For tables, trees and other object containers it can be useful to search for child objects based on their property values, for example their row and column values. This can be done via the object.children() function as shown in the following example.

def main():
    startApplication("itemviews")

    o = waitForObject({"occurrence": "2", "type": "QTreeView", "unnamed": "1", "visible": 1})
    children = find_children_by_text(o, "Desmidiaceae")
    mouseClick(children[0])

def find_children_by_text(obj, text, max_count=1):
    children = object.children(obj)
    found_children=[]

    # Add immediate children:
    for c in children:
        if hasattr(c, "text") and c.text == text:
            found_children.append(c)
            if max_count is not None and len(found_children) >= max_count:
                return found_children

    # Add grand children:
    for c in children:
        found_children.extend(find_children_by_text(c, text))
        if max_count is not None and len(found_children) >= max_count:
            found_children = found_children[:max_count]
            break
    return found_children
test.py

The functions below are more powerful in that they allow you to specify a set of properties and their values which should match the found objects.

Python

Example requirements:

from ext_get_objects_by_properties import *

def main():
    startApplication("itemviews")

    o = waitForObject({"occurrence": "2", "type": "QTreeView", "unnamed": 1, "visible": 1})
    o = ext_get_objects_by_properties.get_objects(o, {"text": "Desmidiaceae", "type": "QModelIndex"})
test.py

JavaScript

function main()
{
    // ...

    o = waitForObject({"type": "Table"});

    // Find first child where properties...
    //   "text" == "Kimberly"
    var o1 = objTools.findChildPath(o, {"text": "Kimberly"});

    // Find first child of above object's parent
    // where properties...
    //   text   == "Kimberly"
    //   row    == "1"
    //   column == "1"
    var o2 = objTools.findChildPath(o1[1], {"text": "Kimberly", "row": "1", "column": "1"});

    // Find first child where properties...
    //   row    == "2"
    //   column == "0"
    var o1 = objTools.findChildPath(o, {"row": "2", "column": "0"});

    // Find second child where properties...
    //   row    == "2"
    //   column == "0"
    //
    // "occurrence" is an "artificial" property.
    // The first occurrence of an object is "0".
    var o2 = objTools.findChildPath(o, {"row": "2", "column": "0", "occurrence": "1"});

    // Find first child where properties...
    //   type   == "TableCell"
    //   row    == "2"
    //   column == "0"
    var o1 = objTools.findChildPath(o, {"type": "TableCell", "row": "2", "column": "0"});

    // Find second child where properties...
    //   type   == "TableCell"
    //   row    == "2"
    //   column == "0"
    //
    // "occurrence" is an "artificial" property.
    // The first occurrence of an object is "0".
    var o2 = objTools.findChildPath(o, {"type": "TableCell", "row": "2", "column": "0", "occurrence": "1"});
}

var objTools = {
    /**
     * Find and return object with properties matching those
     * specified in propertiesObject.
     *
     * "obj" can be any container or child object.
     * When passing a child only the children of that child
     * are searched.
     *
     * The return value is an array with the found object
     * at index 0, and all its parents (if any), starting
     * with the closest parent. For example:
     *
     *   [aTableCellObject, aTableRowObject, aTableObject]
     *
     * Example calls:
     *
     *   o = waitForObject("{...}");
     *   path = findChildPath(o, {"text": "Kimberly"});
     *   path = findChildPath(o, {"row": "0"});
     *   path = findChildPath(o, {"row": "0", "column": "0"});
     *
     * propertiesObj can also contain "occurrence". The first
     * occurrence of an object has an occurrence value of 0.
     *
     * propertiesObj can also contain "type", just like in
     * real names. (But Squish wrapper types are not
     * supported, i.e.
     * com.froglogic.squish.swt.TreeItemProxy, etc.).
     */
    findChildPath: function(obj, propertiesObj, verbose)
    {
        if (verbose == null) {
            var verbose = False;
        }

        var path = [obj];

        var occurrenceCountDown = [0];
        if ("occurrence" in propertiesObj) {
            occurrenceCountDown[0] = parseInt(propertiesObj["occurrence"]);
        }

        if (this.findChildPath_impl(obj, propertiesObj, path, occurrenceCountDown, verbose)) {
            path.reverse();
            return path;
        }
        return [];
    },

    hasPropertiesAndValues: function(obj, propertiesObj, verbose) {
        if (verbose) {
            test.log("findChildPath: Checking properties of: " + this.classOrTypeName(obj));
        }

        // If this is an item proxied by a Squish object,
        // check the actual item instead
        if (("class" in obj)
            && ("item" in obj)
            && (obj.item != null)
            && (obj["class"].indexOf("com.froglogic.") != -1)) {
            obj = obj.item;
        }

        for (propertyName in propertiesObj) {
            // Ignore the occurrence property here (a special check
            // for that (based on our own occurrence counting) is in
            // findChildPath_impl())
            if (propertyName == "occurrence") {
                continue;
            }

            var propertyValue = propertiesObj[propertyName];

            // Allow searching for "type" too, for consistency
            // with real name properties
            if (propertyName == "type") {
                var t = this.classOrTypeName(obj);
                if (t == propertyValue) {
                    continue;
                }

                if (verbose) {
                    test.log("findChildPath: \tProperty mismatch: type: Expected: " + propertyValue + "; Actual: " + t);
                }
                return false;
            }

            var propertyNames = propertyName.split(".");
            if (!(propertyNames[0] in obj)) {
                if (verbose) {
                    test.log("findChildPath: \tProperty not in obj: " + propertyNames[0]);
                }
                return false;
            }

            try {
                var v = this.fetchPropertyValue(obj, propertyNames, verbose);
                if (v != propertyValue) {
                    if (verbose) {
                        test.log("findChildPath: \tProperty mismatch: " + propertyName + ": Expected: " + propertyValue + ": Actual: " + v);
                    }
                    return false;
                    }
            } catch (e) {
                if (verbose) {
                    test.log("findChildPath: \tProperty not found: " + propertyName, ""+e);
                }
            }
        }
        return true;
    },

    fetchPropertyValue: function(obj, propertyNames, verbose) {
        if (propertyNames.length == 1) {
            return obj[propertyNames[0]] ;
        }

        var currentObj = obj;
        var nameSoFar = ""
        for (var i = 0; i < propertyNames.length - 1; i++) {
            var n = propertyNames[i];
            currentObj = currentObj[n];
            nameSoFar = nameSoFar + "." + n;
            if (verbose) {
                test.log("findChildPath: \tProperty in obj: " + nameSoFar.substr(1));
            }
        }
        var n = propertyNames[propertyNames.length-1];
        nameSoFar = nameSoFar + "." + n;
        var v = currentObj[n];
        if (verbose) {
            test.log("findChildPath: \tProperty in obj: " + nameSoFar.substr(1));
        }
        return v;
    },

    findChildPath_impl: function (obj, propertiesObj, path, occurrenceCountDown, verbose)
    {
        if (verbose) {
            test.log("findChildPath: Iterating over children of: " + this.classOrTypeName(obj));
        }

        var children = object.children(obj);
        for (var i = 0; i < children.length; i++) {
           var c = children[i];
           if (verbose) {
               test.log("findChildPath: Next child's type: " + this.classOrTypeName(c));
           }
            if (!this.hasPropertiesAndValues(c, propertiesObj, verbose)) {
                path.push(c);
                if (this.findChildPath_impl(c, propertiesObj, path, occurrenceCountDown, verbose)) {
                    return true;
                }

                path.pop();
                continue;
            }

            if ([occurrenceCountDown] <= 0) {
                path.push(c);
                return true;
            }

            occurrenceCountDown[0] = occurrenceCountDown[0] - 1;
            if (this.findChildPath_impl(c, propertiesObj, path, occurrenceCountDown, verbose)) {
                return true;
            }
        }

        return false;
    },

    classOrTypeName: function (obj) {
        if ("class" in obj) {
            return obj["class"];
        }
        return typeName(obj);
    }
};
test.js