summaryrefslogtreecommitdiff
path: root/grc/gui/Block.py
diff options
context:
space:
mode:
Diffstat (limited to 'grc/gui/Block.py')
-rw-r--r--grc/gui/Block.py549
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)