Skip to content

Commit 6757375

Browse files
Fix unwanted signal triggers (#133)
* Add tests that check no signals are triggered * Disable signals when updating attributes for standarditems and treewidgetitems * Bump version to v0.8.5
1 parent 7f1a1f1 commit 6757375

4 files changed

Lines changed: 226 additions & 32 deletions

File tree

collagraph/renderers/pyside/objects/standarditem.py

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -41,18 +41,27 @@ def remove(self, el):
4141

4242
@PySideRenderer.register_set_attr(QStandardItem)
4343
def set_attribute(self, attr, value):
44-
if attr == "model_index":
45-
if model := self.model():
46-
index = model.indexFromItem(self)
47-
row, column = value
48-
if index.row() != row or index.column() != column:
49-
if parent := self.parent():
50-
# `it` is `self`
51-
it = parent.takeChild(index.row(), index.column())
52-
parent.setChild(row, column, self)
53-
else:
54-
# `it` is `self`
55-
it = model.takeItem(index.row(), index.column())
56-
model.setItem(row, column, it)
44+
# Before setting any attribute, make sure to disable
45+
# all signals for the associated model
46+
model = self.model()
47+
if model:
48+
model.blockSignals(True)
49+
50+
if attr == "model_index" and model:
51+
index = model.indexFromItem(self)
52+
row, column = value
53+
if index.row() != row or index.column() != column:
54+
if parent := self.parent():
55+
# `it` is `self`
56+
it = parent.takeChild(index.row(), index.column())
57+
parent.setChild(row, column, self)
58+
else:
59+
# `it` is `self`
60+
it = model.takeItem(index.row(), index.column())
61+
model.setItem(row, column, it)
5762

5863
qobject_set_attribute(self, attr, value)
64+
65+
# And don't forget to enable signals when done
66+
if model:
67+
model.blockSignals(True)

collagraph/renderers/pyside/objects/treewidgetitem.py

Lines changed: 26 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -35,21 +35,29 @@ def remove(self, el: QTreeWidgetItem):
3535

3636
@PySideRenderer.register_set_attr(QTreeWidgetItem)
3737
def set_attribute(self, attr, value):
38-
if attr == "content":
39-
for col, data in value.items():
40-
self.setText(col, data)
41-
return
42-
elif attr == "expanded":
43-
if not self.parent():
44-
self._expanded = value
45-
else:
46-
self.setExpanded(value)
47-
return
48-
elif attr == "selected":
49-
if not self.parent():
50-
self._selected = value
51-
else:
52-
self.setSelected(value)
53-
return
54-
55-
qobject_set_attribute(self, attr, value)
38+
# Before setting any attribute, make sure to disable
39+
# all signals for the tree widget
40+
tree_widget = self.treeWidget()
41+
if tree_widget:
42+
tree_widget.blockSignals(True)
43+
44+
match attr:
45+
case "content":
46+
for col, data in value.items():
47+
self.setText(col, data)
48+
case "expanded":
49+
if not self.parent():
50+
self._expanded = value
51+
else:
52+
self.setExpanded(value)
53+
case "selected":
54+
if not self.parent():
55+
self._selected = value
56+
else:
57+
self.setSelected(value)
58+
case _:
59+
qobject_set_attribute(self, attr, value)
60+
61+
# And don't forget to enable signals when done
62+
if tree_widget:
63+
tree_widget.blockSignals(True)

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "collagraph"
3-
version = "0.8.4"
3+
version = "0.8.5"
44
description = "Reactive user interfaces"
55
authors = [
66
{ name = "Berend Klein Haneveld", email = "berendkleinhaneveld@gmail.com" },
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
from functools import partial
2+
3+
import pytest
4+
from observ import reactive
5+
6+
pytest.importorskip("PySide6")
7+
8+
from PySide6 import QtWidgets
9+
10+
import collagraph as cg
11+
12+
TREE_WIDGET = """
13+
<treewidget @item-changed="item_changed">
14+
<treewidgetitem
15+
v-for="item in items"
16+
:content="{0: item}"
17+
/>
18+
</treewidget>
19+
20+
<script>
21+
import collagraph as cg
22+
23+
class Tree(cg.Component):
24+
changed = 0
25+
26+
def init(self):
27+
pass
28+
29+
def item_changed(self, item, column):
30+
Tree.changed += 1
31+
</script>
32+
"""
33+
34+
TREE_VIEW = """
35+
<treeview>
36+
<itemmodel @item_changed="item_changed">
37+
<standarditem
38+
v-for="item in items"
39+
:text="item"
40+
/>
41+
</itemmodel>
42+
</treewidget>
43+
44+
<script>
45+
import collagraph as cg
46+
47+
class Tree(cg.Component):
48+
changed = 0
49+
50+
def init(self):
51+
pass
52+
53+
def item_changed(self, item):
54+
Tree.changed += 1
55+
</script>
56+
"""
57+
58+
59+
def tree_widget_nr_items_found(container, count):
60+
tree_widget = container.findChild(QtWidgets.QTreeWidget)
61+
assert tree_widget
62+
root_item = tree_widget.invisibleRootItem()
63+
assert root_item.childCount() == count
64+
65+
66+
def tree_view_nr_items_found(container, count):
67+
tree_widget = container.findChild(QtWidgets.QTreeView)
68+
assert tree_widget
69+
model = tree_widget.model()
70+
assert model.rowCount() == count
71+
72+
73+
def test_qtreewidgetitem_change_list_no_trigger(qtbot, parse_source):
74+
"""
75+
When a qstandarditem is adjusted by the renderer, it should not trigger any signals.
76+
"""
77+
Tree, _ = parse_source(TREE_WIDGET)
78+
state = reactive({"items": ["a", "b", "c", "d"]})
79+
80+
renderer = cg.PySideRenderer(autoshow=False)
81+
gui = cg.Collagraph(renderer=renderer)
82+
container = renderer.create_element("widget")
83+
gui.render(Tree, container, state=state)
84+
assert Tree.changed == 0
85+
86+
qtbot.waitUntil(partial(tree_widget_nr_items_found, container, 4), timeout=500)
87+
assert Tree.changed == 0
88+
89+
state["items"].pop(2)
90+
assert Tree.changed == 0
91+
92+
qtbot.waitUntil(partial(tree_widget_nr_items_found, container, 3), timeout=500)
93+
assert Tree.changed == 0
94+
95+
96+
def test_qtreewidgetitem_change_item_no_trigger(qtbot, parse_source):
97+
"""
98+
When a qstandarditem is adjusted by the renderer, it should not trigger any signals.
99+
"""
100+
Tree, _ = parse_source(TREE_WIDGET)
101+
state = reactive({"items": ["a", "b", "c", "d"]})
102+
103+
renderer = cg.PySideRenderer(autoshow=False)
104+
gui = cg.Collagraph(renderer=renderer)
105+
container = renderer.create_element("widget")
106+
gui.render(Tree, container, state=state)
107+
assert Tree.changed == 0
108+
109+
qtbot.waitUntil(partial(tree_widget_nr_items_found, container, 4), timeout=500)
110+
assert Tree.changed == 0
111+
112+
def item_in_list(index: int, value: str):
113+
tree_widget = container.findChild(QtWidgets.QTreeView)
114+
assert tree_widget
115+
root_item = tree_widget.invisibleRootItem()
116+
item = root_item.child(index)
117+
assert item.text(0) == value, item.text(0)
118+
119+
state["items"][2] = "e"
120+
assert Tree.changed == 0
121+
122+
qtbot.waitUntil(partial(item_in_list, 2, "e"), timeout=500)
123+
assert Tree.changed == 0
124+
125+
126+
def test_qstandarditem_change_list_no_trigger(qtbot, parse_source):
127+
"""
128+
When a qstandarditem is adjusted by the renderer, it should not trigger any signals.
129+
"""
130+
Tree, _ = parse_source(TREE_VIEW)
131+
state = reactive({"items": ["a", "b", "c", "d"]})
132+
133+
renderer = cg.PySideRenderer(autoshow=False)
134+
gui = cg.Collagraph(renderer=renderer)
135+
container = renderer.create_element("widget")
136+
gui.render(Tree, container, state=state)
137+
assert Tree.changed == 0
138+
139+
qtbot.waitUntil(partial(tree_view_nr_items_found, container, 4), timeout=500)
140+
assert Tree.changed == 0
141+
142+
state["items"].pop(2)
143+
assert Tree.changed == 0
144+
145+
qtbot.waitUntil(partial(tree_view_nr_items_found, container, 3), timeout=500)
146+
assert Tree.changed == 0
147+
148+
149+
def test_qstandarditem_change_item_no_trigger(qtbot, parse_source):
150+
"""
151+
When a qstandarditem is adjusted by the renderer, it should not trigger any signals.
152+
"""
153+
Tree, _ = parse_source(TREE_VIEW)
154+
state = reactive({"items": ["a", "b", "c", "d"]})
155+
156+
renderer = cg.PySideRenderer(autoshow=False)
157+
gui = cg.Collagraph(renderer=renderer)
158+
container = renderer.create_element("widget")
159+
gui.render(Tree, container, state=state)
160+
assert Tree.changed == 0
161+
162+
qtbot.waitUntil(partial(tree_view_nr_items_found, container, 4), timeout=500)
163+
assert Tree.changed == 0
164+
165+
def item_in_list(index: int, value: str):
166+
tree_widget = container.findChild(QtWidgets.QTreeView)
167+
assert tree_widget
168+
model = tree_widget.model()
169+
assert model.hasIndex(index, 0)
170+
item_index = model.index(index, 0)
171+
assert model.itemData(item_index)[0] == value, model.itemData(item_index)
172+
173+
state["items"][2] = "e"
174+
assert Tree.changed == 0
175+
176+
qtbot.waitUntil(partial(item_in_list, 2, "e"), timeout=500)
177+
assert Tree.changed == 0

0 commit comments

Comments
 (0)