ripple_update_group.py 6.08 KB
Newer Older
1
# -*- coding: utf-8 -*-
2
# Pitivi video editor
3 4 5 6 7 8 9 10 11 12 13 14 15 16
# 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
Hicham HAOUARI's avatar
Hicham HAOUARI committed
17 18
# Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
# Boston, MA 02110-1301, USA.
19 20 21


class RippleUpdateGroup(object):
22
    """Allows for event-driven spreadsheet-like ripple updates.
23

24
    Detects infinite loops.
25

26 27 28 29
    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.
30

31 32 33
    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.
34

35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
    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.

50
    An edge between two vertices represents a sequence of operations. If an
luz.paz's avatar
luz.paz committed
51
    edge exists from object A to object B, then whenever A is performed, B
52 53 54 55
    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
56
    associated with it:
57 58 59 60 61 62

        - 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.

63
        - a function to be called whenever the edge is visited during an update
64
          cycle. this function will not be called if the condition function
65 66
          returns False.

67 68 69 70
    Attributes:
        arcs (dict): A map from widget to a list of edges originating in
            the widget.
        update_funcs (dict): A map from widget to a (callable, args) tuple.
71
    """
72

73
    def __init__(self):
74 75 76 77
        self.arcs = {}
        self.update_funcs = {}
        self.ignore_new_signals = False

78
    def addVertex(self, widget, signal=None, update_func=None,
79
                  update_func_args=()):
80
        """Adds a widget to the list of vertices.
81 82 83 84 85 86 87 88

        Args:
            widget (Gtk.Widget): The vertex to be added.
            signal (Optional[str]): A signal of the widget to be monitored.
            update_func (Optional[function]): A callable object called when the
                vertex is visited.
            update_func_args (Optional[List]): The arguments for calling
                update_func.
89
        """
90
        if signal:
91 92
            widget.connect(signal, self._widgetValueChanged)
        self.update_funcs[widget] = (update_func, update_func_args)
93 94
        self.arcs[widget] = []

95
    def addEdge(self, widget_a, widget_b, predicate=None, edge_func=None):
96 97 98 99 100 101 102 103 104
        """Adds a directional edge from widget_a to widget_b.

        Args:
            widget_a (Gtk.Widget): The source vertex.
            widget_b (Gtk.Widget): The target vertex.
            predicate (Optional[function]): A callable object returning whether
                the edge may be traversed.
            edge_func (Optional[function]): A callable object called when the
                edge is traversed.
105 106 107 108
        """
        self.arcs[widget_a].append((widget_b, predicate, edge_func))

    def addBiEdge(self, widget_a, widget_b, predicate=None, edge_func=None):
109
        """Adds a bidirectional edge between the specified vertices.
110

111
        See `addEdge`.
112 113 114 115 116
        """
        self.addEdge(widget_a, widget_b, predicate, edge_func)
        self.addEdge(widget_b, widget_a, predicate, edge_func)

    def _widgetValueChanged(self, widget, *unused):
117
        """Handles an event generated by the specified widget."""
118 119 120 121
        if self.ignore_new_signals:
            return

        self.ignore_new_signals = True
122 123 124 125
        try:
            self._updateValues(widget)
        finally:
            self.ignore_new_signals = False
126 127

    def _updateValues(self, widget):
128
        """Traverses the graph starting from the specified vertex."""
129 130
        # Initialize the list of (source_widget, arc) to be traversed.
        queue = [(widget, arc) for arc in self.arcs[widget]]
131 132
        visited = set([widget])
        while queue:
133 134
            source_widget, arc = queue.pop(0)
            target_widget, predicate, edge_func = arc
135 136

            # ignore nodes we've seen
137
            if target_widget in visited:
138 139 140
                continue

            # check whether conditions permit this edge to be followed
141
            if predicate and not predicate():
142 143
                continue

144
            # traverse the edge
145 146 147
            if edge_func:
                edge_func()

148 149
            # visit the target node
            update_func, update_func_args = self.update_funcs[target_widget]
150
            if update_func:
151 152
                update_func(source_widget, target_widget, *update_func_args)
            visited.add(target_widget)
153 154

            # enqueue children
155 156
            for arc in self.arcs[target_widget]:
                queue.append((target_widget, arc))