From d4398c52acc54fb27a4b8bba2735a41f55b8f402 Mon Sep 17 00:00:00 2001 From: Kali Kaneko Date: Thu, 17 Oct 2013 12:04:12 -0300 Subject: Mail State Machine refactor. Closes: #4059 --- src/leap/bitmask/gui/statemachines.py | 414 ++++++++++++++++++++++++++++++++-- 1 file changed, 396 insertions(+), 18 deletions(-) (limited to 'src/leap/bitmask/gui/statemachines.py') 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) -- cgit v1.2.3