""" Copyright 2007, 2008, 2009 Free Software Foundation, Inc. This file is part of GNU Radio GNU Radio Companion is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. GNU Radio Companion is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA """ from __future__ import absolute_import, division import math import six from gi.repository import Gtk, Pango, PangoCairo from . import colors from .drawable import Drawable from .. import Actions, Utils, Constants from ..Constants import ( BLOCK_LABEL_PADDING, PORT_SPACING, PORT_SEPARATION, LABEL_SEPARATION, PORT_BORDER_SEPARATION, BLOCK_FONT, PARAM_FONT ) from ...core import utils from ...core.blocks import Block as CoreBlock class Block(CoreBlock, Drawable): """The graphical signal block.""" def __init__(self, parent, **n): """ Block constructor. Add graphics related params to the block. """ super(self.__class__, self).__init__(parent, **n) self.states.update(coordinate=(0, 0), rotation=0) self.width = self.height = 0 Drawable.__init__(self) # needs the states and initial sizes self._surface_layouts = [ None, # title None, # params ] self._surface_layouts_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 """ return Utils.scale(self.states['coordinate']) @coordinate.setter def coordinate(self, coor): """ Set the coordinate into the position param. Args: coor: the coordinate tuple (x, y) """ coor = Utils.scale(coor, reverse=True) if Actions.TOGGLE_SNAP_TO_GRID.get_active(): 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.states['coordinate'] = coor @property def rotation(self): """ Get the rotation from the position param. Returns: the rotation in degrees or 0 if failure """ return self.states['rotation'] @rotation.setter def rotation(self, rot): """ Set the rotation into the position param. Args: rot: the rotation in degrees """ 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.""" 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'] bussified = False, False 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 port in ports: port.create_shapes() port.coordinate = { 0: (+self.width, offset), 90: (offset, -port.width), 180: (-port.width, offset), 270: (offset, +self.width), }[port.connector_direction] offset += PORT_SEPARATION if not has_busses else port.height + PORT_SPACING def create_labels(self, cr=None): """Create the labels for the signal block.""" # (Re-)creating layouts here, because layout.context_changed() doesn't seems to work (after zoom) title_layout, params_layout = self._surface_layouts = [ Gtk.DrawingArea().create_pango_layout(''), # title Gtk.DrawingArea().create_pango_layout(''), # params ] if cr: # to fix up extents after zooming PangoCairo.update_layout(cr, title_layout) PangoCairo.update_layout(cr, params_layout) title_layout.set_markup( '<span {foreground} font_desc="{font}"><b>{label}</b></span>'.format( foreground='foreground="red"' if not self.is_valid() else '', font=BLOCK_FONT, label=Utils.encode(self.label) ) ) title_width, title_height = title_layout.get_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.hide not in ('all', 'part')] else: 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_size() if markups else (0, 0) label_width = max(title_width, params_width) / Pango.SCALE label_height = title_height / Pango.SCALE if markups: label_height += LABEL_SEPARATION + params_height / Pango.SCALE # 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 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.dtype == '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_layouts_offsets = [ (0, (height - label_height) / 2.0), (0, (height - label_height) / 2.0 + LABEL_SEPARATION + title_height / Pango.SCALE) ] title_layout.set_width(width * Pango.SCALE) title_layout.set_alignment(Pango.Alignment.CENTER) params_layout.set_indent((width - label_width) / 2.0 * Pango.SCALE) 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.flow_graph_complexity.calculate(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_layout = None def draw(self, cr): """ Draw the signal block with label and inputs/outputs. """ 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.set_source_rgba(*self._font_color) for layout, offset in zip(self._surface_layouts, self._surface_layouts_offsets): cr.save() cr.translate(*offset) PangoCairo.update_layout(cr, layout) PangoCairo.show_layout(cr, layout) cr.restore() def what_is_selected(self, coor, coor_m=None): """ Get the element that is selected. Args: coor: the (x,y) tuple coor_m: the (x_m, y_m) tuple Returns: this block, a port, or None """ 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 Drawable.what_is_selected(self, coor, coor_m) def draw_comment(self, cr): if not self._comment_layout: return x, y = self.coordinate if self.is_horizontal(): y += self.height + BLOCK_LABEL_PADDING else: 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() def get_extents(self): extent = Drawable.get_extents(self) x, y = self.coordinate for port in self.active_ports(): extent = (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), extent, port.get_extents() )) return tuple(extent) ############################################## # 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.params.values()) type_templates += ' '.join(p.get_raw('dtype') for p in (self.sinks + self.sources)) 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: values = list(type_param.options) old_index = values.index(type_param.get_value()) new_index = (old_index + direction + len(values)) % len(values) type_param.set_value(values[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(str(port.get_raw('multiplicity')) for port in self.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