class NameView(Element): def __init__(self, name, size): super(NameView, self).__init__(size[0], size[1]) # Reapply size, as Gaphas sets default minimum size to 1, which is too large for highly nested states self.min_width = self.min_height = 0 self.width = size[0] self.height = size[1] self._name = None self.name = name self.moving = False self._view = None self._image_cache = ImageCache(multiplicator=1.5) def remove(self): self.canvas.remove(self) def update_minimum_size(self): min_side_length = min(self.parent.width, self.parent.height) / constants.MAXIMUM_NAME_TO_PARENT_STATE_SIZE_RATIO if min_side_length != self.min_width: self.min_width = min_side_length if min_side_length != self.min_height: self.min_height = min_side_length @property def name(self): return self._name @name.setter def name(self, name): assert isinstance(name, basestring) self._name = name @property def parent(self): return self.canvas.get_parent(self) @property def model(self): return self.parent.model @property def position(self): _, _, _, _, x0, y0 = self.matrix return x0, y0 @position.setter def position(self, pos): self.matrix = Matrix(x0=pos[0], y0=pos[1]) @property def view(self): if not self._view: self._view = self.canvas.get_first_view() return self._view @property def transparency(self): if self.parent.show_content(with_content=True): return gui_config.get_config_value('SHOW_CONTENT_LIBRARY_NAME_TRANSPARENCY', 0.5) return self.parent.transparency def apply_meta_data(self): name_meta = self.parent.model.get_meta_data_editor()['name'] # logger.info("name rel_pos {}".format(name_meta['rel_pos'])) # logger.info("name size {}".format(name_meta['size'])) self.position = name_meta['rel_pos'] # print "name pos from meta", name_meta['rel_pos'] self.width = name_meta['size'][0] self.height = name_meta['size'][1] def draw(self, context): # Do not draw if # * state (or its parent) is currently moved # * core element is no longer existing (must have just been removed) # * is root state of a library (drawing would hide the LibraryState itself) if not self.model.state or self.moving or self.parent.model.state.is_root_state_of_library: return width = self.width height = self.height view_width, view_height = self.view.get_matrix_i2v(self).transform_distance(width, height) if min(view_width, view_height) < constants.MINIMUM_NAME_SIZE_FOR_DISPLAY and not context.draw_all: return font_transparency = self.transparency c = context.cairo parameters = { 'name': self.name, 'selected': context.selected, 'transparency': font_transparency } upper_left_corner = (0, 0) current_zoom = self.view.get_zoom_factor() from_cache, image, zoom = self._image_cache.get_cached_image(width, height, current_zoom, parameters) # The parameters for drawing haven't changed, thus we can just copy the content from the last rendering result if from_cache: self._image_cache.copy_image_to_context(c, upper_left_corner) # Parameters have changed or nothing in cache => redraw else: c = self._image_cache.get_context_for_image(current_zoom) if context.selected: # Draw light background color if selected c.rectangle(0, 0, width, height) c.set_source_rgba(*gap_draw_helper.get_col_rgba(gui_config.gtk_colors['LABEL'], transparency=.9)) c.fill_preserve() c.set_source_rgba(0, 0, 0, 0) c.stroke() c.set_antialias(cairo.ANTIALIAS_SUBPIXEL) layout = c.create_layout() layout.set_wrap(WRAP_WORD) layout.set_width(int(round(BASE_WIDTH * SCALE))) layout.set_text(self.name) def set_font_description(font_size): font = FontDescription(font_name + " " + str(font_size)) layout.set_font_description(font) font_name = constants.INTERFACE_FONT zoom_scale = BASE_WIDTH / width scaled_height = height * zoom_scale font_size_parameters = {"text": self.name, "height": scaled_height} font_size = self.view.value_cache.get_value("font_size", font_size_parameters) if font_size: set_font_description(font_size) else: available_size = (BASE_WIDTH * SCALE, scaled_height * SCALE) word_count = len(self.name.split(" ")) # Set max font size to available height max_font_size = scaled_height * 0.9 # Calculate minimum size that is still to be drawn min_name_height = max_font_size / 10. # Calculate line height if all words are wrapped line_height = max_font_size / word_count # Use minimum if previous values and add safety margin min_font_size = min(line_height * 0.5, min_name_height) # Iteratively calculate font size by always choosing the average of the maximum and minimum size working_font_size = None current_font_size = (max_font_size + min_font_size) / 2. set_font_description(current_font_size) while True: logical_extents = layout.get_size() width_factor = logical_extents[0] / available_size[0] height_factor = logical_extents[1] / available_size[1] max_factor = max(width_factor, height_factor) if max_factor > 1: # font size too large max_font_size = current_font_size elif max_factor > 0.9: # font size fits! break else: # font size too small # Nevertheless store the font size in case we do not find anything better if not working_font_size or current_font_size > working_font_size: working_font_size = current_font_size min_font_size = current_font_size if 0.99 < min_font_size / max_font_size < 1.01: # Stop criterion: changes too small if working_font_size: current_font_size = working_font_size set_font_description(current_font_size) break current_font_size = (max_font_size + min_font_size) / 2. set_font_description(current_font_size) self.view.value_cache.store_value("font_size", current_font_size, font_size_parameters) c.move_to(*self.handles()[NW].pos) c.set_source_rgba(*get_col_rgba(gui_config.gtk_colors['STATE_NAME'], font_transparency)) c.save() # The pango layout has a fixed width and needs to be fitted to the context size c.scale(1. / zoom_scale, 1. / zoom_scale) c.update_layout(layout) c.show_layout(layout) c.restore() # Copy image surface to current cairo context self._image_cache.copy_image_to_context(context.cairo, upper_left_corner, zoom=current_zoom)
class PortView(object): def __init__(self, in_port, name=None, parent=None, side=SnappedSide.RIGHT): self.handle = Handle(connectable=True) self.port = RectanglePointPort(self.handle.pos, self) self._is_in_port = in_port self._side = None self.direction = None self.side = side self._parent = ref(parent) self._view = None self.text_color = gui_config.gtk_colors['LABEL'] self.fill_color = gui_config.gtk_colors['LABEL'] self._incoming_handles = [] self._outgoing_handles = [] self._connected_connections = [] self._tmp_incoming_connected = False self._tmp_outgoing_connected = False self._name = name self.label_print_inside = True self._port_image_cache = ImageCache() self._label_image_cache = ImageCache() self._last_label_size = self.port_side_size, self.port_side_size self._last_label_relative_pos = 0, 0 def __getattr__(self, name): """Return parental attributes for unknown attributes The PortView class is now Gaphas item, however it is often treated like that. Therefore, several expected attributes are missing. In these cases, the corresponding attribute of the parental StateView is returned. :param str name: Name of teh requested attribute :return: Parental value of the attribute """ try: return getattr(self.parent, name) except Exception: # This workarounds are needed because parent is a weak reference and if the the state is already destroy # the name of its parent can not be accessed even if the view of the port still exists # TODO D-check if ports can be destroyed proper before the state view is collected by the garbage collector if name == "name": return self._name raise def handles(self): return [self.handle] @property def side(self): return self._side @side.setter @observed def side(self, side): self._side = side self.direction = None if self.side is SnappedSide.LEFT: self.direction = Direction.RIGHT if self._is_in_port else Direction.LEFT elif self.side is SnappedSide.TOP: self.direction = Direction.DOWN if self._is_in_port else Direction.UP elif self.side is SnappedSide.RIGHT: self.direction = Direction.LEFT if self._is_in_port else Direction.RIGHT elif self.side is SnappedSide.BOTTOM: self.direction = Direction.UP if self._is_in_port else Direction.DOWN @property def port_side_size(self): parent = self.parent if not parent: logger.warning("PortView without parent: {}".format(self)) return 1 return parent.border_width @property def name(self): return self._name @property def parent(self): return self._parent() @property def pos(self): return self.handle.pos @property def handle_pos(self): return self.handle.pos @property def port_pos(self): return self.port.point @property def port_size(self): return self.port_side_size / 1.5, self.port_side_size @property def view(self): if not self._view: self._view = self.parent.canvas.get_first_view() return self._view def has_outgoing_connection(self): return len(self._outgoing_handles) > 0 def has_incoming_connection(self): return len(self._incoming_handles) > 0 def add_connected_handle(self, handle, connection_view, moving=False): from rafcon.gui.mygaphas.items.connection import ConnectionView assert isinstance(handle, Handle) assert isinstance(connection_view, ConnectionView) if not moving and handle is connection_view.from_handle( ) and handle not in self._outgoing_handles: self._outgoing_handles.append(handle) self._add_connection(connection_view) elif not moving and handle is connection_view.to_handle( ) and handle not in self._incoming_handles: self._incoming_handles.append(handle) self._add_connection(connection_view) def has_label(self): return False def is_selected(self): return self in self.parent.canvas.get_first_view().selected_items def _add_connection(self, connection_view): if connection_view not in self.connected_connections: self._connected_connections.append(ref(connection_view)) def remove_connected_handle(self, handle): assert isinstance(handle, Handle) if handle in self._incoming_handles: self._incoming_handles.remove(handle) for conn in self.connected_connections: if conn.to_handle() is handle: self._connected_connections.remove(ref(conn)) elif handle in self._outgoing_handles: self._outgoing_handles.remove(handle) for conn in self.connected_connections: if conn.from_handle() is handle: self._connected_connections.remove(ref(conn)) def tmp_connect(self, handle, connection_view): if handle is connection_view.from_handle(): self._tmp_outgoing_connected = True elif handle is connection_view.to_handle(): self._tmp_incoming_connected = True def tmp_disconnect(self): self._tmp_incoming_connected = False self._tmp_outgoing_connected = False @property def connected(self): return self.connected_incoming or self.connected_outgoing @property def connected_outgoing(self): if len(self._outgoing_handles) == 0: return self._tmp_outgoing_connected return True @property def connected_incoming(self): if len(self._incoming_handles) == 0: return self._tmp_incoming_connected return True @property def connected_connections(self): return [connection() for connection in self._connected_connections] def get_port_area(self, view): """Calculates the drawing area affected by the (hovered) port """ state_v = self.parent center = self.handle.pos margin = self.port_side_size / 4. if self.side in [SnappedSide.LEFT, SnappedSide.RIGHT]: height, width = self.port_size else: width, height = self.port_size upper_left = center[0] - width / 2 - margin, center[ 1] - height / 2 - margin lower_right = center[0] + width / 2 + margin, center[ 1] + height / 2 + margin port_upper_left = view.get_matrix_i2v(state_v).transform_point( *upper_left) port_lower_right = view.get_matrix_i2v(state_v).transform_point( *lower_right) size = port_lower_right[0] - port_upper_left[0], port_lower_right[ 1] - port_upper_left[1] return port_upper_left[0], port_upper_left[1], size[0], size[1] def draw(self, context, state): raise NotImplementedError def draw_port(self, context, fill_color, transparency, value=None): c = context.cairo view = self.parent.canvas.get_first_view() side_length = self.port_side_size position = self.pos # Do not draw ports below a certain threshold size matrix_i2v = view.get_matrix_i2v(self.parent) view_length, _ = matrix_i2v.transform_distance(side_length, 0) if view_length < constants.MINIMUM_PORT_SIZE_FOR_DISPLAY and not context.draw_all: return # Do not draw port outside of the view center = (position.x.value, position.y.value) view_center = matrix_i2v.transform_point(*center) if view_center[0] + view_length / 2. < 0 or \ view_center[0] - view_length / 2. > view.get_allocation().width or \ view_center[1] + view_length / 2. < 0 or \ view_center[1] - view_length / 2. > view.get_allocation().height: if not context.draw_all: return parent_state_m = self.parent.model is_library_state_with_content_shown = self.parent.show_content() parameters = { 'selected': self.is_selected(), 'direction': self.direction, 'side_length': side_length, 'fill_color': fill_color, 'transparency': transparency, 'incoming': self.connected_incoming, 'outgoing': self.connected_outgoing, 'is_library_state_with_content_shown': is_library_state_with_content_shown } upper_left_corner = (position.x.value - side_length / 2., position.y.value - side_length / 2.) current_zoom = view.get_zoom_factor() from_cache, image, zoom = self._port_image_cache.get_cached_image( side_length, side_length, current_zoom, parameters) # The parameters for drawing haven't changed, thus we can just copy the content from the last rendering result if from_cache: # print("from cache") self._port_image_cache.copy_image_to_context(c, upper_left_corner) # Parameters have changed or nothing in cache => redraw else: # print("draw") c = self._port_image_cache.get_context_for_image(current_zoom) c.move_to(0, 0) if isinstance(parent_state_m, ContainerStateModel ) or is_library_state_with_content_shown: self._draw_container_state_port(c, self.direction, fill_color, transparency) else: self._draw_simple_state_port(c, self.direction, fill_color, transparency) # Copy image surface to current cairo context self._port_image_cache.copy_image_to_context(context.cairo, upper_left_corner, zoom=current_zoom) if self.name and self.has_label(): self.draw_name(context, transparency, value) if self.is_selected( ) or self.handle is view.hovered_handle or context.draw_all: context.cairo.move_to(*self.pos) self._draw_hover_effect(context.cairo, self.direction, fill_color, transparency) def draw_name(self, context, transparency, value): c = context.cairo port_height = self.port_size[1] label_position = self.side if not self.label_print_inside else self.side.opposite( ) position = self.pos show_additional_value = False if global_runtime_config.get_config_value( "SHOW_DATA_FLOW_VALUE_LABELS", True) and value is not None: show_additional_value = True parameters = { 'name': self.name, 'port_height': port_height, 'side': label_position, 'transparency': transparency, 'show_additional_value': show_additional_value } # add value to parameters only when value is shown on label if show_additional_value: parameters['value'] = value upper_left_corner = (position[0] + self._last_label_relative_pos[0], position[1] + self._last_label_relative_pos[1]) current_zoom = self.parent.canvas.get_first_view().get_zoom_factor() from_cache, image, zoom = self._label_image_cache.get_cached_image( self._last_label_size[0], self._last_label_size[1], current_zoom, parameters) # The parameters for drawing haven't changed, thus we can just copy the content from the last rendering result if from_cache and not context.draw_all: # print("draw port name from cache") self._label_image_cache.copy_image_to_context(c, upper_left_corner) # Parameters have changed or nothing in cache => redraw else: # print("draw port name") # First we have to do a "dry run", in order to determine the size of the new label c.move_to(position.x.value, position.y.value) extents = gap_draw_helper.draw_port_label( c, self, transparency, False, label_position, show_additional_value, value, only_extent_calculations=True) from rafcon.gui.mygaphas.utils.gap_helper import extend_extents extents = extend_extents(extents, factor=1.1) label_pos = extents[0], extents[1] relative_pos = label_pos[0] - position[0], label_pos[1] - position[ 1] label_size = extents[2] - extents[0], extents[3] - extents[1] # print(label_size[0], self.name, self.parent.model.state.name) # if label_size[0] < constants.MINIMUM_PORT_NAME_SIZE_FOR_DISPLAY and self.parent: # return self._last_label_relative_pos = relative_pos self._last_label_size = label_size # The size information is used to update the caching parameters and retrieve an image with the correct size self._label_image_cache.get_cached_image(label_size[0], label_size[1], current_zoom, parameters, clear=True) c = self._label_image_cache.get_context_for_image(current_zoom) c.move_to(-relative_pos[0], -relative_pos[1]) gap_draw_helper.draw_port_label(c, self, transparency, False, label_position, show_additional_value, value) # Copy image surface to current cairo context upper_left_corner = (position[0] + relative_pos[0], position[1] + relative_pos[1]) self._label_image_cache.copy_image_to_context(context.cairo, upper_left_corner, zoom=current_zoom) # draw_all means, the bounding box of the state is calculated # As we are using drawing operation, not supported by Gaphas, we manually need to update the bounding box if context.draw_all: from gaphas.geometry import Rectangle view = self.parent.canvas.get_first_view() abs_pos = view.get_matrix_i2v( self.parent).transform_point(*label_pos) abs_pos1 = view.get_matrix_i2v(self.parent).transform_point( extents[2], extents[3]) bounds = Rectangle(abs_pos[0], abs_pos[1], x1=abs_pos1[0], y1=abs_pos1[1]) context.cairo._update_bounds(bounds) def _draw_simple_state_port(self, context, direction, color, transparency): """Draw the port of a simple state (ExecutionState, LibraryState) Connector for execution states can only be connected to the outside. Thus the connector fills the whole border of the state. :param context: Cairo context :param direction: The direction the port is pointing to :param color: Desired color of the port :param transparency: The level of transparency """ c = context width, height = self.port_size c.set_line_width(self.port_side_size * 0.03 * self._port_image_cache.multiplicator) # Save/restore context, as we move and rotate the connector to the desired pose c.save() c.rel_move_to(self.port_side_size / 2., self.port_side_size / 2.) PortView._rotate_context(c, direction) PortView._draw_single_connector(c, width, height) c.restore() # Colorize the generated connector path if self.connected: c.set_source_rgba( *gap_draw_helper.get_col_rgba(color, transparency)) else: c.set_source_rgb( *gui_config.gtk_colors['PORT_UNCONNECTED'].to_floats()) c.fill_preserve() c.set_source_rgba(*gap_draw_helper.get_col_rgba(color, transparency)) c.stroke() def _draw_container_state_port(self, context, direction, color, transparency): """Draw the port of a container state Connector for container states are split in an inner connector and an outer connector. :param context: Cairo context :param direction: The direction the port is pointing to :param color: Desired color of the port :param float transparency: The level of transparency """ c = context width, height = self.port_size c.set_line_width(self.port_side_size / constants.BORDER_WIDTH_OUTLINE_WIDTH_FACTOR * self._port_image_cache.multiplicator) # Save/restore context, as we move and rotate the connector to the desired pose cur_point = c.get_current_point() c.save() c.rel_move_to(self.port_side_size / 2., self.port_side_size / 2.) PortView._rotate_context(c, direction) PortView._draw_inner_connector(c, width, height) c.restore() if self.connected_incoming: c.set_source_rgba( *gap_draw_helper.get_col_rgba(color, transparency)) else: c.set_source_rgb( *gui_config.gtk_colors['PORT_UNCONNECTED'].to_floats()) c.fill_preserve() c.set_source_rgba(*gap_draw_helper.get_col_rgba(color, transparency)) c.stroke() c.move_to(*cur_point) c.save() c.rel_move_to(self.port_side_size / 2., self.port_side_size / 2.) PortView._rotate_context(c, direction) PortView._draw_outer_connector(c, width, height) c.restore() if self.connected_outgoing: c.set_source_rgba( *gap_draw_helper.get_col_rgba(color, transparency)) else: c.set_source_rgb( *gui_config.gtk_colors['PORT_UNCONNECTED'].to_floats()) c.fill_preserve() c.set_source_rgba(*gap_draw_helper.get_col_rgba(color, transparency)) c.stroke() def _draw_hover_effect(self, context, direction, color, transparency): c = context width, height = self.port_size c.set_line_width(self.port_side_size * 0.03 * self._port_image_cache.multiplicator) margin = self.port_side_size / 4. # Save/restore context, as we move and rotate the connector to the desired pose c.save() PortView._rotate_context(c, direction) PortView._draw_rectangle(c, width + margin, height + margin) c.restore() c.set_source_rgba(*gap_draw_helper.get_col_rgba(color, transparency)) c.stroke() @staticmethod def _draw_single_connector(context, width, height): """Draw the connector for execution states Connector for execution states can only be connected to the outside. Thus the connector fills the whole border of the state. :param context: Cairo context :param float port_size: The side length of the port """ c = context # Current pos is center # Arrow is drawn upright arrow_height = height / 2.0 # First move to bottom left corner c.rel_move_to(-width / 2., height / 2.) # Draw line to bottom right corner c.rel_line_to(width, 0) # Draw line to upper right corner c.rel_line_to(0, -(height - arrow_height)) # Draw line to center top (arrow) c.rel_line_to(-width / 2., -arrow_height) # Draw line to upper left corner c.rel_line_to(-width / 2., arrow_height) # Draw line back to the origin (lower left corner) c.close_path() @staticmethod def _draw_inner_connector(context, width, height): """Draw the connector for container states Connector for container states can be connected from the inside and the outside. Thus the connector is split in two parts: A rectangle on the inside and an arrow on the outside. This methods draws the inner rectangle. :param context: Cairo context :param float port_size: The side length of the port """ c = context # Current pos is center # Arrow is drawn upright gap = height / 6. connector_height = (height - gap) / 2. # First move to bottom left corner c.rel_move_to(-width / 2., height / 2.) # Draw inner connector (rectangle) c.rel_line_to(width, 0) c.rel_line_to(0, -connector_height) c.rel_line_to(-width, 0) c.close_path() @staticmethod def _draw_outer_connector(context, width, height): """Draw the outer connector for container states Connector for container states can be connected from the inside and the outside. Thus the connector is split in two parts: A rectangle on the inside and an arrow on the outside. This method draws the outer arrow. :param context: Cairo context :param float port_size: The side length of the port """ c = context # Current pos is center # Arrow is drawn upright arrow_height = height / 2.5 gap = height / 6. connector_height = (height - gap) / 2. # Move to bottom left corner of outer connector c.rel_move_to(-width / 2., -gap / 2.) # Draw line to bottom right corner c.rel_line_to(width, 0) # Draw line to upper right corner c.rel_line_to(0, -(connector_height - arrow_height)) # Draw line to center top (arrow) c.rel_line_to(-width / 2., -arrow_height) # Draw line to upper left corner c.rel_line_to(-width / 2., arrow_height) # Draw line back to the origin (lower left corner) c.close_path() @staticmethod def _draw_rectangle(context, width, height): """Draw a rectangle Assertion: The current point is the center point of the rectangle :param context: Cairo context :param width: Width of the rectangle :param height: Height of the rectangle """ c = context # First move to upper left corner c.rel_move_to(-width / 2., -height / 2.) # Draw closed rectangle c.rel_line_to(width, 0) c.rel_line_to(0, height) c.rel_line_to(-width, 0) c.close_path() @staticmethod def _rotate_context(context, direction): """Moves the current position to 'position' and rotates the context according to 'direction' :param context: Cairo context :param direction: Direction enum """ if direction is Direction.UP: pass elif direction is Direction.RIGHT: context.rotate(deg2rad(90)) elif direction is Direction.DOWN: context.rotate(deg2rad(180)) elif direction is Direction.LEFT: context.rotate(deg2rad(-90))
class StateView(Element): """ A State has 4 handles (for a start): NW +---+ NE SW +---+ SE """ _map_handles_port_v = {} def __init__(self, state_m, size, hierarchy_level): super(StateView, self).__init__(size[0], size[1]) assert isinstance(state_m, AbstractStateModel) # Reapply size, as Gaphas sets default minimum size to 1, which is too large for highly nested states self.min_width = self.min_height = 0 self.width = size[0] self.height = size[1] self.is_root_state_of_library = state_m.state.is_root_state_of_library self._state_m = ref(state_m) self.hierarchy_level = hierarchy_level self._income = None self._outcomes = [] self._inputs = [] self._outputs = [] self._scoped_variables = [] self._scoped_variables_ports = [] self.keep_rect_constraints = {} self.port_constraints = {} self._moving = False self._view = None self.__symbol_size_cache = {} self._image_cache = ImageCache() self._border_width = Variable(min(self.width, self.height) / constants.BORDER_WIDTH_STATE_SIZE_FACTOR) border_width_constraint = BorderWidthConstraint(self._handles[NW].pos, self._handles[SE].pos, self._border_width, constants.BORDER_WIDTH_STATE_SIZE_FACTOR) self._constraints.append(border_width_constraint) # Initialize NameView name_meta = state_m.get_meta_data_editor()['name'] if not contains_geometric_info(name_meta['size']): name_width = self.width * 0.8 name_height = self.height * 0.4 name_meta = state_m.set_meta_data_editor('name.size', (name_width, name_height))['name'] name_size = name_meta['size'] self._name_view = NameView(state_m.state.name, name_size) if not contains_geometric_info(name_meta['rel_pos']): name_meta['rel_pos'] = (self.border_width, self.border_width) name_pos = name_meta['rel_pos'] self.name_view.matrix.translate(*name_pos) @property def selected(self): return self in self.view.selected_items @property def hovered(self): return self is self.view.hovered_item @property def view(self): if not self._view: self._view = self.canvas.get_first_view() return self._view def setup_canvas(self): self._income = self.add_income() canvas = self.canvas parent = self.parent self.update_minimum_size() # Draw NameView beneath all other state elements canvas.add(self.name_view, self, index=0) self.name_view.update_minimum_size() self.add_keep_rect_within_constraint(canvas, self, self._name_view) if parent is not None: assert isinstance(parent, StateView) self.add_keep_rect_within_constraint(canvas, parent, self) # Registers local constraints super(StateView, self).setup_canvas() def update_minimum_size(self): if not self.parent: self.min_width = 1 self.min_height = 1 else: min_side_length = min(self.parent.width, self.parent.height) / \ constants.MAXIMUM_CHILD_TO_PARENT_STATE_SIZE_RATIO if min_side_length != self.min_width: self.min_width = min_side_length if min_side_length != self.min_height: self.min_height = min_side_length def update_minimum_size_of_children(self): if self.canvas: for constraint in self.constraints: self.canvas.solver.request_resolve_constraint(constraint) if not self.canvas.solver._solving: self.canvas.solver.solve() for item in self.canvas.get_all_children(self): if isinstance(item, (StateView, NameView)): item.update_minimum_size() def get_all_ports(self): port_list = [self.income] port_list += self.outcomes port_list += self.inputs port_list += self.outputs port_list += self.scoped_variables return port_list def get_logic_ports(self): port_list = [self.income] port_list += self.outcomes return port_list def get_data_ports(self): port_list = self.inputs port_list += self.outputs port_list += self.scoped_variables return port_list def remove(self): """Remove recursively all children and then the StateView itself """ self.canvas.get_first_view().unselect_item(self) for child in self.canvas.get_children(self)[:]: child.remove() self.remove_income() for outcome_v in self.outcomes[:]: self.remove_outcome(outcome_v) for input_port_v in self.inputs[:]: self.remove_input_port(input_port_v) for output_port_v in self.outputs[:]: self.remove_output_port(output_port_v) for scoped_variable_port_v in self.scoped_variables[:]: self.remove_scoped_variable(scoped_variable_port_v) self.remove_keep_rect_within_constraint_from_parent() for constraint in self._constraints[:]: self.canvas.solver.remove_constraint(constraint) self._constraints.remove(constraint) self.canvas.remove(self) @staticmethod def add_keep_rect_within_constraint(canvas, parent, child): solver = canvas.solver child_nw = ItemProjection(child.handles()[NW].pos, child, parent) child_se = ItemProjection(child.handles()[SE].pos, child, parent) constraint = KeepRectangleWithinConstraint(parent.handles()[NW].pos, parent.handles()[SE].pos, child_nw, child_se, child, lambda: parent.border_width) solver.add_constraint(constraint) parent.keep_rect_constraints[child] = constraint def remove_keep_rect_within_constraint_from_parent(self): canvas = self.canvas solver = canvas.solver name_constraint = self.keep_rect_constraints.pop(self.name_view) solver.remove_constraint(name_constraint) parent_state_v = self.parent if parent_state_v is not None and isinstance(parent_state_v, StateView): constraint = parent_state_v.keep_rect_constraints.pop(self) solver.remove_constraint(constraint) def has_selected_child(self): for child in self.canvas.get_children(self): if isinstance(child, StateView) and child.selected: return True return False @property def position(self): _, _, _, _, x0, y0 = self.matrix return x0, y0 @position.setter def position(self, pos): self.matrix = Matrix(x0=pos[0], y0=pos[1]) @property def show_data_port_label(self): return global_runtime_config.get_config_value("SHOW_DATA_FLOWS", True) @property def moving(self): return self._moving @moving.setter def moving(self, moving): assert isinstance(moving, bool) self._moving = moving for child in self.canvas.get_children(self): if isinstance(child, (StateView, NameView)): child.moving = moving @property def border_width(self): return self._border_width.value @property def parent(self): return self.canvas.get_parent(self) @property def corner_handles(self): return [self.handles()[NW], self.handles()[NE], self.handles()[SW], self.handles()[SE]] @property def aborted_preempted_handles(self): return [self.outcomes[-1].handle, self.outcomes[-2].handle] @property def model(self): return self._state_m() @model.setter def model(self, state_m): if self.model: self.canvas.exchange_model(self.model, state_m) self._state_m = ref(state_m) @property def income(self): return self._income @property def outcomes(self): return self._outcomes @property def outputs(self): return self._outputs @property def inputs(self): return copy(self._inputs) @property def scoped_variables(self): return self._scoped_variables_ports @property def name_view(self): return self._name_view @property def transparency(self): """Calculates the transparency for the state :return: State transparency :rtype: float """ # TODO: Implement transparency logic here (e.g. for different viewing modes) return 0. def child_state_views(self): for child in self.canvas.get_children(self): if isinstance(child, StateView): yield child def show_content(self, with_content=False): """Checks if the state is a library with the `show_content` flag set :param with_content: If this parameter is `True`, the method return only True if the library represents a ContainerState :return: Whether the content of a library state is shown """ if isinstance(self.model, LibraryStateModel) and self.model.show_content(): return not with_content or isinstance(self.model.state_copy, ContainerStateModel) return False @staticmethod def get_state_drawing_area(state): assert isinstance(state, StateView) border_width = state.border_width state_nw_pos_x = state.handles()[NW].pos.x + border_width state_nw_pos_y = state.handles()[NW].pos.y + border_width state_nw_pos = Position((state_nw_pos_x, state_nw_pos_y)) state_se_pos_x = state.handles()[SE].pos.x - border_width state_se_pos_y = state.handles()[SE].pos.y - border_width state_se_pos = Position((state_se_pos_x, state_se_pos_y)) return state_nw_pos, state_se_pos def apply_meta_data(self, recursive=False): state_meta = self.model.get_meta_data_editor() self.position = state_meta['rel_pos'] self.width = state_meta['size'][0] self.height = state_meta['size'][1] self.update_minimum_size_of_children() def update_port_position(port_v, meta_data): if isinstance(meta_data['rel_pos'], tuple) and len(meta_data['rel_pos']) == 2: port_v.handle.pos = meta_data['rel_pos'] self.port_constraints[port_v].update_position(meta_data['rel_pos']) if isinstance(state_meta['income']['rel_pos'], tuple) and len(state_meta['income']['rel_pos']) == 2: update_port_position(self.income, state_meta['income']) for outcome_v in self.outcomes: update_port_position(outcome_v, outcome_v.model.get_meta_data_editor()) for data_port_v in self.inputs + self.outputs: update_port_position(data_port_v, data_port_v.model.get_meta_data_editor()) self.name_view.apply_meta_data() if isinstance(self.model, ContainerStateModel): for scoped_port_v in self.scoped_variables: update_port_position(scoped_port_v, scoped_port_v.model.get_meta_data_editor()) for transition_m in self.model.transitions: transition_v = self.canvas.get_view_for_model(transition_m) transition_v.apply_meta_data() if recursive: for state_v in self.canvas.get_children(self): if isinstance(state_v, StateView): state_v.apply_meta_data(recursive=True) def draw(self, context): # Do not draw if # * state (or its parent) is currently moved # * core element is no longer existing (must have just been removed) # * is root state of a library (drawing would hide the LibraryState itself) if not self.model.state or self.moving and self.parent and self.parent.moving or \ self.model.state.is_root_state_of_library: if not context.draw_all: return width = self.width height = self.height border_width = self.border_width view_width, view_height = self.view.get_matrix_i2v(self).transform_distance(width, height) if min(view_width, view_height) < constants.MINIMUM_STATE_SIZE_FOR_DISPLAY and self.parent and not \ context.draw_all: return c = context.cairo nw = self._handles[NW].pos parameters = { 'execution_state': self.model.state.state_execution_status, 'selected': self.selected, 'moving': self.moving, 'border_width': border_width, 'transparency': self.transparency, 'draw_all': context.draw_all } upper_left_corner = (nw.x.value, nw.y.value) current_zoom = self.view.get_zoom_factor() from_cache, image, zoom = self._image_cache.get_cached_image(width, height, current_zoom, parameters) # The parameters for drawing haven't changed, thus we can just copy the content from the last rendering result if from_cache: self._image_cache.copy_image_to_context(c, upper_left_corner) # Parameters have changed or nothing in cache => redraw else: c = self._image_cache.get_context_for_image(current_zoom) multiplicator = self._image_cache.multiplicator default_line_width = border_width / constants.BORDER_WIDTH_OUTLINE_WIDTH_FACTOR * multiplicator c.rectangle(nw.x, nw.y, width, height) state_background_color = gui_config.gtk_colors['STATE_BACKGROUND'] state_border_color = gui_config.gtk_colors['STATE_BORDER'] state_border_outline_color = gui_config.gtk_colors['STATE_BORDER_OUTLINE'] if self.model.state.state_execution_status == StateExecutionStatus.WAIT_FOR_NEXT_STATE: state_border_color = gui_config.gtk_colors['STATE_WAITING_BORDER'] state_border_outline_color = gui_config.gtk_colors['STATE_WAITING_BORDER_OUTLINE'] elif self.model.state.active: state_border_color = gui_config.gtk_colors['STATE_ACTIVE_BORDER'] state_border_outline_color = gui_config.gtk_colors['STATE_ACTIVE_BORDER_OUTLINE'] elif self.selected: state_border_color = gui_config.gtk_colors['STATE_SELECTED_BORDER'] state_border_outline_color = gui_config.gtk_colors['STATE_SELECTED_BORDER_OUTLINE'] c.set_source_rgba(*get_col_rgba(state_border_color, self.transparency)) c.fill_preserve() c.set_source_rgba(*get_col_rgba(state_border_outline_color, self.transparency)) # The line gets cropped at the context border, therefore the line width must be doubled c.set_line_width(default_line_width * 2) c.stroke() inner_nw, inner_se = self.get_state_drawing_area(self) c.rectangle(inner_nw.x, inner_nw.y, inner_se.x - inner_nw.x, inner_se.y - inner_nw.y) c.set_source_rgba(*get_col_rgba(state_background_color)) c.fill_preserve() c.set_source_rgba(*get_col_rgba(state_border_outline_color, self.transparency)) c.set_line_width(default_line_width) c.stroke() # Copy image surface to current cairo context self._image_cache.copy_image_to_context(context.cairo, upper_left_corner, zoom=current_zoom) self._income.draw(context, self) for outcome_v in self._outcomes: highlight = self.model.state.active and outcome_v.model.outcome is self.model.state.final_outcome outcome_v.draw(context, self, highlight) for input_v in self._inputs: input_v.draw(context, self) for output_v in self._outputs: output_v.draw(context, self) for scoped_variable_v in self._scoped_variables_ports: scoped_variable_v.draw(context, self) if isinstance(self.model, LibraryStateModel) and not self.moving: symbol_transparency = 0.9 if self.show_content(with_content=True) else 0.75 self._draw_symbol(context, constants.SIGN_LIB, gui_config.gtk_colors['STATE_NAME'], symbol_transparency) if self.moving: self._draw_symbol(context, constants.SIGN_ARROW, gui_config.gtk_colors['STATE_NAME']) def _draw_symbol(self, context, symbol, color, transparency=0.): c = context.cairo width = self.width height = self.height c.set_antialias(cairo.ANTIALIAS_SUBPIXEL) layout = c.create_layout() font_name = constants.ICON_FONT def set_font_description(): layout.set_markup('<span font_desc="%s %s">&#x%s;</span>' % (font_name, font_size, symbol)) if symbol in self.__symbol_size_cache and \ self.__symbol_size_cache[symbol]['width'] == width and \ self.__symbol_size_cache[symbol]['height'] == height: font_size = self.__symbol_size_cache[symbol]['size'] set_font_description() else: font_size = 30 set_font_description() pango_size = (width * SCALE, height * SCALE) while layout.get_size()[0] > pango_size[0] or layout.get_size()[1] > pango_size[1]: font_size *= 0.9 set_font_description() self.__symbol_size_cache[symbol] = {'width': width, 'height': height, 'size': font_size} c.move_to(width / 2. - layout.get_size()[0] / float(SCALE) / 2., height / 2. - layout.get_size()[1] / float(SCALE) / 2.) c.set_source_rgba(*gap_draw_helper.get_col_rgba(color, transparency)) c.update_layout(layout) c.show_layout(layout) def get_transitions(self): transitions = [] for child in self.canvas.get_children(self): if isinstance(child, TransitionView): transitions.append(child) return transitions def connect_connection_to_port(self, connection_v, port, as_target=True): handle = connection_v.to_handle() if as_target else connection_v.from_handle() if isinstance(port, IncomeView): self.connect_to_income(connection_v, handle) elif isinstance(port, OutcomeView): self.connect_to_outcome(port.outcome_id, connection_v, handle) elif isinstance(port, InputPortView): self.connect_to_input_port(port.port_id, connection_v, handle) elif isinstance(port, OutputPortView): self.connect_to_output_port(port.port_id, connection_v, handle) elif isinstance(port, ScopedVariablePortView): self.connect_to_scoped_variable_port(port.port_id, connection_v, handle) def connect_to_income(self, connection_v, handle): self._income.add_connected_handle(handle, connection_v) connection_v.set_port_for_handle(self._income, handle) self._connect_to_port(self._income.port, connection_v, handle) def connect_to_outcome(self, outcome_id, connection_v, handle): outcome_v = self.outcome_port(outcome_id) outcome_v.add_connected_handle(handle, connection_v) connection_v.set_port_for_handle(outcome_v, handle) self._connect_to_port(outcome_v.port, connection_v, handle) def connect_to_input_port(self, port_id, connection_v, handle): port_v = self.input_port(port_id) port_v.add_connected_handle(handle, connection_v) connection_v.set_port_for_handle(port_v, handle) self._connect_to_port(port_v.port, connection_v, handle) def connect_to_output_port(self, port_id, connection_v, handle): port_v = self.output_port(port_id) port_v.add_connected_handle(handle, connection_v) connection_v.set_port_for_handle(port_v, handle) self._connect_to_port(port_v.port, connection_v, handle) def connect_to_scoped_variable_port(self, scoped_variable_id, connection_v, handle): port_v = self.scoped_variable(scoped_variable_id) port_v.add_connected_handle(handle, connection_v) connection_v.set_port_for_handle(port_v, handle) self._connect_to_port(port_v.port, connection_v, handle) def _connect_to_port(self, port, connection_v, handle): c = port.constraint(self.canvas, connection_v, handle, self) self.canvas.connect_item(connection_v, handle, self, port, c) def income_port(self): return self._income def outcome_port(self, outcome_id): for outcome in self._outcomes: if outcome.outcome_id == outcome_id: return outcome raise AttributeError("Outcome with id '{0}' not found in state".format(outcome_id, self.model.state.name)) def input_port(self, port_id): return self._data_port(self._inputs, port_id) def output_port(self, port_id): return self._data_port(self._outputs, port_id) def scoped_variable(self, scoped_variable_id): return self._data_port(self._scoped_variables_ports, scoped_variable_id) def get_port_for_handle(self, handle): if handle in self._map_handles_port_v: return self._map_handles_port_v[handle] return None def _data_port(self, port_list, port_id): for port in port_list: if port.port_id == port_id: return port raise AttributeError("Port with id '{0}' not found in state".format(port_id, self.model.state.name)) def add_income(self): income_v = IncomeView(self) self._ports.append(income_v.port) self._handles.append(income_v.handle) self._map_handles_port_v[income_v.handle] = income_v port_meta = self.model.get_meta_data_editor()['income'] if not contains_geometric_info(port_meta['rel_pos']): # print "generate rel_pos" # Position income on the top of the left state side income_v.side = SnappedSide.LEFT pos_x = 0 pos_y = self._calculate_port_pos_on_line(1, self.height) port_meta = self.model.set_meta_data_editor('income.rel_pos', (pos_x, pos_y))['income'] # print "add income", self.model, self.model.parent, port_meta['rel_pos'] income_v.handle.pos = port_meta['rel_pos'] self.add_rect_constraint_for_port(income_v) return income_v def remove_income(self): income_v = self._income del self._map_handles_port_v[income_v.handle] self._income = None self._ports.remove(income_v.port) self._handles.remove(income_v.handle) if income_v in self.port_constraints: self.canvas.solver.remove_constraint(self.port_constraints.pop(income_v)) def add_outcome(self, outcome_m): outcome_v = OutcomeView(outcome_m, self) self.canvas.add_port(outcome_v) self._outcomes.append(outcome_v) self._ports.append(outcome_v.port) self._handles.append(outcome_v.handle) self._map_handles_port_v[outcome_v.handle] = outcome_v port_meta = outcome_m.get_meta_data_editor() if not contains_geometric_info(port_meta['rel_pos']): # print "generate rel_pos" if outcome_m.outcome.outcome_id < 0: # Position aborted/preempted in upper right corner outcome_v.side = SnappedSide.TOP pos_x = self.width - self._calculate_port_pos_on_line(abs(outcome_m.outcome.outcome_id), self.width) pos_y = 0 else: # Distribute outcomes on the right side of the state, starting from top outcome_v.side = SnappedSide.RIGHT pos_x = self.width number_of_outcome = [o.model for o in self.outcomes if o.model.outcome.outcome_id >= 0].index(outcome_m) + 1 pos_y = self._calculate_port_pos_on_line(number_of_outcome, self.height) port_meta = outcome_m.set_meta_data_editor('rel_pos', (pos_x, pos_y)) # print "add outcome", self.model, self.model.parent, port_meta['rel_pos'] outcome_v.handle.pos = port_meta['rel_pos'] self.add_rect_constraint_for_port(outcome_v) def remove_outcome(self, outcome_v): del self._map_handles_port_v[outcome_v.handle] self._outcomes.remove(outcome_v) self._ports.remove(outcome_v.port) self._handles.remove(outcome_v.handle) self.canvas.remove_port(outcome_v) if outcome_v in self.port_constraints: self.canvas.solver.remove_constraint(self.port_constraints.pop(outcome_v)) def add_input_port(self, port_m): input_port_v = InputPortView(self, port_m) self.canvas.add_port(input_port_v) self._inputs.append(input_port_v) self._ports.append(input_port_v.port) self._handles.append(input_port_v.handle) self._map_handles_port_v[input_port_v.handle] = input_port_v port_meta = port_m.get_meta_data_editor() if not contains_geometric_info(port_meta['rel_pos']): # print "generate rel_pos" # Distribute input ports on the left side of the state, starting from bottom input_port_v.side = SnappedSide.LEFT number_of_input = self.model.input_data_ports.index(port_m) + 1 pos_x = 0 pos_y = self.height - self._calculate_port_pos_on_line(number_of_input, self.height) port_meta = port_m.set_meta_data_editor('rel_pos', (pos_x, pos_y)) # print "add input_port", self.model, self.model.parent, port_meta['rel_pos'] input_port_v.handle.pos = port_meta['rel_pos'] self.add_rect_constraint_for_port(input_port_v) def remove_input_port(self, input_port_v): del self._map_handles_port_v[input_port_v.handle] self._inputs.remove(input_port_v) self._ports.remove(input_port_v.port) self._handles.remove(input_port_v.handle) self.canvas.remove_port(input_port_v) if input_port_v in self.port_constraints: self.canvas.solver.remove_constraint(self.port_constraints.pop(input_port_v)) def add_output_port(self, port_m): output_port_v = OutputPortView(self, port_m) self.canvas.add_port(output_port_v) self._outputs.append(output_port_v) self._ports.append(output_port_v.port) self._handles.append(output_port_v.handle) self._map_handles_port_v[output_port_v.handle] = output_port_v port_meta = port_m.get_meta_data_editor() if not contains_geometric_info(port_meta['rel_pos']): # Distribute output ports on the right side of the state, starting from bottom # print "generate rel_pos" output_port_v.side = SnappedSide.RIGHT number_of_output = self.model.output_data_ports.index(port_m) + 1 pos_x = self.width pos_y = self.height - self._calculate_port_pos_on_line(number_of_output, self.height) port_meta = port_m.set_meta_data_editor('rel_pos', (pos_x, pos_y)) # print "add output_port", self.model, self.model.parent, port_meta['rel_pos'] output_port_v.handle.pos = port_meta['rel_pos'] self.add_rect_constraint_for_port(output_port_v) def remove_output_port(self, output_port_v): del self._map_handles_port_v[output_port_v.handle] self._outputs.remove(output_port_v) self._ports.remove(output_port_v.port) self._handles.remove(output_port_v.handle) self.canvas.remove_port(output_port_v) if output_port_v in self.port_constraints: self.canvas.solver.remove_constraint(self.port_constraints.pop(output_port_v)) def add_scoped_variable(self, scoped_variable_m): scoped_variable_port_v = ScopedVariablePortView(self, scoped_variable_m) self.canvas.add_port(scoped_variable_port_v) self._scoped_variables_ports.append(scoped_variable_port_v) self._ports.append(scoped_variable_port_v.port) self._handles.append(scoped_variable_port_v.handle) self._map_handles_port_v[scoped_variable_port_v.handle] = scoped_variable_port_v scoped_variable_port_v.handle.pos = self.width * (0.1 * len(self._scoped_variables_ports)), 0 port_meta = scoped_variable_m.get_meta_data_editor() if not contains_geometric_info(port_meta['rel_pos']): # Distribute scoped variables on the top side of the state, starting from left # print "generate rel_pos" scoped_variable_port_v.side = SnappedSide.BOTTOM number_of_scoped_var = self.model.scoped_variables.index(scoped_variable_m) + 1 pos_x = self._calculate_port_pos_on_line(number_of_scoped_var, self.width, port_width=self.border_width * 4) pos_y = self.height port_meta = scoped_variable_m.set_meta_data_editor('rel_pos', (pos_x, pos_y)) # print "add scoped_variable", self.model, self.model.parent, port_meta['rel_pos'] scoped_variable_port_v.handle.pos = port_meta['rel_pos'] self.add_rect_constraint_for_port(scoped_variable_port_v) def remove_scoped_variable(self, scoped_variable_port_v): del self._map_handles_port_v[scoped_variable_port_v.handle] self._scoped_variables_ports.remove(scoped_variable_port_v) self._ports.remove(scoped_variable_port_v.port) self._handles.remove(scoped_variable_port_v.handle) self.canvas.remove_port(scoped_variable_port_v) if scoped_variable_port_v in self.port_constraints: self.canvas.solver.remove_constraint(self.port_constraints.pop(scoped_variable_port_v)) def add_rect_constraint_for_port(self, port): constraint = PortRectConstraint((self.handles()[NW].pos, self.handles()[SE].pos), port.pos, port) solver = self.canvas.solver solver.add_constraint(constraint) self.port_constraints[port] = constraint def _calculate_port_pos_on_line(self, port_num, side_length, port_width=None): """Calculate the position of a port on a line The position depends on the number of element. Elements are equally spaced. If the end of the line is reached, ports are stacked. :param int port_num: The number of the port of that type :param float side_length: The length of the side the element is placed on :param float port_width: The width of one port :return: The position on the line for the given port :rtype: float """ if port_width is None: port_width = 2 * self.border_width border_size = self.border_width pos = 0.5 * border_size + port_num * port_width outermost_pos = max(side_length / 2., side_length - 0.5 * border_size - port_width) pos = min(pos, outermost_pos) return pos def resize_all_children(self, old_size, paste=False): def calc_new_rel_pos(old_rel_pos, old_parent_size, new_parent_size): new_rel_pos_x = old_rel_pos[0] * new_parent_size[0] / old_parent_size[0] new_rel_pos_y = old_rel_pos[1] * new_parent_size[1] / old_parent_size[1] return new_rel_pos_x, new_rel_pos_y def set_item_properties(item, size, rel_pos): prefix = 'name.' if isinstance(item, NameView) else '' item_m = item.model if isinstance(item, StateView) else item.parent.model item.width = size[0] item.height = size[1] item_m.set_meta_data_editor(prefix + 'size', size) if item is not self: item.position = rel_pos item_m.set_meta_data_editor(prefix + 'rel_pos', rel_pos) if isinstance(item, StateView): item.update_minimum_size_of_children() def resize_state_v(state_v, old_state_size, new_state_size, use_meta_data): width_factor = float(new_state_size[0]) / old_state_size[0] height_factor = float(new_state_size[1]) / old_state_size[1] # Set new state view properties old_state_rel_pos = state_v.position new_state_rel_pos = calc_new_rel_pos(old_state_rel_pos, old_state_size, new_state_size) set_item_properties(state_v, new_state_size, new_state_rel_pos) # Set new name view properties name_v = state_v.name_view if use_meta_data: old_name_size = state_v.model.get_meta_data_editor()['name']['size'] else: old_name_size = (name_v.width, name_v.height) new_name_size = (old_name_size[0] * width_factor, old_name_size[1] * height_factor) old_name_rel_pos = state_v.model.get_meta_data_editor()['name']['rel_pos'] new_name_rel_pos = calc_new_rel_pos(old_name_rel_pos, old_state_size, new_state_size) set_item_properties(name_v, new_name_size, new_name_rel_pos) def resize_child_state_v(child_state_v): if use_meta_data: old_child_size = child_state_v.model.get_meta_data_editor()['size'] else: old_child_size = (child_state_v.width, child_state_v.height) new_child_size = (old_child_size[0] * width_factor, old_child_size[1] * height_factor) resize_state_v(child_state_v, old_child_size, new_child_size, use_meta_data) # Set new port view properties for port_v in state_v.get_all_ports(): new_port_rel_pos = calc_new_rel_pos(port_v.handle.pos, old_state_size, new_state_size) port_v.handle.pos = new_port_rel_pos if isinstance(state_v.model, ContainerStateModel): for transition_v in state_v.get_transitions(): for waypoint in transition_v.waypoints: old_rel_pos = self.canvas.get_matrix_i2i(transition_v, transition_v.parent).transform_point( *waypoint.pos) new_rel_pos = calc_new_rel_pos(old_rel_pos, old_state_size, new_state_size) waypoint.pos = self.canvas.get_matrix_i2i(transition_v.parent, transition_v).transform_point( *new_rel_pos) for child_state_v in state_v.child_state_views(): resize_child_state_v(child_state_v) elif state_v.show_content(): state_copy_v = self.canvas.get_view_for_model(state_v.model.state_copy) resize_child_state_v(state_copy_v) new_size = (self.width, self.height) resize_state_v(self, old_size, new_size, paste)
class PerpLine(Line): def __init__(self, hierarchy_level): from rafcon.gui.mygaphas.segment import Segment super(PerpLine, self).__init__() self._from_handle = self.handles()[0] self._to_handle = self.handles()[1] self._segment = Segment(self, view=self.canvas) self.hierarchy_level = hierarchy_level self._from_port = None self._from_waypoint = None self._from_port_constraint = None self._to_port = None self._to_waypoint = None self._to_port_constraint = None self._waypoint_constraints = [] self._arrow_color = None self._line_color = None self._parent = None self._parent_state_v = None self._view = None self._label_image_cache = ImageCache() self._last_label_size = 0, 0 @property def name(self): if self.from_port: return self.from_port.name return None @property def waypoints(self): waypoints = [] for handle in self.handles(): if handle not in self.end_handles(include_waypoints=True): waypoints.append(handle) return waypoints @property def parent(self): return self.canvas.get_parent(self) @property def from_port(self): return self._from_port @property def to_port(self): return self._to_port @from_port.setter def from_port(self, port): assert isinstance(port, PortView) self._from_port = port if not self._from_waypoint: self._from_waypoint = self.add_perp_waypoint() self._from_port_constraint = KeepPortDistanceConstraint(self.from_handle().pos, self._from_waypoint.pos, port, lambda: self._head_length(self.from_port) + self._head_offset(self.from_port), self.is_out_port(port)) self.canvas.solver.add_constraint(self._from_port_constraint) @to_port.setter def to_port(self, port): assert isinstance(port, PortView) self._to_port = port if not self._to_waypoint: self._to_waypoint = self.add_perp_waypoint(begin=False) self._to_port_constraint = KeepPortDistanceConstraint(self.to_handle().pos, self._to_waypoint.pos, port, lambda: self._head_length(self.to_port) + self._head_offset(self.to_port), self.is_in_port(port)) self.canvas.solver.add_constraint(self._to_port_constraint) def remove(self): self.reset_from_port() self.reset_to_port() self.remove_all_waypoints() self.canvas.remove(self) @property def view(self): if not self._view: self._view = self.canvas.get_first_view() return self._view def end_handles(self, include_waypoints=False): end_handles = [self.from_handle(), self.to_handle()] if include_waypoints: if self._from_waypoint: end_handles.insert(1, self._from_waypoint) if self._to_waypoint: end_handles.insert(-1, self._to_waypoint) return end_handles def reset_from_port(self): if self._from_port: self._from_port = None self.canvas.solver.remove_constraint(self._from_port_constraint) self._from_port_constraint = None self._handles.remove(self._from_waypoint) self._from_waypoint = None def reset_to_port(self): if self._to_port: self._to_port = None self.canvas.solver.remove_constraint(self._to_port_constraint) self._to_port_constraint = None self._handles.remove(self._to_waypoint) self._to_waypoint = None def from_handle(self): return self._from_handle def to_handle(self): return self._to_handle def get_parent_state_v(self): if not self._parent_state_v: if not self.from_port: return None if isinstance(self.from_port, (IncomeView, InputPortView, ScopedVariablePortView)): self._parent_state_v = self.from_port.parent else: self._parent_state_v = self.from_port.parent.parent return self._parent_state_v def draw_head(self, context, port): offset = self._head_offset(port) length = self._head_length(port) cr = context.cairo cr.move_to(offset, 0) cr.line_to(offset + length, 0) cr.set_source_rgba(*self._arrow_color) cr.set_line_width(self._calc_line_width(port)) cr.set_line_cap(LINE_CAP_BUTT) cr.stroke() def draw_tail(self, context, port): offset = self._head_offset(port) length = self._head_length(port) cr = context.cairo cr.move_to(offset, 0) cr.line_to(offset + length, 0) cr.set_source_rgba(*self._arrow_color) cr.set_line_width(self._calc_line_width(port)) cr.set_line_cap(LINE_CAP_BUTT) cr.stroke() def draw(self, context): if self.parent and self.parent.moving: return def draw_line_end(pos, angle, port, draw): cr.save() cr.translate(*pos) cr.rotate(angle) draw(context, port) cr.restore() self.line_width = self._calc_line_width() cr = context.cairo cr.set_line_cap(LINE_CAP_ROUND) cr.set_line_width(self.line_width) # Draw connection tail (line perpendicular to from_port) start_segment_index = 0 if self.from_port: draw_line_end(self._handles[0].pos, self._head_angle, self.from_port, self.draw_tail) start_segment_index = 1 # Draw connection head (line perpendicular to to_port) end_segment_index = len(self._handles) if self.to_port: draw_line_end(self._handles[-1].pos, self._tail_angle, self.to_port, self.draw_head) end_segment_index -= 1 # Draw connection line from waypoint to waypoint cr.move_to(*self._handles[start_segment_index].pos) for h in self._handles[start_segment_index+1:end_segment_index]: cr.line_to(*h.pos) cr.set_source_rgba(*self._line_color) cr.stroke() if self.name and (isinstance(self.from_port, LogicPortView) or global_gui_config.get_config_value("SHOW_NAMES_ON_DATA_FLOWS", default=True)): self._draw_name(context) def _draw_name(self, context): c = context.cairo if len(self._handles) % 2: # uneven index = int(floor(len(self._handles) / 2)) cx, cy = self._handles[index].pos angle = 0 else: index = int(len(self._handles) / 2) - 1 p1, p2 = self._handles[index].pos, self._handles[index + 1].pos cx = (p1.x + p2.x) / 2 cy = (p1.y + p2.y) / 2 if global_gui_config.get_config_value("ROTATE_NAMES_ON_CONNECTIONS", default=False): angle = atan2(p2.y - p1.y, p2.x - p1.x) if angle < -pi / 2.: angle += pi elif angle > pi / 2.: angle -= pi else: angle = 0 # c.set_antialias(Antialias.GOOD) parameters = { 'name': self.name, 'line_width': self.line_width, 'color': self._arrow_color } upper_left_corner = cx, cy current_zoom = self.view.get_zoom_factor() from_cache, image, zoom = self._label_image_cache.get_cached_image(self._last_label_size[0], self._last_label_size[1], current_zoom, parameters) # The parameters for drawing haven't changed, thus we can just copy the content from the last rendering result if from_cache: # print("draw port name from cache") self._label_image_cache.copy_image_to_context(c, upper_left_corner, angle) # Parameters have changed or nothing in cache => redraw else: # First retrieve pango layout to determine and store size of label cairo_context = c if isinstance(c, CairoBoundingBoxContext): cairo_context = c._cairo layout = get_text_layout(cairo_context, self.name, FONT_SIZE) ink_extents, logical_extents = layout.get_extents() extents = [extent / float(SCALE) for extent in [logical_extents.x, logical_extents.y, logical_extents.width, logical_extents.height]] real_label_size = extents[2], extents[3] desired_height = self.line_width * 2.5 scale_factor = real_label_size[1] / desired_height label_size = real_label_size[0] / scale_factor, desired_height self._last_label_size = label_size # The size information is used to update the caching parameters and retrieve a new context with an image # surface of the correct size self._label_image_cache.get_cached_image(label_size[0], label_size[1], current_zoom, parameters, clear=True) c = self._label_image_cache.get_context_for_image(current_zoom) cairo_context = c layout = get_text_layout(cairo_context, self.name, FONT_SIZE) c.set_source_rgba(*self._arrow_color) c.scale(1. / scale_factor, 1. / scale_factor) PangoCairo.update_layout(cairo_context, layout) PangoCairo.show_layout(cairo_context, layout) self._label_image_cache.copy_image_to_context(context.cairo, upper_left_corner, angle, zoom=current_zoom) def _calc_line_width(self, for_port=None): parent_state_v = self.get_parent_state_v() if not parent_state_v: return 0 line_width = parent_state_v.border_width / constants.BORDER_WIDTH_LINE_WIDTH_FACTOR if for_port: return min(line_width, for_port.port_size[0]) return line_width def _head_length(self, port): """Distance from the center of the port to the perpendicular waypoint""" if not port: return 0. parent_state_v = self.get_parent_state_v() if parent_state_v is port.parent: # port of connection's parent state return port.port_size[1] return max(port.port_size[1] * 1.5, self._calc_line_width() / 1.3) def _head_offset(self, port): """How far away from the port center does the line begin""" if not port: return 0. return port.port_size[1] / 2 def _update_ports(self): assert len(self._handles) >= 2, 'Not enough segments' self._ports = [] handles = self._handles for h1, h2 in zip(handles[:-1], handles[1:]): self._ports.append(self._create_port(h1.pos, h2.pos)) def _reversible_insert_handle(self, index, handle): super(PerpLine, self)._reversible_insert_handle(index, handle) self._keep_handle_in_parent_state(handle) def add_waypoint(self, pos): pos = self.canvas.get_matrix_i2i(self.parent, self).transform_point(*pos) handle = self._create_handle(pos) if self._to_waypoint: self._handles.insert(-2, handle) else: self._handles.insert(-1, handle) self._keep_handle_in_parent_state(handle) self._update_ports() return handle def remove_all_waypoints(self): waypoints = self.waypoints for waypoint in waypoints: self._handles.remove(waypoint) self._update_ports() for constraint in self._waypoint_constraints: self.canvas.solver.remove_constraint(constraint) self._waypoint_constraints = [] def add_perp_waypoint(self, pos=(0, 0), begin=True): handle = self._create_handle(pos) if begin: self._handles.insert(1, handle) else: self._handles.insert(len(self._handles) - 1, handle) self._update_ports() return handle @staticmethod def is_in_port(port): return isinstance(port, (IncomeView, InputPortView)) @staticmethod def is_out_port(port): return isinstance(port, (OutcomeView, OutputPortView)) def point(self, pos): distance = super(PerpLine, self).point(pos) return distance - self.line_width / 1.5 def _keep_handle_in_parent_state(self, handle): canvas = self.canvas parent = canvas.get_parent(self) solver = canvas.solver if parent is None: return handle_pos = ItemProjection(handle.pos, self, self.parent) constraint = KeepPointWithinConstraint(parent.handles()[NW].pos, parent.handles()[SE].pos, handle_pos, lambda: parent.border_width) solver.add_constraint(constraint) self._waypoint_constraints.append(constraint)