1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
|
# PiTiVi , Non-linear video editor
#
# ui/ripple_update_group.py
#
# Copyright (c) 2010, Brandon Lewis <brandon.lewis@collabora.co.uk>
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this program; if not, write to the
# Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
# Boston, MA 02110-1301, USA.
class RippleUpdateGroup(object):
"""Allows for event-driven spreadsheet-like ripple updates without
infinite loops.
This class allows you to express an event-driven sequence of operations in
terms of a directed graph. It is not a constraint solver: The goal is to
allow the programmer to reduce complex logic to a set of simple functions
and predicates combined declaratively.
Events propagate through the graph in breadth first order. During an
update cycle, each vertex is visited only once, so cycles can exist in the
graph without creating infinite loops.
Each vertex represents a unique object. The following may also be
associated with a vertex:
- the name of a signal on the object. when this signal fires, it
triggers an update cycle beginning at this object. during an update
cycle, further signal emissions from this or any other vertex will
be ignored to prevent infinite loops.
- an update function, which will be called when the vertex is visited
as part of an update cycle. It will not be called when the object
emits a signal.
- zero or more user-specified arguments, passed to the
update_function.
An edge between two verticies represents a sequence of operations. If an
edge exists from object A to object B, then whenever A is perfomred, B
should be performed too -- unless it has already been visited as part of
this update cycle.
In addition to a a pair of objects, each edge also has the following
assoicated with it:
- a predicate function. called during an update cycle when this edge
is reached, and before any other processing is done. If this
function returns false, it will be as if this edge otherwise did not
exist.
- a function to be called whenver the edge is visited during an update
cycle. this function will not be called if the condition function
returns False.
@ivar arcs: A map from widget to a list of edges originating in the widget.
@ivar update_funcs: A map from widget to a (callable, args) tuple.
"""
def __init__(self):
self.arcs = {}
self.update_funcs = {}
self.ignore_new_signals = False
def addVertex(self, widget, signal=None, update_func=None,
update_func_args=()):
"""Add a widget to the list of vertexes.
@param widget: The vertex to be added.
@type widget: gtk.Widget
@param signal: A signal of the widget to be monitored.
@type signal: str
@param update_func: A callable object called when the vertex is visited.
@type update_func: function
@param update_func_args: The arguments for calling update_func.
@type update_func_args: tuple
"""
if signal:
widget.connect(signal, self._widgetValueChanged)
self.update_funcs[widget] = (update_func, update_func_args)
self.arcs[widget] = []
def addEdge(self, widget_a, widget_b, predicate=None, edge_func=None):
"""Add a directional edge from widget_a to widget_b.
@param widget_a: The source vertex.
@type widget_a: gtk.Widget
@param widget_b: The target vertex.
@type widget_b: gtk.Widget
@param predicate: A callable object returning whether the edge may be
traversed.
@type predicate: function
@param edge_func: A callable object called when the edge is traversed.
@type edge_func: function
"""
self.arcs[widget_a].append((widget_b, predicate, edge_func))
def addBiEdge(self, widget_a, widget_b, predicate=None, edge_func=None):
"""Add a bidirectional edge between the specified vertexes.
@see: addEdge
"""
self.addEdge(widget_a, widget_b, predicate, edge_func)
self.addEdge(widget_b, widget_a, predicate, edge_func)
def _widgetValueChanged(self, widget, *unused):
"""Handle an event generated by the specified widget."""
if self.ignore_new_signals:
return
self.ignore_new_signals = True
try:
self._updateValues(widget)
finally:
self.ignore_new_signals = False
def _updateValues(self, widget):
"""Traverse the graph starting from the specified widget."""
# Initialize the list of (source_widget, arc) to be traversed.
queue = [(widget, arc) for arc in self.arcs[widget]]
visited = set([widget])
while queue:
source_widget, arc = queue.pop(0)
target_widget, predicate, edge_func = arc
# ignore nodes we've seen
if target_widget in visited:
continue
# check whether conditions permit this edge to be followed
if predicate and not predicate():
continue
# traverse the edge
if edge_func:
edge_func()
# visit the target node
update_func, update_func_args = self.update_funcs[target_widget]
if update_func:
update_func(source_widget, target_widget, *update_func_args)
visited.add(target_widget)
# enqueue children
for arc in self.arcs[target_widget]:
queue.append((target_widget, arc))
|