summaryrefslogtreecommitdiff
path: root/grc/src/platforms/gui
diff options
context:
space:
mode:
Diffstat (limited to 'grc/src/platforms/gui')
-rw-r--r--grc/src/platforms/gui/Block.py196
-rw-r--r--grc/src/platforms/gui/Colors.py34
-rw-r--r--grc/src/platforms/gui/Connection.py129
-rw-r--r--grc/src/platforms/gui/Constants.py44
-rw-r--r--grc/src/platforms/gui/Element.py229
-rw-r--r--grc/src/platforms/gui/FlowGraph.py563
-rw-r--r--grc/src/platforms/gui/Makefile.am37
-rw-r--r--grc/src/platforms/gui/Param.py221
-rw-r--r--grc/src/platforms/gui/Platform.py48
-rw-r--r--grc/src/platforms/gui/Port.py185
-rw-r--r--grc/src/platforms/gui/Utils.py71
-rw-r--r--grc/src/platforms/gui/__init__.py1
12 files changed, 1758 insertions, 0 deletions
diff --git a/grc/src/platforms/gui/Block.py b/grc/src/platforms/gui/Block.py
new file mode 100644
index 000000000..d38e17133
--- /dev/null
+++ b/grc/src/platforms/gui/Block.py
@@ -0,0 +1,196 @@
+"""
+Copyright 2007 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 ... gui import Preferences
+from Element import Element
+import Utils
+import Colors
+from ... gui.Constants import BORDER_PROXIMITY_SENSITIVITY
+from Constants import \
+ BLOCK_FONT, LABEL_PADDING_WIDTH, \
+ LABEL_PADDING_HEIGHT, PORT_HEIGHT, \
+ PORT_SEPARATION, LABEL_SEPARATION, \
+ PORT_BORDER_SEPARATION, POSSIBLE_ROTATIONS
+import pygtk
+pygtk.require('2.0')
+import gtk
+import pango
+
+class Block(Element):
+ """The graphical signal block."""
+
+ def __init__(self, *args, **kwargs):
+ """
+ Block contructor.
+ Add graphics related params to the block.
+ """
+ #add the position param
+ self._params['_coordinate'] = self.get_parent().get_parent().Param(
+ self,
+ {
+ 'name': 'GUI Coordinate',
+ 'key': '_coordinate',
+ 'type': 'raw',
+ 'value': '(0, 0)',
+ 'hide': 'all',
+ }
+ )
+ self._params['_rotation'] = self.get_parent().get_parent().Param(
+ self,
+ {
+ 'name': 'GUI Rotation',
+ 'key': '_rotation',
+ 'type': 'raw',
+ 'value': '0',
+ 'hide': 'all',
+ }
+ )
+ Element.__init__(self)
+
+ def get_coordinate(self):
+ """
+ Get the coordinate from the position param.
+ @return the coordinate tuple (x, y) or (0, 0) if failure
+ """
+ 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 - BORDER_PROXIMITY_SENSITIVITY:
+ x = fgW - BORDER_PROXIMITY_SENSITIVITY
+ if y <= 0:
+ y = 0
+ elif y >= fgH - BORDER_PROXIMITY_SENSITIVITY:
+ y = fgH - BORDER_PROXIMITY_SENSITIVITY
+ return (x, y)
+ except:
+ self.set_coordinate((0, 0))
+ return (0, 0)
+
+ def set_coordinate(self, coor):
+ """
+ Set the coordinate into the position param.
+ @param coor the coordinate tuple (x, y)
+ """
+ self.get_param('_coordinate').set_value(str(coor))
+
+ def get_rotation(self):
+ """
+ Get the rotation from the position param.
+ @return 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(POSSIBLE_ROTATIONS[0])
+ return POSSIBLE_ROTATIONS[0]
+
+ def set_rotation(self, rot):
+ """
+ Set the rotation into the position param.
+ @param rot the rotation in degrees
+ """
+ self.get_param('_rotation').set_value(str(rot))
+
+ def update(self):
+ """Update the block, parameters, and ports when a change occurs."""
+ self.bg_color = self.get_enabled() and Colors.BG_COLOR or Colors.DISABLED_BG_COLOR
+ self.clear()
+ self._create_labels()
+ self.W = self.label_width + 2*LABEL_PADDING_WIDTH
+ max_ports = max(len(self.get_sinks()), len(self.get_sources()), 1)
+ self.H = max(self.label_height+2*LABEL_PADDING_HEIGHT, 2*PORT_BORDER_SEPARATION + max_ports*PORT_HEIGHT + (max_ports-1)*PORT_SEPARATION)
+ 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))
+ map(lambda p: p.update(), self.get_sinks() + self.get_sources())
+
+ def _create_labels(self):
+ """Create the labels for the signal block."""
+ layouts = list()
+ #create the main layout
+ layout = gtk.DrawingArea().create_pango_layout('')
+ layouts.append(layout)
+ if self.is_valid(): layout.set_markup('<b>'+Utils.xml_encode(self.get_name())+'</b>')
+ else: layout.set_markup('<span foreground="red"><b>'+Utils.xml_encode(self.get_name())+'</b></span>')
+ desc = pango.FontDescription(BLOCK_FONT)
+ layout.set_font_description(desc)
+ self.label_width, self.label_height = layout.get_pixel_size()
+ #display the params (except for the special params id and position)
+ if Preferences.show_params():
+ for param in filter(lambda p: p.get_hide() not in ('all', 'part'), self.get_params()):
+ if not Preferences.show_id() and param.get_key() == 'id': continue
+ layout = param.get_layout()
+ layouts.append(layout)
+ w,h = layout.get_pixel_size()
+ self.label_width = max(w, self.label_width)
+ self.label_height = self.label_height + h + LABEL_SEPARATION
+ width = self.label_width
+ height = self.label_height
+ #setup the pixmap
+ pixmap = gtk.gdk.Pixmap(self.get_parent().get_window(), width, height, -1)
+ gc = pixmap.new_gc()
+ gc.foreground = self.bg_color
+ pixmap.draw_rectangle(gc, True, 0, 0, width, height)
+ gc.foreground = Colors.TXT_COLOR
+ #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 + LABEL_SEPARATION
+ #create vertical and horizontal images
+ self.horizontal_label = image = pixmap.get_image(0, 0, width, height)
+ if self.is_vertical():
+ self.vertical_label = vimage = gtk.gdk.Image(gtk.gdk.IMAGE_NORMAL, pixmap.get_visual(), height, width)
+ for i in range(width):
+ for j in range(height): vimage.put_pixel(j, width-i-1, image.get_pixel(i, j))
+
+ def draw(self, window):
+ """
+ Draw the signal block with label and inputs/outputs.
+ @param window the gtk window to draw on
+ """
+ x, y = self.get_coordinate()
+ #draw main block
+ Element.draw(self, window, BG_color=self.bg_color)
+ #draw label image
+ gc = self.get_gc()
+ if self.is_horizontal():
+ window.draw_image(gc, self.horizontal_label, 0, 0, x+LABEL_PADDING_WIDTH, y+(self.H-self.label_height)/2, -1, -1)
+ elif self.is_vertical():
+ window.draw_image(gc, self.vertical_label, 0, 0, x+(self.H-self.label_height)/2, y+LABEL_PADDING_WIDTH, -1, -1)
+ #draw ports
+ map(lambda p: p.draw(window), self.get_ports())
+
+ def what_is_selected(self, coor, coor_m=None):
+ """
+ Get the element that is selected.
+ @param coor the (x,y) tuple
+ @param coor_m the (x_m, y_m) tuple
+ @return this block, a port, or None
+ """
+ for port in self.get_ports():
+ port_selected = port.what_is_selected(coor, coor_m)
+ if port_selected: return port_selected
+ return Element.what_is_selected(self, coor, coor_m)
diff --git a/grc/src/platforms/gui/Colors.py b/grc/src/platforms/gui/Colors.py
new file mode 100644
index 000000000..353cd1c9f
--- /dev/null
+++ b/grc/src/platforms/gui/Colors.py
@@ -0,0 +1,34 @@
+"""
+Copyright 2008 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
+"""
+
+import pygtk
+pygtk.require('2.0')
+import gtk
+
+COLORMAP = gtk.gdk.colormap_get_system() #create all of the colors
+def get_color(color_code): return COLORMAP.alloc_color(color_code, True, True)
+
+BACKGROUND_COLOR = get_color('#FFF9FF') #main window background
+FG_COLOR = get_color('black') #normal border color
+BG_COLOR = get_color('#F1ECFF') #default background
+DISABLED_BG_COLOR = get_color('#CCCCCC') #disabled background
+DISABLED_FG_COLOR = get_color('#999999') #disabled foreground
+H_COLOR = get_color('#00FFFF') #Highlight border color
+TXT_COLOR = get_color('black') #text color
+ERROR_COLOR = get_color('red') #error color
diff --git a/grc/src/platforms/gui/Connection.py b/grc/src/platforms/gui/Connection.py
new file mode 100644
index 000000000..44048e181
--- /dev/null
+++ b/grc/src/platforms/gui/Connection.py
@@ -0,0 +1,129 @@
+"""
+Copyright 2007, 2008 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
+"""
+
+import Utils
+from Element import Element
+import Colors
+from Constants import CONNECTOR_ARROW_BASE, CONNECTOR_ARROW_HEIGHT
+
+class Connection(Element):
+ """A graphical connection for ports."""
+
+ def get_coordinate(self):
+ """
+ Get the 0,0 coordinate.
+ Coordinates are irrelevant in connection.
+ @return 0, 0
+ """
+ return (0, 0)
+
+ def get_rotation(self):
+ """
+ Get the 0 degree rotation.
+ Rotations are irrelevant in connection.
+ @return 0
+ """
+ return 0
+
+ def update(self):
+ """Precalculate relative coordinates."""
+ self._sink_rot = None
+ self._source_rot = None
+ self._sink_coor = None
+ self._source_coor = None
+ #get the source coordinate
+ connector_length = self.get_source().get_connector_length()
+ self.x1, self.y1 = Utils.get_rotated_coordinate((connector_length, 0), self.get_source().get_rotation())
+ #get the sink coordinate
+ connector_length = self.get_sink().get_connector_length() + CONNECTOR_ARROW_HEIGHT
+ self.x2, self.y2 = Utils.get_rotated_coordinate((-connector_length, 0), self.get_sink().get_rotation())
+ #build the arrow
+ self.arrow = [(0, 0),
+ Utils.get_rotated_coordinate((-CONNECTOR_ARROW_HEIGHT, -CONNECTOR_ARROW_BASE/2), self.get_sink().get_rotation()),
+ Utils.get_rotated_coordinate((-CONNECTOR_ARROW_HEIGHT, CONNECTOR_ARROW_BASE/2), self.get_sink().get_rotation()),
+ ]
+ self._update_after_move()
+
+ def _update_after_move(self):
+ """Calculate coordinates."""
+ self.clear()
+ #source connector
+ source = self.get_source()
+ X, Y = source.get_connector_coordinate()
+ x1, y1 = self.x1 + X, self.y1 + Y
+ self.add_line((x1, y1), (X, Y))
+ #sink connector
+ sink = self.get_sink()
+ X, Y = sink.get_connector_coordinate()
+ x2, y2 = self.x2 + X, self.y2 + Y
+ self.add_line((x2, y2), (X, Y))
+ #adjust arrow
+ self._arrow = [(x+X, y+Y) for x,y in self.arrow]
+ #add the horizontal and vertical lines in this connection
+ if abs(source.get_connector_direction() - sink.get_connector_direction()) == 180:
+ #2 possible point sets to create a 3-line connector
+ mid_x, mid_y = (x1 + x2)/2.0, (y1 + y2)/2.0
+ points = [((mid_x, y1), (mid_x, y2)), ((x1, mid_y), (x2, mid_y))]
+ #source connector -> points[0][0] should be in the direction of source (if possible)
+ if Utils.get_angle_from_coordinates((x1, y1), points[0][0]) != source.get_connector_direction(): points.reverse()
+ #points[0][0] -> sink connector should not be in the direction of sink
+ if Utils.get_angle_from_coordinates(points[0][0], (x2, y2)) == sink.get_connector_direction(): points.reverse()
+ #points[0][0] -> source connector should not be in the direction of source
+ if Utils.get_angle_from_coordinates(points[0][0], (x1, y1)) == source.get_connector_direction(): points.reverse()
+ #create 3-line connector
+ p1, p2 = map(int, points[0][0]), map(int, points[0][1])
+ self.add_line((x1, y1), p1)
+ self.add_line(p1, p2)
+ self.add_line((x2, y2), p2)
+ else:
+ #2 possible points to create a right-angled connector
+ points = [(x1, y2), (x2, y1)]
+ #source connector -> points[0] should be in the direction of source (if possible)
+ if Utils.get_angle_from_coordinates((x1, y1), points[0]) != source.get_connector_direction(): points.reverse()
+ #points[0] -> sink connector should not be in the direction of sink
+ if Utils.get_angle_from_coordinates(points[0], (x2, y2)) == sink.get_connector_direction(): points.reverse()
+ #points[0] -> source connector should not be in the direction of source
+ if Utils.get_angle_from_coordinates(points[0], (x1, y1)) == source.get_connector_direction(): points.reverse()
+ #create right-angled connector
+ self.add_line((x1, y1), points[0])
+ self.add_line((x2, y2), points[0])
+
+ def draw(self, window):
+ """
+ Draw the connection.
+ @param window the gtk window to draw on
+ """
+ sink = self.get_sink()
+ source = self.get_source()
+ #check for changes
+ if self._sink_rot != sink.get_rotation() or self._source_rot != source.get_rotation(): self.update()
+ elif self._sink_coor != sink.get_coordinate() or self._source_coor != source.get_coordinate(): self._update_after_move()
+ #cache values
+ self._sink_rot = sink.get_rotation()
+ self._source_rot = source.get_rotation()
+ self._sink_coor = sink.get_coordinate()
+ self._source_coor = source.get_coordinate()
+ #draw
+ fg_color = self.get_enabled() and Colors.FG_COLOR or Colors.DISABLED_FG_COLOR
+ Element.draw(self, window, FG_color=fg_color)
+ gc = self.get_gc()
+ if self.is_valid(): gc.foreground = Colors.FG_COLOR
+ else: gc.foreground = Colors.ERROR_COLOR
+ #draw arrow on sink port
+ window.draw_polygon(gc, True, self._arrow)
diff --git a/grc/src/platforms/gui/Constants.py b/grc/src/platforms/gui/Constants.py
new file mode 100644
index 000000000..b2e9bfed5
--- /dev/null
+++ b/grc/src/platforms/gui/Constants.py
@@ -0,0 +1,44 @@
+#
+# Copyright 2008 Free Software Foundation, Inc.
+#
+# This file is part of GNU Radio
+#
+# GNU Radio 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 3, or (at your option)
+# any later version.
+#
+# GNU Radio 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 GNU Radio; see the file COPYING. If not, write to
+# the Free Software Foundation, Inc., 51 Franklin Street,
+# Boston, MA 02110-1301, USA.
+#
+
+#label constraint dimensions
+LABEL_SEPARATION = 3
+LABEL_PADDING_WIDTH = 9
+LABEL_PADDING_HEIGHT = 9
+#port constraint dimensions
+PORT_SEPARATION = 17
+PORT_HEIGHT = 15
+PORT_WIDTH = 25
+PORT_BORDER_SEPARATION = 9
+#fonts
+PARAM_LABEL_FONT = 'Sans 9.5'
+PARAM_FONT = 'Sans 7.5'
+BLOCK_FONT = 'Sans 8'
+PORT_FONT = 'Sans 7.5'
+#minimal length of connector
+CONNECTOR_EXTENSION_MINIMAL = 11
+#increment length for connector
+CONNECTOR_EXTENSION_INCREMENT = 11
+#connection arrow dimensions
+CONNECTOR_ARROW_BASE = 13
+CONNECTOR_ARROW_HEIGHT = 17
+#possible rotations in degrees
+POSSIBLE_ROTATIONS = (0, 90, 180, 270)
diff --git a/grc/src/platforms/gui/Element.py b/grc/src/platforms/gui/Element.py
new file mode 100644
index 000000000..f97d85ff6
--- /dev/null
+++ b/grc/src/platforms/gui/Element.py
@@ -0,0 +1,229 @@
+"""
+Copyright 2007 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
+"""
+
+import Colors
+import pygtk
+pygtk.require('2.0')
+import gtk
+import pango
+from ... gui.Constants import CONNECTION_SELECT_SENSITIVITY
+from Constants import POSSIBLE_ROTATIONS
+
+class Element(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.
+ """
+
+ def __init__(self, *args, **kwargs):
+ """
+ Make a new list of rectangular areas and lines, and set the coordinate and the rotation.
+ """
+ self.set_rotation(POSSIBLE_ROTATIONS[0])
+ self.set_coordinate((0, 0))
+ self.clear()
+ self.set_highlighted(False)
+
+ def is_horizontal(self, rotation=None):
+ """
+ Is this element horizontal?
+ If rotation is None, use this element's rotation.
+ @param rotation the optional rotation
+ @return true if rotation is horizontal
+ """
+ rotation = rotation or self.get_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.
+ @param rotation the optional rotation
+ @return true if rotation is vertical
+ """
+ rotation = rotation or self.get_rotation()
+ return rotation in (90, 270)
+
+ def get_gc(self): return self._gc
+
+ def draw(self, window, BG_color=Colors.BG_COLOR, FG_color=Colors.FG_COLOR):
+ """
+ Draw in the given window.
+ @param window the gtk window to draw on
+ @param BG_color the background color
+ @param FG_color the foreground color
+ """
+ gc = self.get_parent().get_gc()
+ self._gc = gc
+ X,Y = self.get_coordinate()
+ for (rX,rY),(W,H) in self.areas_dict[self.get_rotation()]:
+ aX = X + rX
+ aY = Y + rY
+ gc.foreground = BG_color
+ window.draw_rectangle(gc, True, aX, aY, W, H)
+ gc.foreground = self.is_highlighted() and Colors.H_COLOR or FG_color
+ window.draw_rectangle(gc, False, aX, aY, W, H)
+ for (x1, y1),(x2, y2) in self.lines_dict[self.get_rotation()]:
+ gc.foreground = self.is_highlighted() and Colors.H_COLOR or FG_color
+ window.draw_line(gc, X+x1, Y+y1, X+x2, Y+y2)
+
+ def rotate(self, direction):
+ """
+ Rotate all of the areas by 90 degrees.
+ @param direction 90 or 270 degrees
+ """
+ self.set_rotation((self.get_rotation() + direction)%360)
+
+ def clear(self):
+ """Empty the lines and areas."""
+ self.areas_dict = dict((rotation, list()) for rotation in POSSIBLE_ROTATIONS)
+ self.lines_dict = dict((rotation, list()) for rotation in POSSIBLE_ROTATIONS)
+
+ def set_coordinate(self, coor):
+ """
+ Set the reference coordinate.
+ @param coor the coordinate tuple (x,y)
+ """
+ self.coor = coor
+
+ def get_parent(self):
+ """
+ Get the parent of this element.
+ @return the parent
+ """
+ return self.parent
+
+ def set_highlighted(self, highlighted):
+ """
+ Set the highlight status.
+ @param highlighted true to enable highlighting
+ """
+ self.highlighted = highlighted
+
+ def is_highlighted(self):
+ """
+ Get the highlight status.
+ @return true if highlighted
+ """
+ return self.highlighted
+
+ def get_coordinate(self):
+ """Get the coordinate.
+ @return the coordinate tuple (x,y)
+ """
+ return self.coor
+
+ def move(self, delta_coor):
+ """
+ Move the element by adding the delta_coor to the current coordinate.
+ @param delta_coor (delta_x,delta_y) tuple
+ """
+ deltaX, deltaY = delta_coor
+ X, Y = self.get_coordinate()
+ self.set_coordinate((X+deltaX, Y+deltaY))
+
+ def add_area(self, rel_coor, area, rotation=None):
+ """
+ Add an area to the area list.
+ An area is actually a coordinate relative to the main coordinate
+ with a width/height pair relative to the area coordinate.
+ A positive width is to the right of the coordinate.
+ A positive height is above the coordinate.
+ The area is associated with a rotation.
+ If rotation is not specified, the element's current rotation is used.
+ @param rel_coor (x,y) offset from this element's coordinate
+ @param area (width,height) tuple
+ @param rotation rotation in degrees
+ """
+ self.areas_dict[rotation or self.get_rotation()].append((rel_coor, area))
+
+ def add_line(self, rel_coor1, rel_coor2, rotation=None):
+ """
+ Add a line to the line list.
+ A line is defined by 2 relative coordinates.
+ Lines must be horizontal or vertical.
+ The line is associated with a rotation.
+ If rotation is not specified, the element's current rotation is used.
+ @param rel_coor1 relative (x1,y1) tuple
+ @param rel_coor2 relative (x2,y2) tuple
+ @param rotation rotation in degrees
+ """
+ self.lines_dict[rotation or self.get_rotation()].append((rel_coor1, rel_coor2))
+
+ 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?
+ @param coor the selection coordinate, tuple x, y
+ @param coor_m an additional selection coordinate.
+ @return self if one of the areas/lines encompasses coor, else None.
+ """
+ #function to test if p is between a and b (inclusive)
+ in_between = lambda p, a, b: p >= min(a, b) and p <= max(a, b)
+ #relative coordinate
+ x, y = [a-b for a,b in zip(coor, self.get_coordinate())]
+ if coor_m:
+ x_m, y_m = [a-b for a,b in zip(coor_m, self.get_coordinate())]
+ #handle rectangular areas
+ for (x1,y1), (w,h) in self.areas_dict[self.get_rotation()]:
+ if in_between(x1, x, x_m) and in_between(y1, y, y_m) or \
+ in_between(x1+w, x, x_m) and in_between(y1, y, y_m) or \
+ in_between(x1, x, x_m) and in_between(y1+h, y, y_m) or \
+ in_between(x1+w, x, x_m) and in_between(y1+h, y, y_m):
+ return self
+ #handle horizontal or vertical lines
+ for (x1, y1), (x2, y2) in self.lines_dict[self.get_rotation()]:
+ if in_between(x1, x, x_m) and in_between(y1, y, y_m) or \
+ in_between(x2, x, x_m) and in_between(y2, y, y_m):
+ return self
+ return None
+ else:
+ #handle rectangular areas
+ for (x1,y1), (w,h) in self.areas_dict[self.get_rotation()]:
+ if in_between(x, x1, x1+w) and in_between(y, y1, y1+h): return self
+ #handle horizontal or vertical lines
+ for (x1, y1), (x2, y2) in self.lines_dict[self.get_rotation()]:
+ if x1 == x2: x1, x2 = x1-CONNECTION_SELECT_SENSITIVITY, x2+CONNECTION_SELECT_SENSITIVITY
+ if y1 == y2: y1, y2 = y1-CONNECTION_SELECT_SENSITIVITY, y2+CONNECTION_SELECT_SENSITIVITY
+ if in_between(x, x1, x2) and in_between(y, y1, y2): return self
+ return None
+
+ def get_rotation(self):
+ """
+ Get the rotation in degrees.
+ @return the rotation
+ """
+ return self.rotation
+
+ def set_rotation(self, rotation):
+ """
+ Set the rotation in degrees.
+ @param rotation the rotation"""
+ if rotation not in POSSIBLE_ROTATIONS:
+ raise Exception('"%s" is not one of the possible rotations: (%s)'%(rotation, POSSIBLE_ROTATIONS))
+ self.rotation = rotation
+
+ def update(self):
+ """Do nothing for the update. Dummy method."""
+ pass
diff --git a/grc/src/platforms/gui/FlowGraph.py b/grc/src/platforms/gui/FlowGraph.py
new file mode 100644
index 000000000..1e654e1bf
--- /dev/null
+++ b/grc/src/platforms/gui/FlowGraph.py
@@ -0,0 +1,563 @@
+"""
+Copyright 2007 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 ... gui import Preferences
+from ... gui.Constants import \
+ DIR_LEFT, DIR_RIGHT, \
+ SCROLL_PROXIMITY_SENSITIVITY, SCROLL_DISTANCE, \
+ MOTION_DETECT_REDRAWING_SENSITIVITY
+from ... gui.Actions import \
+ ELEMENT_CREATE, ELEMENT_SELECT, \
+ BLOCK_PARAM_MODIFY, BLOCK_MOVE
+import Colors
+import Utils
+from ... gui.ParamsDialog import ParamsDialog
+from Element import Element
+from .. base import FlowGraph as _FlowGraph
+import pygtk
+pygtk.require('2.0')
+import gtk
+import random
+import time
+from ... gui import Messages
+
+class FlowGraph(Element):
+ """
+ FlowGraph is the data structure to store graphical signal blocks,
+ graphical inputs and outputs,
+ and the connections between inputs and outputs.
+ """
+
+ def __init__(self, *args, **kwargs):
+ """
+ FlowGraph contructor.
+ Create a list for signal blocks and connections. Connect mouse handlers.
+ """
+ Element.__init__(self)
+ #when is the flow graph selected? (used by keyboard event handler)
+ self.is_selected = lambda: bool(self.get_selected_elements())
+ #important vars dealing with mouse event tracking
+ self.element_moved = False
+ self.mouse_pressed = False
+ self.unselect()
+ self.time = 0
+ self.press_coor = (0, 0)
+ #selected ports
+ self._old_selected_port = None
+ self._new_selected_port = None
+
+ def _get_unique_id(self, base_id=''):
+ """
+ Get a unique id starting with the base id.
+ @param base_id the id starts with this and appends a count
+ @return a unique id
+ """
+ index = -1
+ while True:
+ id = (index < 0) and base_id or '%s%d'%(base_id, index)
+ index = index + 1
+ #make sure that the id is not used by another block
+ if not filter(lambda b: b.get_id() == id, self.get_blocks()): return id
+
+###########################################################################
+# Access Drawing Area
+###########################################################################
+ def get_drawing_area(self): return self.drawing_area
+ def get_gc(self): return self.get_drawing_area().gc
+ def get_pixmap(self): return self.get_drawing_area().pixmap
+ def get_size(self): return self.get_drawing_area().get_size_request()
+ def set_size(self, *args): self.get_drawing_area().set_size_request(*args)
+ def get_window(self): return self.get_drawing_area().window
+ def get_scroll_pane(self): return self.drawing_area.get_parent()
+ def get_ctrl_mask(self): return self.drawing_area.ctrl_mask
+
+ def add_new_block(self, key):
+ """
+ Add a block of the given key to this flow graph.
+ @param key the block key
+ """
+ id = self._get_unique_id(key)
+ #calculate the position coordinate
+ h_adj = self.get_scroll_pane().get_hadjustment()
+ v_adj = self.get_scroll_pane().get_vadjustment()
+ x = int(random.uniform(.25, .75)*h_adj.page_size + h_adj.get_value())
+ y = int(random.uniform(.25, .75)*v_adj.page_size + v_adj.get_value())
+ #get the new block
+ block = self.get_new_block(key)
+ block.set_coordinate((x, y))
+ block.set_rotation(0)
+ block.get_param('id').set_value(id)
+ self.handle_states(ELEMENT_CREATE)
+
+ ###########################################################################
+ # Copy Paste
+ ###########################################################################
+ def copy_to_clipboard(self):
+ """
+ Copy the selected blocks and connections into the clipboard.
+ @return the clipboard
+ """
+ #get selected blocks
+ blocks = self.get_selected_blocks()
+ if not blocks: return None
+ #calc x and y min
+ x_min, y_min = blocks[0].get_coordinate()
+ for block in blocks:
+ x, y = block.get_coordinate()
+ x_min = min(x, x_min)
+ y_min = min(y, y_min)
+ #get connections between selected blocks
+ connections = filter(
+ lambda c: c.get_source().get_parent() in blocks and c.get_sink().get_parent() in blocks,
+ self.get_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.
+ @param clipboard the nested data of blocks, connections
+ """
+ selected = set()
+ (x_min, y_min), blocks_n, connections_n = clipboard
+ old_id2block = dict()
+ #recalc the position
+ h_adj = self.get_scroll_pane().get_hadjustment()
+ v_adj = self.get_scroll_pane().get_vadjustment()
+ x_off = h_adj.get_value() - x_min + h_adj.page_size/4
+ y_off = v_adj.get_value() - y_min + v_adj.page_size/4
+ #create blocks
+ for block_n in blocks_n:
+ block_key = block_n['key']
+ if block_key == 'options': continue
+ block_id = self._get_unique_id(block_key)
+ block = self.get_new_block(block_key)
+ selected.add(block)
+ #set params
+ params_n = Utils.listify(block_n, 'param')
+ for param_n in params_n:
+ param_key = param_n['key']
+ param_value = param_n['value']
+ #setup id parameter
+ if param_key == 'id':
+ old_id2block[param_value] = block
+ param_value = block_id
+ #set value to key
+ block.get_param(param_key).set_value(param_value)
+ #move block to offset coordinate
+ block.move((x_off, y_off))
+ #create connections
+ for connection_n in connections_n:
+ source = old_id2block[connection_n['source_block_id']].get_source(connection_n['source_key'])
+ sink = old_id2block[connection_n['sink_block_id']].get_sink(connection_n['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 = list(selected)
+
+ ###########################################################################
+ # Modify Selected
+ ###########################################################################
+ def type_controller_modify_selected(self, direction):
+ """
+ Change the registered type controller for the selected signal blocks.
+ @param direction +1 or -1
+ @return true for change
+ """
+ changed = False
+ for selected_block in self.get_selected_blocks():
+ for child in selected_block.get_params() + selected_block.get_ports():
+ #find a param that controls a type
+ type_param = None
+ for param in selected_block.get_params():
+ if not type_param and param.is_enum(): type_param = param
+ if param.is_enum() and param.get_key() in child._type: type_param = param
+ if type_param:
+ #try to increment the enum by direction
+ try:
+ keys = type_param.get_option_keys()
+ old_index = keys.index(type_param.get_value())
+ new_index = (old_index + direction + len(keys))%len(keys)
+ type_param.set_value(keys[new_index])
+ changed = True
+ except: pass
+ return changed
+
+ def port_controller_modify_selected(self, direction):
+ """
+ Change port controller for the selected signal blocks.
+ @param direction +1 or -1
+ @return true for changed
+ """
+ changed = False
+ for selected_block in self.get_selected_blocks():
+ for ports in (selected_block.get_sources(), selected_block.get_sinks()):
+ if ports and hasattr(ports[0], 'get_nports') and ports[0].get_nports():
+ #find the param that controls port0
+ for param in selected_block.get_params():
+ if param.get_key() in ports[0]._nports:
+ #try to increment the port controller by direction
+ try:
+ value = param.evaluate()
+ value = value + direction
+ assert 0 < value
+ param.set_value(value)
+ changed = True
+ except: pass
+ return changed
+
+ def param_modify_selected(self):
+ """
+ Create and show a param modification dialog for the selected block.
+ @return true if parameters were changed
+ """
+ if self.get_selected_block():
+ signal_block_params_dialog = ParamsDialog(self.get_selected_block())
+ return signal_block_params_dialog.run()
+ return False
+
+ def enable_selected(self, enable):
+ """
+ Enable/disable the selected blocks.
+ @param enable true to enable
+ @return true if changed
+ """
+ changed = False
+ for selected_block in self.get_selected_blocks():
+ if selected_block.get_enabled() != enable:
+ selected_block.set_enabled(enable)
+ changed = True
+ return changed
+
+ def move_selected(self, delta_coordinate):
+ """
+ Move the element and by the change in coordinates.
+ @param delta_coordinate the change in coordinates
+ """
+ for selected_block in self.get_selected_blocks():
+ selected_block.move(delta_coordinate)
+ self.element_moved = True
+
+ def rotate_selected(self, direction):
+ """
+ Rotate the selected blocks by 90 degrees.
+ @param direction DIR_LEFT or DIR_RIGHT
+ @return true if changed, otherwise false.
+ """
+ if not self.get_selected_blocks(): return False
+ #determine the number of degrees to rotate
+ rotation = {DIR_LEFT: 90, DIR_RIGHT:270}[direction]
+ #initialize min and max coordinates
+ min_x, min_y = self.get_selected_block().get_coordinate()
+ max_x, max_y = self.get_selected_block().get_coordinate()
+ #rotate each selected block, and find min/max coordinate
+ for selected_block in self.get_selected_blocks():
+ selected_block.rotate(rotation)
+ #update the min/max coordinate
+ x, y = selected_block.get_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.get_selected_blocks():
+ x, y = selected_block.get_coordinate()
+ x, y = Utils.get_rotated_coordinate((x - ctr_x, y - ctr_y), rotation)
+ selected_block.set_coordinate((x + ctr_x, y + ctr_y))
+ return True
+
+ def remove_selected(self):
+ """
+ Remove selected elements
+ @return true if changed.
+ """
+ changed = False
+ for selected_element in self.get_selected_elements():
+ self.remove_element(selected_element)
+ changed = True
+ return changed
+
+ def draw(self):
+ """
+ Draw the background and grid if enabled.
+ Draw all of the elements in this flow graph onto the pixmap.
+ Draw the pixmap to the drawable window of this flow graph.
+ """
+ if self.get_gc():
+ W,H = self.get_size()
+ #draw the background
+ self.get_gc().foreground = Colors.BACKGROUND_COLOR
+ self.get_pixmap().draw_rectangle(self.get_gc(), True, 0, 0, W, H)
+ #draw grid (depends on prefs)
+ if Preferences.show_grid():
+ grid_size = Preferences.get_grid_size()
+ points = list()
+ for i in range(W/grid_size):
+ for j in range(H/grid_size):
+ points.append((i*grid_size, j*grid_size))
+ self.get_gc().foreground = Colors.TXT_COLOR
+ self.get_pixmap().draw_points(self.get_gc(), points)
+ #draw multi select rectangle
+ if self.mouse_pressed and (not self.get_selected_elements() or self.get_ctrl_mask()):
+ #coordinates
+ x1, y1 = self.press_coor
+ x2, y2 = self.get_coordinate()
+ #calculate top-left coordinate and width/height
+ x, y = int(min(x1, x2)), int(min(y1, y2))
+ w, h = int(abs(x1 - x2)), int(abs(y1 - y2))
+ #draw
+ self.get_gc().foreground = Colors.H_COLOR
+ self.get_pixmap().draw_rectangle(self.get_gc(), True, x, y, w, h)
+ self.get_gc().foreground = Colors.TXT_COLOR
+ self.get_pixmap().draw_rectangle(self.get_gc(), False, x, y, w, h)
+ #draw blocks on top of connections
+ for element in self.get_connections() + self.get_blocks():
+ element.draw(self.get_pixmap())
+ #draw selected blocks on top of selected connections
+ for selected_element in self.get_selected_connections() + self.get_selected_blocks():
+ selected_element.draw(self.get_pixmap())
+ self.get_drawing_area().draw()
+
+ def update(self):
+ """
+ Update highlighting so only the selected is highlighted.
+ Call update on all elements.
+ Resize the window if size changed.
+ """
+ #update highlighting
+ map(lambda e: e.set_highlighted(False), self.get_elements())
+ for selected_element in self.get_selected_elements():
+ selected_element.set_highlighted(True)
+ #update all elements
+ map(lambda e: e.update(), self.get_elements())
+ #set the size of the flow graph area
+ old_x, old_y = self.get_size()
+ try: new_x, new_y = self.get_option('window_size')
+ except: new_x, new_y = old_x, old_y
+ if new_x != old_x or new_y != old_y: self.set_size(new_x, new_y)
+
+ ##########################################################################
+ ## Get Selected
+ ##########################################################################
+ def unselect(self):
+ """
+ Set selected elements to an empty set.
+ """
+ self._selected_elements = []
+
+ 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 backwardssince 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.
+ @param coor the coordinate of the mouse click
+ @param coor_m the coordinate for multi select
+ @return the selected blocks and connections or an empty list
+ """
+ selected_port = None
+ selected = set()
+ #check the elements
+ for element in reversed(self.get_elements()):
+ 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.get_parent()
+ selected.add(selected_element)
+ #single select mode, break
+ if not coor_m:
+ self.get_elements().remove(element)
+ self.get_elements().append(element)
+ break;
+ #update selected ports
+ self._old_selected_port = self._new_selected_port
+ self._new_selected_port = selected_port
+ return list(selected)
+
+ def get_selected_connections(self):
+ """
+ Get a group of selected connections.
+ @return sub set of connections in this flow graph
+ """
+ selected = set()
+ for selected_element in self.get_selected_elements():
+ if selected_element.is_connection(): selected.add(selected_element)
+ return list(selected)
+
+ def get_selected_blocks(self):
+ """
+ Get a group of selected blocks.
+ @return sub set of blocks in this flow graph
+ """
+ selected = set()
+ for selected_element in self.get_selected_elements():
+ if selected_element.is_block(): selected.add(selected_element)
+ return list(selected)
+
+ def get_selected_block(self):
+ """
+ Get the selected block when a block or port is selected.
+ @return a block or None
+ """
+ return self.get_selected_blocks() and self.get_selected_blocks()[0] or None
+
+ def get_selected_elements(self):
+ """
+ Get the group of selected elements.
+ @return sub set of elements in this flow graph
+ """
+ return self._selected_elements
+
+ def get_selected_element(self):
+ """
+ Get the selected element.
+ @return a block, port, or connection or None
+ """
+ return self.get_selected_elements() and self.get_selected_elements()[0] or None
+
+ 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_selection = self.what_is_selected(self.get_coordinate())
+ #update the selections if the new selection is not in the current selections
+ #allows us to move entire selected groups of elements
+ if self.get_ctrl_mask() or not (
+ new_selection and new_selection[0] in self.get_selected_elements()
+ ): selected_elements = new_selection
+ else: #called from a mouse release
+ if not self.element_moved and (not self.get_selected_elements() or self.get_ctrl_mask()):
+ selected_elements = self.what_is_selected(self.get_coordinate(), self.press_coor)
+ #this selection and the last were ports, try to connect them
+ if self._old_selected_port and self._new_selected_port and \
+ self._old_selected_port is not self._new_selected_port:
+ try:
+ self.connect(self._old_selected_port, self._new_selected_port)
+ self.handle_states(ELEMENT_CREATE)
+ except: Messages.send_fail_connection()
+ self._old_selected_port = None
+ self._new_selected_port = None
+ return
+ #update selected elements
+ if selected_elements is None: return
+ old_elements = set(self.get_selected_elements())
+ self._selected_elements = list(set(selected_elements))
+ new_elements = set(self.get_selected_elements())
+ #if ctrl, set the selected elements to the union - intersection of old and new
+ if self.get_ctrl_mask():
+ self._selected_elements = list(
+ set.union(old_elements, new_elements) - set.intersection(old_elements, new_elements)
+ )
+ self.handle_states(ELEMENT_SELECT)
+
+ ##########################################################################
+ ## Event Handlers
+ ##########################################################################
+ def handle_mouse_button_press(self, left_click, double_click, coordinate):
+ """
+ A mouse button is pressed, only respond to left clicks.
+ 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.
+ """
+ if not left_click: return
+ self.press_coor = coordinate
+ self.set_coordinate(coordinate)
+ self.time = 0
+ self.mouse_pressed = True
+ self.update_selected_elements()
+ #double click detected, bring up params dialog if possible
+ if double_click and self.get_selected_block():
+ self.mouse_pressed = False
+ self.handle_states(BLOCK_PARAM_MODIFY)
+
+ def handle_mouse_button_release(self, left_click, coordinate):
+ """
+ A mouse button is released, record the state.
+ """
+ if not left_click: return
+ self.set_coordinate(coordinate)
+ self.time = 0
+ self.mouse_pressed = False
+ if self.element_moved:
+ if Preferences.snap_to_grid():
+ grid_size = Preferences.get_grid_size()
+ X,Y = self.get_selected_element().get_coordinate()
+ deltaX = X%grid_size
+ if deltaX < grid_size/2: deltaX = -1 * deltaX
+ else: deltaX = grid_size - deltaX
+ deltaY = Y%grid_size
+ if deltaY < grid_size/2: deltaY = -1 * deltaY
+ else: deltaY = grid_size - deltaY
+ self.move_selected((deltaX, deltaY))
+ self.handle_states(BLOCK_MOVE)
+ self.element_moved = False
+ self.update_selected_elements()
+ self.draw()
+
+ def handle_mouse_motion(self, coordinate):
+ """
+ The mouse has moved, respond to mouse dragging.
+ 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, timediff large enough
+ if not self.mouse_pressed: return
+ if time.time() - self.time < MOTION_DETECT_REDRAWING_SENSITIVITY: return
+ #perform autoscrolling
+ width, height = self.get_size()
+ x, y = coordinate
+ h_adj = self.get_scroll_pane().get_hadjustment()
+ v_adj = self.get_scroll_pane().get_vadjustment()
+ for pos, length, adj, adj_val, adj_len in (
+ (x, width, h_adj, h_adj.get_value(), h_adj.page_size),
+ (y, height, v_adj, v_adj.get_value(), v_adj.page_size),
+ ):
+ #scroll if we moved near the border
+ if pos-adj_val > adj_len-SCROLL_PROXIMITY_SENSITIVITY and adj_val+SCROLL_DISTANCE < length-adj_len:
+ adj.set_value(adj_val+SCROLL_DISTANCE)
+ adj.emit('changed')
+ elif pos-adj_val < SCROLL_PROXIMITY_SENSITIVITY:
+ adj.set_value(adj_val-SCROLL_DISTANCE)
+ adj.emit('changed')
+ #move the selected element and record the new coordinate
+ X, Y = self.get_coordinate()
+ if not self.get_ctrl_mask(): self.move_selected((int(x - X), int(y - Y)))
+ self.draw()
+ self.set_coordinate((x, y))
+ #update time
+ self.time = time.time()
diff --git a/grc/src/platforms/gui/Makefile.am b/grc/src/platforms/gui/Makefile.am
new file mode 100644
index 000000000..2e3972ef3
--- /dev/null
+++ b/grc/src/platforms/gui/Makefile.am
@@ -0,0 +1,37 @@
+#
+# Copyright 2008 Free Software Foundation, Inc.
+#
+# This file is part of GNU Radio
+#
+# GNU Radio 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 3, or (at your option)
+# any later version.
+#
+# GNU Radio 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 GNU Radio; see the file COPYING. If not, write to
+# the Free Software Foundation, Inc., 51 Franklin Street,
+# Boston, MA 02110-1301, USA.
+#
+
+include $(top_srcdir)/grc/Makefile.inc
+
+ourpythondir = $(grc_src_prefix)/platforms/gui
+
+ourpython_PYTHON = \
+ Block.py \
+ Colors.py \
+ Constants.py \
+ Connection.py \
+ Element.py \
+ FlowGraph.py \
+ Param.py \
+ Platform.py \
+ Port.py \
+ Utils.py \
+ __init__.py
diff --git a/grc/src/platforms/gui/Param.py b/grc/src/platforms/gui/Param.py
new file mode 100644
index 000000000..f45d80bba
--- /dev/null
+++ b/grc/src/platforms/gui/Param.py
@@ -0,0 +1,221 @@
+"""
+Copyright 2007 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
+"""
+
+import Utils
+from Element import Element
+import pygtk
+pygtk.require('2.0')
+import gtk
+import pango
+import gobject
+from Constants import PARAM_LABEL_FONT, PARAM_FONT
+from os import path
+
+######################################################################################################
+# gtk objects for handling input
+######################################################################################################
+
+class InputParam(gtk.HBox):
+ """The base class for an input parameter inside the input parameters dialog."""
+
+ def __init__(self, param, _handle_changed):
+ gtk.HBox.__init__(self)
+ self.param = param
+ self._handle_changed = _handle_changed
+ self.label = gtk.Label('') #no label, markup is added by set_markup
+ self.label.set_size_request(150, -1)
+ self.pack_start(self.label, False)
+ self.set_markup = lambda m: self.label.set_markup(m)
+ self.tp = None
+
+class EntryParam(InputParam):
+ """Provide an entry box for strings and numbers."""
+
+ def __init__(self, *args, **kwargs):
+ InputParam.__init__(self, *args, **kwargs)
+ self.entry = input = gtk.Entry()
+ input.set_text(self.param.get_value())
+ input.connect('changed', self._handle_changed)
+ self.pack_start(input, True)
+ self.get_text = input.get_text
+ #tool tip
+ self.tp = gtk.Tooltips()
+ self.tp.set_tip(self.entry, '')
+ self.tp.enable()
+
+class FileParam(EntryParam):
+ """Provide an entry box for filename and a button to browse for a file."""
+
+ def __init__(self, *args, **kwargs):
+ EntryParam.__init__(self, *args, **kwargs)
+ input = gtk.Button('...')
+ input.connect('clicked', self._handle_clicked)
+ self.pack_start(input, False)
+
+ def _handle_clicked(self, widget=None):
+ """
+ If the button was clicked, open a file dialog in open/save format.
+ Replace the text in the entry with the new filename from the file dialog.
+ """
+ file_path = self.param.is_valid() and self.param.evaluate() or ''
+ #bad file paths will be redirected to default
+ if not path.exists(path.dirname(file_path)): file_path = DEFAULT_FILE_PATH
+ if self.param.get_type() == 'file_open':
+ file_dialog = gtk.FileChooserDialog('Open a Data File...', None,
+ gtk.FILE_CHOOSER_ACTION_OPEN, ('gtk-cancel',gtk.RESPONSE_CANCEL,'gtk-open',gtk.RESPONSE_OK))
+ elif self.param.get_type() == 'file_save':
+ file_dialog = gtk.FileChooserDialog('Save a Data File...', None,
+ gtk.FILE_CHOOSER_ACTION_SAVE, ('gtk-cancel',gtk.RESPONSE_CANCEL, 'gtk-save',gtk.RESPONSE_OK))
+ file_dialog.set_do_overwrite_confirmation(True)
+ file_dialog.set_current_name(path.basename(file_path)) #show the current filename
+ file_dialog.set_current_folder(path.dirname(file_path)) #current directory
+ file_dialog.set_select_multiple(False)
+ file_dialog.set_local_only(True)
+ if gtk.RESPONSE_OK == file_dialog.run(): #run the dialog
+ file_path = file_dialog.get_filename() #get the file path
+ self.entry.set_text(file_path)
+ self._handle_changed()
+ file_dialog.destroy() #destroy the dialog
+
+class EnumParam(InputParam):
+ """Provide an entry box for Enum types with a drop down menu."""
+
+ def __init__(self, *args, **kwargs):
+ InputParam.__init__(self, *args, **kwargs)
+ input = gtk.ComboBox(gtk.ListStore(gobject.TYPE_STRING))
+ cell = gtk.CellRendererText()
+ input.pack_start(cell, True)
+ input.add_attribute(cell, 'text', 0)
+ for option in self.param.get_options(): input.append_text(option.get_name())
+ input.set_active(int(self.param.get_option_keys().index(self.param.get_value())))
+ input.connect("changed", self._handle_changed)
+ self.pack_start(input, False)
+ self.get_text = lambda: str(input.get_active()) #the get text parses the selected index to a string
+
+######################################################################################################
+# A Flow Graph Parameter
+######################################################################################################
+
+class Param(Element):
+ """The graphical parameter."""
+
+ def update(self):
+ """
+ Called when an external change occurs.
+ Update the graphical input by calling the change handler.
+ """
+ if hasattr(self, 'input'): self._handle_changed()
+
+ def get_input_object(self, callback=None):
+ """
+ Get the graphical gtk class to represent this parameter.
+ Create the input object with this data type and the handle changed method.
+ @param callback a function of one argument(this param) to be called from the change handler
+ @return gtk input object
+ """
+ self.callback = callback
+ if self.is_enum(): input = EnumParam
+ elif self.get_type() in ('file_open', 'file_save'): input = FileParam
+ else: input = EntryParam
+ self.input = input(self, self._handle_changed)
+ if not callback: self.update()
+ return self.input
+
+ def _handle_changed(self, widget=None):
+ """
+ When the input changes, write the inputs to the data type.
+ Finish by calling the exteral callback.
+ """
+ value = self.input.get_text()
+ if self.is_enum(): value = self.get_option_keys()[int(value)]
+ self.set_value(value)
+ #set the markup on the label, red for errors in corresponding data type.
+ name = '<span font_desc="%s">%s</span>'%(PARAM_LABEL_FONT, Utils.xml_encode(self.get_name()))
+ #special markups if param is involved in a callback
+ if hasattr(self.get_parent(), 'get_callbacks') and \
+ filter(lambda c: self.get_key() in c, self.get_parent()._callbacks):
+ name = '<span underline="low">%s</span>'%name
+ if not self.is_valid():
+ self.input.set_markup('<span foreground="red">%s</span>'%name)
+ tip = '- ' + '\n- '.join(self.get_error_messages())
+ else:
+ self.input.set_markup(name)
+ tip = self.evaluate()
+ #hide/show
+ if self.get_hide() == 'all': self.input.hide_all()
+ else: self.input.show_all()
+ #set the tooltip
+ if self.input.tp: self.input.tp.set_tip(self.input.entry, str(tip))
+ #execute the external callback
+ if self.callback: self.callback(self)
+
+ def get_markup(self):
+ """
+ Create a markup to display the Param as a label on the SignalBlock.
+ If the data type is an Enum type, use the cname of the Enum's current choice.
+ Otherwise, use parsed the data type and use its string representation.
+ If the data type is not valid, use a red foreground color.
+ @return pango markup string
+ """
+ ###########################################################################
+ # display logic for numbers
+ ###########################################################################
+ def float_to_str(var):
+ if var-int(var) == 0: return '%d'%int(var)
+ if var*10-int(var*10) == 0: return '%.1f'%var
+ if var*100-int(var*100) == 0: return '%.2f'%var
+ if var*1000-int(var*1000) == 0: return '%.3f'%var
+ else: return '%.3g'%var
+ def to_str(var):
+ if isinstance(var, str): return var
+ elif isinstance(var, complex):
+ if var.imag == var.real == 0: return '0' #value is zero
+ elif var.imag == 0: return '%s'%float_to_str(var.real) #value is real
+ elif var.real == 0: return '%sj'%float_to_str(var.imag) #value is imaginary
+ elif var.imag < 0: return '%s-%sj'%(float_to_str(var.real), float_to_str(var.imag*-1))
+ else: return '%s+%sj'%(float_to_str(var.real), float_to_str(var.imag))
+ elif isinstance(var, float): return float_to_str(var)
+ elif isinstance(var, int): return '%d'%var
+ else: return str(var)
+ ###########################################################################
+ if self.is_valid():
+ data = self.evaluate()
+ t = self.get_type()
+ if self.is_enum():
+ dt_str = self.get_option(self.get_value()).get_name()
+ elif isinstance(data, (list, tuple, set)): #vector types
+ dt_str = ', '.join(map(to_str, data))
+ else: dt_str = to_str(data) #other types
+ #truncate
+ max_len = max(42 - len(self.get_name()), 3)
+ if len(dt_str) > max_len:
+ dt_str = dt_str[:max_len-3] + '...'
+ return '<b>%s:</b> %s'%(Utils.xml_encode(self.get_name()), Utils.xml_encode(dt_str))
+ else: return '<span foreground="red"><b>%s:</b> error</span>'%Utils.xml_encode(self.get_name())
+
+ def get_layout(self):
+ """
+ Create a layout based on the current markup.
+ @return the pango layout
+ """
+ layout = gtk.DrawingArea().create_pango_layout('')
+ layout.set_markup(self.get_markup())
+ desc = pango.FontDescription(PARAM_FONT)
+ layout.set_font_description(desc)
+ return layout
diff --git a/grc/src/platforms/gui/Platform.py b/grc/src/platforms/gui/Platform.py
new file mode 100644
index 000000000..a32b0209f
--- /dev/null
+++ b/grc/src/platforms/gui/Platform.py
@@ -0,0 +1,48 @@
+"""
+Copyright 2008 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 FlowGraph import FlowGraph
+from Connection import Connection
+from Block import Block
+from Port import Port
+from Param import Param
+
+def conjoin_classes(name, c1, c2):
+ exec("""
+class %s(c1, c2):
+ def __init__(self, *args, **kwargs):
+ c1.__init__(self, *args, **kwargs)
+ c2.__init__(self, *args, **kwargs)
+"""%name, locals())
+ return locals()[name]
+
+def Platform(platform):
+ #combine with gui class
+ for attr, value in (
+ ('FlowGraph', FlowGraph),
+ ('Connection', Connection),
+ ('Block', Block),
+ ('Source', Port),
+ ('Sink', Port),
+ ('Param', Param),
+ ):
+ old_value = getattr(platform, attr)
+ c = conjoin_classes(attr, old_value, value)
+ setattr(platform, attr, c)
+ return platform
diff --git a/grc/src/platforms/gui/Port.py b/grc/src/platforms/gui/Port.py
new file mode 100644
index 000000000..aeab85ea0
--- /dev/null
+++ b/grc/src/platforms/gui/Port.py
@@ -0,0 +1,185 @@
+"""
+Copyright 2007 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 Element import Element
+from Constants import \
+ PORT_HEIGHT, PORT_SEPARATION, \
+ PORT_WIDTH, CONNECTOR_EXTENSION_MINIMAL, \
+ CONNECTOR_EXTENSION_INCREMENT, PORT_FONT
+import Colors
+import pygtk
+pygtk.require('2.0')
+import gtk
+import pango
+
+class Port(Element):
+ """The graphical port."""
+
+ def __init__(self, *args, **kwargs):
+ """
+ Port contructor.
+ Create list of connector coordinates.
+ """
+ Element.__init__(self)
+ self.connector_coordinates = dict()
+
+ def update(self):
+ """Create new areas and labels for the port."""
+ self.clear()
+ self.BG_color = Colors.get_color(self.get_color())
+ self._create_labels()
+ #get current rotation
+ rotation = self.get_rotation()
+ #get all sibling ports
+ if self.is_source(): ports = self.get_parent().get_sources()
+ elif self.is_sink(): ports = self.get_parent().get_sinks()
+ #get a numeric index for this port relative to its sibling ports
+ index = ports.index(self)
+ length = len(ports)
+ #reverse the order of ports for these rotations
+ if rotation in (180, 270): index = length-index-1
+ offset = (self.get_parent().H - length*PORT_HEIGHT - (length-1)*PORT_SEPARATION)/2
+ #create areas and connector coordinates
+ if (self.is_sink() and rotation == 0) or (self.is_source() and rotation == 180):
+ x = -1*PORT_WIDTH
+ y = (PORT_SEPARATION+PORT_HEIGHT)*index+offset
+ self.add_area((x, y), (PORT_WIDTH, PORT_HEIGHT))
+ self._connector_coordinate = (x-1, y+PORT_HEIGHT/2)
+ elif (self.is_source() and rotation == 0) or (self.is_sink() and rotation == 180):
+ x = self.get_parent().W
+ y = (PORT_SEPARATION+PORT_HEIGHT)*index+offset
+ self.add_area((x, y), (PORT_WIDTH, PORT_HEIGHT))
+ self._connector_coordinate = (x+1+PORT_WIDTH, y+PORT_HEIGHT/2)
+ elif (self.is_source() and rotation == 90) or (self.is_sink() and rotation == 270):
+ y = -1*PORT_WIDTH
+ x = (PORT_SEPARATION+PORT_HEIGHT)*index+offset
+ self.add_area((x, y), (PORT_HEIGHT, PORT_WIDTH))
+ self._connector_coordinate = (x+PORT_HEIGHT/2, y-1)
+ elif (self.is_sink() and rotation == 90) or (self.is_source() and rotation == 270):
+ y = self.get_parent().W
+ x = (PORT_SEPARATION+PORT_HEIGHT)*index+offset
+ self.add_area((x, y), (PORT_HEIGHT, PORT_WIDTH))
+ self._connector_coordinate = (x+PORT_HEIGHT/2, y+1+PORT_WIDTH)
+ #the connector length
+ self._connector_length = CONNECTOR_EXTENSION_MINIMAL + CONNECTOR_EXTENSION_INCREMENT*index
+
+ def _create_labels(self):
+ """Create the labels for the socket."""
+ #create the layout
+ layout = gtk.DrawingArea().create_pango_layout(self.get_name())
+ desc = pango.FontDescription(PORT_FONT)
+ layout.set_font_description(desc)
+ w,h = self.w,self.h = layout.get_pixel_size()
+ #create the pixmap
+ pixmap = gtk.gdk.Pixmap(self.get_parent().get_parent().get_window(), w, h, -1)
+ gc = pixmap.new_gc()
+ gc.foreground = self.BG_color
+ pixmap.draw_rectangle(gc, True, 0, 0, w, h)
+ gc.foreground = Colors.TXT_COLOR
+ pixmap.draw_layout(gc, 0, 0, layout)
+ #create the images
+ self.horizontal_label = image = pixmap.get_image(0, 0, w, h)
+ if self.is_vertical():
+ self.vertical_label = vimage = gtk.gdk.Image(gtk.gdk.IMAGE_NORMAL, pixmap.get_visual(), h, w)
+ for i in range(w):
+ for j in range(h): vimage.put_pixel(j, w-i-1, image.get_pixel(i, j))
+
+ def draw(self, window):
+ """
+ Draw the socket with a label.
+ @param window the gtk window to draw on
+ """
+ Element.draw(self, window, BG_color=self.BG_color)
+ gc = self.get_gc()
+ gc.foreground = Colors.TXT_COLOR
+ X,Y = self.get_coordinate()
+ (x,y),(w,h) = self.areas_dict[self.get_rotation()][0] #use the first area's sizes to place the labels
+ if self.is_horizontal():
+ window.draw_image(gc, self.horizontal_label, 0, 0, x+X+(PORT_WIDTH-self.w)/2, y+Y+(PORT_HEIGHT-self.h)/2, -1, -1)
+ elif self.is_vertical():
+ window.draw_image(gc, self.vertical_label, 0, 0, x+X+(PORT_HEIGHT-self.h)/2, y+Y+(PORT_WIDTH-self.w)/2, -1, -1)
+
+ def get_connector_coordinate(self):
+ """
+ Get the coordinate where connections may attach to.
+ @return the connector coordinate (x, y) tuple
+ """
+ x,y = self._connector_coordinate
+ X,Y = self.get_coordinate()
+ return (x+X, y+Y)
+
+ def get_connector_direction(self):
+ """
+ Get the direction that the socket points: 0,90,180,270.
+ This is the rotation degree if the socket is an output or
+ the rotation degree + 180 if the socket is an input.
+ @return the direction in degrees
+ """
+ if self.is_source(): return self.get_rotation()
+ elif self.is_sink(): return (self.get_rotation() + 180)%360
+
+ def get_connector_length(self):
+ """
+ Get the length of the connector.
+ The connector length increases as the port index changes.
+ @return the length in pixels
+ """
+ return self._connector_length
+
+ def get_rotation(self):
+ """
+ Get the parent's rotation rather than self.
+ @return the parent's rotation
+ """
+ return self.get_parent().get_rotation()
+
+ def move(self, delta_coor):
+ """
+ Move the parent rather than self.
+ @param delta_corr the (delta_x, delta_y) tuple
+ """
+ self.get_parent().move(delta_coor)
+
+ def rotate(self, direction):
+ """
+ Rotate the parent rather than self.
+ @param direction degrees to rotate
+ """
+ self.get_parent().rotate(direction)
+
+ def get_coordinate(self):
+ """
+ Get the parent's coordinate rather than self.
+ @return the parents coordinate
+ """
+ return self.get_parent().get_coordinate()
+
+ def set_highlighted(self, highlight):
+ """
+ Set the parent highlight rather than self.
+ @param highlight true to enable highlighting
+ """
+ self.get_parent().set_highlighted(highlight)
+
+ def is_highlighted(self):
+ """
+ Get the parent's is highlight rather than self.
+ @return the parent's highlighting status
+ """
+ return self.get_parent().is_highlighted()
diff --git a/grc/src/platforms/gui/Utils.py b/grc/src/platforms/gui/Utils.py
new file mode 100644
index 000000000..17750ef45
--- /dev/null
+++ b/grc/src/platforms/gui/Utils.py
@@ -0,0 +1,71 @@
+"""
+Copyright 2008 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 Constants import POSSIBLE_ROTATIONS
+
+def get_rotated_coordinate(coor, rotation):
+ """
+ Rotate the coordinate by the given rotation.
+ @param coor the coordinate x, y tuple
+ @param rotation the angle in degrees
+ @return the rotated coordinates
+ """
+ #handles negative angles
+ rotation = (rotation + 360)%360
+ assert rotation in POSSIBLE_ROTATIONS
+ #determine the number of degrees to rotate
+ cos_r, sin_r = {
+ 0: (1, 0),
+ 90: (0, 1),
+ 180: (-1, 0),
+ 270: (0, -1),
+ }[rotation]
+ x, y = coor
+ return (x*cos_r + y*sin_r, -x*sin_r + y*cos_r)
+
+def get_angle_from_coordinates((x1,y1), (x2,y2)):
+ """
+ Given two points, calculate the vector direction from point1 to point2, directions are multiples of 90 degrees.
+ @param (x1,y1) the coordinate of point 1
+ @param (x2,y2) the coordinate of point 2
+ @return the direction in degrees
+ """
+ if y1 == y2:#0 or 180
+ if x2 > x1: return 0
+ else: return 180
+ else:#90 or 270
+ if y2 > y1: return 270
+ else: return 90
+
+def xml_encode(string):
+ """
+ Encode a string into an xml safe string by replacing special characters.
+ Needed for gtk pango markup in labels.
+ @param string the input string
+ @return output string with safe characters
+ """
+ string = str(string)
+ for char, safe in (
+ ('&', '&amp;'),
+ ('<', '&lt;'),
+ ('>', '&gt;'),
+ ('"', '&quot;'),
+ ("'", '&apos;'),
+ ): string = string.replace(char, safe)
+ return string
diff --git a/grc/src/platforms/gui/__init__.py b/grc/src/platforms/gui/__init__.py
new file mode 100644
index 000000000..8b1378917
--- /dev/null
+++ b/grc/src/platforms/gui/__init__.py
@@ -0,0 +1 @@
+