summaryrefslogtreecommitdiff
path: root/grc/gui/canvas
diff options
context:
space:
mode:
Diffstat (limited to 'grc/gui/canvas')
-rw-r--r--grc/gui/canvas/__init__.py22
-rw-r--r--grc/gui/canvas/block.py398
-rw-r--r--grc/gui/canvas/colors.py78
-rw-r--r--grc/gui/canvas/connection.py254
-rw-r--r--grc/gui/canvas/drawable.py183
-rw-r--r--grc/gui/canvas/flowgraph.py785
-rw-r--r--grc/gui/canvas/param.py162
-rw-r--r--grc/gui/canvas/port.py225
8 files changed, 2107 insertions, 0 deletions
diff --git a/grc/gui/canvas/__init__.py b/grc/gui/canvas/__init__.py
new file mode 100644
index 0000000000..f90d10c4e6
--- /dev/null
+++ b/grc/gui/canvas/__init__.py
@@ -0,0 +1,22 @@
+# Copyright 2016 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 .block import Block
+from .connection import Connection
+from .flowgraph import FlowGraph
+from .param import Param
+from .port import Port
diff --git a/grc/gui/canvas/block.py b/grc/gui/canvas/block.py
new file mode 100644
index 0000000000..d336bc139a
--- /dev/null
+++ b/grc/gui/canvas/block.py
@@ -0,0 +1,398 @@
+"""
+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.Block 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']
+ 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>{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_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 = ['<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.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_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.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_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.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
+
diff --git a/grc/gui/canvas/colors.py b/grc/gui/canvas/colors.py
new file mode 100644
index 0000000000..77d3203e78
--- /dev/null
+++ b/grc/gui/canvas/colors.py
@@ -0,0 +1,78 @@
+"""
+Copyright 2008,2013 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
+
+from gi.repository import Gtk, Gdk, cairo
+# import pycairo
+
+from .. import Constants
+
+
+def get_color(color_code):
+ color = Gdk.RGBA()
+ color.parse(color_code)
+ return color.red, color.green, color.blue, color.alpha
+ # chars_per_color = 2 if len(color_code) > 4 else 1
+ # offsets = range(1, 3 * chars_per_color + 1, chars_per_color)
+ # return tuple(int(color_code[o:o + 2], 16) / 255.0 for o in offsets)
+
+#################################################################################
+# fg colors
+#################################################################################
+
+HIGHLIGHT_COLOR = get_color('#00FFFF')
+BORDER_COLOR = get_color('#616161')
+BORDER_COLOR_DISABLED = get_color('#888888')
+FONT_COLOR = get_color('#000000')
+
+# Missing blocks stuff
+MISSING_BLOCK_BACKGROUND_COLOR = get_color('#FFF2F2')
+MISSING_BLOCK_BORDER_COLOR = get_color('#FF0000')
+
+# Flow graph color constants
+FLOWGRAPH_BACKGROUND_COLOR = get_color('#FFFFFF')
+COMMENT_BACKGROUND_COLOR = get_color('#F3F3F3')
+FLOWGRAPH_EDGE_COLOR = COMMENT_BACKGROUND_COLOR
+
+# Block color constants
+BLOCK_ENABLED_COLOR = get_color('#F1ECFF')
+BLOCK_DISABLED_COLOR = get_color('#CCCCCC')
+BLOCK_BYPASSED_COLOR = get_color('#F4FF81')
+
+# Connection color constants
+CONNECTION_ENABLED_COLOR = get_color('#000000')
+CONNECTION_DISABLED_COLOR = get_color('#BBBBBB')
+CONNECTION_ERROR_COLOR = get_color('#FF0000')
+
+DEFAULT_DOMAIN_COLOR = get_color('#777777')
+
+
+#################################################################################
+# port colors
+#################################################################################
+
+PORT_TYPE_TO_COLOR = {key: get_color(color) for name, key, sizeof, color in Constants.CORE_TYPES}
+PORT_TYPE_TO_COLOR.update((key, get_color(color)) for key, (_, color) in Constants.ALIAS_TYPES.items())
+
+
+#################################################################################
+# param box colors
+#################################################################################
+
diff --git a/grc/gui/canvas/connection.py b/grc/gui/canvas/connection.py
new file mode 100644
index 0000000000..ff790503ef
--- /dev/null
+++ b/grc/gui/canvas/connection.py
@@ -0,0 +1,254 @@
+"""
+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
+
+from argparse import Namespace
+from math import pi
+
+from . import colors
+from .drawable import Drawable
+from .. import Utils
+from ..Constants import (
+ CONNECTOR_ARROW_BASE,
+ CONNECTOR_ARROW_HEIGHT,
+ GR_MESSAGE_DOMAIN,
+ LINE_SELECT_SENSITIVITY,
+)
+from ...core.Connection import Connection as CoreConnection
+from ...core.Element import nop_write
+
+
+class Connection(CoreConnection, Drawable):
+ """
+ A graphical connection for ports.
+ The connection has 2 parts, the arrow and the wire.
+ The coloring of the arrow and wire exposes the status of 3 states:
+ enabled/disabled, valid/invalid, highlighted/non-highlighted.
+ The wire coloring exposes the enabled and highlighted states.
+ The arrow coloring exposes the enabled and valid states.
+ """
+
+ def __init__(self, *args, **kwargs):
+ super(self.__class__, self).__init__(*args, **kwargs)
+ Drawable.__init__(self)
+
+ self._line = []
+ self._line_width_factor = 1.0
+ self._color1 = self._color2 = None
+
+ self._current_port_rotations = self._current_coordinates = None
+
+ self._rel_points = None # connection coordinates relative to sink/source
+ self._arrow_rotation = 0.0 # rotation of the arrow in radians
+ self._current_cr = None # for what_is_selected() of curved line
+ self._line_path = None
+
+ @nop_write
+ @property
+ def coordinate(self):
+ return self.source_port.connector_coordinate_absolute
+
+ @nop_write
+ @property
+ def rotation(self):
+ """
+ Get the 0 degree rotation.
+ Rotations are irrelevant in connection.
+
+ Returns:
+ 0
+ """
+ return 0
+
+ def create_shapes(self):
+ """Pre-calculate relative coordinates."""
+ source = self.source_port
+ sink = self.sink_port
+ rotate = Utils.get_rotated_coordinate
+
+ # first two components relative to source connector, rest relative to sink connector
+ self._rel_points = [
+ rotate((15, 0), source.rotation), # line from 0,0 to here, bezier curve start
+ rotate((50, 0), source.rotation), # bezier curve control point 1
+ rotate((-50, 0), sink.rotation), # bezier curve control point 2
+ rotate((-15, 0), sink.rotation), # bezier curve end
+ rotate((-CONNECTOR_ARROW_HEIGHT, 0), sink.rotation), # line to arrow head
+ ]
+ self._current_coordinates = None # triggers _make_path()
+
+ def get_domain_color(domain_name):
+ domain = self.parent_platform.domains.get(domain_name, {})
+ color_spec = domain.get('color')
+ return colors.get_color(color_spec) if color_spec else colors.DEFAULT_DOMAIN_COLOR
+
+ if source.domain == GR_MESSAGE_DOMAIN:
+ self._line_width_factor = 1.0
+ self._color1 = None
+ self._color2 = colors.CONNECTION_ENABLED_COLOR
+ else:
+ if source.domain != sink.domain:
+ self._line_width_factor = 2.0
+ self._color1 = get_domain_color(source.domain)
+ self._color2 = get_domain_color(sink.domain)
+
+ self._arrow_rotation = -sink.rotation / 180 * pi
+
+ if not self._bounding_points:
+ self._make_path() # no cr set --> only sets bounding_points for extent
+
+ def _make_path(self, cr=None):
+ x_pos, y_pos = self.coordinate # is source connector coordinate
+ # x_start, y_start = self.source_port.get_connector_coordinate()
+ x_end, y_end = self.sink_port.connector_coordinate_absolute
+
+ # sink connector relative to sink connector
+ x_e, y_e = x_end - x_pos, y_end - y_pos
+
+ # make rel_point all relative to source connector
+ p0 = 0, 0 # x_start - x_pos, y_start - y_pos
+ p1, p2, (dx_e1, dy_e1), (dx_e2, dy_e2), (dx_e3, dy_e3) = self._rel_points
+ p3 = x_e + dx_e1, y_e + dy_e1
+ p4 = x_e + dx_e2, y_e + dy_e2
+ p5 = x_e + dx_e3, y_e + dy_e3
+ self._bounding_points = p0, p1, p4, p5 # ignores curved part =(
+
+ if cr:
+ cr.move_to(*p0)
+ cr.line_to(*p1)
+ cr.curve_to(*(p2 + p3 + p4))
+ cr.line_to(*p5)
+ self._line_path = cr.copy_path()
+
+ def draw(self, cr):
+ """
+ Draw the connection.
+ """
+ self._current_cr = cr
+ sink = self.sink_port
+ source = self.source_port
+
+ # check for changes
+ port_rotations = (source.rotation, sink.rotation)
+ if self._current_port_rotations != port_rotations:
+ self.create_shapes() # triggers _make_path() call below
+ self._current_port_rotations = port_rotations
+
+ new_coordinates = (source.parent_block.coordinate, sink.parent_block.coordinate)
+ if self._current_coordinates != new_coordinates:
+ self._make_path(cr)
+ self._current_coordinates = new_coordinates
+
+ color1, color2 = (
+ None if color is None else
+ colors.HIGHLIGHT_COLOR if self.highlighted else
+ colors.CONNECTION_DISABLED_COLOR if not self.enabled else
+ colors.CONNECTION_ERROR_COLOR if not self.is_valid() else
+ color
+ for color in (self._color1, self._color2)
+ )
+
+ cr.translate(*self.coordinate)
+ cr.set_line_width(self._line_width_factor * cr.get_line_width())
+ cr.new_path()
+ cr.append_path(self._line_path)
+
+ arrow_pos = cr.get_current_point()
+
+ if color1: # not a message connection
+ cr.set_source_rgba(*color1)
+ cr.stroke_preserve()
+
+ if color1 != color2:
+ cr.save()
+ cr.set_dash([5.0, 5.0], 5.0 if color1 else 0.0)
+ cr.set_source_rgba(*color2)
+ cr.stroke()
+ cr.restore()
+ else:
+ cr.new_path()
+
+ cr.move_to(*arrow_pos)
+ cr.set_source_rgba(*color2)
+ cr.rotate(self._arrow_rotation)
+ cr.rel_move_to(CONNECTOR_ARROW_HEIGHT, 0)
+ cr.rel_line_to(-CONNECTOR_ARROW_HEIGHT, -CONNECTOR_ARROW_BASE/2)
+ cr.rel_line_to(0, CONNECTOR_ARROW_BASE)
+ cr.close_path()
+ cr.fill()
+
+ def what_is_selected(self, coor, coor_m=None):
+ """
+ Returns:
+ self if one of the areas/lines encompasses coor, else None.
+ """
+ if coor_m:
+ return Drawable.what_is_selected(self, coor, coor_m)
+
+ x, y = [a - b for a, b in zip(coor, self.coordinate)]
+
+ cr = self._current_cr
+
+ if cr is None:
+ return
+ cr.save()
+ cr.new_path()
+ cr.append_path(self._line_path)
+ cr.set_line_width(cr.get_line_width() * LINE_SELECT_SENSITIVITY)
+ hit = cr.in_stroke(x, y)
+ cr.restore()
+
+ if hit:
+ return self
+
+
+class DummyCoreConnection(object):
+ def __init__(self, source_port, **kwargs):
+ self.parent_platform = source_port.parent_platform
+ self.source_port = source_port
+ self.sink_port = self._dummy_port = Namespace(
+ domain=source_port.domain,
+ rotation=0,
+ coordinate=(0, 0),
+ connector_coordinate_absolute=(0, 0),
+ connector_direction=0,
+ parent_block=Namespace(coordinate=(0, 0)),
+ )
+
+ self.enabled = True
+ self.highlighted = False,
+ self.is_valid = lambda: True
+ self.update(**kwargs)
+
+ def update(self, coordinate=None, rotation=None, sink_port=None):
+ dp = self._dummy_port
+ self.sink_port = sink_port if sink_port else dp
+ if coordinate:
+ dp.coordinate = coordinate
+ dp.connector_coordinate_absolute = coordinate
+ dp.parent_block.coordinate = coordinate
+ if rotation is not None:
+ dp.rotation = rotation
+ dp.connector_direction = (180 + rotation) % 360
+
+ @property
+ def has_real_sink(self):
+ return self.sink_port is not self._dummy_port
+
+DummyConnection = Connection.make_cls_with_base(DummyCoreConnection)
diff --git a/grc/gui/canvas/drawable.py b/grc/gui/canvas/drawable.py
new file mode 100644
index 0000000000..d755d4418d
--- /dev/null
+++ b/grc/gui/canvas/drawable.py
@@ -0,0 +1,183 @@
+"""
+Copyright 2007, 2008, 2009, 2016 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
+from ..Constants import LINE_SELECT_SENSITIVITY
+
+from six.moves import zip
+
+
+class Drawable(object):
+ """
+ GraphicalElement is the base class for all graphical elements.
+ It contains an X,Y coordinate, a list of rectangular areas that the element occupies,
+ and methods to detect selection of those areas.
+ """
+
+ @classmethod
+ def make_cls_with_base(cls, super_cls):
+ name = super_cls.__name__
+ bases = (super_cls,) + cls.__bases__[1:]
+ namespace = cls.__dict__.copy()
+ return type(name, bases, namespace)
+
+ def __init__(self):
+ """
+ Make a new list of rectangular areas and lines, and set the coordinate and the rotation.
+ """
+ self.coordinate = (0, 0)
+ self.rotation = 0
+ self.highlighted = False
+
+ self._bounding_rects = []
+ self._bounding_points = []
+
+ def is_horizontal(self, rotation=None):
+ """
+ Is this element horizontal?
+ If rotation is None, use this element's rotation.
+
+ Args:
+ rotation: the optional rotation
+
+ Returns:
+ true if rotation is horizontal
+ """
+ rotation = rotation or self.rotation
+ return rotation in (0, 180)
+
+ def is_vertical(self, rotation=None):
+ """
+ Is this element vertical?
+ If rotation is None, use this element's rotation.
+
+ Args:
+ rotation: the optional rotation
+
+ Returns:
+ true if rotation is vertical
+ """
+ rotation = rotation or self.rotation
+ return rotation in (90, 270)
+
+ def rotate(self, rotation):
+ """
+ Rotate all of the areas by 90 degrees.
+
+ Args:
+ rotation: multiple of 90 degrees
+ """
+ self.rotation = (self.rotation + rotation) % 360
+
+ def move(self, delta_coor):
+ """
+ Move the element by adding the delta_coor to the current coordinate.
+
+ Args:
+ delta_coor: (delta_x,delta_y) tuple
+ """
+ x, y = self.coordinate
+ dx, dy = delta_coor
+ self.coordinate = (x + dx, y + dy)
+
+ def create_labels(self, cr=None):
+ """
+ Create labels (if applicable) and call on all children.
+ Call this base method before creating labels in the element.
+ """
+
+ def create_shapes(self):
+ """
+ Create shapes (if applicable) and call on all children.
+ Call this base method before creating shapes in the element.
+ """
+
+ def draw(self, cr):
+ raise NotImplementedError()
+
+ def bounds_from_area(self, area):
+ x1, y1, w, h = area
+ x2 = x1 + w
+ y2 = y1 + h
+ self._bounding_rects = [(x1, y1, x2, y2)]
+ self._bounding_points = [(x1, y1), (x2, y1), (x1, y2), (x2, y2)]
+
+ def bounds_from_line(self, line):
+ self._bounding_rects = rects = []
+ self._bounding_points = list(line)
+ last_point = line[0]
+ for x2, y2 in line[1:]:
+ (x1, y1), last_point = last_point, (x2, y2)
+ if x1 == x2:
+ x1, x2 = x1 - LINE_SELECT_SENSITIVITY, x2 + LINE_SELECT_SENSITIVITY
+ if y2 < y1:
+ y1, y2 = y2, y1
+ elif y1 == y2:
+ y1, y2 = y1 - LINE_SELECT_SENSITIVITY, y2 + LINE_SELECT_SENSITIVITY
+ if x2 < x1:
+ x1, x2 = x2, x1
+
+ rects.append((x1, y1, x2, y2))
+
+ def what_is_selected(self, coor, coor_m=None):
+ """
+ One coordinate specified:
+ Is this element selected at given coordinate?
+ ie: is the coordinate encompassed by one of the areas or lines?
+ Both coordinates specified:
+ Is this element within the rectangular region defined by both coordinates?
+ ie: do any area corners or line endpoints fall within the region?
+
+ Args:
+ coor: the selection coordinate, tuple x, y
+ coor_m: an additional selection coordinate.
+
+ Returns:
+ self if one of the areas/lines encompasses coor, else None.
+ """
+ x, y = [a - b for a, b in zip(coor, self.coordinate)]
+
+ if not coor_m:
+ for x1, y1, x2, y2 in self._bounding_rects:
+ if x1 <= x <= x2 and y1 <= y <= y2:
+ return self
+ else:
+ x_m, y_m = [a - b for a, b in zip(coor_m, self.coordinate)]
+ if y_m < y:
+ y, y_m = y_m, y
+ if x_m < x:
+ x, x_m = x_m, x
+
+ for x1, y1 in self._bounding_points:
+ if x <= x1 <= x_m and y <= y1 <= y_m:
+ return self
+
+ def get_extents(self):
+ x_min, y_min = x_max, y_max = self.coordinate
+ x_min += min(x for x, y in self._bounding_points)
+ y_min += min(y for x, y in self._bounding_points)
+ x_max += max(x for x, y in self._bounding_points)
+ y_max += max(y for x, y in self._bounding_points)
+ return x_min, y_min, x_max, y_max
+
+ def mouse_over(self):
+ pass
+
+ def mouse_out(self):
+ pass
diff --git a/grc/gui/canvas/flowgraph.py b/grc/gui/canvas/flowgraph.py
new file mode 100644
index 0000000000..14326fd3f6
--- /dev/null
+++ b/grc/gui/canvas/flowgraph.py
@@ -0,0 +1,785 @@
+"""
+Copyright 2007-2011, 2016q 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
+
+import ast
+import functools
+import random
+from distutils.spawn import find_executable
+from itertools import count
+
+import six
+from gi.repository import GLib
+from six.moves import filter
+
+from . import colors
+from .drawable import Drawable
+from .connection import DummyConnection
+from .. import Actions, Constants, Utils, Bars, Dialogs
+from ..external_editor import ExternalEditor
+from ...core import Messages
+from ...core.FlowGraph import FlowGraph as CoreFlowgraph
+
+
+class FlowGraph(CoreFlowgraph, Drawable):
+ """
+ FlowGraph is the data structure to store graphical signal blocks,
+ graphical inputs and outputs,
+ and the connections between inputs and outputs.
+ """
+
+ def __init__(self, parent, **kwargs):
+ """
+ FlowGraph constructor.
+ Create a list for signal blocks and connections. Connect mouse handlers.
+ """
+ super(self.__class__, self).__init__(parent, **kwargs)
+ Drawable.__init__(self)
+ self.drawing_area = None
+ # important vars dealing with mouse event tracking
+ self.element_moved = False
+ self.mouse_pressed = False
+ self.press_coor = (0, 0)
+ # selected
+ self.selected_elements = set()
+ self._old_selected_port = None
+ self._new_selected_port = None
+ # current mouse hover element
+ self.element_under_mouse = None
+ # context menu
+ self._context_menu = Bars.ContextMenu()
+ self.get_context_menu = lambda: self._context_menu
+
+ self._new_connection = None
+ self._elements_to_draw = []
+ self._external_updaters = {}
+
+ def _get_unique_id(self, base_id=''):
+ """
+ Get a unique id starting with the base id.
+
+ Args:
+ base_id: the id starts with this and appends a count
+
+ Returns:
+ a unique id
+ """
+ block_ids = set(b.get_id() for b in self.blocks)
+ for index in count():
+ block_id = '{}_{}'.format(base_id, index)
+ if block_id not in block_ids:
+ break
+ return block_id
+
+ def install_external_editor(self, param):
+ target = (param.parent_block.get_id(), param.key)
+
+ if target in self._external_updaters:
+ editor = self._external_updaters[target]
+ else:
+ config = self.parent_platform.config
+ editor = (find_executable(config.editor) or
+ Dialogs.choose_editor(None, config)) # todo: pass in parent
+ if not editor:
+ return
+ updater = functools.partial(
+ self.handle_external_editor_change, target=target)
+ editor = self._external_updaters[target] = ExternalEditor(
+ editor=editor,
+ name=target[0], value=param.get_value(),
+ callback=functools.partial(GLib.idle_add, updater)
+ )
+ editor.start()
+ try:
+ editor.open_editor()
+ except Exception as e:
+ # Problem launching the editor. Need to select a new editor.
+ Messages.send('>>> Error opening an external editor. Please select a different editor.\n')
+ # Reset the editor to force the user to select a new one.
+ self.parent_platform.config.editor = ''
+
+ def handle_external_editor_change(self, new_value, target):
+ try:
+ block_id, param_key = target
+ self.get_block(block_id).get_param(param_key).set_value(new_value)
+
+ except (IndexError, ValueError): # block no longer exists
+ self._external_updaters[target].stop()
+ del self._external_updaters[target]
+ return
+ Actions.EXTERNAL_UPDATE()
+
+ def add_new_block(self, key, coor=None):
+ """
+ Add a block of the given key to this flow graph.
+
+ Args:
+ key: the block key
+ coor: an optional coordinate or None for random
+ """
+ id = self._get_unique_id(key)
+ scroll_pane = self.drawing_area.get_parent().get_parent()
+ # calculate the position coordinate
+ h_adj = scroll_pane.get_hadjustment()
+ v_adj = scroll_pane.get_vadjustment()
+ if coor is None: coor = (
+ int(random.uniform(.25, .75)*h_adj.get_page_size() + h_adj.get_value()),
+ int(random.uniform(.25, .75)*v_adj.get_page_size() + v_adj.get_value()),
+ )
+ # get the new block
+ block = self.new_block(key)
+ block.coordinate = coor
+ block.get_param('id').set_value(id)
+ Actions.ELEMENT_CREATE()
+ return id
+
+ def make_connection(self):
+ """this selection and the last were ports, try to connect them"""
+ if self._new_connection and self._new_connection.has_real_sink:
+ self._old_selected_port = self._new_connection.source_port
+ self._new_selected_port = self._new_connection.sink_port
+ if self._old_selected_port and self._new_selected_port:
+ try:
+ self.connect(self._old_selected_port, self._new_selected_port)
+ Actions.ELEMENT_CREATE()
+ except Exception as e:
+ Messages.send_fail_connection(e)
+ self._old_selected_port = None
+ self._new_selected_port = None
+ return True
+ return False
+
+ def update(self):
+ """
+ Call the top level rewrite and validate.
+ Call the top level create labels and shapes.
+ """
+ self.rewrite()
+ self.validate()
+ self.update_elements_to_draw()
+ self.create_labels()
+ self.create_shapes()
+
+ def reload(self):
+ """
+ Reload flow-graph (with updated blocks)
+
+ Args:
+ page: the page to reload (None means current)
+ Returns:
+ False if some error occurred during import
+ """
+ success = False
+ data = self.export_data()
+ if data:
+ self.unselect()
+ success = self.import_data(data)
+ self.update()
+ return success
+
+ ###########################################################################
+ # Copy Paste
+ ###########################################################################
+ def copy_to_clipboard(self):
+ """
+ Copy the selected blocks and connections into the clipboard.
+
+ Returns:
+ the clipboard
+ """
+ #get selected blocks
+ blocks = list(self.selected_blocks())
+ if not blocks:
+ return None
+ #calc x and y min
+ x_min, y_min = blocks[0].coordinate
+ for block in blocks:
+ x, y = block.coordinate
+ x_min = min(x, x_min)
+ y_min = min(y, y_min)
+ #get connections between selected blocks
+ connections = list(filter(
+ lambda c: c.source_block in blocks and c.sink_block in blocks,
+ self.connections,
+ ))
+ clipboard = (
+ (x_min, y_min),
+ [block.export_data() for block in blocks],
+ [connection.export_data() for connection in connections],
+ )
+ return clipboard
+
+ def paste_from_clipboard(self, clipboard):
+ """
+ Paste the blocks and connections from the clipboard.
+
+ Args:
+ clipboard: the nested data of blocks, connections
+ """
+ # todo: rewrite this...
+ selected = set()
+ (x_min, y_min), blocks_n, connections_n = clipboard
+ old_id2block = dict()
+ # recalc the position
+ scroll_pane = self.drawing_area.get_parent().get_parent()
+ h_adj = scroll_pane.get_hadjustment()
+ v_adj = scroll_pane.get_vadjustment()
+ x_off = h_adj.get_value() - x_min + h_adj.get_page_size() / 4
+ y_off = v_adj.get_value() - y_min + v_adj.get_page_size() / 4
+ if len(self.get_elements()) <= 1:
+ x_off, y_off = 0, 0
+ #create blocks
+ for block_n in blocks_n:
+ block_key = block_n.get('key')
+ if block_key == 'options':
+ continue
+ block = self.new_block(block_key)
+ if not block:
+ continue # unknown block was pasted (e.g. dummy block)
+ selected.add(block)
+ #set params
+ param_data = {n['key']: n['value'] for n in block_n.get('param', [])}
+ for key in block.states:
+ try:
+ block.states[key] = ast.literal_eval(param_data.pop(key))
+ except (KeyError, SyntaxError, ValueError):
+ pass
+ if block_key == 'epy_block':
+ block.get_param('_io_cache').set_value(param_data.pop('_io_cache'))
+ block.get_param('_source_code').set_value(param_data.pop('_source_code'))
+ block.rewrite() # this creates the other params
+ for param_key, param_value in six.iteritems(param_data):
+ #setup id parameter
+ if param_key == 'id':
+ old_id2block[param_value] = block
+ #if the block id is not unique, get a new block id
+ if param_value in (blk.get_id() for blk in self.blocks):
+ param_value = self._get_unique_id(param_value)
+ #set value to key
+ block.get_param(param_key).set_value(param_value)
+ #move block to offset coordinate
+ block.move((x_off, y_off))
+ #update before creating connections
+ self.update()
+ #create connections
+ for connection_n in connections_n:
+ source = old_id2block[connection_n.get('source_block_id')].get_source(connection_n.get('source_key'))
+ sink = old_id2block[connection_n.get('sink_block_id')].get_sink(connection_n.get('sink_key'))
+ self.connect(source, sink)
+ #set all pasted elements selected
+ for block in selected:
+ selected = selected.union(set(block.get_connections()))
+ self.selected_elements = set(selected)
+
+ ###########################################################################
+ # Modify Selected
+ ###########################################################################
+ def type_controller_modify_selected(self, direction):
+ """
+ Change the registered type controller for the selected signal blocks.
+
+ Args:
+ direction: +1 or -1
+
+ Returns:
+ true for change
+ """
+ return any(sb.type_controller_modify(direction) for sb in self.selected_blocks())
+
+ def port_controller_modify_selected(self, direction):
+ """
+ Change port controller for the selected signal blocks.
+
+ Args:
+ direction: +1 or -1
+
+ Returns:
+ true for changed
+ """
+ return any(sb.port_controller_modify(direction) for sb in self.selected_blocks())
+
+ def change_state_selected(self, new_state):
+ """
+ Enable/disable the selected blocks.
+
+ Args:
+ new_state: a block state
+
+ Returns:
+ true if changed
+ """
+ changed = False
+ for block in self.selected_blocks():
+ changed |= block.state != new_state
+ block.state = new_state
+ return changed
+
+ def move_selected(self, delta_coordinate):
+ """
+ Move the element and by the change in coordinates.
+
+ Args:
+ delta_coordinate: the change in coordinates
+ """
+ for selected_block in self.selected_blocks():
+ selected_block.move(delta_coordinate)
+ self.element_moved = True
+
+ def align_selected(self, calling_action=None):
+ """
+ Align the selected blocks.
+
+ Args:
+ calling_action: the action initiating the alignment
+
+ Returns:
+ True if changed, otherwise False
+ """
+ blocks = list(self.selected_blocks())
+ if calling_action is None or not blocks:
+ return False
+
+ # compute common boundary of selected objects
+ min_x, min_y = max_x, max_y = blocks[0].coordinate
+ for selected_block in blocks:
+ x, y = selected_block.coordinate
+ min_x, min_y = min(min_x, x), min(min_y, y)
+ x += selected_block.width
+ y += selected_block.height
+ max_x, max_y = max(max_x, x), max(max_y, y)
+ ctr_x, ctr_y = (max_x + min_x)/2, (max_y + min_y)/2
+
+ # align the blocks as requested
+ transform = {
+ Actions.BLOCK_VALIGN_TOP: lambda x, y, w, h: (x, min_y),
+ Actions.BLOCK_VALIGN_MIDDLE: lambda x, y, w, h: (x, ctr_y - h/2),
+ Actions.BLOCK_VALIGN_BOTTOM: lambda x, y, w, h: (x, max_y - h),
+ Actions.BLOCK_HALIGN_LEFT: lambda x, y, w, h: (min_x, y),
+ Actions.BLOCK_HALIGN_CENTER: lambda x, y, w, h: (ctr_x-w/2, y),
+ Actions.BLOCK_HALIGN_RIGHT: lambda x, y, w, h: (max_x - w, y),
+ }.get(calling_action, lambda *args: args)
+
+ for selected_block in blocks:
+ x, y = selected_block.coordinate
+ w, h = selected_block.width, selected_block.height
+ selected_block.coordinate = transform(x, y, w, h)
+
+ return True
+
+ def rotate_selected(self, rotation):
+ """
+ Rotate the selected blocks by multiples of 90 degrees.
+
+ Args:
+ rotation: the rotation in degrees
+
+ Returns:
+ true if changed, otherwise false.
+ """
+ if not any(self.selected_blocks()):
+ return False
+ #initialize min and max coordinates
+ min_x, min_y = max_x, max_y = self.selected_block.coordinate
+ # rotate each selected block, and find min/max coordinate
+ for selected_block in self.selected_blocks():
+ selected_block.rotate(rotation)
+ #update the min/max coordinate
+ x, y = selected_block.coordinate
+ min_x, min_y = min(min_x, x), min(min_y, y)
+ max_x, max_y = max(max_x, x), max(max_y, y)
+ #calculate center point of slected blocks
+ ctr_x, ctr_y = (max_x + min_x)/2, (max_y + min_y)/2
+ #rotate the blocks around the center point
+ for selected_block in self.selected_blocks():
+ x, y = selected_block.coordinate
+ x, y = Utils.get_rotated_coordinate((x - ctr_x, y - ctr_y), rotation)
+ selected_block.coordinate = (x + ctr_x, y + ctr_y)
+ return True
+
+ def remove_selected(self):
+ """
+ Remove selected elements
+
+ Returns:
+ true if changed.
+ """
+ changed = False
+ for selected_element in self.selected_elements:
+ self.remove_element(selected_element)
+ changed = True
+ return changed
+
+ def update_selected(self):
+ """
+ Remove deleted elements from the selected elements list.
+ Update highlighting so only the selected are highlighted.
+ """
+ selected_elements = self.selected_elements
+ elements = self.get_elements()
+ # remove deleted elements
+ for selected in list(selected_elements):
+ if selected in elements:
+ continue
+ selected_elements.remove(selected)
+ if self._old_selected_port and self._old_selected_port.parent not in elements:
+ self._old_selected_port = None
+ if self._new_selected_port and self._new_selected_port.parent not in elements:
+ self._new_selected_port = None
+ # update highlighting
+ for element in elements:
+ element.highlighted = element in selected_elements
+
+ ###########################################################################
+ # Draw stuff
+ ###########################################################################
+
+ def update_elements_to_draw(self):
+ hide_disabled_blocks = Actions.TOGGLE_HIDE_DISABLED_BLOCKS.get_active()
+ hide_variables = Actions.TOGGLE_HIDE_VARIABLES.get_active()
+
+ def draw_order(elem):
+ return elem.highlighted, elem.is_block, elem.enabled
+
+ elements = sorted(self.get_elements(), key=draw_order)
+ del self._elements_to_draw[:]
+
+ for element in elements:
+ if hide_disabled_blocks and not element.enabled:
+ continue # skip hidden disabled blocks and connections
+ if hide_variables and (element.is_variable or element.is_import):
+ continue # skip hidden disabled blocks and connections
+ self._elements_to_draw.append(element)
+
+ def create_labels(self, cr=None):
+ for element in self._elements_to_draw:
+ element.create_labels(cr)
+
+ def create_shapes(self):
+ for element in self._elements_to_draw:
+ element.create_shapes()
+
+ def _drawables(self):
+ # todo: cache that
+ show_comments = Actions.TOGGLE_SHOW_BLOCK_COMMENTS.get_active()
+ for element in self._elements_to_draw:
+ if element.is_block and show_comments and element.enabled:
+ yield element.draw_comment
+ if self._new_connection is not None:
+ yield self._new_connection.draw
+ for element in self._elements_to_draw:
+ yield element.draw
+
+ def draw(self, cr):
+ """Draw blocks connections comment and select rectangle"""
+ for draw_element in self._drawables():
+ cr.save()
+ draw_element(cr)
+ cr.restore()
+
+ draw_multi_select_rectangle = (
+ self.mouse_pressed and
+ (not self.selected_elements or self.drawing_area.ctrl_mask) and
+ not self._new_connection
+ )
+ if draw_multi_select_rectangle:
+ x1, y1 = self.press_coor
+ x2, y2 = self.coordinate
+ x, y = int(min(x1, x2)), int(min(y1, y2))
+ w, h = int(abs(x1 - x2)), int(abs(y1 - y2))
+ cr.set_source_rgba(
+ colors.HIGHLIGHT_COLOR[0],
+ colors.HIGHLIGHT_COLOR[1],
+ colors.HIGHLIGHT_COLOR[2],
+ 0.5,
+ )
+ cr.rectangle(x, y, w, h)
+ cr.fill()
+ cr.rectangle(x, y, w, h)
+ cr.stroke()
+
+ ##########################################################################
+ # selection handling
+ ##########################################################################
+ def update_selected_elements(self):
+ """
+ Update the selected elements.
+ The update behavior depends on the state of the mouse button.
+ When the mouse button pressed the selection will change when
+ the control mask is set or the new selection is not in the current group.
+ When the mouse button is released the selection will change when
+ the mouse has moved and the control mask is set or the current group is empty.
+ Attempt to make a new connection if the old and ports are filled.
+ If the control mask is set, merge with the current elements.
+ """
+ selected_elements = None
+ if self.mouse_pressed:
+ new_selections = self.what_is_selected(self.coordinate)
+ # update the selections if the new selection is not in the current selections
+ # allows us to move entire selected groups of elements
+ if not new_selections:
+ selected_elements = set()
+ elif self.drawing_area.ctrl_mask or self.selected_elements.isdisjoint(new_selections):
+ selected_elements = new_selections
+
+ if self._old_selected_port:
+ self._old_selected_port.force_show_label = False
+ self.create_shapes()
+ self.drawing_area.queue_draw()
+ elif self._new_selected_port:
+ self._new_selected_port.force_show_label = True
+
+ else: # called from a mouse release
+ if not self.element_moved and (not self.selected_elements or self.drawing_area.ctrl_mask) and not self._new_connection:
+ selected_elements = self.what_is_selected(self.coordinate, self.press_coor)
+
+ # this selection and the last were ports, try to connect them
+ if self.make_connection():
+ return
+
+ # update selected elements
+ if selected_elements is None:
+ return
+
+ # if ctrl, set the selected elements to the union - intersection of old and new
+ if self.drawing_area.ctrl_mask:
+ self.selected_elements ^= selected_elements
+ else:
+ self.selected_elements.clear()
+ self.selected_elements.update(selected_elements)
+ Actions.ELEMENT_SELECT()
+
+ def what_is_selected(self, coor, coor_m=None):
+ """
+ What is selected?
+ At the given coordinate, return the elements found to be selected.
+ If coor_m is unspecified, return a list of only the first element found to be selected:
+ Iterate though the elements backwards since top elements are at the end of the list.
+ If an element is selected, place it at the end of the list so that is is drawn last,
+ and hence on top. Update the selected port information.
+
+ Args:
+ coor: the coordinate of the mouse click
+ coor_m: the coordinate for multi select
+
+ Returns:
+ the selected blocks and connections or an empty list
+ """
+ selected_port = None
+ selected = set()
+ # check the elements
+ for element in reversed(self._elements_to_draw):
+ selected_element = element.what_is_selected(coor, coor_m)
+ if not selected_element:
+ continue
+ # update the selected port information
+ if selected_element.is_port:
+ if not coor_m:
+ selected_port = selected_element
+ selected_element = selected_element.parent_block
+
+ selected.add(selected_element)
+ if not coor_m:
+ break
+
+ if selected_port and selected_port.is_source:
+ selected.remove(selected_port.parent_block)
+ self._new_connection = DummyConnection(selected_port, coordinate=coor)
+ self.drawing_area.queue_draw()
+ # update selected ports
+ if selected_port is not self._new_selected_port:
+ self._old_selected_port = self._new_selected_port
+ self._new_selected_port = selected_port
+ return selected
+
+ def unselect(self):
+ """
+ Set selected elements to an empty set.
+ """
+ self.selected_elements.clear()
+
+ def select_all(self):
+ """Select all blocks in the flow graph"""
+ self.selected_elements.clear()
+ self.selected_elements.update(self._elements_to_draw)
+
+ def selected_blocks(self):
+ """
+ Get a group of selected blocks.
+
+ Returns:
+ sub set of blocks in this flow graph
+ """
+ return (e for e in self.selected_elements if e.is_block)
+
+ @property
+ def selected_block(self):
+ """
+ Get the selected block when a block or port is selected.
+
+ Returns:
+ a block or None
+ """
+ return next(self.selected_blocks(), None)
+
+ def get_selected_elements(self):
+ """
+ Get the group of selected elements.
+
+ Returns:
+ sub set of elements in this flow graph
+ """
+ return self.selected_elements
+
+ def get_selected_element(self):
+ """
+ Get the selected element.
+
+ Returns:
+ a block, port, or connection or None
+ """
+ return next(iter(self.selected_elements), None)
+
+ ##########################################################################
+ # Event Handlers
+ ##########################################################################
+ def handle_mouse_context_press(self, coordinate, event):
+ """
+ The context mouse button was pressed:
+ If no elements were selected, perform re-selection at this coordinate.
+ Then, show the context menu at the mouse click location.
+ """
+ selections = self.what_is_selected(coordinate)
+ if not selections.intersection(self.selected_elements):
+ self.coordinate = coordinate
+ self.mouse_pressed = True
+ self.update_selected_elements()
+ self.mouse_pressed = False
+ if self._new_connection:
+ self._new_connection = None
+ self.drawing_area.queue_draw()
+ self._context_menu.popup(None, None, None, None, event.button, event.time)
+
+ def handle_mouse_selector_press(self, double_click, coordinate):
+ """
+ The selector mouse button was pressed:
+ Find the selected element. Attempt a new connection if possible.
+ Open the block params window on a double click.
+ Update the selection state of the flow graph.
+ """
+ self.press_coor = coordinate
+ self.coordinate = coordinate
+ self.mouse_pressed = True
+ if double_click:
+ self.unselect()
+ self.update_selected_elements()
+
+ if double_click and self.selected_block:
+ self.mouse_pressed = False
+ Actions.BLOCK_PARAM_MODIFY()
+
+ def handle_mouse_selector_release(self, coordinate):
+ """
+ The selector mouse button was released:
+ Update the state, handle motion (dragging).
+ And update the selected flowgraph elements.
+ """
+ self.coordinate = coordinate
+ self.mouse_pressed = False
+ if self.element_moved:
+ Actions.BLOCK_MOVE()
+ self.element_moved = False
+ self.update_selected_elements()
+ if self._new_connection:
+ self._new_connection = None
+ self.drawing_area.queue_draw()
+
+ def handle_mouse_motion(self, coordinate):
+ """
+ The mouse has moved, respond to mouse dragging or notify elements
+ Move a selected element to the new coordinate.
+ Auto-scroll the scroll bars at the boundaries.
+ """
+ # to perform a movement, the mouse must be pressed
+ # (no longer checking pending events via Gtk.events_pending() - always true in Windows)
+ redraw = False
+ if not self.mouse_pressed or self._new_connection:
+ redraw = self._handle_mouse_motion_move(coordinate)
+ if self.mouse_pressed:
+ redraw = redraw or self._handle_mouse_motion_drag(coordinate)
+ if redraw:
+ self.drawing_area.queue_draw()
+
+ def _handle_mouse_motion_move(self, coordinate):
+ # only continue if mouse-over stuff is enabled (just the auto-hide port label stuff for now)
+ if not Actions.TOGGLE_AUTO_HIDE_PORT_LABELS.get_active():
+ return
+ redraw = False
+ for element in self._elements_to_draw:
+ over_element = element.what_is_selected(coordinate)
+ if not over_element:
+ continue
+ if over_element != self.element_under_mouse: # over sth new
+ if self.element_under_mouse:
+ redraw |= self.element_under_mouse.mouse_out() or False
+ self.element_under_mouse = over_element
+ redraw |= over_element.mouse_over() or False
+ break
+ else:
+ if self.element_under_mouse:
+ redraw |= self.element_under_mouse.mouse_out() or False
+ self.element_under_mouse = None
+ if redraw:
+ # self.create_labels()
+ self.create_shapes()
+ return redraw
+
+ def _handle_mouse_motion_drag(self, coordinate):
+ redraw = False
+ # remove the connection if selected in drag event
+ if len(self.selected_elements) == 1 and self.get_selected_element().is_connection:
+ Actions.ELEMENT_DELETE()
+ redraw = True
+
+ if self._new_connection:
+ e = self.element_under_mouse
+ if e and e.is_port and e.is_sink:
+ self._new_connection.update(sink_port=self.element_under_mouse)
+ else:
+ self._new_connection.update(coordinate=coordinate, rotation=0)
+ return True
+ # move the selected elements and record the new coordinate
+ x, y = coordinate
+ if not self.drawing_area.ctrl_mask:
+ X, Y = self.coordinate
+ dX, dY = int(x - X), int(y - Y)
+ active = Actions.TOGGLE_SNAP_TO_GRID.get_active() or self.drawing_area.mod1_mask
+ if not active or abs(dX) >= Constants.CANVAS_GRID_SIZE or abs(dY) >= Constants.CANVAS_GRID_SIZE:
+ self.move_selected((dX, dY))
+ self.coordinate = (x, y)
+ redraw = True
+ return redraw
+
+ def get_extents(self):
+ extent = 100000, 100000, 0, 0
+ for element in self._elements_to_draw:
+ extent = (min_or_max(xy, e_xy) for min_or_max, xy, e_xy in zip(
+ (min, min, max, max), extent, element.get_extents()
+ ))
+ return tuple(extent)
diff --git a/grc/gui/canvas/param.py b/grc/gui/canvas/param.py
new file mode 100644
index 0000000000..2ec99e70d8
--- /dev/null
+++ b/grc/gui/canvas/param.py
@@ -0,0 +1,162 @@
+# Copyright 2007-2016 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
+
+from .drawable import Drawable
+
+from .. import ParamWidgets, Utils, Constants
+
+from ...core.Param import Param as CoreParam
+
+
+class Param(CoreParam):
+ """The graphical parameter."""
+
+ make_cls_with_base = classmethod(Drawable.make_cls_with_base.__func__)
+
+ def get_input(self, *args, **kwargs):
+ """
+ Get the graphical gtk class to represent this parameter.
+ An enum requires and combo parameter.
+ A non-enum with options gets a combined entry/combo parameter.
+ All others get a standard entry parameter.
+
+ Returns:
+ gtk input class
+ """
+ type_ = self.get_type()
+ if type_ in ('file_open', 'file_save'):
+ input_widget_cls = ParamWidgets.FileParam
+
+ elif self.is_enum():
+ input_widget_cls = ParamWidgets.EnumParam
+
+ elif self.options:
+ input_widget_cls = ParamWidgets.EnumEntryParam
+
+ elif type_ == '_multiline':
+ input_widget_cls = ParamWidgets.MultiLineEntryParam
+
+ elif type_ == '_multiline_python_external':
+ input_widget_cls = ParamWidgets.PythonEditorParam
+
+ else:
+ input_widget_cls = ParamWidgets.EntryParam
+
+ return input_widget_cls(self, *args, **kwargs)
+
+ def format_label_markup(self, have_pending_changes=False):
+ block = self.parent
+ # fixme: using non-public attribute here
+ has_callback = \
+ hasattr(block, 'get_callbacks') and \
+ any(self.key in callback for callback in block._callbacks)
+
+ return '<span {underline} {foreground} font_desc="Sans 9">{label}</span>'.format(
+ underline='underline="low"' if has_callback else '',
+ foreground='foreground="blue"' if have_pending_changes else
+ 'foreground="red"' if not self.is_valid() else '',
+ label=Utils.encode(self.name)
+ )
+
+ def format_tooltip_text(self):
+ errors = self.get_error_messages()
+ tooltip_lines = ['Key: ' + self.key, 'Type: ' + self.get_type()]
+ if self.is_valid():
+ value = str(self.get_evaluated())
+ if len(value) > 100:
+ value = '{}...{}'.format(value[:50], value[-50:])
+ tooltip_lines.append('Value: ' + value)
+ elif len(errors) == 1:
+ tooltip_lines.append('Error: ' + errors[0])
+ elif len(errors) > 1:
+ tooltip_lines.append('Error:')
+ tooltip_lines.extend(' * ' + msg for msg in errors)
+ return '\n'.join(tooltip_lines)
+
+ def pretty_print(self):
+ """
+ Get the repr (nice string format) for this param.
+
+ Returns:
+ the string representation
+ """
+ ##################################################
+ # Truncate helper method
+ ##################################################
+ def _truncate(string, style=0):
+ max_len = max(27 - len(self.name), 3)
+ if len(string) > max_len:
+ if style < 0: # Front truncate
+ string = '...' + string[3-max_len:]
+ elif style == 0: # Center truncate
+ string = string[:max_len/2 - 3] + '...' + string[-max_len/2:]
+ elif style > 0: # Rear truncate
+ string = string[:max_len-3] + '...'
+ return string
+
+ ##################################################
+ # Simple conditions
+ ##################################################
+ value = self.get_value()
+ if not self.is_valid():
+ return _truncate(value)
+ if value in self.options:
+ return self.options_names[self.options.index(value)]
+
+ ##################################################
+ # Split up formatting by type
+ ##################################################
+ # Default center truncate
+ truncate = 0
+ e = self.get_evaluated()
+ t = self.get_type()
+ if isinstance(e, bool):
+ return str(e)
+ elif isinstance(e, Constants.COMPLEX_TYPES):
+ dt_str = Utils.num_to_str(e)
+ elif isinstance(e, Constants.VECTOR_TYPES):
+ # Vector types
+ if len(e) > 8:
+ # Large vectors use code
+ dt_str = self.get_value()
+ truncate = 1
+ else:
+ # Small vectors use eval
+ dt_str = ', '.join(map(Utils.num_to_str, e))
+ elif t in ('file_open', 'file_save'):
+ dt_str = self.get_value()
+ truncate = -1
+ else:
+ # Other types
+ dt_str = str(e)
+
+ # Done
+ return _truncate(dt_str, truncate)
+
+ def format_block_surface_markup(self):
+ """
+ Get the markup for this param.
+
+ Returns:
+ a pango markup string
+ """
+ return '<span {foreground} font_desc="{font}"><b>{label}:</b> {value}</span>'.format(
+ foreground='foreground="red"' if not self.is_valid() else '', font=Constants.PARAM_FONT,
+ label=Utils.encode(self.name), value=Utils.encode(self.pretty_print().replace('\n', ' '))
+ )
diff --git a/grc/gui/canvas/port.py b/grc/gui/canvas/port.py
new file mode 100644
index 0000000000..b74e4adfcc
--- /dev/null
+++ b/grc/gui/canvas/port.py
@@ -0,0 +1,225 @@
+"""
+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
+
+from gi.repository import Gtk, PangoCairo, Pango
+
+from . import colors
+from .drawable import Drawable
+from .. import Actions, Utils, Constants
+from ...core.Element import nop_write
+from ...core.Port import Port as CorePort
+
+
+class Port(CorePort, Drawable):
+ """The graphical port."""
+
+ def __init__(self, parent, direction, **n):
+ """
+ Port constructor.
+ Create list of connector coordinates.
+ """
+ super(self.__class__, self).__init__(parent, direction, **n)
+ Drawable.__init__(self)
+ self._connector_coordinate = (0, 0)
+ self._hovering = False
+ self.force_show_label = False
+
+ self._area = []
+ self._bg_color = self._border_color = 0, 0, 0, 0
+ self._font_color = list(colors.FONT_COLOR)
+
+ self._line_width_factor = 1.0
+ self._label_layout_offsets = 0, 0
+
+ self.width_with_label = self.height = 0
+
+ self.label_layout = None
+
+ @property
+ def width(self):
+ return self.width_with_label if self._show_label else Constants.PORT_LABEL_HIDDEN_WIDTH
+
+ @width.setter
+ def width(self, value):
+ self.width_with_label = value
+ self.label_layout.set_width(value * Pango.SCALE)
+
+ def _update_colors(self):
+ """
+ Get the color that represents this port's type.
+ Codes differ for ports where the vec length is 1 or greater than 1.
+
+ Returns:
+ a hex color code.
+ """
+ if not self.parent_block.enabled:
+ self._font_color[-1] = 0.4
+ color = colors.BLOCK_DISABLED_COLOR
+ else:
+ self._font_color[-1] = 1.0
+ color = colors.PORT_TYPE_TO_COLOR.get(self.get_type()) or colors.PORT_TYPE_TO_COLOR.get('')
+ vlen = self.get_vlen()
+ if vlen > 1:
+ dark = (0, 0, 30 / 255.0, 50 / 255.0, 70 / 255.0)[min(4, vlen)]
+ color = tuple(max(c - dark, 0) for c in color)
+ self._bg_color = color
+ self._border_color = tuple(max(c - 0.3, 0) for c in color)
+
+ def create_shapes(self):
+ """Create new areas and labels for the port."""
+ 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)
+
+ self._connector_coordinate = {
+ 0: (self.width, self.height / 2),
+ 90: (self.height / 2, 0),
+ 180: (0, self.height / 2),
+ 270: (self.height / 2, self.width)
+ }[self.connector_direction]
+
+ def create_labels(self, cr=None):
+ """Create the labels for the socket."""
+ self.label_layout = Gtk.DrawingArea().create_pango_layout('')
+ self.label_layout.set_alignment(Pango.Alignment.CENTER)
+
+ if cr:
+ PangoCairo.update_layout(cr, self.label_layout)
+
+ if self.domain in (Constants.GR_MESSAGE_DOMAIN, Constants.DEFAULT_DOMAIN):
+ self._line_width_factor = 1.0
+ else:
+ self._line_width_factor = 2.0
+
+ self._update_colors()
+
+ layout = self.label_layout
+ layout.set_markup('<span font_desc="{font}">{name}</span>'.format(
+ name=Utils.encode(self.name), font=Constants.PORT_FONT
+ ))
+ label_width, label_height = self.label_layout.get_size()
+
+ self.width = 2 * Constants.PORT_LABEL_PADDING + label_width / Pango.SCALE
+ self.height = 2 * Constants.PORT_LABEL_PADDING + label_height / Pango.SCALE
+ self._label_layout_offsets = [0, Constants.PORT_LABEL_PADDING]
+ if self.get_type() == 'bus':
+ self.height += Constants.PORT_EXTRA_BUS_HEIGHT
+ self._label_layout_offsets[1] += Constants.PORT_EXTRA_BUS_HEIGHT / 2
+ self.height += self.height % 2 # uneven height
+
+ def draw(self, cr):
+ """
+ Draw the socket with a label.
+ """
+ border_color = self._border_color
+ cr.set_line_width(self._line_width_factor * cr.get_line_width())
+ cr.translate(*self.coordinate)
+
+ cr.rectangle(*self._area)
+ cr.set_source_rgba(*self._bg_color)
+ cr.fill_preserve()
+ cr.set_source_rgba(*border_color)
+ cr.stroke()
+
+ if not self._show_label:
+ return # this port is folded (no label)
+
+ if self.is_vertical():
+ cr.rotate(-math.pi / 2)
+ cr.translate(-self.width, 0)
+ cr.translate(*self._label_layout_offsets)
+
+ cr.set_source_rgba(*self._font_color)
+ PangoCairo.update_layout(cr, self.label_layout)
+ PangoCairo.show_layout(cr, self.label_layout)
+
+ @property
+ def connector_coordinate_absolute(self):
+ """the coordinate where connections may attach to"""
+ return [sum(c) for c in zip(
+ self._connector_coordinate, # relative to port
+ self.coordinate, # relative to block
+ self.parent_block.coordinate # abs
+ )]
+
+ @property
+ def connector_direction(self):
+ """Get the direction that the socket points: 0,90,180,270."""
+ if self.is_source:
+ return self.rotation
+ elif self.is_sink:
+ return (self.rotation + 180) % 360
+
+ @nop_write
+ @property
+ def rotation(self):
+ return self.parent_block.rotation
+
+ def rotate(self, direction):
+ """
+ Rotate the parent rather than self.
+
+ Args:
+ direction: degrees to rotate
+ """
+ self.parent_block.rotate(direction)
+
+ def move(self, delta_coor):
+ """Move the parent rather than self."""
+ self.parent_block.move(delta_coor)
+
+ @property
+ def highlighted(self):
+ return self.parent_block.highlighted
+
+ @highlighted.setter
+ def highlighted(self, value):
+ self.parent_block.highlighted = value
+
+ @property
+ def _show_label(self):
+ """
+ Figure out if the label should be hidden
+
+ Returns:
+ true if the label should not be shown
+ """
+ return self._hovering or self.force_show_label or not Actions.TOGGLE_AUTO_HIDE_PORT_LABELS.get_active()
+
+ def mouse_over(self):
+ """
+ Called from flow graph on mouse-over
+ """
+ changed = not self._show_label
+ self._hovering = True
+ return changed
+
+ def mouse_out(self):
+ """
+ Called from flow graph on mouse-out
+ """
+ label_was_shown = self._show_label
+ self._hovering = False
+ return label_was_shown != self._show_label