def testSelectionExpansion(self):

        d = {}
        for i in range(0, 10):
            dd = {}
            for j in range(0, 10):
                dd[str(j)] = j
            d[str(i)] = dd

        p = Gaffer.DictPath(d, "/")

        w = GafferUI.PathListingWidget(
            p,
            allowMultipleSelection=True,
            displayMode=GafferUI.PathListingWidget.DisplayMode.Tree)
        _GafferUI._pathListingWidgetAttachTester(
            GafferUI._qtAddress(w._qtWidget()))

        self.assertTrue(w.getSelection().isEmpty())
        self.assertTrue(w.getExpansion().isEmpty())

        s = IECore.PathMatcher(["/1", "/2", "/9", "/2/5"])
        w.setSelection(s, expandNonLeaf=True, scrollToFirst=False)
        self.assertEqual(w.getSelection(), s)
        _GafferUI._pathModelWaitForPendingUpdates(
            GafferUI._qtAddress(w._qtWidget().model()))
        self.assertEqual(w.getExpansion(),
                         IECore.PathMatcher(["/1", "/2", "/9"]))
    def testModelDoesntUpdateUnexpandedPaths(self):
        class InfinitePath(Gaffer.Path):

            # `self.visitedPaths` is a PathMatcher that will be filled with all the
            # children visited by the PathModel.
            def __init__(self, path, root="/", filter=None, visitedPaths=None):

                Gaffer.Path.__init__(self, path, root, filter=filter)

                self.visitedPaths = visitedPaths if visitedPaths is not None else IECore.PathMatcher(
                )
                self.visitedPaths.addPath(str(self))

            def isValid(self, canceller=None):

                return True

            def isLeaf(self, canceller=None):

                return False

            def copy(self):

                return InfinitePath(self[:], self.root(), self.getFilter(),
                                    self.visitedPaths)

            def _children(self, canceller=None):

                return [
                    InfinitePath(self[:] + [x], self.root(), self.getFilter(),
                                 self.visitedPaths) for x in ["a", "b"]
                ]

        # Create an infinite model and expand it up to a fixed depth.

        path1 = InfinitePath("/")

        widget = GafferUI.PathListingWidget(
            path=path1,
            displayMode=GafferUI.PathListingWidget.DisplayMode.Tree)
        _GafferUI._pathListingWidgetAttachTester(
            GafferUI._qtAddress(widget._qtWidget()))

        self.__expandModel(widget._qtWidget().model(), depth=4)

        # Replace with a new path, to force the PathModel into evaluating
        # it to see if there are any changes. The PathModel should only
        # visit paths that have been visited before, because there is no
        # need to notify Qt of layout or data changes for items that haven't
        # been visited yet.

        path2 = InfinitePath("/")
        widget.setPath(path2)
        _GafferUI._pathModelWaitForPendingUpdates(
            GafferUI._qtAddress(widget._qtWidget().model()))

        self.assertEqual(path2.visitedPaths, path1.visitedPaths)
    def testModelChangingData(self):

        root = {
            "a": 10,
        }

        widget = GafferUI.PathListingWidget(
            path=Gaffer.DictPath(root, "/"),
            columns=[
                GafferUI.PathListingWidget.defaultNameColumn,
                GafferUI.PathListingWidget.StandardColumn(
                    "Value", "dict:value")
            ],
        )
        _GafferUI._pathListingWidgetAttachTester(
            GafferUI._qtAddress(widget._qtWidget()))

        # Do initial query and wait for async update.

        model = widget._qtWidget().model()
        self.assertEqual(model.rowCount(), 0)
        _GafferUI._pathModelWaitForPendingUpdates(GafferUI._qtAddress(model))
        self.assertEqual(model.rowCount(), 1)
        self.assertEqual(model.data(model.index(0, 1, QtCore.QModelIndex())),
                         10)

        # Set up change tracking.

        changes = []

        def dataChanged(*args):
            changes.append(args)

        model.dataChanged.connect(dataChanged)

        # Change value in column 1. Check that `dataChanged` is emitted
        # and we can query the new value.

        root["a"] = 20
        self.__emitPathChanged(widget)
        self.assertEqual(len(changes), 1)
        self.assertEqual(model.data(model.index(0, 1, QtCore.QModelIndex())),
                         20)

        # Trigger an artificial update and assert that the data has not changed,
        # and `dataChanged` has not been emitted.

        self.__emitPathChanged(widget)
        self.assertEqual(len(changes), 1)
        self.assertEqual(model.data(model.index(0, 1, QtCore.QModelIndex())),
                         20)
    def __expandModel(cls, model, index=None, queryData=False, depth=10):

        if depth <= 0:
            return

        index = index if index is not None else QtCore.QModelIndex()
        model.rowCount(index)
        if queryData:
            model.data(index)
        _GafferUI._pathModelWaitForPendingUpdates(GafferUI._qtAddress(model))

        for row in range(0, model.rowCount(index)):
            cls.__expandModel(model, model.index(row, 0, index), queryData,
                              depth - 1)
    def testDeeperExpandedPaths(self):

        p = Gaffer.DictPath({"a": {"b": {"c": {"d": 10}}}}, "/")

        w = GafferUI.PathListingWidget(p)
        _GafferUI._pathListingWidgetAttachTester(
            GafferUI._qtAddress(w._qtWidget()))

        w.setPathExpanded(p.copy().setFromString("/a/b/c"), True)
        self.assertTrue(w.getPathExpanded(p.copy().setFromString("/a/b/c")))

        _GafferUI._pathModelWaitForPendingUpdates(
            GafferUI._qtAddress(w._qtWidget().model()))
        self.assertEqual(self.__expansionFromQt(w),
                         IECore.PathMatcher(["/a/b/c"]))
    def testSetExpansionClearsExpansionByUser(self):

        w = GafferUI.PathListingWidget(
            Gaffer.DictPath({
                "a": 1,
                "b": 2,
                "c": 3
            }, "/"),
            displayMode=GafferUI.PathListingWidget.DisplayMode.Tree)
        model = w._qtWidget().model()
        model.rowCount()  # Trigger population of top level of the model
        _GafferUI._pathModelWaitForPendingUpdates(GafferUI._qtAddress(model))
        self.assertEqual(w.getExpansion(), IECore.PathMatcher())

        w._qtWidget().expand(model.index(
            0, 0))  # Equivalent to a click by the user
        self.assertEqual(w.getExpansion(), IECore.PathMatcher(["/a"]))

        w.setExpansion(IECore.PathMatcher(["/b"]))
        _GafferUI._pathModelWaitForPendingUpdates(GafferUI._qtAddress(model))
        self.assertEqual(self.__expansionFromQt(w), IECore.PathMatcher(["/b"]))

        w._qtWidget().collapse(model.index(
            1, 0))  # Equivalent to a click by the user
        self.assertEqual(w.getExpansion(), IECore.PathMatcher())
        w.setExpansion(IECore.PathMatcher(["/b"]))
        _GafferUI._pathModelWaitForPendingUpdates(GafferUI._qtAddress(model))
        self.assertEqual(self.__expansionFromQt(w), IECore.PathMatcher(["/b"]))
    def testExpansionByUser(self):

        w = GafferUI.PathListingWidget(
            Gaffer.DictPath({"a": {
                "b": {
                    "c": 10
                }
            }}, "/"),
            displayMode=GafferUI.PathListingWidget.DisplayMode.Tree)
        model = w._qtWidget().model()
        model.rowCount()  # Trigger population of top level of the model
        _GafferUI._pathModelWaitForPendingUpdates(GafferUI._qtAddress(model))
        self.assertEqual(w.getExpansion(), IECore.PathMatcher())

        cs = GafferTest.CapturingSlot(w.expansionChangedSignal())
        w._qtWidget().setExpanded(model.index(0, 0),
                                  True)  # Equivalent to a click by the user
        self.assertEqual(len(cs), 1)
        self.assertEqual(w.getExpansion(), IECore.PathMatcher(["/a"]))

        w._qtWidget().setExpanded(model.index(0, 0),
                                  False)  # Equivalent to a click by the user
        self.assertEqual(len(cs), 2)
        self.assertEqual(w.getExpansion(), IECore.PathMatcher())
    def __emitPathChanged(widget):

        widget.getPath().pathChangedSignal()(widget.getPath())
        _GafferUI._pathModelWaitForPendingUpdates(
            GafferUI._qtAddress(widget._qtWidget().model()))
    def testExpansion(self):

        d = {}
        for i in range(0, 10):
            dd = {}
            for j in range(0, 10):
                dd[str(j)] = j
            d[str(i)] = dd

        p = Gaffer.DictPath(d, "/")
        w = GafferUI.PathListingWidget(
            p, displayMode=GafferUI.PathListingWidget.DisplayMode.Tree)
        _GafferUI._pathListingWidgetAttachTester(
            GafferUI._qtAddress(w._qtWidget()))

        self.assertTrue(w.getExpansion().isEmpty())

        cs = GafferTest.CapturingSlot(w.expansionChangedSignal())
        e = IECore.PathMatcher(["/1", "/2", "/2/4", "/1/5", "/3"])

        w.setExpansion(e)
        self.assertEqual(w.getExpansion(), e)
        self.assertEqual(len(cs), 1)

        # Wait for asynchronous update to expand model. Then check
        # that the expected indices are expanded in the QTreeView.
        _GafferUI._pathModelWaitForPendingUpdates(
            GafferUI._qtAddress(w._qtWidget().model()))
        self.assertEqual(self.__expansionFromQt(w), e)

        # Delete a path that was expanded.
        d2 = d["2"]
        del d["2"]
        self.__emitPathChanged(w)
        # We don't expect this to affect the result of `getExpansion()` because
        # the expansion state is independent of the model contents.
        self.assertEqual(w.getExpansion(), e)
        # But it should affect what is mirrored in Qt.
        e2 = IECore.PathMatcher(e)
        e2.removePath("/2")
        e2.removePath("/2/4")
        self.assertEqual(self.__expansionFromQt(w), e2)

        # If we reintroduce the deleted path, it should be expanded again in Qt.
        # This behaviour is particularly convenient when switching between
        # different scenes in the HierarchyView, and matches the behaviour of
        # the SceneGadget.
        d["2"] = d2
        self.__emitPathChanged(w)
        self.assertEqual(self.__expansionFromQt(w), e)

        # Now try to set expansion twice in succession, so the model doesn't have
        # chance to finish one update before starting the next.

        e1 = IECore.PathMatcher(["/9", "/9/10", "/8/6"])
        e2 = IECore.PathMatcher(["/9", "/9/9", "/5/6", "3"])
        w.setExpansion(e1)
        w.setExpansion(e2)
        _GafferUI._pathModelWaitForPendingUpdates(
            GafferUI._qtAddress(w._qtWidget().model()))
        self.assertEqual(self.__expansionFromQt(w), e2)
    def testModelFirstQueryDoesntEmitDataChanged(self):

        for sortable in (False, True):

            # Not making widget visible, so it doesn't make any
            # queries to the model (leaving just our queries and
            # those made by the attached tester).
            widget = GafferUI.PathListingWidget(
                path=Gaffer.DictPath({"v": 10}, "/"),
                columns=[
                    GafferUI.PathListingWidget.defaultNameColumn,
                    GafferUI.PathListingWidget.StandardColumn(
                        "Value", "dict:value")
                ],
                sortable=sortable,
                displayMode=GafferUI.PathListingWidget.DisplayMode.Tree)
            _GafferUI._pathListingWidgetAttachTester(
                GafferUI._qtAddress(widget._qtWidget()))

            # Make initial child queries to populate the model with
            # items, but without evaluating data. We should start
            # without any rows on the root item.
            model = widget._qtWidget().model()
            self.assertEqual(model.rowCount(), 0)
            # Meaning that we can't even get an index to the first row.
            self.assertFalse(model.index(0, 0).isValid())
            # But if we wait for the update we've just triggered then
            # we should see a row appear.
            _GafferUI._pathModelWaitForPendingUpdates(
                GafferUI._qtAddress(model))
            self.assertEqual(model.rowCount(), 1)
            self.assertTrue(model.index(0, 0).isValid())

            # Set up recording for data changes.

            changes = []

            def dataChanged(*args):
                changes.append(args)

            model.dataChanged.connect(dataChanged)

            # We are testing the columns containing "dict:value".
            valueIndex = model.index(0, 1)
            if sortable:
                # Data will have been generated during sorting, so
                # we can query it immediately.
                self.assertEqual(model.data(valueIndex), 10)
                self.assertEqual(len(changes), 0)
            else:
                # Data not generated yet. The initial query will receive empty
                # data and should not trigger `dataChanged`.
                self.assertIsNone(model.data(valueIndex))
                self.assertEqual(len(changes), 0)
                # But the act of querying will have launched an async
                # update that should update the value and signal the
                # the change.
                _GafferUI._pathModelWaitForPendingUpdates(
                    GafferUI._qtAddress(model))
                self.assertEqual(len(changes), 1)
                self.assertEqual(model.data(valueIndex), 10)
    def testModelPathSwap(self):

        # Make a model for a small hierarchy, and expand the
        # model fully.

        root1 = Gaffer.GraphComponent()
        root1["c"] = Gaffer.GraphComponent()
        root1["c"]["d"] = Gaffer.GraphComponent()

        widget = GafferUI.PathListingWidget(
            path=Gaffer.GraphComponentPath(root1, "/"),
            columns=[GafferUI.PathListingWidget.defaultNameColumn],
            displayMode=GafferUI.PathListingWidget.DisplayMode.Tree,
        )
        _GafferUI._pathListingWidgetAttachTester(
            GafferUI._qtAddress(widget._qtWidget()))

        path = Gaffer.GraphComponentPath(root1, "/")
        self.assertEqual(path.property("graphComponent:graphComponent"), root1)
        path.append("c")
        self.assertEqual(path.property("graphComponent:graphComponent"),
                         root1["c"])
        path.append("d")
        self.assertEqual(path.property("graphComponent:graphComponent"),
                         root1["c"]["d"])

        model = widget._qtWidget().model()
        self.__expandModel(model)

        # Get an index for `/c/d` and check that we can retrieve
        # the right path for it.

        def assertIndexRefersTo(index, graphComponent):

            if isinstance(index, QtCore.QPersistentModelIndex):
                index = QtCore.QModelIndex(index)

            path = widget._PathListingWidget__pathForIndex(index)
            self.assertEqual(path.property("graphComponent:graphComponent"),
                             graphComponent)

        dIndex = model.index(0, 0, model.index(0, 0))
        assertIndexRefersTo(dIndex, root1["c"]["d"])
        persistentDIndex = QtCore.QPersistentModelIndex(dIndex)
        assertIndexRefersTo(persistentDIndex, root1["c"]["d"])

        # Replace the path with another one referencing an identical hierarchy.

        root2 = Gaffer.GraphComponent()
        root2["c"] = Gaffer.GraphComponent()
        root2["c"]["d"] = Gaffer.GraphComponent()

        widget.setPath(Gaffer.GraphComponentPath(root2, "/"))
        _GafferUI._pathModelWaitForPendingUpdates(GafferUI._qtAddress(model))

        # Check that the model now references the new path and the
        # new hierarchy.

        dIndex = model.index(0, 0, model.index(0, 0))
        assertIndexRefersTo(dIndex, root2["c"]["d"])
        assertIndexRefersTo(persistentDIndex, root2["c"]["d"])
    def testModelSorting(self):

        # Make a widget with sorting enabled.

        root = Gaffer.GraphComponent()
        root["c"] = Gaffer.GraphComponent()
        root["b"] = Gaffer.GraphComponent()

        path = Gaffer.GraphComponentPath(root, "/")
        widget = GafferUI.PathListingWidget(
            path,
            columns=[GafferUI.PathListingWidget.defaultNameColumn],
            sortable=True)
        _GafferUI._pathListingWidgetAttachTester(
            GafferUI._qtAddress(widget._qtWidget()))

        model = widget._qtWidget().model()
        self.__expandModel(model)
        self.assertEqual(model.rowCount(), 2)
        self.assertEqual(model.columnCount(), 1)

        # Check that the paths appear in sorted order.

        self.assertEqual(model.data(model.index(0, 0)), "b")
        self.assertEqual(model.data(model.index(1, 0)), "c")
        bIndex = QtCore.QPersistentModelIndex(model.index(0, 0))
        cIndex = QtCore.QPersistentModelIndex(model.index(1, 0))

        # And sorting is maintained when adding another path.

        root["a"] = Gaffer.GraphComponent()
        self.__emitPathChanged(widget)

        self.assertEqual(model.rowCount(), 3)
        self.assertEqual(model.columnCount(), 1)
        self.assertEqual(model.data(model.index(0, 0)), "a")
        self.assertEqual(model.data(model.index(1, 0)), "b")
        self.assertEqual(model.data(model.index(2, 0)), "c")

        self.assertTrue(bIndex.isValid())
        self.assertTrue(cIndex.isValid())
        self.assertEqual(model.data(bIndex), "b")
        self.assertEqual(model.data(cIndex), "c")

        # Turning sorting off should revert to the order in
        # the path itself.

        aIndex = QtCore.QPersistentModelIndex(model.index(0, 0))
        widget.setSortable(False)
        _GafferUI._pathModelWaitForPendingUpdates(GafferUI._qtAddress(model))

        self.assertEqual(model.rowCount(), 3)
        self.assertEqual(model.columnCount(), 1)

        self.assertTrue(aIndex.isValid())
        self.assertTrue(bIndex.isValid())
        self.assertTrue(cIndex.isValid())
        self.assertEqual(model.data(aIndex), "a")
        self.assertEqual(model.data(bIndex), "b")
        self.assertEqual(model.data(cIndex), "c")

        self.assertEqual(model.data(model.index(0, 0)), "c")
        self.assertEqual(model.data(model.index(1, 0)), "b")
        self.assertEqual(model.data(model.index(2, 0)), "a")