Пример #1
0
    def __init__(self, state_machine, meta=None, load_meta_data=True):
        """Constructor
        """
        MetaModel.__init__(self)  # pass columns as separate parameters

        assert isinstance(state_machine, StateMachine)

        self.state_machine = state_machine
        self.state_machine_id = state_machine.state_machine_id

        root_state = self.state_machine.root_state
        if isinstance(root_state, ContainerState):
            self.root_state = ContainerStateModel(
                root_state, parent=self, load_meta_data=load_meta_data)
        else:
            self.root_state = StateModel(root_state,
                                         parent=self,
                                         load_meta_data=load_meta_data)

        if isinstance(meta, Vividict):
            self.meta = meta
        else:
            self.meta = Vividict()

        # ongoing_complex_actions is updated by ComplexActionObserver -> secure encapsulated observation
        # and made observable by state machine model here
        self.ongoing_complex_actions = {}
        self.complex_action_observer = ComplexActionObserver(self)

        self.meta_signal = Signal()
        self.state_meta_signal = Signal()
        self.action_signal = Signal()
        self.state_action_signal = Signal()
        self.sm_selection_changed_signal = Signal()
        self.destruction_signal = Signal()

        self.temp = Vividict()

        if load_meta_data:
            self.load_meta_data(recursively=False)

        self.selection = Selection(self.sm_selection_changed_signal)

        self.storage_lock = threading.Lock(
        )  # lock can not be substituted by the state machine lock -> maybe because it is a RLock

        self.history = None
        if global_gui_config.get_config_value('HISTORY_ENABLED'):
            from rafcon.gui.models.modification_history import ModificationsHistoryModel
            self.history = ModificationsHistoryModel(self)
        else:
            logger.info("The modification history is disabled")

        self.auto_backup = None
        if global_gui_config.get_config_value('AUTO_BACKUP_ENABLED'):
            from rafcon.gui.models.auto_backup import AutoBackupModel
            self.auto_backup = AutoBackupModel(self)

        self.root_state.register_observer(self)
        self.register_observer(self)
Пример #2
0
    def __init__(self, parent, meta=None):
        MetaModel.__init__(self, meta)

        self.parent = parent
        self.destruction_signal = Signal()

        # this class is an observer of its own properties:
        self.register_observer(self)
Пример #3
0
    def __init__(self, parent_signal=None):
        ModelMT.__init__(self)

        self._selected = set()
        self._incomes = set()
        self._input_data_ports = set()
        self._output_data_ports = set()
        self._outcomes = set()
        self._data_flows = set()
        self._transitions = set()
        self._states = set()
        self._scoped_variables = set()
        self.selection_changed_signal = Signal()
        self.focus_signal = Signal()
        self.parent_signal = parent_signal
Пример #4
0
    def __init__(self, meta=None):
        ModelMT.__init__(self)

        if isinstance(meta, dict):
            self.meta = Vividict(meta)
        else:
            self.meta = Vividict()
        self.temp = Vividict()

        self.meta_signal = Signal()
Пример #5
0
    class TestModel(Model):
        a = 0
        b = []
        c = set()
        d = TestObservable()
        e = Signal()
        passed = False

        __observables__ = ('a', 'b', 'c', 'd', 'e')

        def __init__(self):
            super().__init__()
Пример #6
0
    def __init__(self, state, parent=None, meta=None):
        AbstractStateModel.state_counter += 1
        if type(self) == AbstractStateModel:
            raise NotImplementedError

        MetaModel.__init__(self, meta)
        assert isinstance(state, State)
        self.is_start = False

        self.state = state

        self.parent = parent

        self.meta_signal = Signal()
        self.action_signal = Signal()
        self.destruction_signal = Signal()

        self.register_observer(self)

        self.input_data_ports = []
        self.output_data_ports = []
        self.outcomes = []
        self._load_port_models()
Пример #7
0
class StateElementModel(MetaModel, Hashable):
    """This model class serves as base class for all models within a state model (ports, connections)

    Each state element model has a parent, meta and temp data. If observes itself and informs the parent about changes.

    :param rafcon.gui.models.abstract_state.AbstractStateModel parent: The state model of the state element
    :param rafcon.utils.vividict.Vividict meta: The meta data of the state element model
    """

    _parent = None
    meta_signal = Signal()
    destruction_signal = Signal()

    __observables__ = ("meta_signal", "destruction_signal")

    def __init__(self, parent, meta=None):
        MetaModel.__init__(self, meta)

        self.parent = parent
        self.destruction_signal = Signal()

        # this class is an observer of its own properties:
        self.register_observer(self)

    def __eq__(self, other):
        if type(self) != type(other):
            return False
        if self.core_element != other.core_element:
            return False
        if self.meta != other.meta:
            return False
        return True

    def __hash__(self):
        return id(self)

    def __ne__(self, other):
        return not self.__eq__(other)

    def __cmp__(self, other):
        if isinstance(other, StateElementModel):
            return self.core_element.__cmp__(other.core_element)

    def __lt__(self, other):
        return self.__cmp__(other) < 0

    def update_hash(self, obj_hash):
        self.update_hash_from_dict(obj_hash, self.core_element)
        if self.parent and not self.parent.state.get_next_upper_library_root_state(
        ):
            self.update_hash_from_dict(obj_hash, self.meta)

    @property
    def parent(self):
        """Getter for the parent state model of the state element

        :return: None if parent is not defined, else the model of the parent state
        :rtype: rafcon.gui.models.abstract_state.AbstractState
        """
        if not self._parent:
            return None
        return self._parent()

    @parent.setter
    def parent(self, parent_m):
        """Setter for the parent state model of the state element

        :param rafcon.gui.models.abstract_state.AbstractState parent_m: Parent state model or None
        """
        if isinstance(parent_m, AbstractStateModel):
            self._parent = ref(parent_m)
        else:
            self._parent = None

    @property
    def core_element(self):
        """Return the core element represented by this model

        :return: core element of the model
        :rtype: rafcon.core.state_elements.state_element.StateElement
        """
        raise NotImplementedError()

    def get_state_machine_m(self):
        if self.parent:
            return self.parent.get_state_machine_m()
        return None

    def prepare_destruction(self):
        """Prepares the model for destruction

        Unregisters the model from observing itself.
        """
        if self.core_element is None:
            logger.verbose(
                "Multiple calls of prepare destruction for {0}".format(self))
        self.destruction_signal.emit()
        try:
            self.unregister_observer(self)
        except KeyError:  # Might happen if the observer was already unregistered
            pass
        super(StateElementModel, self).prepare_destruction()

    def model_changed(self, model, prop_name, info):
        """This method notifies the parent state about changes made to the state element
        """
        if self.parent is not None:
            self.parent.model_changed(model, prop_name, info)

    @ModelMT.observe("meta_signal", signal=True)
    def meta_changed(self, model, prop_name, info):
        """This method notifies the parent state about changes made to the meta data
        """
        if self.parent is not None:
            msg = info.arg
            # Add information about notification to the signal message
            notification = Notification(model, prop_name, info)
            msg = msg._replace(notification=notification)
            info.arg = msg
            self.parent.meta_changed(model, prop_name, info)
Пример #8
0
class Selection(ModelMT):
    """ This class contains the selected models of a state_machine """
    _selected = None
    _input_data_ports = None
    _output_data_ports = None
    _scoped_variables = None
    _incomes = None
    _outcomes = None
    _data_flows = None
    _transitions = None
    _states = None

    _focus = None

    selection_changed_signal = Signal()
    focus_signal = Signal()

    __observables__ = ("selection_changed_signal", "focus_signal")

    def __init__(self, parent_signal=None):
        ModelMT.__init__(self)

        self._selected = set()
        self._incomes = set()
        self._input_data_ports = set()
        self._output_data_ports = set()
        self._outcomes = set()
        self._data_flows = set()
        self._transitions = set()
        self._states = set()
        self._scoped_variables = set()
        self.selection_changed_signal = Signal()
        self.focus_signal = Signal()
        self.parent_signal = parent_signal

    def __str__(self):
        return_string = "Selected: "
        for item in self._selected:
            return_string = "%s, %s" % (return_string, str(item))
        return return_string

    def _check_model_types(self, models):
        """ Check types of passed models for correctness and in case raise exception

        :rtype: set
        :returns: set of models that are valid for the class"""
        if not hasattr(models, "__iter__"):
            models = {models}
        if not all([isinstance(model, (AbstractStateModel, StateElementModel)) for model in models]):
            raise TypeError("The selection supports only models with base class AbstractStateModel or "
                            "StateElementModel, see handed elements {0}".format(models))
        return models if isinstance(models, set) else set(models)

    @updates_selection
    def add(self, models):
        """ Adds the passed model(s) to the selection"""
        if models is None:
            return

        models = self._check_model_types(models)
        self._selected.update(models)
        self._selected = reduce_to_parent_states(self._selected)

    @updates_selection
    def remove(self, models):
        """ Removed the passed model(s) from the selection"""
        models = self._check_model_types(models)
        for model in models:
            if model in self._selected:
                self._selected.remove(model)

    @updates_selection
    def set(self, models):
        """ Sets the selection to the passed model(s) """
        # Do not add None values to selection
        if models is None:
            models = set()

        models = self._check_model_types(models)
        if len(models) > 1:
            models = reduce_to_parent_states(models)

        self._selected = set(models)

    @updates_selection
    def clear(self):
        """ Removes all models from the selection """
        self._selected.clear()

    @updates_selection
    def handle_prepared_selection_of_core_class_elements(self, core_class, models):
        """Handles the selection for TreeStore widgets maintaining lists of a specific `core_class` elements

        If widgets hold a TreeStore with elements of a specific `core_class`, the local selection of that element
        type is handled by that widget. This method is called to integrate the local selection with the overall
        selection of the state machine.

        If no modifier key (indicating to extend the selection) is pressed, the state machine selection is set to the
        passed selection. If the selection is to be extended, the state machine collection will consist of the widget's
        selection plus all previously selected elements not having the core class `core_class`.

        :param State | StateElement core_class: The core class of the elements the widget handles
        :param models: The list of models that are currently being selected locally
        """
        if extend_selection():
            self._selected.difference_update(self.get_selected_elements_of_core_class(core_class))
        else:
            self._selected.clear()

        models = self._check_model_types(models)
        if len(models) > 1:
            models = reduce_to_parent_states(models)

        self._selected.update(models)

    @updates_selection
    def handle_new_selection(self, models):
        """Handles the selection for generic widgets

        This is a helper method for generic widgets that want to modify the selection. These widgets can pass a list
        of newly selected (or clicked on) models.

        The method looks at the previous selection, the passed models and the list of pressed (modifier) keys:

        * If no modifier key is pressed, the previous selection is cleared and the new selection is set to the passed
          models
        * If the extend-selection modifier key is pressed, elements of `models` that are _not_ in the previous
          selection are selected, those that are in the previous selection are deselected

        :param models: The list of models that are newly selected/clicked on
        """
        models = self._check_model_types(models)

        if extend_selection():
            already_selected_elements = models & self._selected
            newly_selected_elements = models - self._selected
            self._selected.difference_update(already_selected_elements)
            self._selected.update(newly_selected_elements)
        else:
            self._selected = models
        self._selected = reduce_to_parent_states(self._selected)

    @property
    def focus(self):
        """ Returns the currently focused element """
        return self._focus

    @focus.setter
    def focus(self, model):
        """Sets the passed model as focused element

        :param ModelMT model: The element to be focused
        """
        if model is None:
            del self.focus
            return

        self._check_model_types(model)
        self.add(model)
        focus_msg = FocusSignalMsg(model, self._focus)
        self._focus = model
        self._selected.add(model)
        self._selected = reduce_to_parent_states(self._selected)
        self.focus_signal.emit(focus_msg)

    @focus.deleter
    def focus(self):
        """ Unsets the focused element """
        focus_msg = FocusSignalMsg(None, self._focus)
        self._focus = None
        self.focus_signal.emit(focus_msg)

    def __iter__(self):
        return self._selected.__iter__()

    def __len__(self):
        return len(self._selected)

    def __contains__(self, item):
        return item in self._selected

    def __getitem__(self, key):
        return list(self._selected)[key]

    def update_core_element_lists(self):
        """ Maintains inner lists of selected elements with a specific core element class """
        def get_selected_elements_of_core_class(core_class):
            return set(element for element in self._selected if isinstance(element.core_element, core_class))
        self._states = get_selected_elements_of_core_class(State)
        self._transitions = get_selected_elements_of_core_class(Transition)
        self._data_flows = get_selected_elements_of_core_class(DataFlow)
        self._input_data_ports = get_selected_elements_of_core_class(InputDataPort)
        self._output_data_ports = get_selected_elements_of_core_class(OutputDataPort)
        self._scoped_variables = get_selected_elements_of_core_class(ScopedVariable)
        self._incomes = get_selected_elements_of_core_class(Income)
        self._outcomes = get_selected_elements_of_core_class(Outcome)

    @property
    def states(self):
        """Returns all selected states

        :return: Subset of the selection, only containing states
        :rtype: set
        """
        return self._states

    @property
    def transitions(self):
        """Returns all selected transitions

        :return: Subset of the selection, only containing transitions
        :rtype: set
        """
        return self._transitions

    @property
    def data_flows(self):
        """Returns all selected data flows

        :return: Subset of the selection, only containing data flows
        :rtype: set
        """
        return self._data_flows

    @property
    def incomes(self):
        """Returns all selected incomes

        :return: Subset of the selection, only containing incomes
        :rtype: set
        """
        return self._incomes

    @property
    def income(self):
        """Alias for ``incomes()``"""
        return self.incomes

    @property
    def outcomes(self):
        """Returns all selected outcomes

        :return: Subset of the selection, only containing outcomes
        :rtype: set
        """
        return self._outcomes

    @property
    def input_data_ports(self):
        """Returns all selected input data ports

        :return: Subset of the selection, only containing input data ports
        :rtype: set
        """
        return self._input_data_ports

    @property
    def output_data_ports(self):
        """Returns all selected output data ports

        :return: Subset of the selection, only containing output data ports
        :rtype: set
        """
        return self._output_data_ports

    @property
    def scoped_variables(self):
        """Returns all selected scoped variables

        :return: Subset of the selection, only containing scoped variables
        :rtype: set
        """
        return self._scoped_variables

    def get_selected_elements_of_core_class(self, core_element_type):
        """Returns all selected elements having the specified `core_element_type` as state element class

        :return: Subset of the selection, only containing elements having `core_element_type` as state element class
        :rtype: set
        """
        if core_element_type is Outcome:
            return self.outcomes
        elif core_element_type is InputDataPort:
            return self.input_data_ports
        elif core_element_type is OutputDataPort:
            return self.output_data_ports
        elif core_element_type is ScopedVariable:
            return self.scoped_variables
        elif core_element_type is Transition:
            return self.transitions
        elif core_element_type is DataFlow:
            return self.data_flows
        elif core_element_type is State:
            return self.states
        raise RuntimeError("Invalid core element type: " + core_element_type)

    def is_selected(self, model):
        """Checks whether the given model is selected

        :param model:
        :return: True if the model is within the selection, False else
        :rtype: bool
        """
        if model is None:
            return len(self._selected) == 0
        return model in self._selected

    def get_all(self):
        """Return a copy of the selection

        :return: Copy of the set of selected elements
        :rtype: set
        """
        return set(s for s in self._selected)

    def get_selected_state(self):
        """Return the first state within the selection

        :return: First state within the selection or None if there is none
        :rtype: AbstractStateModel
        """
        if not self.states:
            return None
        else:
            return next(iter(self.states))  # sets don't support indexing

    @ModelMT.observe("destruction_signal", signal=True)
    def on_model_destruct(self, destructed_model, signal, info):
        """ Deselect models that are being destroyed """
        self.remove(destructed_model)
Пример #9
0
class StateMachineModel(MetaModel, Hashable):
    """This model class manages a :class:`rafcon.core.state_machine.StateMachine`

    The model class is part of the MVC architecture. It holds the data to be shown (in this case a state machine).

    :param StateMachine state_machine: The state machine to be controlled and modified
    """

    state_machine = None
    selection = None
    root_state = None
    meta = None
    ongoing_complex_actions = None
    meta_signal = Signal()
    state_meta_signal = Signal()
    action_signal = Signal()
    state_action_signal = Signal()
    sm_selection_changed_signal = Signal()
    destruction_signal = Signal()

    suppress_new_root_state_model_one_time = False

    __observables__ = ("state_machine", "root_state", "meta_signal",
                       "state_meta_signal", "sm_selection_changed_signal",
                       "action_signal", "state_action_signal",
                       "destruction_signal", "ongoing_complex_actions")

    @measure_time
    def __init__(self, state_machine, meta=None, load_meta_data=True):
        """Constructor
        """
        MetaModel.__init__(self)  # pass columns as separate parameters

        assert isinstance(state_machine, StateMachine)

        self.state_machine = state_machine
        self.state_machine_id = state_machine.state_machine_id

        root_state = self.state_machine.root_state
        if isinstance(root_state, ContainerState):
            self.root_state = ContainerStateModel(
                root_state, parent=self, load_meta_data=load_meta_data)
        else:
            self.root_state = StateModel(root_state,
                                         parent=self,
                                         load_meta_data=load_meta_data)

        if isinstance(meta, Vividict):
            self.meta = meta
        else:
            self.meta = Vividict()

        # ongoing_complex_actions is updated by ComplexActionObserver -> secure encapsulated observation
        # and made observable by state machine model here
        self.ongoing_complex_actions = {}
        self.complex_action_observer = ComplexActionObserver(self)

        self.meta_signal = Signal()
        self.state_meta_signal = Signal()
        self.action_signal = Signal()
        self.state_action_signal = Signal()
        self.sm_selection_changed_signal = Signal()
        self.destruction_signal = Signal()

        self.temp = Vividict()

        if load_meta_data:
            self.load_meta_data(recursively=False)

        self.selection = Selection(self.sm_selection_changed_signal)

        self.storage_lock = threading.Lock(
        )  # lock can not be substituted by the state machine lock -> maybe because it is a RLock

        self.history = None
        if global_gui_config.get_config_value('HISTORY_ENABLED'):
            from rafcon.gui.models.modification_history import ModificationsHistoryModel
            self.history = ModificationsHistoryModel(self)
        else:
            logger.info("The modification history is disabled")

        self.auto_backup = None
        if global_gui_config.get_config_value('AUTO_BACKUP_ENABLED'):
            from rafcon.gui.models.auto_backup import AutoBackupModel
            self.auto_backup = AutoBackupModel(self)

        self.root_state.register_observer(self)
        self.register_observer(self)

    def __eq__(self, other):
        if isinstance(other, StateMachineModel):
            return self.root_state == other.root_state and self.meta == other.meta
        else:
            return False

    def __hash__(self):
        return id(self)

    def __copy__(self):
        sm_m = self.__class__(copy(self.state_machine))
        sm_m.root_state.copy_meta_data_from_state_m(self.root_state)
        sm_m.meta = deepcopy(self.meta)
        return sm_m

    def __deepcopy__(self, memo=None, _nil=[]):
        return self.__copy__()

    @property
    def core_element(self):
        return self.state_machine

    def __destroy__(self):
        self.destroy()

    def destroy(self):
        if self.auto_backup is not None:
            self.auto_backup.destroy()
            self.auto_backup = None

    def prepare_destruction(self):
        """Prepares the model for destruction

        Unregister itself as observer from the state machine and the root state
        """
        if self.state_machine is None:
            logger.verbose(
                "Multiple calls of prepare destruction for {0}".format(self))
        self.destruction_signal.emit()
        if self.history is not None:
            self.history.prepare_destruction()
        if self.auto_backup is not None:
            self.auto_backup.prepare_destruction()
        try:
            self.unregister_observer(self)
            self.root_state.unregister_observer(self)
        except KeyError:  # Might happen if the observer was already unregistered
            pass
        with self.state_machine.modification_lock():
            self.root_state.prepare_destruction()
        self.root_state = None
        self.state_machine = None
        super(StateMachineModel, self).prepare_destruction()

    def update_hash(self, obj_hash):
        self.update_hash_from_dict(obj_hash, self.root_state)
        self.update_hash_from_dict(obj_hash, self.meta)

    def update_meta_data_hash(self, obj_hash):
        super(StateMachineModel, self).update_meta_data_hash(obj_hash)
        self.root_state.update_meta_data_hash(obj_hash)

    @ModelMT.observe("state", before=True)
    @ModelMT.observe("income", before=True)
    @ModelMT.observe("outcomes", before=True)
    @ModelMT.observe("is_start", before=True)
    @ModelMT.observe("states", before=True)
    @ModelMT.observe("transitions", before=True)
    @ModelMT.observe("data_flows", before=True)
    @ModelMT.observe("input_data_ports", before=True)
    @ModelMT.observe("scoped_variables", before=True)
    def root_state_model_before_change(self, model, prop_name, info):
        if not self._list_modified(prop_name, info):
            self._send_root_state_notification(model, prop_name, info)

    @ModelMT.observe("state", after=True)
    @ModelMT.observe("income", after=True)
    @ModelMT.observe("outcomes", after=True)
    @ModelMT.observe("is_start", after=True)
    @ModelMT.observe("states", after=True)
    @ModelMT.observe("transitions", after=True)
    @ModelMT.observe("data_flows", after=True)
    @ModelMT.observe("input_data_ports", after=True)
    @ModelMT.observe("output_data_ports", after=True)
    @ModelMT.observe("scoped_variables", after=True)
    def root_state_model_after_change(self, model, prop_name, info):
        if not self._list_modified(prop_name, info):
            self._send_root_state_notification(model, prop_name, info)

    @ModelMT.observe("state_machine", after=True)
    def state_machine_model_after_change(self, model, prop_name, info):
        overview = NotificationOverview(info)
        if overview.caused_modification():
            if not self.state_machine.marked_dirty:
                self.state_machine.marked_dirty = True

    @ModelMT.observe("meta_signal", signal=True)
    def meta_changed(self, model, prop_name, info):
        """When the meta was changed, we have to set the dirty flag, as the changes are unsaved"""
        self.state_machine.marked_dirty = True
        msg = info.arg
        if model is not self and msg.change.startswith(
                'sm_notification_'):  # Signal was caused by the root state
            # Emit state_meta_signal to inform observing controllers about changes made to the meta data within the
            # state machine
            # -> removes mark of "sm_notification_"-prepend to mark root-state msg forwarded to state machine label
            msg = msg._replace(
                change=msg.change.replace('sm_notification_', '', 1))
            self.state_meta_signal.emit(msg)

    @ModelMT.observe("action_signal", signal=True)
    def action_signal_triggered(self, model, prop_name, info):
        """When the action was performed, we have to set the dirty flag, as the changes are unsaved"""
        self.state_machine.marked_dirty = True
        msg = info.arg
        if model is not self and msg.action.startswith(
                'sm_notification_'):  # Signal was caused by the root state
            # Emit state_action_signal to inform observing controllers about changes made to the state within the
            # state machine
            # -> removes mark of "sm_notification_"-prepend to mark root-state msg forwarded to state machine label
            msg = msg._replace(
                action=msg.action.replace('sm_notification_', '', 1))
            self.state_action_signal.emit(msg)

    @staticmethod
    def _list_modified(prop_name, info):
        """Check whether the given operation is a list operation

        The function checks whether the property that has been changed is a list. If so, the operation is investigated
        further. If the operation is a basic list operation, the function return True.
        :param prop_name: The property that was changed
        :param info: Dictionary with information about the operation
        :return: True if the operation was a list operation, False else
        """
        if prop_name in [
                "states", "transitions", "data_flows", "input_data_ports",
                "output_data_ports", "scoped_variables", "outcomes"
        ]:
            if info['method_name'] in [
                    "append", "extend", "insert", "pop", "remove", "reverse",
                    "sort", "__delitem__", "__setitem__"
            ]:
                return True
        return False

    def get_state_model_by_path(self, path):
        """Returns the `StateModel` for the given `path` 
        
        Searches a `StateModel` in the state machine, who's path is given by `path`.
        
        :param str path: Path of the searched state 
        :return: The state with that path
        :rtype: StateModel
        :raises: ValueError, if path is invalid/not existing with this state machine  
        """
        path_elements = path.split('/')
        path_elements.pop(0)
        current_state_model = self.root_state
        for state_id in path_elements:
            if isinstance(current_state_model, ContainerStateModel):
                if state_id in current_state_model.states:
                    current_state_model = current_state_model.states[state_id]
                else:
                    raise ValueError(
                        "Invalid path: State with id '{}' not found in state with id {}"
                        .format(state_id, current_state_model.state.state_id))
            elif isinstance(current_state_model, LibraryStateModel):
                if state_id == current_state_model.state_copy.state.state_id:
                    current_state_model = current_state_model.state_copy
                else:
                    raise ValueError(
                        "Invalid path: state id '{}' does not coincide with state id '{}' of state_copy "
                        "of library state with id '{}'".format(
                            state_id,
                            current_state_model.state_copy.state.state_id,
                            current_state_model.state.state_id))
            else:
                raise ValueError(
                    "Invalid path: State with id '{}' has no children".format(
                        current_state_model.state.state_id))
        return current_state_model

    @ModelMT.observe("state_machine", after=True)
    def root_state_assign(self, model, prop_name, info):
        if info.method_name != 'root_state':
            return
        if self.suppress_new_root_state_model_one_time:
            self.suppress_new_root_state_model_one_time = False
            return
        try:
            self.root_state.unregister_observer(self)
        except KeyError:
            pass
        if isinstance(self.state_machine.root_state,
                      ContainerState):  # could not be a LibraryState
            self.root_state = ContainerStateModel(
                self.state_machine.root_state)
        else:
            assert not isinstance(self.state_machine.root_state, LibraryState)
            self.root_state = StateModel(self.state_machine.root_state)
        self.root_state.register_observer(self)

    @ModelMT.observe("state_machine", after=True, before=True)
    def change_root_state_type(self, model, prop_name, info):
        if info.method_name != 'change_root_state_type':
            return

        if 'before' in info:
            self._send_root_state_notification(model, prop_name, info)
        else:
            # Do not forward the notification yet, but store its parameters locally at the function
            # The function helpers.state.change_state_type will forward the notification after some preparation
            self.change_root_state_type.__func__.suppressed_notification_parameters = [
                model, prop_name, info
            ]

    def _send_root_state_notification(self, model, prop_name, info):
        cause = 'root_state_change'
        try:
            if 'before' in info:
                self.state_machine.notify_before(self.state_machine, cause,
                                                 (self.state_machine, ), info)
            elif 'after' in info:
                self.state_machine.notify_after(self.state_machine, cause,
                                                None, (self.state_machine, ),
                                                info)
        except AssertionError as e:
            # This fixes an AssertionError raised by GTKMVC. It can probably occur, when a controller unregisters
            # itself from a model, while the notification chain still propagates upwards.
            logger.exception(
                "A exception occurs in the _send_root_state_notification method {0}."
                .format(e))

    #######################################################
    # --------------------- meta data methods ---------------------
    #######################################################

    def load_meta_data(self, path=None, recursively=True):
        """Load meta data of state machine model from the file system

        The meta data of the state machine model is loaded from the file system and stored in the meta property of the
        model. Existing meta data is removed. Also the meta data of root state and children is loaded.

        :param str path: Optional path to the meta data file. If not given, the path will be derived from the state
            machine's path on the filesystem
        """
        meta_data_path = path if path is not None else self.state_machine.file_system_path

        if meta_data_path:
            path_meta_data = os.path.join(meta_data_path,
                                          storage.FILE_NAME_META_DATA)

            try:
                tmp_meta = storage.load_data_file(path_meta_data)
            except ValueError:
                tmp_meta = {}
        else:
            tmp_meta = {}

        # JSON returns a dict, which must be converted to a Vividict
        tmp_meta = Vividict(tmp_meta)

        if recursively:
            root_state_path = None if not path else os.path.join(
                path, self.root_state.state.state_id)
            self.root_state.load_meta_data(root_state_path)

        if tmp_meta:
            # assign the meta data to the state
            self.meta = tmp_meta
            self.meta_signal.emit(MetaSignalMsg("load_meta_data", "all", True))

    def store_meta_data(self, copy_path=None):
        """Save meta data of the state machine model to the file system

        This method generates a dictionary of the meta data of the state machine and stores it on the filesystem.

        :param str copy_path: Optional, if the path is specified, it will be used instead of the file system path
        """
        if copy_path:
            meta_file_json = os.path.join(copy_path,
                                          storage.FILE_NAME_META_DATA)
        else:
            meta_file_json = os.path.join(self.state_machine.file_system_path,
                                          storage.FILE_NAME_META_DATA)

        storage_utils.write_dict_to_json(self.meta, meta_file_json)

        self.root_state.store_meta_data(copy_path)
Пример #10
0
class AbstractStateModel(MetaModel, Hashable):
    """This is an abstract class serving as base class for state models

    The model class is part of the MVC architecture. It holds the data to be shown (in this case a state).

    :param state: The state to be managed which can be any derivative of rafcon.core.states.state.State.
    :param AbstractStateModel parent: The state to be managed
    :param rafcon.utils.vividict.Vividict meta: The meta data of the state
     """

    _parent = None
    _is_about_to_be_destroyed_recursively = False
    is_start = None
    state = None
    income = None
    outcomes = []
    input_data_ports = []
    output_data_ports = []
    meta_signal = Signal()
    action_signal = Signal()
    destruction_signal = Signal()

    state_counter = 0

    __observables__ = ("state", "input_data_ports", "output_data_ports",
                       "income", "outcomes", "is_start", "meta_signal",
                       "action_signal", "destruction_signal")

    def __init__(self, state, parent=None, meta=None):
        AbstractStateModel.state_counter += 1
        if type(self) == AbstractStateModel:
            raise NotImplementedError

        MetaModel.__init__(self, meta)
        assert isinstance(state, State)
        self.is_start = False

        self.state = state

        self.parent = parent

        self.meta_signal = Signal()
        self.action_signal = Signal()
        self.destruction_signal = Signal()

        self.register_observer(self)

        self.input_data_ports = []
        self.output_data_ports = []
        self.outcomes = []
        self._load_port_models()

    def __str__(self):
        return "Model of state: {0}".format(self.state)

    def __eq__(self, other):
        if type(self) != type(other):
            return False
        if self.state != other.state:
            return False
        if self.meta != other.meta:
            return False
        for attr in self.state.state_element_attrs:
            my_attr = getattr(self, attr)
            other_attr = getattr(other, attr)
            if isinstance(my_attr, wrappers.IterableWrapper):
                if isinstance(my_attr, wrappers.ListWrapper):
                    elements = my_attr
                    other_elements = other_attr
                elif isinstance(my_attr, wrappers.DictWrapper):
                    elements = my_attr.items()
                    other_elements = other_attr.items()
                else:
                    raise ValueError("Unsupported state element type: " +
                                     str(type(my_attr)))
                if len(elements) != len(other_elements):
                    return False
                if not all(
                    [element in other_elements for element in other_elements]):
                    return False
            else:
                return my_attr == other_attr
        return True

    def __hash__(self):
        return id(self)

    def __ne__(self, other):
        return not self.__eq__(other)

    def __cmp__(self, other):
        if isinstance(other, AbstractStateModel):
            return self.core_element.__cmp__(other.core_element)

    def __lt__(self, other):
        return self.__cmp__(other) < 0

    def __contains__(self, item):
        """Checks whether `item` is an element of the state model

        Following child items are checked: outcomes, input data ports, output data ports

        :param item: :class:`StateModel` or :class:`StateElementModel`
        :return: Whether item is a direct child of this state
        :rtype: bool
        """
        from rafcon.gui.models.state_element import StateElementModel
        if not isinstance(item, StateElementModel):
            return False
        return item is self.income or item in self.outcomes or \
               item in self.input_data_ports or item in self.output_data_ports

    def __copy__(self):
        state = copy(self.state)
        state_m = self.__class__(state,
                                 parent=None,
                                 meta=None,
                                 load_meta_data=False)
        state_m.copy_meta_data_from_state_m(self)
        return state_m

    def __deepcopy__(self, memo=None, _nil=[]):
        return self.__copy__()

    def update_is_start(self):
        """Updates the `is_start` property of the state
        
        A state is a start state, if it is the root state, it has no parent, the parent is a LibraryState or the state's
        state_id is identical with the ContainerState.start_state_id of the ContainerState it is within.
        """
        self.is_start = self.state.is_root_state or \
                        self.parent is None or \
                        isinstance(self.parent.state, LibraryState) or \
                        self.state.state_id == self.state.parent.start_state_id

    @property
    def core_element(self):
        return self.state

    @property
    def hierarchy_level(self):
        # TODO rewrite it to be more efficient -> try a recursive pattern on parent
        return len(self.state.get_path().split('/'))

    def prepare_destruction(self, recursive=True):
        """Prepares the model for destruction

        Recursively un-registers all observers and removes references to child models
        """
        if self.state is None:
            logger.verbose(
                "Multiple calls of prepare destruction for {0}".format(self))
        self.destruction_signal.emit()
        try:
            self.unregister_observer(self)
        except KeyError:  # Might happen if the observer was already unregistered
            logger.verbose("Observer already unregistered!")
            pass
        if recursive:
            if self.income:
                self.income.prepare_destruction()
            for port in self.input_data_ports[:] + self.output_data_ports[:] + self.outcomes[:]:
                port.prepare_destruction()
        del self.input_data_ports[:]
        del self.output_data_ports[:]
        del self.outcomes[:]
        self.state = None
        self.input_data_ports = None
        self.output_data_ports = None
        self.income = None
        self.outcomes = None
        # History TODO: these are needed by the modification history
        # self.action_signal = None
        # self.meta_signal = None
        # self.destruction_signal = None
        self.observe = None
        super(AbstractStateModel, self).prepare_destruction()

    def update_hash(self, obj_hash):
        self.update_hash_from_dict(obj_hash, self.core_element)
        for state_element in sorted([self.income] + self.outcomes[:] +
                                    self.input_data_ports[:] +
                                    self.output_data_ports[:]):
            self.update_hash_from_dict(obj_hash, state_element)
        if not self.state.get_next_upper_library_root_state():
            self.update_hash_from_dict(obj_hash, self.meta)

    def update_meta_data_hash(self, obj_hash):
        super(AbstractStateModel, self).update_meta_data_hash(obj_hash)
        for state_element in sorted([self.income] + self.outcomes[:] +
                                    self.input_data_ports[:] +
                                    self.output_data_ports[:]):
            state_element.update_meta_data_hash(obj_hash)

    @property
    def parent(self):
        if not self._parent:
            return None
        return self._parent()

    @parent.setter
    def parent(self, parent_m):
        if isinstance(parent_m, AbstractStateModel):
            self._parent = ref(parent_m)
        else:
            self._parent = None
        self.update_is_start()

    @property
    def is_about_to_be_destroyed_recursively(self):
        return self._is_about_to_be_destroyed_recursively

    @is_about_to_be_destroyed_recursively.setter
    def is_about_to_be_destroyed_recursively(self, value):
        if not isinstance(value, bool):
            raise TypeError(
                "The is_about_to_be_destroyed_recursively property has to be boolean."
            )
        self._is_about_to_be_destroyed_recursively = value

    def get_state_machine_m(self, two_factor_check=True):
        """ Get respective state machine model

        Get a reference of the state machine model the state model belongs to. As long as the root state model
        has no direct reference to its state machine model the state machine manager model is checked respective model.

        :rtype: rafcon.gui.models.state_machine.StateMachineModel
        :return: respective state machine model
        """
        from rafcon.gui.singleton import state_machine_manager_model
        state_machine = self.state.get_state_machine()
        if state_machine:
            if state_machine.state_machine_id in state_machine_manager_model.state_machines:
                sm_m = state_machine_manager_model.state_machines[
                    state_machine.state_machine_id]
                if not two_factor_check or sm_m.get_state_model_by_path(
                        self.state.get_path()) is self:
                    return sm_m
                else:
                    logger.debug(
                        "State model requesting its state machine model parent seems to be obsolete. "
                        "This is a hint to duplicated models and dirty coding")

        return None

    def get_input_data_port_m(self, data_port_id):
        """Returns the input data port model for the given data port id

        :param data_port_id: The data port id to search for
        :return: The model of the data port with the given id
        """
        for data_port_m in self.input_data_ports:
            if data_port_m.data_port.data_port_id == data_port_id:
                return data_port_m
        return None

    def get_output_data_port_m(self, data_port_id):
        """Returns the output data port model for the given data port id

        :param data_port_id: The data port id to search for
        :return: The model of the data port with the given id
        """
        for data_port_m in self.output_data_ports:
            if data_port_m.data_port.data_port_id == data_port_id:
                return data_port_m
        return None

    def get_data_port_m(self, data_port_id):
        """Searches and returns the model of a data port of a given state

        The method searches a port with the given id in the data ports of the given state model. If the state model
        is a container state, not only the input and output data ports are looked at, but also the scoped variables.

        :param data_port_id: The data port id to be searched
        :return: The model of the data port or None if it is not found
        """
        from itertools import chain
        data_ports_m = chain(self.input_data_ports, self.output_data_ports)
        for data_port_m in data_ports_m:
            if data_port_m.data_port.data_port_id == data_port_id:
                return data_port_m
        return None

    def get_outcome_m(self, outcome_id):
        """Returns the outcome model for the given outcome id

        :param outcome_id: The outcome id to search for
        :return: The model of the outcome with the given id
        """
        for outcome_m in self.outcomes:
            if outcome_m.outcome.outcome_id == outcome_id:
                return outcome_m
        return False

    def _load_port_models(self):
        self._load_income_model()
        self._load_outcome_models()
        self._load_input_data_port_models()
        self._load_output_data_port_models()

    def _load_input_data_port_models(self):
        raise NotImplementedError

    def _load_output_data_port_models(self):
        raise NotImplementedError

    def _load_income_model(self):
        raise NotImplementedError

    def _load_outcome_models(self):
        raise NotImplementedError

    def child_model_changed(self, notification_overview):
        return self.state != notification_overview.get_affected_core_element()

    @ModelMT.observe("state", after=True, before=True)
    def model_changed(self, model, prop_name, info):
        """This method notifies parent state about changes made to the state
        """
        # Notify the parent state about the change (this causes a recursive call up to the root state)
        if self.parent is not None:
            self.parent.model_changed(model, prop_name, info)

        if prop_name == 'parent':
            self.update_is_start()

    @ModelMT.observe("action_signal", signal=True)
    def action_signal_triggered(self, model, prop_name, info):
        """This method notifies the parent state and child state models about complex actions
        """
        msg = info.arg
        if msg.action.startswith('sm_notification_'):
            return
        if any([m in self for m in info['arg'].affected_models]):
            if not msg.action.startswith('parent_notification_'):
                new_msg = msg._replace(action='parent_notification_' +
                                       msg.action)
            else:
                new_msg = msg
            for m in info['arg'].affected_models:
                if isinstance(m, AbstractStateModel) and m in self:
                    m.action_signal.emit(new_msg)

        if msg.action.startswith('parent_notification_'):
            return

        # recursive propagation of action signal TODO remove finally
        if self.parent is not None:
            # Notify parent about change of meta data
            info.arg = msg
            self.parent.action_signal_triggered(model, prop_name, info)
        # state machine propagation of action signal (indirect) TODO remove finally
        elif not msg.action.startswith(
                'sm_notification_'):  # Prevent recursive call
            # If we are the root state, inform the state machine model by emitting our own meta signal.
            # To make the signal distinguishable for a change of meta data to our state, the change property of
            # the message is prepended with 'sm_notification_'
            new_msg = msg._replace(action='sm_notification_' + msg.action)
            self.action_signal.emit(new_msg)

    @ModelMT.observe("meta_signal", signal=True)
    def meta_changed(self, model, prop_name, info):
        """This method notifies the parent state about changes made to the meta data
        """
        msg = info.arg
        if msg.notification is None:
            # Meta data of this state was changed, add information about notification to the signal message
            notification = Notification(model, prop_name, info)
            msg = msg._replace(notification=notification)

        if self.parent is not None:
            # Notify parent about change of meta data
            info.arg = msg
            self.parent.meta_changed(model, prop_name, info)
        elif not msg.change.startswith(
                'sm_notification_'):  # Prevent recursive call
            # If we are the root state, inform the state machine model by emitting our own meta signal.
            # To make the signal distinguishable for a change of meta data to our state, the change property of
            # the message is prepended with 'sm_notification_'
            msg = msg._replace(change='sm_notification_' + msg.change)
            self.meta_signal.emit(msg)

    # ---------------------------------------- meta data methods ---------------------------------------------

    def load_meta_data(self, path=None):
        """Load meta data of state model from the file system

        The meta data of the state model is loaded from the file system and stored in the meta property of the model.
        Existing meta data is removed. Also the meta data of all state elements (data ports, outcomes,
        etc) are loaded, as those stored in the same file as the meta data of the state.

        This is either called on the __init__ of a new state model or if a state model for a container state is created,
        which then calls load_meta_data for all its children.

        :param str path: Optional file system path to the meta data file. If not given, the path will be derived from
            the state's path on the filesystem
        :return: if meta data file was loaded True otherwise False
        :rtype: bool
        """
        if not path:
            path = self.state.file_system_path
        if path is None:
            self.meta = Vividict({})
            return False
        path_meta_data = os.path.join(path, storage.FILE_NAME_META_DATA)
        try:
            tmp_meta = storage.load_data_file(path_meta_data)
        except ValueError as e:
            if not path.startswith(
                    constants.RAFCON_TEMP_PATH_STORAGE) and not os.path.exists(
                        os.path.dirname(path)):
                logger.debug(
                    "Because '{1}' meta data of {0} was not loaded properly.".
                    format(self, e))
            tmp_meta = {}

        # JSON returns a dict, which must be converted to a Vividict
        tmp_meta = Vividict(tmp_meta)

        if tmp_meta:
            self._parse_for_element_meta_data(tmp_meta)
            # assign the meta data to the state
            self.meta = tmp_meta
            self.meta_signal.emit(MetaSignalMsg("load_meta_data", "all", True))
            return True
        else:
            return False

    def store_meta_data(self, copy_path=None):
        """Save meta data of state model to the file system

        This method generates a dictionary of the meta data of the state together with the meta data of all state
        elements (data ports, outcomes, etc.) and stores it on the filesystem.
        Secure that the store meta data method is called after storing the core data otherwise the last_stored_path is
        maybe wrong or None.
        The copy path is considered to be a state machine file system path but not the current one but e.g.
        of a as copy saved state machine. The meta data will be stored in respective relative state folder in the state
        machine  hierarchy. This folder has to exist.
        Dues the core elements of the state machine has to be stored first.

        :param str copy_path: Optional copy path if meta data is not stored to the file system path of state machine
        """
        if copy_path:
            meta_file_path_json = os.path.join(copy_path,
                                               self.state.get_storage_path(),
                                               storage.FILE_NAME_META_DATA)
        else:
            if self.state.file_system_path is None:
                logger.error(
                    "Meta data of {0} can be stored temporary arbitrary but by default first after the "
                    "respective state was stored and a file system path is set."
                    .format(self))
                return
            meta_file_path_json = os.path.join(self.state.file_system_path,
                                               storage.FILE_NAME_META_DATA)
        meta_data = deepcopy(self.meta)
        self._generate_element_meta_data(meta_data)
        storage_utils.write_dict_to_json(meta_data, meta_file_path_json)

    def copy_meta_data_from_state_m(self, source_state_m):
        """Dismiss current meta data and copy meta data from given state model

        The meta data of the given state model is used as meta data for this state. Also the meta data of all state
        elements (data ports, outcomes, etc.) is overwritten with the meta data of the elements of the given state.

        :param source_state_m: State model to load the meta data from
        """

        self.meta = deepcopy(source_state_m.meta)

        for input_data_port_m in self.input_data_ports:
            source_data_port_m = source_state_m.get_input_data_port_m(
                input_data_port_m.data_port.data_port_id)
            input_data_port_m.meta = deepcopy(source_data_port_m.meta)
        for output_data_port_m in self.output_data_ports:
            source_data_port_m = source_state_m.get_output_data_port_m(
                output_data_port_m.data_port.data_port_id)
            output_data_port_m.meta = deepcopy(source_data_port_m.meta)
        for outcome_m in self.outcomes:
            source_outcome_m = source_state_m.get_outcome_m(
                outcome_m.outcome.outcome_id)
            outcome_m.meta = deepcopy(source_outcome_m.meta)
        self.income.meta = deepcopy(source_state_m.income.meta)

        self.meta_signal.emit(MetaSignalMsg("copy_state_m", "all", True))

    def _parse_for_element_meta_data(self, meta_data):
        """Load meta data for state elements

        The meta data of the state meta data file also contains the meta data for state elements (data ports,
        outcomes, etc). This method parses the loaded meta data for each state element model. The meta data of the
        elements is removed from the passed dictionary.

        :param meta_data: Dictionary of loaded meta data
        """
        for data_port_m in self.input_data_ports:
            self._copy_element_meta_data_from_meta_file_data(
                meta_data, data_port_m, "input_data_port",
                data_port_m.data_port.data_port_id)
        for data_port_m in self.output_data_ports:
            self._copy_element_meta_data_from_meta_file_data(
                meta_data, data_port_m, "output_data_port",
                data_port_m.data_port.data_port_id)
        for outcome_m in self.outcomes:
            self._copy_element_meta_data_from_meta_file_data(
                meta_data, outcome_m, "outcome", outcome_m.outcome.outcome_id)
        if "income" in meta_data:
            if "gui" in meta_data and "editor_gaphas" in meta_data["gui"] and \
                    "income" in meta_data["gui"]["editor_gaphas"]:  # chain necessary to prevent key generation
                del meta_data["gui"]["editor_gaphas"]["income"]
        elif "gui" in meta_data and "editor_gaphas" in meta_data["gui"] and \
                "income" in meta_data["gui"]["editor_gaphas"]:  # chain necessary to prevent key generation in meta data
            meta_data["income"]["gui"]["editor_gaphas"] = meta_data["gui"][
                "editor_gaphas"]["income"]
            del meta_data["gui"]["editor_gaphas"]["income"]
        self._copy_element_meta_data_from_meta_file_data(
            meta_data, self.income, "income", "")

    @staticmethod
    def _copy_element_meta_data_from_meta_file_data(meta_data, element_m,
                                                    element_name, element_id):
        """Helper method to assign the meta of the given element

        The method assigns the meta data of the elements from the given meta data dictionary. The copied meta data is
        then removed from the dictionary.

        :param meta_data: The loaded meta data
        :param element_m: The element model that is supposed to retrieve the meta data
        :param element_name: The name string of the element type in the dictionary
        :param element_id: The id of the element
        """
        meta_data_element_id = element_name + str(element_id)
        meta_data_element = meta_data[meta_data_element_id]
        element_m.meta = meta_data_element
        del meta_data[meta_data_element_id]

    def _generate_element_meta_data(self, meta_data):
        """Generate meta data for state elements and add it to the given dictionary

        This method retrieves the meta data of the state elements (data ports, outcomes, etc) and adds it to the
        given meta data dictionary.

        :param meta_data: Dictionary of meta data
        """
        for data_port_m in self.input_data_ports:
            self._copy_element_meta_data_to_meta_file_data(
                meta_data, data_port_m, "input_data_port",
                data_port_m.data_port.data_port_id)
        for data_port_m in self.output_data_ports:
            self._copy_element_meta_data_to_meta_file_data(
                meta_data, data_port_m, "output_data_port",
                data_port_m.data_port.data_port_id)
        for outcome_m in self.outcomes:
            self._copy_element_meta_data_to_meta_file_data(
                meta_data, outcome_m, "outcome", outcome_m.outcome.outcome_id)

        self._copy_element_meta_data_to_meta_file_data(meta_data, self.income,
                                                       "income", "")

    @staticmethod
    def _copy_element_meta_data_to_meta_file_data(meta_data, element_m,
                                                  element_name, element_id):
        """Helper method to generate meta data for meta data file for the given element

        The methods loads teh meta data of the given element and copies it into the given meta data dictionary
        intended for being stored on the filesystem.

        :param meta_data: The meta data to be stored
        :param element_m: The element model to get the meta data from
        :param element_name: The name string of the element type in the dictionary
        :param element_id: The id of the element
        """
        meta_data_element_id = element_name + str(element_id)
        meta_data_element = element_m.meta
        meta_data[meta_data_element_id] = meta_data_element

    def _meta_data_editor_gaphas2opengl(self, vividict):
        vividict = mirror_y_axis_in_vividict_element(vividict, 'rel_pos')
        if 'income' in vividict:
            del vividict['income']
        if 'name' in vividict:
            del vividict['name']
        return vividict

    def _meta_data_editor_opengl2gaphas(self, vividict):
        from rafcon.gui.helpers.meta_data import contains_geometric_info
        vividict = mirror_y_axis_in_vividict_element(vividict, 'rel_pos')
        if contains_geometric_info(vividict['size']):
            self.temp['conversion_from_opengl'] = True
            size = vividict['size']

            # Determine size and position of NameView
            margin = min(size) / 12.
            name_height = min(size) / 8.
            name_width = size[0] - 2 * margin
            vividict['name']['size'] = (name_width, name_height)
            vividict['name']['rel_pos'] = (margin, margin)
        return vividict