summaryrefslogtreecommitdiff
path: root/src/leap/bitmask/gui/statemachines.py
diff options
context:
space:
mode:
Diffstat (limited to 'src/leap/bitmask/gui/statemachines.py')
-rw-r--r--src/leap/bitmask/gui/statemachines.py414
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..386cb75f 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::%s entered. Emitting signal ...'
+ % (self._name, self.objectName()))
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)