diff options
Diffstat (limited to 'src/leap/bitmask/gui/statemachines.py')
| -rw-r--r-- | src/leap/bitmask/gui/statemachines.py | 414 | 
1 files changed, 396 insertions, 18 deletions
| diff --git a/src/leap/bitmask/gui/statemachines.py b/src/leap/bitmask/gui/statemachines.py index 94726720..ee16a4c6 100644 --- a/src/leap/bitmask/gui/statemachines.py +++ b/src/leap/bitmask/gui/statemachines.py @@ -19,7 +19,8 @@ State machines for the Bitmask app.  """  import logging -from PySide.QtCore import QStateMachine, QState +from PySide import QtCore +from PySide.QtCore import QStateMachine, QState, Signal  from PySide.QtCore import QObject  from leap.bitmask.services import connections @@ -36,28 +37,255 @@ _CON = "connecting"  _DIS = "disconnecting" -class IntermediateState(QState): +class SignallingState(QState):      """ -    Intermediate state that emits a custom signal on entry +    A state that emits a custom signal on entry.      """ -    def __init__(self, signal): +    def __init__(self, signal, parent=None, name=None):          """          Initializer.          :param signal: the signal to be emitted on entry on this state.          :type signal: QtCore.QSignal          """ -        super(IntermediateState, self).__init__() +        super(SignallingState, self).__init__(parent)          self._signal = signal +        self._name = name      def onEntry(self, *args):          """          Emits the signal on entry.          """ -        logger.debug('IntermediateState entered. Emitting signal ...') +        logger.debug('State %s entered. Emitting signal ...' +                     % (self._name + self.__class__.__name__))          if self._signal is not None:              self._signal.emit() +class States(object): +    """ +    States for composite objects +    """ + +    class Off(SignallingState): +        pass + +    class Connecting(SignallingState): +        pass + +    class On(SignallingState): +        pass + +    class Disconnecting(SignallingState): +        pass + +    class StepsTrack(QObject): +        state_change = Signal() + +        def __init__(self, target): +            super(States.StepsTrack, self).__init__() +            self.received = set([]) +            self.target = set(target) + +        def is_all_done(self): +            return all([ev in self.target for ev in self.received]) + +        def is_any_done(self): +            return any([ev in self.target for ev in self.received]) + +        def seen(self, _type): +            if _type in self.target: +                self.received.add(_type) + +        def reset_seen(self): +            self.received = set([]) + +    class TransitionOR(QtCore.QSignalTransition): + +        def __init__(self, state): +            super(States.TransitionOR, self).__init__( +                state, QtCore.SIGNAL('state_change()')) +            self.state = state + +        def eventTest(self, e): +            self.state.seen(e.type()) +            done = self.state.is_any_done() +            if done: +                self.state.reset_seen() +            return done + +        def onTransition(self, e): +            pass + +    class TransitionAND(QtCore.QSignalTransition): + +        def __init__(self, state): +            super(States.TransitionAND, self).__init__( +                state, QtCore.SIGNAL('state_change()')) +            self.state = state + +        def eventTest(self, e): +            self.state.seen(e.type()) +            done = self.state.is_all_done() +            if done: +                self.state.reset_seen() +            return done + +        def onTransition(self, e): +            pass + + +class CompositeEvent(QtCore.QEvent): +    def __init__(self): +        super(CompositeEvent, self).__init__( +            QtCore.QEvent.Type(self.ID)) + + +class Composite(object): +    # TODO we should generate the connectingEvents dinamycally, +    # depending on how much composite states do we get. +    # This only supports up to 2 composite states. + +    class ConnectingEvent1(CompositeEvent): +        ID = QtCore.QEvent.User + 1 + +    class ConnectingEvent2(CompositeEvent): +        ID = QtCore.QEvent.User + 2 + +    class ConnectedEvent1(CompositeEvent): +        ID = QtCore.QEvent.User + 3 + +    class ConnectedEvent2(CompositeEvent): +        ID = QtCore.QEvent.User + 4 + +    class DisconnectingEvent1(CompositeEvent): +        ID = QtCore.QEvent.User + 5 + +    class DisconnectingEvent2(CompositeEvent): +        ID = QtCore.QEvent.User + 6 + +    class DisconnectedEvent1(CompositeEvent): +        ID = QtCore.QEvent.User + 7 + +    class DisconnectedEvent2(CompositeEvent): +        ID = QtCore.QEvent.User + 8 + + +class Events(QtCore.QObject): +    """ +    A Wrapper object for containing the events that will be +    posted to a composite state machine. +    """ +    def __init__(self, parent=None): +        """ +        Initializes the QObject with the given parent. +        """ +        QtCore.QObject.__init__(self, parent) + + +class CompositeMachine(QStateMachine): + +    def __init__(self, parent=None): +        QStateMachine.__init__(self, parent) + +        # events +        self.events = Events(parent) +        self.create_events() + +    def create_events(self): +        """ +        Creates a bunch of events to be posted to the state machine when +        the transitions say so. +        """ +        # XXX refactor into a dictionary? +        self.events.con_ev1 = Composite.ConnectingEvent1() +        self.events.con_ev2 = Composite.ConnectingEvent2() +        self.events.on_ev1 = Composite.ConnectedEvent1() +        self.events.on_ev2 = Composite.ConnectedEvent2() +        self.events.dis_ev1 = Composite.DisconnectingEvent1() +        self.events.dis_ev2 = Composite.DisconnectingEvent2() +        self.events.off_ev1 = Composite.DisconnectedEvent1() +        self.events.off_ev2 = Composite.DisconnectedEvent2() + +    def beginSelectTransitions(self, e): +        """ +        Weird. Having this method makes underlying backtraces +        to appear magically on the transitions. +        :param e: the received event +        :type e: QEvent +        """ +        pass + +    def _connect_children(self, child1, child2): +        """ +        Connects the state transition signals for children machines. + +        :param child1: the first child machine +        :type child1: QStateMachine +        :param child2: the second child machine +        :type child2: QStateMachine +        """ +        # TODO refactor and generalize for composites +        # of more than 2 connections. + +        c1 = child1.conn +        c1.qtsigs.connecting_signal.connect(self.con_ev1_slot) +        c1.qtsigs.connected_signal.connect(self.on_ev1_slot) +        c1.qtsigs.disconnecting_signal.connect(self.dis_ev1_slot) +        c1.qtsigs.disconnected_signal.connect(self.off_ev1_slot) + +        c2 = child2.conn +        c2.qtsigs.connecting_signal.connect(self.con_ev2_slot) +        c2.qtsigs.connected_signal.connect(self.on_ev2_slot) +        c2.qtsigs.disconnecting_signal.connect(self.dis_ev2_slot) +        c2.qtsigs.disconnected_signal.connect(self.off_ev2_slot) + +    # XXX why is this getting deletec in c++? +    #Traceback (most recent call last): +    #self.postEvent(self.events.on_ev2) +    #RuntimeError: Internal C++ object (ConnectedEvent2) already deleted. +    # XXX trying the following workaround, since +    # I cannot find why in the world this is getting deleted :( +    # XXX refactor! + +    # slots connection1 + +    def con_ev1_slot(self): +        # XXX if we just postEvent, we get the Internal C++ object deleted... +        # so the workaround is to re-create it each time. +        self.events.con_ev1 = Composite.ConnectingEvent1() +        self.postEvent(self.events.con_ev1) + +    def on_ev1_slot(self): +        self.events.on_ev1 = Composite.ConnectedEvent1() +        self.postEvent(self.events.on_ev1) + +    def dis_ev1_slot(self): +        self.events.dis_ev1 = Composite.DisconnectingEvent1() +        self.postEvent(self.events.dis_ev1) + +    def off_ev1_slot(self): +        self.events.off_ev1 = Composite.DisconnectedEvent1() +        self.postEvent(self.events.off_ev1) + +    # slots connection2 + +    def con_ev2_slot(self): +        self.events.con_ev2 = Composite.ConnectingEvent2() +        self.postEvent(self.events.con_ev2) + +    def on_ev2_slot(self): +        self.events.on_ev2 = Composite.ConnectedEvent2() +        self.postEvent(self.events.on_ev2) + +    def dis_ev2_slot(self): +        self.events.dis_ev2 = Composite.DisconnectingEvent2() +        self.postEvent(self.events.dis_ev2) + +    def off_ev2_slot(self): +        self.events.off_ev2 = Composite.DisconnectedEvent2() +        self.postEvent(self.events.off_ev2) + +  class ConnectionMachineBuilder(object):      """      Builder class for state machines made from LEAPConnections. @@ -65,16 +293,161 @@ class ConnectionMachineBuilder(object):      def __init__(self, connection):          """          :param connection: an instance of a concrete LEAPConnection -                      we will be building a state machine for. +                           we will be building a state machine for.          :type connection: AbstractLEAPConnection          """          self._conn = connection          leap_assert_type(self._conn, connections.AbstractLEAPConnection) -    def make_machine(self, button=None, action=None, label=None): +    def make_machine(self, **kwargs):          """          Creates a statemachine associated with the passed controls. +        It returns the state machine if the connection used for initializing +        the ConnectionMachineBuilder inherits exactly from +        LEAPAbstractConnection, and a tuple with the Composite Machine and its +        individual parts in case that it is a composite machine which +        connection definition inherits from more than one class that, on their +        time, inherit from LEAPAbstractConnection. + +        :params: see parameters for ``_make_simple_machine`` +        :returns: a QStateMachine, or a tuple with the form: +                  (CompositeStateMachine, (StateMachine1, StateMachine2)) +        :rtype: QStateMachine or tuple +        """ +        components = self._conn.components + +        if components is None: +        # simple case: connection definition inherits directly from +        # the abstract connection. + +            leap_assert_type(self._conn, connections.AbstractLEAPConnection) +            return self._make_simple_machine(self._conn, **kwargs) + +        if components: +            # composite case: connection definition inherits from several +            # classes, each one of which inherit from the abstract connection. +            child_machines = tuple( +                [ConnectionMachineBuilder(connection()).make_machine() +                    for connection in components]) +            composite_machine = self._make_composite_machine( +                self._conn, child_machines, **kwargs) + +            composite_machine._connect_children( +                *child_machines) + +            # XXX should also connect its own states with the signals +            # for the composite machine itself + +            return (composite_machine, child_machines) + +    def _make_composite_machine(self, conn, children, +                                **kwargs): +        """ +        Creates a composite machine. + +        :param conn: an instance of a connection definition. +        :type conn: LEAPAbstractConnection +        :param children: children machines +        :type children: tuple of state machines +        :returns: A composite state machine +        :rtype: QStateMachine +        """ +        # TODO split this method in smaller utility functions. +        parent = kwargs.get('parent', None) + +        # 1. create machine +        machine = CompositeMachine(parent=parent) + +        # 2. create states +        off = States.Off(conn.qtsigs.disconnected_signal, +                         parent=machine, +                         name=conn.name) +        off.setObjectName("off") + +        on = States.On(conn.qtsigs.connected_signal, +                       parent=machine, +                       name=conn.name) +        on.setObjectName("on") + +        connecting_state = States.Connecting( +            conn.qtsigs.connecting_signal, +            parent=machine, +            name=conn.name) +        connecting_state.setObjectName("connecting") + +        disconnecting_state = States.Disconnecting( +            conn.qtsigs.disconnecting_signal, +            parent=machine, +            name=conn.name) +        disconnecting_state.setObjectName("disconnecting") + +        # 3. TODO create as many connectingEvents as needed (dynamically create +        # classses for that) +        # (we have manually created classes for events under CompositeEvent for +        # now, to begin with the simple 2 states case for mail. + +        # 4. state tracking objects for each transition stage + +        connecting_track0 = States.StepsTrack( +            (Composite.ConnectingEvent1.ID, +             Composite.ConnectingEvent2.ID)) +        connecting_track0.setObjectName("connecting_step_0") + +        connecting_track1 = States.StepsTrack( +            (Composite.ConnectedEvent1.ID, +             Composite.ConnectedEvent2.ID)) +        connecting_track1.setObjectName("connecting_step_1") + +        disconnecting_track0 = States.StepsTrack( +            (Composite.DisconnectingEvent1.ID, +             Composite.DisconnectingEvent2.ID)) +        disconnecting_track0.setObjectName("disconnecting_step_0") + +        disconnecting_track1 = States.StepsTrack( +            (Composite.DisconnectedEvent1.ID, +             Composite.DisconnectedEvent2.ID)) +        disconnecting_track1.setObjectName("disconnecting_step_1") + +        # 5. definte the transitions with the matching state-tracking +        # objects. + +        # off -> connecting +        connecting_transition = States.TransitionOR( +            connecting_track0) +        connecting_transition.setTargetState(connecting_state) +        off.addTransition(connecting_transition) + +        # connecting -> on +        connected_transition = States.TransitionAND( +            connecting_track1) +        connected_transition.setTargetState(on) +        connecting_state.addTransition(connected_transition) + +        # on -> disconnecting +        disconnecting_transition = States.TransitionOR( +            disconnecting_track0) +        disconnecting_transition.setTargetState(disconnecting_state) +        on.addTransition(disconnecting_transition) + +        # disconnecting -> off +        disconnected_transition = States.TransitionAND( +            disconnecting_track1) +        disconnected_transition.setTargetState(off) +        disconnecting_state.addTransition(disconnected_transition) + +        machine.setInitialState(off) +        machine.conn = conn +        return machine + +    def _make_simple_machine(self, conn, +                             button=None, action=None, label=None): +        """ +        Creates a statemachine associated with the passed controls. + +        :param conn: the connection instance that defines this machine. +        :type conn: AbstractLEAPConnection +          :param button: the switch button.          :type button: QPushButton @@ -88,9 +461,7 @@ class ConnectionMachineBuilder(object):          :rtype: QStateMachine          """          machine = QStateMachine() -        conn = self._conn - -        states = self._make_states(button, action, label) +        states = self._make_states(conn, button, action, label)          # transitions: @@ -151,11 +522,17 @@ class ConnectionMachineBuilder(object):          for state in states.itervalues():              machine.addState(state)          machine.setInitialState(states[_OFF]) + +        machine.conn = conn          return machine -    def _make_states(self, button, action, label): +    def _make_states(self, conn, button, action, label):          """ -        Creates the four states for the state machine +        Creates the four states for the simple state machine. +        Adds the needed properties for the passed controls. + +        :param conn: the connection instance that defines this machine. +        :type conn: AbstractLEAPConnection          :param button: the switch button.          :type button: QPushButton @@ -169,7 +546,6 @@ class ConnectionMachineBuilder(object):          :returns: a dict of states          :rtype: dict          """ -        conn = self._conn          states = {}          # TODO add tooltip @@ -190,8 +566,9 @@ class ConnectionMachineBuilder(object):          states[_OFF] = off          # CONNECTING State ---------------- -        connecting = IntermediateState( -            conn.qtsigs.connecting_signal) +        connecting = SignallingState( +            conn.qtsigs.connecting_signal, +            name=conn.name)          on_label = _tr("Turn {0}").format(              conn.Disconnected.short_label)          if button: @@ -224,8 +601,9 @@ class ConnectionMachineBuilder(object):          states[_ON] = on          # DISCONNECTING State ------------- -        disconnecting = IntermediateState( -            conn.qtsigs.disconnecting_signal) +        disconnecting = SignallingState( +            conn.qtsigs.disconnecting_signal, +            name=conn.name)          if button:              disconnecting.assignProperty(                  button, 'enabled', False) | 
