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 __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 __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 __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()
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__()
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()
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)
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)
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)
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