diff options
Diffstat (limited to 'grc/gui/Block.py')
-rw-r--r-- | grc/gui/Block.py | 549 |
1 files changed, 293 insertions, 256 deletions
diff --git a/grc/gui/Block.py b/grc/gui/Block.py index 55c8805fae..b37bec6dfa 100644 --- a/grc/gui/Block.py +++ b/grc/gui/Block.py @@ -17,101 +17,60 @@ along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA """ -import pygtk -pygtk.require('2.0') -import gtk -import pango +from __future__ import absolute_import +import math + +import six +from gi.repository import Gtk, Pango, PangoCairo from . import Actions, Colors, Utils, Constants +from .Constants import ( + BLOCK_LABEL_PADDING, PORT_SPACING, PORT_SEPARATION, LABEL_SEPARATION, + PORT_BORDER_SEPARATION, BLOCK_FONT, PARAM_FONT +) from . Element import Element -from ..core.Param import num_to_str -from ..core.utils import odict -from ..core.utils.complexity import calculate_flowgraph_complexity -from ..core.Block import Block as _Block - -BLOCK_MARKUP_TMPL="""\ -#set $foreground = $block.is_valid() and 'black' or 'red' -<span foreground="$foreground" font_desc="$font"><b>$encode($block.get_name())</b></span>""" - -# Includes the additional complexity markup if enabled -COMMENT_COMPLEXITY_MARKUP_TMPL="""\ -#set $foreground = $block.get_enabled() and '#444' or '#888' -#if $complexity -<span foreground="#444" size="medium" font_desc="$font"><b>$encode($complexity)</b></span>#slurp -#end if -#if $complexity and $comment -<span></span> -#end if -#if $comment -<span foreground="$foreground" font_desc="$font">$encode($comment)</span>#slurp -#end if -""" +from ..core import utils +from ..core.Block import Block as CoreBlock -class Block(Element, _Block): +class Block(CoreBlock, Element): """The graphical signal block.""" - def __init__(self, flow_graph, n): + def __init__(self, parent, **n): """ - Block contructor. + Block constructor. Add graphics related params to the block. """ - _Block.__init__(self, flow_graph, n) - - self.W = 0 - self.H = 0 - #add the position param - self.get_params().append(self.get_parent().get_parent().Param( - block=self, - n=odict({ - 'name': 'GUI Coordinate', - 'key': '_coordinate', - 'type': 'raw', - 'value': '(0, 0)', - 'hide': 'all', - }) - )) - self.get_params().append(self.get_parent().get_parent().Param( - block=self, - n=odict({ - 'name': 'GUI Rotation', - 'key': '_rotation', - 'type': 'raw', - 'value': '0', - 'hide': 'all', - }) - )) - Element.__init__(self) - self._comment_pixmap = None - self.has_busses = [False, False] # source, sink - - def get_coordinate(self): + super(self.__class__, self).__init__(parent, **n) + + self.states.update(_coordinate=(0, 0), _rotation=0) + self.width = self.height = 0 + Element.__init__(self) # needs the states and initial sizes + + self._surface_layouts = [ + Gtk.DrawingArea().create_pango_layout(''), # title + Gtk.DrawingArea().create_pango_layout(''), # params + ] + self._surface_layout_offsets = 0, 0 + self._comment_layout = None + + self._area = [] + self._border_color = self._bg_color = Colors.BLOCK_ENABLED_COLOR + self._font_color = list(Colors.FONT_COLOR) + + @property + def coordinate(self): """ Get the coordinate from the position param. Returns: the coordinate tuple (x, y) or (0, 0) if failure """ - proximity = Constants.BORDER_PROXIMITY_SENSITIVITY - try: #should evaluate to tuple - coor = eval(self.get_param('_coordinate').get_value()) - x, y = map(int, coor) - fgW,fgH = self.get_parent().get_size() - if x <= 0: - x = 0 - elif x >= fgW - proximity: - x = fgW - proximity - if y <= 0: - y = 0 - elif y >= fgH - proximity: - y = fgH - proximity - return (x, y) - except: - self.set_coordinate((0, 0)) - return (0, 0) + return self.states['_coordinate'] - def set_coordinate(self, coor): + @coordinate.setter + def coordinate(self, coor): """ Set the coordinate into the position param. @@ -119,208 +78,202 @@ class Block(Element, _Block): coor: the coordinate tuple (x, y) """ if Actions.TOGGLE_SNAP_TO_GRID.get_active(): - offset_x, offset_y = (0, self.H/2) if self.is_horizontal() else (self.H/2, 0) + offset_x, offset_y = (0, self.height / 2) if self.is_horizontal() else (self.height / 2, 0) coor = ( Utils.align_to_grid(coor[0] + offset_x) - offset_x, Utils.align_to_grid(coor[1] + offset_y) - offset_y ) - self.get_param('_coordinate').set_value(str(coor)) - - def bound_move_delta(self, delta_coor): - """ - Limit potential moves from exceeding the bounds of the canvas - - Args: - delta_coor: requested delta coordinate (dX, dY) to move - - Returns: - The delta coordinate possible to move while keeping the block on the canvas - or the input (dX, dY) on failure - """ - dX, dY = delta_coor - - try: - fgW, fgH = self.get_parent().get_size() - x, y = map(int, eval(self.get_param("_coordinate").get_value())) - if self.is_horizontal(): - sW, sH = self.W, self.H - else: - sW, sH = self.H, self.W - - if x + dX < 0: - dX = -x - elif dX + x + sW >= fgW: - dX = fgW - x - sW - if y + dY < 0: - dY = -y - elif dY + y + sH >= fgH: - dY = fgH - y - sH - except: - pass + self.states['_coordinate'] = coor - return ( dX, dY ) - - def get_rotation(self): + @property + def rotation(self): """ Get the rotation from the position param. Returns: the rotation in degrees or 0 if failure """ - try: #should evaluate to dict - rotation = eval(self.get_param('_rotation').get_value()) - return int(rotation) - except: - self.set_rotation(Constants.POSSIBLE_ROTATIONS[0]) - return Constants.POSSIBLE_ROTATIONS[0] + return self.states['_rotation'] - def set_rotation(self, rot): + @rotation.setter + def rotation(self, rot): """ Set the rotation into the position param. Args: rot: the rotation in degrees """ - self.get_param('_rotation').set_value(str(rot)) + self.states['_rotation'] = rot + + def _update_colors(self): + self._bg_color = ( + Colors.MISSING_BLOCK_BACKGROUND_COLOR if self.is_dummy_block else + Colors.BLOCK_BYPASSED_COLOR if self.state == 'bypassed' else + Colors.BLOCK_ENABLED_COLOR if self.state == 'enabled' else + Colors.BLOCK_DISABLED_COLOR + ) + self._font_color[-1] = 1.0 if self.state == 'enabled' else 0.4 + self._border_color = ( + Colors.MISSING_BLOCK_BORDER_COLOR if self.is_dummy_block else + Colors.BORDER_COLOR_DISABLED if not self.state == 'enabled' else Colors.BORDER_COLOR + ) def create_shapes(self): """Update the block, parameters, and ports when a change occurs.""" - Element.create_shapes(self) - if self.is_horizontal(): self.add_area((0, 0), (self.W, self.H)) - elif self.is_vertical(): self.add_area((0, 0), (self.H, self.W)) + if self.is_horizontal(): + self._area = (0, 0, self.width, self.height) + elif self.is_vertical(): + self._area = (0, 0, self.height, self.width) + self.bounds_from_area(self._area) + + bussified = self.current_bus_structure['source'], self.current_bus_structure['sink'] + for ports, has_busses in zip((self.active_sources, self.active_sinks), bussified): + if not ports: + continue + port_separation = PORT_SEPARATION if not has_busses else ports[0].height + PORT_SPACING + offset = (self.height - (len(ports) - 1) * port_separation - ports[0].height) / 2 + for index, port in enumerate(ports): + port.create_shapes() + + port.coordinate = { + 0: (+self.width, offset), + 90: (offset, -port.width), + 180: (-port.width, offset), + 270: (offset, +self.width), + }[port.get_connector_direction()] + offset += PORT_SEPARATION if not has_busses else port.height + PORT_SPACING + + port.connector_length = Constants.CONNECTOR_EXTENSION_MINIMAL + \ + Constants.CONNECTOR_EXTENSION_INCREMENT * index def create_labels(self): """Create the labels for the signal block.""" - Element.create_labels(self) - self._bg_color = self.is_dummy_block and Colors.MISSING_BLOCK_BACKGROUND_COLOR or \ - self.get_bypassed() and Colors.BLOCK_BYPASSED_COLOR or \ - self.get_enabled() and Colors.BLOCK_ENABLED_COLOR or Colors.BLOCK_DISABLED_COLOR - - layouts = list() - #create the main layout - layout = gtk.DrawingArea().create_pango_layout('') - layouts.append(layout) - layout.set_markup(Utils.parse_template(BLOCK_MARKUP_TMPL, block=self, font=Constants.BLOCK_FONT)) - self.label_width, self.label_height = layout.get_pixel_size() - #display the params - if self.is_dummy_block: - markups = [ - '<span foreground="black" font_desc="{font}"><b>key: </b>{key}</span>' - ''.format(font=Constants.PARAM_FONT, key=self._key) - ] + title_layout, params_layout = self._surface_layouts + + title_layout.set_markup( + '<span {foreground} font_desc="{font}"><b>{name}</b></span>'.format( + foreground='foreground="red"' if not self.is_valid() else '', font=BLOCK_FONT, + name=Utils.encode(self.name) + ) + ) + title_width, title_height = title_layout.get_pixel_size() + + # update the params layout + if not self.is_dummy_block: + markups = [param.format_block_surface_markup() + for param in self.params.values() if param.get_hide() not in ('all', 'part')] else: - markups = [param.get_markup() for param in self.get_params() if param.get_hide() not in ('all', 'part')] - if markups: - layout = gtk.DrawingArea().create_pango_layout('') - layout.set_spacing(Constants.LABEL_SEPARATION * pango.SCALE) - layout.set_markup('\n'.join(markups)) - layouts.append(layout) - w, h = layout.get_pixel_size() - self.label_width = max(w, self.label_width) - self.label_height += h + Constants.LABEL_SEPARATION - width = self.label_width - height = self.label_height - #setup the pixmap - pixmap = self.get_parent().new_pixmap(width, height) - gc = pixmap.new_gc() - gc.set_foreground(self._bg_color) - pixmap.draw_rectangle(gc, True, 0, 0, width, height) - #draw the layouts - h_off = 0 - for i,layout in enumerate(layouts): - w,h = layout.get_pixel_size() - if i == 0: w_off = (width-w)/2 - else: w_off = 0 - pixmap.draw_layout(gc, w_off, h_off, layout) - h_off = h + h_off + Constants.LABEL_SEPARATION - #create vertical and horizontal pixmaps - self.horizontal_label = pixmap - if self.is_vertical(): - self.vertical_label = self.get_parent().new_pixmap(height, width) - Utils.rotate_pixmap(gc, self.horizontal_label, self.vertical_label) - #calculate width and height needed - W = self.label_width + 2 * Constants.BLOCK_LABEL_PADDING - - def get_min_height_for_ports(): - visible_ports = filter(lambda p: not p.get_hide(), ports) - min_height = 2*Constants.PORT_BORDER_SEPARATION + len(visible_ports) * Constants.PORT_SEPARATION - if visible_ports: - min_height -= ports[0].H + markups = ['<span font_desc="{font}"><b>key: </b>{key}</span>'.format(font=PARAM_FONT, key=self.key)] + + params_layout.set_spacing(LABEL_SEPARATION * Pango.SCALE) + params_layout.set_markup('\n'.join(markups)) + params_width, params_height = params_layout.get_pixel_size() if markups else (0, 0) + + label_width = max(title_width, params_width) + label_height = title_height + LABEL_SEPARATION + params_height + + title_layout.set_width(label_width * Pango.SCALE) + title_layout.set_alignment(Pango.Alignment.CENTER) + + # calculate width and height needed + width = label_width + 2 * BLOCK_LABEL_PADDING + height = label_height + 2 * BLOCK_LABEL_PADDING + + self._update_colors() + self.create_port_labels() + + def get_min_height_for_ports(ports): + min_height = 2 * PORT_BORDER_SEPARATION + len(ports) * PORT_SEPARATION + if ports: + min_height -= ports[-1].height return min_height - H = max( - [ # labels - self.label_height + 2 * Constants.BLOCK_LABEL_PADDING - ] + - [ # ports - get_min_height_for_ports() for ports in (self.get_sources_gui(), self.get_sinks_gui()) - ] + - [ # bus ports only - 2 * Constants.PORT_BORDER_SEPARATION + - sum([port.H + Constants.PORT_SPACING for port in ports if port.get_type() == 'bus']) - Constants.PORT_SPACING - for ports in (self.get_sources_gui(), self.get_sinks_gui()) - ] - ) - self.W, self.H = Utils.align_to_grid((W, H)) - self.has_busses = [ - any(port.get_type() == 'bus' for port in ports) - for ports in (self.get_sources_gui(), self.get_sinks_gui()) + + height = max(height, + get_min_height_for_ports(self.active_sinks), + get_min_height_for_ports(self.active_sources)) + + def get_min_height_for_bus_ports(ports): + return 2 * PORT_BORDER_SEPARATION + sum( + port.height + PORT_SPACING for port in ports if port.get_type() == 'bus' + ) - PORT_SPACING + + if self.current_bus_structure['sink']: + height = max(height, get_min_height_for_bus_ports(self.active_sinks)) + if self.current_bus_structure['source']: + height = max(height, get_min_height_for_bus_ports(self.active_sources)) + + self.width, self.height = width, height = Utils.align_to_grid((width, height)) + + self._surface_layout_offsets = [ + (width - label_width) / 2.0, + (height - label_height) / 2.0 ] - self.create_comment_label() - - def create_comment_label(self): - comment = self.get_comment() # Returns None if there are no comments - complexity = None - - # Show the flowgraph complexity on the top block if enabled - if Actions.TOGGLE_SHOW_FLOWGRAPH_COMPLEXITY.get_active() and self.get_key() == "options": - complexity = calculate_flowgraph_complexity(self.get_parent()) - complexity = "Complexity: {}bal".format(num_to_str(complexity)) - - layout = gtk.DrawingArea().create_pango_layout('') - layout.set_markup(Utils.parse_template(COMMENT_COMPLEXITY_MARKUP_TMPL, - block=self, - comment=comment, - complexity=complexity, - font=Constants.BLOCK_FONT)) - - # Setup the pixel map. Make sure that layout not empty - width, height = layout.get_pixel_size() - if width and height: - padding = Constants.BLOCK_LABEL_PADDING - pixmap = self.get_parent().new_pixmap(width + 2 * padding, - height + 2 * padding) - gc = pixmap.new_gc() - gc.set_foreground(Colors.COMMENT_BACKGROUND_COLOR) - pixmap.draw_rectangle( - gc, True, 0, 0, width + 2 * padding, height + 2 * padding) - pixmap.draw_layout(gc, padding, padding, layout) - self._comment_pixmap = pixmap + + self.create_comment_layout() + + def create_port_labels(self): + for ports in (self.active_sinks, self.active_sources): + max_width = 0 + for port in ports: + port.create_labels() + max_width = max(max_width, port.width_with_label) + for port in ports: + port.width = max_width + + def create_comment_layout(self): + markups = [] + + # Show the flow graph complexity on the top block if enabled + if Actions.TOGGLE_SHOW_FLOWGRAPH_COMPLEXITY.get_active() and self.key == "options": + complexity = utils.calculate_flowgraph_complexity(self.parent) + markups.append( + '<span foreground="#444" size="medium" font_desc="{font}">' + '<b>Complexity: {num}bal</b></span>'.format(num=Utils.num_to_str(complexity), font=BLOCK_FONT) + ) + comment = self.comment # Returns None if there are no comments + if comment: + if markups: + markups.append('<span></span>') + + markups.append('<span foreground="{foreground}" font_desc="{font}">{comment}</span>'.format( + foreground='#444' if self.enabled else '#888', font=BLOCK_FONT, comment=Utils.encode(comment) + )) + if markups: + layout = self._comment_layout = Gtk.DrawingArea().create_pango_layout('') + layout.set_markup(''.join(markups)) else: - self._comment_pixmap = None + self._comment_layout = None - def draw(self, gc, window): + def draw(self, cr): """ Draw the signal block with label and inputs/outputs. - - Args: - gc: the graphics context - window: the gtk window to draw on """ - # draw ports - for port in self.get_ports_gui(): - port.draw(gc, window) - # draw main block - x, y = self.get_coordinate() - Element.draw( - self, gc, window, bg_color=self._bg_color, - border_color=self.is_highlighted() and Colors.HIGHLIGHT_COLOR or - self.is_dummy_block and Colors.MISSING_BLOCK_BORDER_COLOR or Colors.BORDER_COLOR, - ) - #draw label image - if self.is_horizontal(): - window.draw_drawable(gc, self.horizontal_label, 0, 0, x+Constants.BLOCK_LABEL_PADDING, y+(self.H-self.label_height)/2, -1, -1) - elif self.is_vertical(): - window.draw_drawable(gc, self.vertical_label, 0, 0, x+(self.H-self.label_height)/2, y+Constants.BLOCK_LABEL_PADDING, -1, -1) + border_color = Colors.HIGHLIGHT_COLOR if self.highlighted else self._border_color + cr.translate(*self.coordinate) + + for port in self.active_ports(): # ports first + cr.save() + port.draw(cr) + cr.restore() + + cr.rectangle(*self._area) + cr.set_source_rgba(*self._bg_color) + cr.fill_preserve() + cr.set_source_rgba(*border_color) + cr.stroke() + + # title and params label + if self.is_vertical(): + cr.rotate(-math.pi / 2) + cr.translate(-self.width, 0) + cr.translate(*self._surface_layout_offsets) + + cr.set_source_rgba(*self._font_color) + for layout in self._surface_layouts: + PangoCairo.update_layout(cr, layout) + PangoCairo.show_layout(cr, layout) + _, h = layout.get_pixel_size() + cr.translate(0, h + LABEL_SEPARATION) def what_is_selected(self, coor, coor_m=None): """ @@ -333,19 +286,103 @@ class Block(Element, _Block): Returns: this block, a port, or None """ - for port in self.get_ports_gui(): - port_selected = port.what_is_selected(coor, coor_m) - if port_selected: return port_selected + for port in self.active_ports(): + port_selected = port.what_is_selected( + coor=[a - b for a, b in zip(coor, self.coordinate)], + coor_m=[a - b for a, b in zip(coor, self.coordinate)] if coor_m is not None else None + ) + if port_selected: + return port_selected return Element.what_is_selected(self, coor, coor_m) - def draw_comment(self, gc, window): - if not self._comment_pixmap: + def draw_comment(self, cr): + if not self._comment_layout: return - x, y = self.get_coordinate() + x, y = self.coordinate if self.is_horizontal(): - y += self.H + Constants.BLOCK_LABEL_PADDING + y += self.height + BLOCK_LABEL_PADDING else: - x += self.H + Constants.BLOCK_LABEL_PADDING + x += self.height + BLOCK_LABEL_PADDING + + cr.save() + cr.translate(x, y) + PangoCairo.update_layout(cr, self._comment_layout) + PangoCairo.show_layout(cr, self._comment_layout) + cr.restore() + + @property + def extend(self): + extend = Element.extend.fget(self) + x, y = self.coordinate + for port in self.active_ports(): + extend = (min_or_max(xy, offset + p_xy) for offset, min_or_max, xy, p_xy in zip( + (x, y, x, y), (min, min, max, max), extend, port.extend + )) + return tuple(extend) + + ############################################## + # Controller Modify + ############################################## + def type_controller_modify(self, direction): + """ + Change the type controller. + + Args: + direction: +1 or -1 + + Returns: + true for change + """ + type_templates = ' '.join(p._type for p in self.get_children()) + type_param = None + for key, param in six.iteritems(self.params): + if not param.is_enum(): + continue + # Priority to the type controller + if param.key in type_templates: + type_param = param + break + # Use param if type param is unset + if not type_param: + type_param = param + if not type_param: + return False + + # Try to increment the enum by direction + try: + keys = list(type_param.options) + old_index = keys.index(type_param.get_value()) + new_index = (old_index + direction + len(keys)) % len(keys) + type_param.set_value(keys[new_index]) + return True + except: + return False + + def port_controller_modify(self, direction): + """ + Change the port controller. + + Args: + direction: +1 or -1 + + Returns: + true for change + """ + changed = False + # Concat the nports string from the private nports settings of all ports + nports_str = ' '.join(port._nports for port in self.get_ports()) + # Modify all params whose keys appear in the nports string + for key, param in six.iteritems(self.params): + if param.is_enum() or param.key not in nports_str: + continue + # Try to increment the port controller by direction + try: + value = param.get_evaluated() + direction + if value > 0: + param.set_value(value) + changed = True + except: + pass + return changed - window.draw_drawable(gc, self._comment_pixmap, 0, 0, x, y, -1, -1) |