summaryrefslogtreecommitdiff
path: root/grc/src/platforms
diff options
context:
space:
mode:
authorjblum2008-09-07 21:38:12 +0000
committerjblum2008-09-07 21:38:12 +0000
commitc86f6c23c6883f73d953d64c28ab42cedb77e4d7 (patch)
tree0193b2a649eb0f7f1065912862de340a02848e16 /grc/src/platforms
parentddec4fc07744a6519086b1b111f29d551b7f19c6 (diff)
downloadgnuradio-c86f6c23c6883f73d953d64c28ab42cedb77e4d7.tar.gz
gnuradio-c86f6c23c6883f73d953d64c28ab42cedb77e4d7.tar.bz2
gnuradio-c86f6c23c6883f73d953d64c28ab42cedb77e4d7.zip
Merged r9481:9518 on jblum/grc_reorganize into trunk. Reorganized grc source under gnuradio.grc module. Trunk passes make distcheck.
git-svn-id: http://gnuradio.org/svn/gnuradio/trunk@9525 221aa14e-8319-0410-a670-987f0aec2ac5
Diffstat (limited to 'grc/src/platforms')
-rw-r--r--grc/src/platforms/Makefile.am31
-rw-r--r--grc/src/platforms/__init__.py1
-rw-r--r--grc/src/platforms/base/Block.py237
-rw-r--r--grc/src/platforms/base/Connection.py88
-rw-r--r--grc/src/platforms/base/Constants.py.in44
-rw-r--r--grc/src/platforms/base/Element.py93
-rw-r--r--grc/src/platforms/base/FlowGraph.py231
-rw-r--r--grc/src/platforms/base/Makefile.am47
-rw-r--r--grc/src/platforms/base/Param.py218
-rw-r--r--grc/src/platforms/base/Platform.py144
-rw-r--r--grc/src/platforms/base/Port.py106
-rw-r--r--grc/src/platforms/base/__init__.py1
-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
-rw-r--r--grc/src/platforms/python/Block.py128
-rw-r--r--grc/src/platforms/python/Connection.py34
-rw-r--r--grc/src/platforms/python/Constants.py.in40
-rw-r--r--grc/src/platforms/python/FlowGraph.py152
-rw-r--r--grc/src/platforms/python/Generator.py134
-rw-r--r--grc/src/platforms/python/Makefile.am51
-rw-r--r--grc/src/platforms/python/Param.py252
-rw-r--r--grc/src/platforms/python/Platform.py73
-rw-r--r--grc/src/platforms/python/Port.py131
-rw-r--r--grc/src/platforms/python/__init__.py1
-rw-r--r--grc/src/platforms/python/utils/Makefile.am30
-rw-r--r--grc/src/platforms/python/utils/__init__.py1
-rw-r--r--grc/src/platforms/python/utils/convert_hier.py78
-rw-r--r--grc/src/platforms/python/utils/expr_utils.py137
-rw-r--r--grc/src/platforms/python/utils/extract_docs.py109
39 files changed, 4350 insertions, 0 deletions
diff --git a/grc/src/platforms/Makefile.am b/grc/src/platforms/Makefile.am
new file mode 100644
index 000000000..1d3c385c2
--- /dev/null
+++ b/grc/src/platforms/Makefile.am
@@ -0,0 +1,31 @@
+#
+# 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
+
+SUBDIRS = \
+ base \
+ gui \
+ python
+
+ourpythondir = $(grc_src_prefix)/platforms
+
+ourpython_PYTHON = __init__.py
diff --git a/grc/src/platforms/__init__.py b/grc/src/platforms/__init__.py
new file mode 100644
index 000000000..8b1378917
--- /dev/null
+++ b/grc/src/platforms/__init__.py
@@ -0,0 +1 @@
+
diff --git a/grc/src/platforms/base/Block.py b/grc/src/platforms/base/Block.py
new file mode 100644
index 000000000..e3ef84d94
--- /dev/null
+++ b/grc/src/platforms/base/Block.py
@@ -0,0 +1,237 @@
+"""
+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 ... import utils
+from ... utils import odict
+from Element import Element
+from Param import Param
+from Port import Port
+
+from Cheetah.Template import Template
+from UserDict import UserDict
+
+class TemplateArg(UserDict):
+ """
+ A cheetah template argument created from a param.
+ The str of this class evaluates to the param's to code method.
+ The use of this class as a dictionary (enum only) will reveal the enum opts.
+ The eval method can return the param evaluated to a raw python data type.
+ """
+
+ def __init__(self, param):
+ UserDict.__init__(self)
+ self._param = param
+ if param.is_enum():
+ for key in param.get_opt_keys():
+ self[key] = str(param.get_opt(key))
+
+ def __str__(self):
+ return str(self._param.to_code())
+
+ def eval(self):
+ return self._param.evaluate()
+
+class Block(Element):
+
+ def __init__(self, flow_graph, n):
+ """
+ Make a new block from nested data.
+ @param flow graph the parent element
+ @param n the nested odict
+ @return block a new block
+ """
+ #grab the data
+ name = n['name']
+ key = n['key']
+ category = utils.exists_or_else(n, 'category', '')
+ params = utils.listify(n, 'param')
+ sources = utils.listify(n, 'source')
+ sinks = utils.listify(n, 'sink')
+ #build the block
+ Element.__init__(self, flow_graph)
+ #store the data
+ self._name = name
+ self._key = key
+ self._category = category
+ #create the param objects
+ self._params = odict()
+ #add the id param
+ self._params['id'] = self.get_parent().get_parent().Param(
+ self,
+ {
+ 'name': 'ID',
+ 'key': 'id',
+ 'type': 'id',
+ }
+ )
+ self._params['_enabled'] = self.get_parent().get_parent().Param(
+ self,
+ {
+ 'name': 'Enabled',
+ 'key': '_enabled',
+ 'type': 'raw',
+ 'value': 'True',
+ 'hide': 'all',
+ }
+ )
+ for param in map(lambda n: self.get_parent().get_parent().Param(self, n), params):
+ key = param.get_key()
+ #test against repeated keys
+ try: assert(key not in self.get_param_keys())
+ except AssertionError: self._exit_with_error('Key "%s" already exists in params'%key)
+ #store the param
+ self._params[key] = param
+ #create the source objects
+ self._sources = odict()
+ for source in map(lambda n: self.get_parent().get_parent().Source(self, n), sources):
+ key = source.get_key()
+ #test against repeated keys
+ try: assert(key not in self.get_source_keys())
+ except AssertionError: self._exit_with_error('Key "%s" already exists in sources'%key)
+ #store the port
+ self._sources[key] = source
+ #create the sink objects
+ self._sinks = odict()
+ for sink in map(lambda n: self.get_parent().get_parent().Sink(self, n), sinks):
+ key = sink.get_key()
+ #test against repeated keys
+ try: assert(key not in self.get_sink_keys())
+ except AssertionError: self._exit_with_error('Key "%s" already exists in sinks'%key)
+ #store the port
+ self._sinks[key] = sink
+ #begin the testing
+ self.test()
+
+ def test(self):
+ """
+ Call test on all children.
+ """
+ map(lambda c: c.test(), self.get_params() + self.get_sinks() + self.get_sources())
+
+ def get_enabled(self):
+ """
+ Get the enabled state of the block.
+ @return true for enabled
+ """
+ try: return eval(self.get_param('_enabled').get_value())
+ except: return True
+
+ def set_enabled(self, enabled):
+ """
+ Set the enabled state of the block.
+ @param enabled true for enabled
+ """
+ self.get_param('_enabled').set_value(str(enabled))
+
+ def validate(self):
+ """
+ Validate the block.
+ All ports and params must be valid.
+ All checks must evaluate to true.
+ """
+ if not self.get_enabled(): return
+ for c in self.get_params() + self.get_sinks() + self.get_sources():
+ try: assert(c.is_valid())
+ except AssertionError:
+ for msg in c.get_error_messages():
+ self._add_error_message('%s: %s'%(c, msg))
+
+ def __str__(self): return 'Block - %s - %s(%s)'%(self.get_id(), self.get_name(), self.get_key())
+
+ def get_id(self): return self.get_param('id').get_value()
+
+ def is_block(self): return True
+
+ def get_doc(self): return self._doc
+
+ def get_name(self): return self._name
+
+ def get_key(self): return self._key
+
+ def get_category(self): return self._category
+
+ def get_doc(self): return ''
+
+ def get_ports(self): return self.get_sources() + self.get_sinks()
+
+ ##############################################
+ # Access Params
+ ##############################################
+ def get_param_keys(self): return self._params.keys()
+ def get_param(self, key): return self._params[key]
+ def get_params(self): return self._params.values()
+
+ ##############################################
+ # Access Sinks
+ ##############################################
+ def get_sink_keys(self): return self._sinks.keys()
+ def get_sink(self, key): return self._sinks[key]
+ def get_sinks(self): return self._sinks.values()
+
+ ##############################################
+ # Access Sources
+ ##############################################
+ def get_source_keys(self): return self._sources.keys()
+ def get_source(self, key): return self._sources[key]
+ def get_sources(self): return self._sources.values()
+
+ def get_connections(self):
+ return sum([port.get_connections() for port in self.get_ports()], [])
+
+ def resolve_dependencies(self, tmpl):
+ """
+ Resolve a paramater dependency with cheetah templates.
+ @param tmpl the string with dependencies
+ @return the resolved value
+ """
+ tmpl = str(tmpl)
+ if '$' not in tmpl: return tmpl
+ n = dict((p.get_key(), TemplateArg(p)) for p in self.get_params())
+ try: return str(Template(tmpl, n))
+ except Exception, e: return "-------->\n%s: %s\n<--------"%(e, tmpl)
+
+ ##############################################
+ ## Import/Export Methods
+ ##############################################
+ def export_data(self):
+ """
+ Export this block's params to nested data.
+ @return a nested data odict
+ """
+ n = odict()
+ n['key'] = self.get_key()
+ n['param'] = map(lambda p: p.export_data(), self.get_params())
+ return n
+
+ def import_data(self, n):
+ """
+ Import this block's params from nested data.
+ Any param keys that do not exist will be ignored.
+ @param n the nested data odict
+ """
+ params_n = utils.listify(n, 'param')
+ for param_n in params_n:
+ #key and value must exist in the n data
+ if 'key' in param_n.keys() and 'value' in param_n.keys():
+ key = param_n['key']
+ value = param_n['value']
+ #the key must exist in this block's params
+ if key in self.get_param_keys():
+ self.get_param(key).set_value(value)
+ self.validate()
diff --git a/grc/src/platforms/base/Connection.py b/grc/src/platforms/base/Connection.py
new file mode 100644
index 000000000..3c0b42d78
--- /dev/null
+++ b/grc/src/platforms/base/Connection.py
@@ -0,0 +1,88 @@
+"""
+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 Element import Element
+from ... utils import odict
+
+class Connection(Element):
+
+ def __init__(self, flow_graph, porta, portb):
+ """
+ Make a new connection given the parent and 2 ports.
+ @param flow_graph the parent of this element
+ @param porta a port (any direction)
+ @param portb a port (any direction)
+ @throws Error cannot make connection
+ @return a new connection
+ """
+ Element.__init__(self, flow_graph)
+ source = sink = None
+ #separate the source and sink
+ for port in (porta, portb):
+ if port.is_source(): source = port
+ if port.is_sink(): sink = port
+ #verify the source and sink
+ assert(source and sink)
+ assert(not source.is_full())
+ assert(not sink.is_full())
+ self._source = source
+ self._sink = sink
+
+ def __str__(self): return 'Connection (%s -> %s)'%(self.get_source(), self.get_sink())
+
+ def is_connection(self): return True
+
+ def validate(self):
+ """
+ Validate the connections.
+ The ports must match in type.
+ """
+ source_type = self.get_source().get_type()
+ sink_type = self.get_sink().get_type()
+ try: assert source_type == sink_type
+ except AssertionError: self._add_error_message('Source type "%s" does not match sink type "%s".'%(source_type, sink_type))
+
+ def get_enabled(self):
+ """
+ Get the enabled state of this connection.
+ @return true if source and sink blocks are enabled
+ """
+ return self.get_source().get_parent().get_enabled() and \
+ self.get_sink().get_parent().get_enabled()
+
+ #############################
+ # Access Ports
+ #############################
+ def get_sink(self): return self._sink
+ def get_source(self): return self._source
+
+ ##############################################
+ ## Import/Export Methods
+ ##############################################
+ def export_data(self):
+ """
+ Export this connection's info.
+ @return a nested data odict
+ """
+ n = odict()
+ n['source_block_id'] = self.get_source().get_parent().get_id()
+ n['sink_block_id'] = self.get_sink().get_parent().get_id()
+ n['source_key'] = self.get_source().get_key()
+ n['sink_key'] = self.get_sink().get_key()
+ return n
diff --git a/grc/src/platforms/base/Constants.py.in b/grc/src/platforms/base/Constants.py.in
new file mode 100644
index 000000000..26ba72d97
--- /dev/null
+++ b/grc/src/platforms/base/Constants.py.in
@@ -0,0 +1,44 @@
+"""
+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 os
+
+##The current version of this code
+VERSION = '@VERSION@'
+
+##Location of external data files.
+DATA_DIR = '@datadir@'
+
+##DTD validator for saved flow graphs.
+FLOW_GRAPH_DTD = os.path.join(DATA_DIR, 'flow_graph.dtd')
+
+##The default file extension for flow graphs.
+FLOW_GRAPH_FILE_EXTENSION = '.grc'
+
+##The default file extension for saving flow graph snap shots.
+IMAGE_FILE_EXTENSION = '.png'
+
+##The default path for the open/save dialogs.
+DEFAULT_FILE_PATH = os.getcwd()
+
+##The default icon for the gtk windows.
+PY_GTK_ICON = os.path.join(DATA_DIR, 'grc-icon-256.png')
+
+##The users home directory.
+HOME_DIR = os.path.expanduser('~')
diff --git a/grc/src/platforms/base/Element.py b/grc/src/platforms/base/Element.py
new file mode 100644
index 000000000..b6602a314
--- /dev/null
+++ b/grc/src/platforms/base/Element.py
@@ -0,0 +1,93 @@
+"""
+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
+"""
+
+class Element(object):
+
+ def __init__(self, parent=None):
+ self._parent = parent
+ self._error_messages = []
+ self.flag()
+
+ def test(self):
+ """
+ Test the element against failures.
+ Overload this method in sub-classes.
+ """
+ pass
+
+ def validate(self):
+ """
+ Validate the data in this element.
+ Set the error message non blank for errors.
+ Overload this method in sub-classes.
+ """
+ pass
+
+ def is_valid(self):
+ self._error_messages = []#reset err msgs
+ try: self.validate()
+ except: pass
+ return not self.get_error_messages()
+
+ def _add_error_message(self, msg):
+ self._error_messages.append(msg)
+
+ def get_error_messages(self):
+ return self._error_messages
+
+ def get_parent(self):
+ return self._parent
+
+ def _exit_with_error(self, error):
+ parent = self
+ #build hier list of elements
+ elements = list()
+ while(parent):
+ elements.insert(0, parent)
+ parent = parent.get_parent()
+ #build error string
+ err_str = ">>> Error:"
+ for i, element in enumerate(elements + [error]):
+ err_str = err_str + '\n' + ''.join(' '*(i+2)) + str(element)
+ err_str = err_str + '\n'
+ exit(err_str)
+
+ ##############################################
+ ## Update flagging
+ ##############################################
+ def is_flagged(self): return self._flag
+ def flag(self):
+ self._flag = True
+ if self.get_parent(): self.get_parent().flag()
+ def deflag(self):
+ self._flag = False
+ if self.get_parent(): self.get_parent().deflag()
+
+ ##############################################
+ ## Type testing methods
+ ##############################################
+ def is_element(self): return True
+ def is_platform(self): return False
+ def is_flow_graph(self): return False
+ def is_connection(self): return False
+ def is_block(self): return False
+ def is_source(self): return False
+ def is_sink(self): return False
+ def is_port(self): return False
+ def is_param(self): return False
diff --git a/grc/src/platforms/base/FlowGraph.py b/grc/src/platforms/base/FlowGraph.py
new file mode 100644
index 000000000..bb20c61d0
--- /dev/null
+++ b/grc/src/platforms/base/FlowGraph.py
@@ -0,0 +1,231 @@
+"""
+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 ... import utils
+from ... utils import odict
+from Element import Element
+from Block import Block
+from Connection import Connection
+from ... gui import Messages
+
+class FlowGraph(Element):
+
+ def __init__(self, platform):
+ """
+ Make a flow graph from the arguments.
+ @param platform a platforms with blocks and contrcutors
+ @return the flow graph object
+ """
+ #hold connections and blocks
+ self._elements = list()
+ #initialize
+ Element.__init__(self, platform)
+ #inital blank import
+ self.import_data({'flow_graph': {}})
+
+ def __str__(self): return 'FlowGraph - "%s"'%self.get_option('name')
+
+ def get_option(self, key):
+ """
+ Get the option for a given key.
+ The option comes from the special options block.
+ @param key the param key for the options block
+ @return the value held by that param
+ """
+ return self._options_block.get_param(key).evaluate()
+
+ def is_flow_graph(self): return True
+
+ ##############################################
+ ## Access Elements
+ ##############################################
+ def get_block(self, id): return filter(lambda b: b.get_id() == id, self.get_blocks())[0]
+ def get_blocks(self): return filter(lambda e: e.is_block(), self.get_elements())
+ def get_connections(self): return filter(lambda e: e.is_connection(), self.get_elements())
+ def get_elements(self):
+ """
+ Get a list of all the elements.
+ Always ensure that the options block is in the list.
+ @return the element list
+ """
+ if self._options_block not in self._elements: self._elements.append(self._options_block)
+ #ensure uniqueness of the elements list
+ element_set = set()
+ element_list = list()
+ for element in self._elements:
+ if element not in element_set: element_list.append(element)
+ element_set.add(element)
+ #store cleaned up list
+ self._elements = element_list
+ return self._elements
+
+ def get_enabled_blocks(self):
+ """
+ Get a list of all blocks that are enabled.
+ @return a list of blocks
+ """
+ return filter(lambda b: b.get_enabled(), self.get_blocks())
+
+ def get_enabled_connections(self):
+ """
+ Get a list of all connections that are enabled.
+ @return a list of connections
+ """
+ return filter(lambda c: c.get_enabled(), self.get_connections())
+
+ def get_new_block(self, key):
+ """
+ Get a new block of the specified key.
+ Add the block to the list of elements.
+ @param key the block key
+ @return the new block or None if not found
+ """
+ self.flag()
+ if key not in self.get_parent().get_block_keys(): return None
+ block = self.get_parent().get_new_block(self, key)
+ self.get_elements().append(block)
+ return block
+
+ def connect(self, porta, portb):
+ """
+ Create a connection between porta and portb.
+ @param porta a port
+ @param portb another port
+ @throw Exception bad connection
+ @return the new connection
+ """
+ self.flag()
+ connection = self.get_parent().Connection(self, porta, portb)
+ self.get_elements().append(connection)
+ return connection
+
+ def remove_element(self, element):
+ """
+ Remove the element from the list of elements.
+ If the element is a port, remove the whole block.
+ If the element is a block, remove its connections.
+ If the element is a connection, just remove the connection.
+ """
+ self.flag()
+ if element not in self.get_elements(): return
+ #found a port, set to parent signal block
+ if element.is_port():
+ element = element.get_parent()
+ #remove block, remove all involved connections
+ if element.is_block():
+ for port in element.get_ports():
+ map(lambda c: self.remove_element(c), port.get_connections())
+ #remove a connection
+ elif element.is_connection(): pass
+ self.get_elements().remove(element)
+
+ def evaluate(self, expr):
+ """
+ Evaluate the expression.
+ @param expr the string expression
+ @throw NotImplementedError
+ """
+ raise NotImplementedError
+
+ def validate(self):
+ """
+ Validate the flow graph.
+ All connections and blocks must be valid.
+ """
+ for c in self.get_elements():
+ try: assert(c.is_valid())
+ except AssertionError: self._add_error_message('Element "%s" is not valid.'%c)
+
+ ##############################################
+ ## Import/Export Methods
+ ##############################################
+ def export_data(self):
+ """
+ Export this flow graph to nested data.
+ Export all block and connection data.
+ @return a nested data odict
+ """
+ import time
+ n = odict()
+ n['timestamp'] = time.ctime()
+ n['block'] = [block.export_data() for block in self.get_blocks()]
+ n['connection'] = [connection.export_data() for connection in self.get_connections()]
+ return {'flow_graph': n}
+
+ def import_data(self, n):
+ """
+ Import blocks and connections into this flow graph.
+ Clear this flowgraph of all previous blocks and connections.
+ Any blocks or connections in error will be ignored.
+ @param n the nested data odict
+ """
+ #remove previous elements
+ self._elements = list()
+ #the flow graph tag must exists, or use blank data
+ if 'flow_graph' in n.keys(): fg_n = n['flow_graph']
+ else:
+ Messages.send_error_load('Flow graph data not found, loading blank flow graph.')
+ fg_n = {}
+ blocks_n = utils.listify(fg_n, 'block')
+ connections_n = utils.listify(fg_n, 'connection')
+ #create option block
+ self._options_block = self.get_parent().get_new_block(self, 'options')
+ self._options_block.get_param('id').set_value('options')
+ #build the blocks
+ for block_n in blocks_n:
+ key = block_n['key']
+ if key == 'options': block = self._options_block
+ else: block = self.get_new_block(key)
+ #only load the block when the block key was valid
+ if block: block.import_data(block_n)
+ else: Messages.send_error_load('Block key "%s" not found in %s'%(key, self.get_parent()))
+ #build the connections
+ for connection_n in connections_n:
+ #test that the data tags exist
+ try:
+ assert('source_block_id' in connection_n.keys())
+ assert('sink_block_id' in connection_n.keys())
+ assert('source_key' in connection_n.keys())
+ assert('sink_key' in connection_n.keys())
+ except AssertionError: continue
+ #try to make the connection
+ try:
+ #get the block ids
+ source_block_id = connection_n['source_block_id']
+ sink_block_id = connection_n['sink_block_id']
+ #get the port keys
+ source_key = connection_n['source_key']
+ sink_key = connection_n['sink_key']
+ #verify the blocks
+ block_ids = map(lambda b: b.get_id(), self.get_blocks())
+ assert(source_block_id in block_ids)
+ assert(sink_block_id in block_ids)
+ #get the blocks
+ source_block = self.get_block(source_block_id)
+ sink_block = self.get_block(sink_block_id)
+ #verify the ports
+ assert(source_key in source_block.get_source_keys())
+ assert(sink_key in sink_block.get_sink_keys())
+ #get the ports
+ source = source_block.get_source(source_key)
+ sink = sink_block.get_sink(sink_key)
+ #build the connection
+ self.connect(source, sink)
+ except AssertionError: Messages.send_error_load('Connection between %s(%s) and %s(%s) could not be made.'%(source_block_id, source_key, sink_block_id, sink_key))
+ self.validate()
diff --git a/grc/src/platforms/base/Makefile.am b/grc/src/platforms/base/Makefile.am
new file mode 100644
index 000000000..dca53b8b5
--- /dev/null
+++ b/grc/src/platforms/base/Makefile.am
@@ -0,0 +1,47 @@
+#
+# 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/base
+
+ourpython_PYTHON = \
+ Block.py \
+ Connection.py \
+ Constants.py \
+ Element.py \
+ FlowGraph.py \
+ Param.py \
+ Platform.py \
+ Port.py \
+ __init__.py
+
+BUILT_SOURCES = Constants.py
+
+Constants.py: Makefile $(srcdir)/Constants.py.in
+ sed \
+ -e 's|@VERSION[@]|$(VERSION)|g' \
+ -e 's|@datadir[@]|$(grc_base_data_dir)|g' \
+ $(srcdir)/Constants.py.in > $@
+
+EXTRA_DIST = Constants.py.in
+
+MOSTLYCLEANFILES = $(BUILT_SOURCES)
diff --git a/grc/src/platforms/base/Param.py b/grc/src/platforms/base/Param.py
new file mode 100644
index 000000000..3a8d98c30
--- /dev/null
+++ b/grc/src/platforms/base/Param.py
@@ -0,0 +1,218 @@
+"""
+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 ... import utils
+from ... utils import odict
+from Element import Element
+
+class Option(Element):
+
+ def __init__(self, param, name, key, opts):
+ Element.__init__(self, param)
+ self._name = name
+ self._key = key
+ self._opts = dict()
+ for opt in opts:
+ #separate the key:value
+ try: key, value = opt.split(':')
+ except: self._exit_with_error('Error separating "%s" into key:value'%opt)
+ #test against repeated keys
+ try: assert(not self._opts.has_key(key))
+ except AssertionError: self._exit_with_error('Key "%s" already exists in option'%key)
+ #store the option
+ self._opts[key] = value
+
+ def __str__(self): return 'Option %s(%s)'%(self.get_name(), self.get_key())
+
+ def get_name(self): return self._name
+
+ def get_key(self): return self._key
+
+ ##############################################
+ # Access Opts
+ ##############################################
+ def get_opt_keys(self): return self._opts.keys()
+ def get_opt(self, key): return self._opts[key]
+ def get_opts(self): return self._opts.values()
+
+ ##############################################
+ ## Static Make Methods
+ ##############################################
+ def make_option_from_n(param, n):
+ """
+ Make a new option from nested data.
+ @param param the parent element
+ @param n the nested odict
+ @return a new option
+ """
+ #grab the data
+ name = n['name']
+ key = n['key']
+ opts = utils.listify(n, 'opt')
+ #build the option
+ return Option(
+ param=param,
+ name=name,
+ key=key,
+ opts=opts,
+ )
+ make_option_from_n = staticmethod(make_option_from_n)
+
+class Param(Element):
+
+ ##possible param types
+ TYPES = ['enum', 'raw']
+
+ def __init__(self, block, n):
+ """
+ Make a new param from nested data.
+ @param block the parent element
+ @param n the nested odict
+ @return a new param
+ """
+ #grab the data
+ name = n['name']
+ key = n['key']
+ value = utils.exists_or_else(n, 'value', '')
+ type = n['type']
+ hide = utils.exists_or_else(n, 'hide', '')
+ options = utils.listify(n, 'option')
+ #build the param
+ Element.__init__(self, block)
+ self._name = name
+ self._key = key
+ self._type = type
+ self._hide = hide
+ #create the Option objects from the n data
+ self._options = odict()
+ for option in map(lambda o: Option.make_option_from_n(self, o), options):
+ key = option.get_key()
+ #test against repeated keys
+ try: assert(key not in self.get_option_keys())
+ except AssertionError: self._exit_with_error('Key "%s" already exists in options'%key)
+ #store the option
+ self._options[key] = option
+ #test the enum options
+ if self._options or self.is_enum():
+ #test against bad combos of type and enum
+ try: assert(self._options)
+ except AssertionError: self._exit_with_error('At least one option must exist when type "enum" is set.')
+ try: assert(self.is_enum())
+ except AssertionError: self._exit_with_error('Type "enum" must be set when options are present.')
+ #test against options with identical keys
+ try: assert(len(set(self.get_option_keys())) == len(self._options))
+ except AssertionError: self._exit_with_error('Options keys "%s" are not unique.'%self.get_option_keys())
+ #test against inconsistent keys in options
+ opt_keys = self._options.values()[0].get_opt_keys()
+ for option in self._options.values():
+ try: assert(set(opt_keys) == set(option.get_opt_keys()))
+ except AssertionError: self._exit_with_error('Opt keys "%s" are not identical across all options.'%opt_keys)
+ #if a value is specified, it must be in the options keys
+ self._value = value or self.get_option_keys()[0]
+ try: assert(self.get_value() in self.get_option_keys())
+ except AssertionError: self._exit_with_error('The value "%s" is not in the possible values of "%s".'%(self.get_value(), self.get_option_keys()))
+ else: self._value = value or ''
+
+ def test(self):
+ """
+ call test on all children
+ """
+ map(lambda c: c.test(), self.get_options())
+
+ def validate(self):
+ """
+ Validate the param.
+ The value must be evaluated and type must a possible type.
+ """
+ try:
+ assert(self.get_type() in self.TYPES)
+ try: self.evaluate()
+ except:
+ #if the evaluate failed but added no error messages, add the generic one below
+ if not self.get_error_messages():
+ self._add_error_message('Value "%s" cannot be evaluated.'%self.get_value())
+ except AssertionError: self._add_error_message('Type "%s" is not a possible type.'%self.get_type())
+
+ def evaluate(self):
+ """
+ Evaluate the value of this param.
+ @throw NotImplementedError
+ """
+ raise NotImplementedError
+
+ def to_code(self):
+ """
+ Convert the value to code.
+ @throw NotImplementedError
+ """
+ raise NotImplementedError
+
+ def __str__(self): return 'Param - %s(%s)'%(self.get_name(), self.get_key())
+
+ def is_param(self): return True
+
+ def get_name(self): return self._name
+
+ def get_key(self): return self._key
+
+ def get_hide(self): return self.get_parent().resolve_dependencies(self._hide)
+
+ def get_value(self):
+ value = self._value
+ if self.is_enum() and value not in self.get_option_keys():
+ value = self.get_option_keys()[0]
+ self.set_value(value)
+ return value
+
+ def set_value(self, value):
+ self.flag()
+ self._value = str(value) #must be a string
+
+ def get_type(self): return self.get_parent().resolve_dependencies(self._type)
+
+ def is_enum(self): return self._type == 'enum'
+
+ def is_type_dependent(self): return '$' in self._type
+
+ ##############################################
+ # Access Options
+ ##############################################
+ def get_option_keys(self): return self._options.keys()
+ def get_option(self, key): return self._options[key]
+ def get_options(self): return self._options.values()
+
+ ##############################################
+ # Access Opts
+ ##############################################
+ def get_opt_keys(self): return self._options[self.get_value()].get_opt_keys()
+ def get_opt(self, key): return self._options[self.get_value()].get_opt(key)
+ def get_opts(self): return self._options[self.get_value()].get_opts()
+
+ ##############################################
+ ## Import/Export Methods
+ ##############################################
+ def export_data(self):
+ """
+ Export this param's key/value.
+ @return a nested data odict
+ """
+ n = odict()
+ n['key'] = self.get_key()
+ n['value'] = self.get_value()
+ return n
diff --git a/grc/src/platforms/base/Platform.py b/grc/src/platforms/base/Platform.py
new file mode 100644
index 000000000..c25b4a050
--- /dev/null
+++ b/grc/src/platforms/base/Platform.py
@@ -0,0 +1,144 @@
+"""
+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 os
+from ... utils import ParseXML
+from ... import utils
+from Element import Element as _Element
+from FlowGraph import FlowGraph as _FlowGraph
+from Connection import Connection as _Connection
+from Block import Block as _Block
+from Port import Port as _Port
+from Param import Param as _Param
+from Constants import DATA_DIR
+
+class Platform(_Element):
+
+ def __init__(self, name, key, block_paths, block_dtd, block_tree, default_flow_graph, generator):
+ """
+ Make a platform from the arguments.
+ @param name the platform name
+ @param key the unique platform key
+ @param block_paths the file paths to blocks in this platform
+ @param block_dtd the dtd validator for xml block wrappers
+ @param block_tree the nested tree of block keys and categories
+ @param default_flow_graph the default flow graph file path
+ @param load_one a single file to load into this platform or None
+ @return a platform object
+ """
+ _Element.__init__(self)
+ self._name = name
+ self._key = key
+ self._block_paths = block_paths
+ self._block_dtd = block_dtd
+ self._block_tree = block_tree
+ self._default_flow_graph = default_flow_graph
+ self._generator = generator
+ #create a dummy flow graph for the blocks
+ self._flow_graph = _Element(self)
+ #load the blocks
+ self._blocks = dict()
+ self._blocks_n = dict()
+ for block_path in self._block_paths:
+ if os.path.isfile(block_path): self._load_block(block_path)
+ elif os.path.isdir(block_path):
+ for dirpath, dirnames, filenames in os.walk(block_path):
+ for filename in filter(lambda f: f.endswith('.xml'), filenames):
+ self._load_block(os.path.join(dirpath, filename))
+
+ def get_prefs_block(self): return self.get_new_flow_graph().get_new_block('preferences')
+
+ def _load_block(self, f):
+ """
+ Load the block wrapper from the file path.
+ The block wrapper must pass validation, and have a unique block key.
+ If any of the checks fail, exit with error.
+ @param f the file path
+ """
+ try: ParseXML.validate_dtd(f, self._block_dtd)
+ except ParseXML.XMLSyntaxError, e: self._exit_with_error('Block definition "%s" failed: \n\t%s'%(f, e))
+ n = ParseXML.from_file(f)['block']
+ block = self.Block(self._flow_graph, n)
+ key = block.get_key()
+ #test against repeated keys
+ try: assert(key not in self.get_block_keys())
+ except AssertionError: self._exit_with_error('Key "%s" already exists in blocks'%key)
+ #store the block
+ self._blocks[key] = block
+ self._blocks_n[key] = n
+
+ def load_block_tree(self, block_tree):
+ """
+ Load a block tree with categories and blocks.
+ Step 1: Load all blocks from the xml specification.
+ Step 2: Load blocks with builtin category specifications.
+ @param block_tree the block tree object
+ """
+ #recursive function to load categories and blocks
+ def load_category(cat_n, parent=''):
+ #add this category
+ parent = '%s/%s'%(parent, cat_n['name'])
+ block_tree.add_block(parent)
+ #recursive call to load sub categories
+ map(lambda c: load_category(c, parent), utils.listify(cat_n, 'cat'))
+ #add blocks in this category
+ for block_key in utils.listify(cat_n, 'block'):
+ block_tree.add_block(parent, self.get_block(block_key))
+ #load the block tree
+ f = self._block_tree
+ try: ParseXML.validate_dtd(f, os.path.join(DATA_DIR, 'block_tree.dtd'))
+ except ParseXML.XMLSyntaxError, e: self._exit_with_error('Block tree "%s" failed: \n\t%s'%(f, e))
+ #add all blocks in the tree
+ load_category(ParseXML.from_file(f)['cat'])
+ #add all other blocks, use the catgory
+ for block in self.get_blocks():
+ #blocks with empty categories are in the xml block tree or hidden
+ if block.get_category(): block_tree.add_block(block.get_category(), block)
+
+ def __str__(self): return 'Platform - %s(%s)'%(self.get_key(), self.get_name())
+
+ def is_platform(self): return True
+
+ def get_new_flow_graph(self): return self.FlowGraph(self)
+
+ def get_default_flow_graph(self): return self._default_flow_graph
+
+ def get_generator(self): return self._generator
+
+ ##############################################
+ # Access Blocks
+ ##############################################
+ def get_block_keys(self): return self._blocks.keys()
+ def get_block(self, key): return self._blocks[key]
+ def get_blocks(self): return self._blocks.values()
+ def get_new_block(self, flow_graph, key): return self.Block(flow_graph, n=self._blocks_n[key])
+
+ def get_name(self): return self._name
+
+ def get_key(self): return self._key
+
+ ##############################################
+ # Constructors
+ ##############################################
+ FlowGraph = _FlowGraph
+ Connection = _Connection
+ Block = _Block
+ Source = _Port
+ Sink = _Port
+ Param = _Param
diff --git a/grc/src/platforms/base/Port.py b/grc/src/platforms/base/Port.py
new file mode 100644
index 000000000..61134791c
--- /dev/null
+++ b/grc/src/platforms/base/Port.py
@@ -0,0 +1,106 @@
+"""
+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 ... import utils
+from Element import Element
+
+class Port(Element):
+
+ ##possible port types
+ TYPES = []
+
+ def __init__(self, block, n):
+ """
+ Make a new port from nested data.
+ @param block the parent element
+ @param n the nested odict
+ @return a new port
+ """
+ #grab the data
+ name = n['name']
+ key = n['key']
+ type = n['type']
+ #build the port
+ Element.__init__(self, block)
+ self._name = name
+ self._key = key
+ self._type = type
+
+ def validate(self):
+ """
+ Validate the port.
+ The port must be non-empty and type must a possible type.
+ """
+ try: assert(not self.is_empty())
+ except AssertionError: self._add_error_message('is empty.')
+ try: assert(self.get_type() in self.TYPES)
+ except AssertionError: self._add_error_message('Type "%s" is not a possible type.'%self.get_type())
+
+ def __str__(self):
+ if self.is_source():
+ return 'Source - %s(%s)'%(self.get_name(), self.get_key())
+ if self.is_sink():
+ return 'Sink - %s(%s)'%(self.get_name(), self.get_key())
+
+ def is_port(self): return True
+
+ def get_color(self): return '#FFFFFF'
+
+ def get_name(self): return self._name
+
+ def get_key(self): return self._key
+
+ def is_sink(self): return self in self.get_parent().get_sinks()
+
+ def is_source(self): return self in self.get_parent().get_sources()
+
+ def get_type(self): return self.get_parent().resolve_dependencies(self._type)
+
+ def get_connections(self):
+ """
+ Get all connections that use this port.
+ @return a list of connection objects
+ """
+ connections = self.get_parent().get_parent().get_connections()
+ connections = filter(lambda c: c.get_source() is self or c.get_sink() is self, connections)
+ return connections
+
+ def is_connected(self):
+ """
+ Is this port connected?
+ @return true if at least one connection
+ """
+ return bool(self.get_connections())
+
+ def is_full(self):
+ """
+ Is this port full of connections?
+ Generally a sink can handle one connection and a source can handle many.
+ @return true if the port is full
+ """
+ if self.is_source(): return False
+ if self.is_sink(): return bool(self.get_connections())
+
+ def is_empty(self):
+ """
+ Is this port empty?
+ An empty port has no connections.
+ @return true if empty
+ """
+ return not self.get_connections()
diff --git a/grc/src/platforms/base/__init__.py b/grc/src/platforms/base/__init__.py
new file mode 100644
index 000000000..8b1378917
--- /dev/null
+++ b/grc/src/platforms/base/__init__.py
@@ -0,0 +1 @@
+
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 @@
+
diff --git a/grc/src/platforms/python/Block.py b/grc/src/platforms/python/Block.py
new file mode 100644
index 000000000..655a6ec55
--- /dev/null
+++ b/grc/src/platforms/python/Block.py
@@ -0,0 +1,128 @@
+"""
+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 .. base.Block import Block as _Block
+from utils import extract_docs
+from ... import utils
+
+class Block(_Block):
+
+ ##for make source to keep track of indexes
+ _source_count = 0
+ ##for make sink to keep track of indexes
+ _sink_count = 0
+
+ def __init__(self, flow_graph, n):
+ """
+ Make a new block from nested data.
+ @param flow graph the parent element
+ @param n the nested odict
+ @return block a new block
+ """
+ #grab the data
+ doc = utils.exists_or_else(n, 'doc', '')
+ imports = map(lambda i: i.strip(), utils.listify(n, 'import'))
+ make = n['make']
+ checks = utils.listify(n, 'check')
+ callbacks = utils.listify(n, 'callback')
+ #build the block
+ _Block.__init__(
+ self,
+ flow_graph=flow_graph,
+ n=n,
+ )
+ self._doc = doc
+ self._imports = imports
+ self._make = make
+ self._callbacks = callbacks
+ self._checks = checks
+
+ def validate(self):
+ """
+ Validate this block.
+ Call the base class validate.
+ Evaluate the checks: each check must evaluate to True.
+ Adjust the nports.
+ """
+ _Block.validate(self)
+ #evaluate the checks
+ for check in self._checks:
+ check_res = self.resolve_dependencies(check)
+ try:
+ check_eval = self.get_parent().evaluate(check_res)
+ try: assert check_eval
+ except AssertionError: self._add_error_message('Check "%s" failed.'%check)
+ except: self._add_error_message('Check "%s" did not evaluate.'%check)
+ for ports, Port in (
+ (self._sources, self.get_parent().get_parent().Source),
+ (self._sinks, self.get_parent().get_parent().Sink),
+ ):
+ #how many ports?
+ num_ports = len(ports)
+ #do nothing for 0 ports
+ if not num_ports: continue
+ #get the nports setting
+ port0 = ports[str(0)]
+ nports = port0.get_nports()
+ #do nothing for no nports
+ if not nports: continue
+ #do nothing if nports is already num ports
+ if nports == num_ports: continue
+ #remove excess ports and connections
+ if nports < num_ports:
+ #remove the connections
+ for key in map(str, range(nports, num_ports)):
+ port = ports[key]
+ for connection in port.get_connections():
+ self.get_parent().remove_element(connection)
+ #remove the ports
+ for key in map(str, range(nports, num_ports)): ports.pop(key)
+ continue
+ #add more ports
+ if nports > num_ports:
+ for key in map(str, range(num_ports, nports)):
+ n = port0._n
+ n['key'] = key
+ port = Port(self, n)
+ ports[key] = port
+ continue
+
+ def get_doc(self):
+ doc = self._doc.strip('\n').replace('\\\n', '')
+ #merge custom doc with doxygen docs
+ return '\n'.join([doc, extract_docs.extract(self.get_key())]).strip('\n')
+
+ def get_imports(self):
+ """
+ Resolve all import statements.
+ Split each import statement at newlines.
+ Combine all import statments into a list.
+ Filter empty imports.
+ @return a list of import statements
+ """
+ return filter(lambda i: i, sum(map(lambda i: self.resolve_dependencies(i).split('\n'), self._imports), []))
+
+ def get_make(self): return self.resolve_dependencies(self._make)
+
+ def get_callbacks(self):
+ """
+ Get a list of function callbacks for this block.
+ @return a list of strings
+ """
+ return map(lambda c: self.get_id() + '.' + self.resolve_dependencies(c), self._callbacks)
diff --git a/grc/src/platforms/python/Connection.py b/grc/src/platforms/python/Connection.py
new file mode 100644
index 000000000..f742ff63d
--- /dev/null
+++ b/grc/src/platforms/python/Connection.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
+"""
+
+from .. base.Connection import Connection as _Connection
+
+class Connection(_Connection):
+
+ def validate(self):
+ """
+ Validate the connections.
+ The ports must match in type and vector length.
+ """
+ _Connection.validate(self) #checks type
+ #check vector length
+ source_vlen = self.get_source().get_vlen()
+ sink_vlen = self.get_sink().get_vlen()
+ try: assert(source_vlen == sink_vlen)
+ except AssertionError: self._add_error_message('Source vector length "%s" does not match sink vector length "%s".'%(source_vlen, sink_vlen))
diff --git a/grc/src/platforms/python/Constants.py.in b/grc/src/platforms/python/Constants.py.in
new file mode 100644
index 000000000..c2d878ba3
--- /dev/null
+++ b/grc/src/platforms/python/Constants.py.in
@@ -0,0 +1,40 @@
+"""
+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 os
+import sys
+import stat
+
+PYEXEC = '@PYTHONW@'
+
+#setup paths
+DOCS_DIR = os.path.join('@docdir@', 'xml')
+DATA_DIR = '@datadir@'
+BLOCKS_DIR = '@blocksdir@'
+HIER_BLOCKS_LIB_DIR = os.path.join(os.path.expanduser('~'), '.grc_gnuradio')
+
+#file creation modes
+TOP_BLOCK_FILE_MODE = stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR | stat.S_IRGRP | stat.S_IWGRP | stat.S_IXGRP | stat.S_IROTH
+HIER_BLOCK_FILE_MODE = stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IWGRP | stat.S_IROTH
+
+#data files
+FLOW_GRAPH_TEMPLATE = os.path.join(DATA_DIR, 'flow_graph.tmpl')
+BLOCK_DTD = os.path.join(DATA_DIR, 'block.dtd')
+BLOCK_TREE = os.path.join(DATA_DIR, 'block_tree.xml')
+DEFAULT_FLOW_GRAPH = os.path.join(DATA_DIR, 'default_flow_graph.grc.xml')
diff --git a/grc/src/platforms/python/FlowGraph.py b/grc/src/platforms/python/FlowGraph.py
new file mode 100644
index 000000000..6c9b7f642
--- /dev/null
+++ b/grc/src/platforms/python/FlowGraph.py
@@ -0,0 +1,152 @@
+"""
+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 utils import expr_utils
+from .. base.FlowGraph import FlowGraph as _FlowGraph
+from Block import Block
+from Connection import Connection
+
+def get_variable_code(variable):
+ """
+ Get the code representation for a variable.
+ Normally this is the value parameter.
+ For the variable chooser, use the index and choices.
+ Avoid using the to_code method of the variables,
+ as this forces evaluation before the variables are evaluated.
+ @param variable the variable block
+ @return the code string
+ """
+ if variable.get_key() == 'variable_chooser':
+ choices = variable.get_param('choices').get_value()
+ value_index = variable.get_param('value_index').get_value()
+ return "(%s)[%s]"%(choices, value_index)
+ return variable.get_param('value').get_value()
+
+class FlowGraph(_FlowGraph):
+
+ def _get_io_signature(self, pad_key):
+ """
+ Get an io signature for this flow graph.
+ The pad key determines the directionality of the io signature.
+ @param pad_key a string of pad_source or pad_sink
+ @return a dict with: type, nports, vlen, size
+ """
+ pads = filter(lambda b: b.get_key() == pad_key, self.get_enabled_blocks())
+ if not pads: return {
+ 'nports': '0',
+ 'type': '',
+ 'vlen': '0',
+ 'size': '0',
+ }
+ pad = pads[0] #take only the first, user should not have more than 1
+ #load io signature
+ return {
+ 'nports': str(pad.get_param('nports').evaluate()),
+ 'type': str(pad.get_param('type').evaluate()),
+ 'vlen': str(pad.get_param('vlen').evaluate()),
+ 'size': pad.get_param('type').get_opt('size'),
+ }
+
+ def get_input_signature(self):
+ """
+ Get the io signature for the input side of this flow graph.
+ The io signature with be "0", "0" if no pad source is present.
+ @return a string tuple of type, num_ports, port_size
+ """
+ return self._get_io_signature('pad_source')
+
+ def get_output_signature(self):
+ """
+ Get the io signature for the output side of this flow graph.
+ The io signature with be "0", "0" if no pad sink is present.
+ @return a string tuple of type, num_ports, port_size
+ """
+ return self._get_io_signature('pad_sink')
+
+ def get_imports(self):
+ """
+ Get a set of all import statments in this flow graph namespace.
+ @return a set of import statements
+ """
+ imports = sum([block.get_imports() for block in self.get_enabled_blocks()], [])
+ imports = sorted(set(imports))
+ return imports
+
+ def get_variables(self):
+ """
+ Get a list of all variables in this flow graph namespace.
+ Exclude paramterized variables.
+ @return a sorted list of variable blocks in order of dependency (indep -> dep)
+ """
+ variables = filter(lambda b: b.get_key() in (
+ 'variable', 'variable_slider', 'variable_chooser', 'variable_text_box'
+ ), self.get_enabled_blocks())
+ #map var id to variable block
+ id2var = dict([(var.get_id(), var) for var in variables])
+ #map var id to variable code
+ #variable code is a concatenation of all param code (without the id param)
+ id2expr = dict([(var.get_id(), get_variable_code(var)) for var in variables])
+ #sort according to dependency
+ sorted_ids = expr_utils.sort_variables(id2expr)
+ #create list of sorted variable blocks
+ variables = [id2var[id] for id in sorted_ids]
+ return variables
+
+ def get_parameters(self):
+ """
+ Get a list of all paramterized variables in this flow graph namespace.
+ @return a list of paramterized variables
+ """
+ parameters = filter(lambda b: b.get_key() == 'parameter', self.get_enabled_blocks())
+ return parameters
+
+ def evaluate(self, expr):
+ """
+ Evaluate the expression.
+ @param expr the string expression
+ @throw Exception bad expression
+ @return the evaluated data
+ """
+ if self.is_flagged():
+ self.deflag()
+ #reload namespace
+ n = dict()
+ #load imports
+ for imp in self.get_imports():
+ try: exec imp in n
+ except: pass
+ #load parameters
+ np = dict()
+ for parameter in self.get_parameters():
+ try:
+ e = eval(parameter.get_param('value').to_code(), n, n)
+ np[parameter.get_id()] = e
+ except: pass
+ n.update(np) #merge param namespace
+ #load variables
+ for variable in self.get_variables():
+ try:
+ e = eval(get_variable_code(variable), n, n)
+ n[variable.get_id()] = e
+ except: pass
+ #make namespace public
+ self.n = n
+ #evaluate
+ e = eval(expr, self.n, self.n)
+ return e
diff --git a/grc/src/platforms/python/Generator.py b/grc/src/platforms/python/Generator.py
new file mode 100644
index 000000000..bd3d69cc2
--- /dev/null
+++ b/grc/src/platforms/python/Generator.py
@@ -0,0 +1,134 @@
+"""
+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 os
+import subprocess
+from Cheetah.Template import Template
+from utils import expr_utils
+from Constants import \
+ TOP_BLOCK_FILE_MODE, HIER_BLOCK_FILE_MODE, \
+ HIER_BLOCKS_LIB_DIR, PYEXEC, \
+ FLOW_GRAPH_TEMPLATE
+from utils import convert_hier
+
+class Generator(object):
+
+ def __init__(self, flow_graph, file_path):
+ """
+ Initialize the generator object.
+ Determine the file to generate.
+ @param flow_graph the flow graph object
+ @param file_path the path to write the file to
+ """
+ self._flow_graph = flow_graph
+ self._generate_options = self._flow_graph.get_option('generate_options')
+ if self._generate_options == 'hb':
+ self._mode = HIER_BLOCK_FILE_MODE
+ dirname = HIER_BLOCKS_LIB_DIR
+ else:
+ self._mode = TOP_BLOCK_FILE_MODE
+ dirname = os.path.dirname(file_path)
+ filename = self._flow_graph.get_option('id') + '.py'
+ self._file_path = os.path.join(dirname, filename)
+
+ def get_file_path(self): return self._file_path
+
+ def write(self):
+ #generate
+ open(self.get_file_path(), 'w').write(str(self))
+ if self._generate_options == 'hb':
+ #convert hier block to xml wrapper
+ convert_hier.convert_hier(self._flow_graph, self.get_file_path())
+ os.chmod(self.get_file_path(), self._mode)
+
+ def get_popen(self):
+ """
+ Execute this python flow graph.
+ @return a popen object
+ """
+ #execute
+ cmds = [PYEXEC, self.get_file_path()]
+ if self._generate_options == 'no_gui':
+ cmds = ['xterm', '-e'] + cmds
+ p = subprocess.Popen(args=cmds, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=False, universal_newlines=True)
+ return p
+
+ def __str__(self):
+ """
+ Convert the flow graph to python code.
+ @return a string of python code
+ """
+ imports = self._flow_graph.get_imports()
+ variables = self._flow_graph.get_variables()
+ parameters = self._flow_graph.get_parameters()
+ #list of variables with controls
+ controls = filter(lambda v: v.get_key().startswith('variable_'), variables)
+ #list of blocks not including variables and imports and parameters and disabled
+ blocks = sorted(self._flow_graph.get_enabled_blocks(), lambda x, y: cmp(x.get_id(), y.get_id()))
+ blocks = filter(lambda b: b not in (imports + parameters + variables), blocks)
+ #list of connections where each endpoint is enabled
+ connections = self._flow_graph.get_enabled_connections()
+ #list of variable names
+ var_ids = [var.get_id() for var in parameters + variables]
+ #list of callbacks (prepend self.)
+ callbacks = [
+ expr_utils.expr_prepend(cb, var_ids, 'self.')
+ for cb in sum([block.get_callbacks() for block in self._flow_graph.get_blocks()], [])
+ ]
+ #map var id to the expression (prepend self.)
+ var_id2expr = dict(
+ [(var.get_id(), expr_utils.expr_prepend(var.get_make().split('\n')[0], var_ids, 'self.'))
+ for var in parameters + variables]
+ )
+ #create graph structure for variables
+ variable_graph = expr_utils.get_graph(var_id2expr)
+ #map var id to direct dependents
+ #for each var id, make a list of all 2nd order edges
+ #use all edges of that id that are not also 2nd order edges
+ #meaning: list variables the ONLY depend directly on this variable
+ #and not variables that also depend indirectly on this variable
+ var_id2deps = dict(
+ [(var_id, filter(lambda e: e not in sum([list(variable_graph.get_edges(edge))
+ for edge in variable_graph.get_edges(var_id)], []), variable_graph.get_edges(var_id)
+ )
+ )
+ for var_id in var_ids]
+ )
+ #map var id to callbacks
+ var_id2cbs = dict(
+ [(var_id, filter(lambda c: var_id in expr_utils.expr_split(c), callbacks))
+ for var_id in var_ids]
+ )
+ #load the namespace
+ namespace = {
+ 'imports': imports,
+ 'flow_graph': self._flow_graph,
+ 'variables': variables,
+ 'controls': controls,
+ 'parameters': parameters,
+ 'blocks': blocks,
+ 'connections': connections,
+ 'generate_options': self._generate_options,
+ 'var_id2expr': var_id2expr,
+ 'var_id2deps': var_id2deps,
+ 'var_id2cbs': var_id2cbs,
+ }
+ #build the template
+ t = Template(open(FLOW_GRAPH_TEMPLATE, 'r').read(), namespace)
+ return str(t)
diff --git a/grc/src/platforms/python/Makefile.am b/grc/src/platforms/python/Makefile.am
new file mode 100644
index 000000000..e5845d0ec
--- /dev/null
+++ b/grc/src/platforms/python/Makefile.am
@@ -0,0 +1,51 @@
+#
+# 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
+
+SUBDIRS = utils
+
+ourpythondir = $(grc_src_prefix)/platforms/python
+
+ourpython_PYTHON = \
+ Block.py \
+ Connection.py \
+ Constants.py \
+ FlowGraph.py \
+ Generator.py \
+ Param.py \
+ Platform.py \
+ Port.py \
+ __init__.py
+
+BUILT_SOURCES = Constants.py
+
+Constants.py: Makefile $(srcdir)/Constants.py.in
+ sed \
+ -e 's|@PYTHONW[@]|$(PYTHONW)|g' \
+ -e 's|@datadir[@]|$(grc_python_data_dir)|g' \
+ -e 's|@blocksdir[@]|$(grc_python_blocks_dir)|g' \
+ -e 's|@docdir[@]|$(gr_docdir)|g' \
+ $(srcdir)/Constants.py.in > $@
+
+EXTRA_DIST = Constants.py.in
+
+MOSTLYCLEANFILES = $(BUILT_SOURCES)
diff --git a/grc/src/platforms/python/Param.py b/grc/src/platforms/python/Param.py
new file mode 100644
index 000000000..ed5c64063
--- /dev/null
+++ b/grc/src/platforms/python/Param.py
@@ -0,0 +1,252 @@
+"""
+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 utils import expr_utils
+from .. base.Param import Param as _Param
+import os
+
+class Param(_Param):
+
+ _init = False
+ _hostage_cells = list()
+
+ ##possible param types
+ TYPES = _Param.TYPES + [
+ 'complex', 'real', 'int',
+ 'complex_vector', 'real_vector', 'int_vector',
+ 'hex', 'string',
+ 'file_open', 'file_save',
+ 'id',
+ 'grid_pos', 'import',
+ ]
+
+ def get_hide(self):
+ """
+ Get the hide value from the base class.
+ If hide was empty, and this is a type controller, set hide to part.
+ If hide was empty, and this is an id of a non variable, set hide to part.
+ @return hide the hide property string
+ """
+ hide = _Param.get_hide(self)
+ #hide IO controlling params
+ if not hide and self.get_key() in (
+ 'type', 'vlen', 'num_inputs', 'num_outputs'
+ ): hide = 'part'
+ #hide ID in non variable blocks
+ elif not hide and self.get_key() == 'id' and self.get_parent().get_key() not in (
+ 'variable', 'variable_slider', 'variable_chooser', 'variable_text_box', 'parameter', 'options'
+ ): hide = 'part'
+ return hide
+
+ def evaluate(self):
+ """
+ Evaluate the value.
+ @return evaluated type
+ """
+ self._lisitify_flag = False
+ self._stringify_flag = False
+ self._hostage_cells = list()
+ def eval_string(v):
+ try:
+ e = self.get_parent().get_parent().evaluate(v)
+ assert(isinstance(e, str))
+ return e
+ except:
+ self._stringify_flag = True
+ return v
+ t = self.get_type()
+ v = self.get_value()
+ #########################
+ # Enum Type
+ #########################
+ if self.is_enum(): return self.get_value()
+ #########################
+ # Numeric Types
+ #########################
+ elif t in ('raw', 'complex', 'real', 'int', 'complex_vector', 'real_vector', 'int_vector', 'hex'):
+ #raise exception if python cannot evaluate this value
+ try: e = self.get_parent().get_parent().evaluate(v)
+ except:
+ self._add_error_message('Value "%s" cannot be evaluated.'%v)
+ raise Exception
+ #raise an exception if the data is invalid
+ if t == 'raw': return e
+ elif t == 'complex':
+ try: assert(isinstance(e, (complex, float, int, long)))
+ except AssertionError:
+ self._add_error_message('Expression "%s" is invalid for type complex.'%str(e))
+ raise Exception
+ return e
+ elif t == 'real':
+ try: assert(isinstance(e, (float, int, long)))
+ except AssertionError:
+ self._add_error_message('Expression "%s" is invalid for type real.'%str(e))
+ raise Exception
+ return e
+ elif t == 'int':
+ try: assert(isinstance(e, (int, long)))
+ except AssertionError:
+ self._add_error_message('Expression "%s" is invalid for type integer.'%str(e))
+ raise Exception
+ return e
+ elif t == 'complex_vector':
+ if not isinstance(e, (tuple, list, set)):
+ self._lisitify_flag = True
+ e = [e]
+ try:
+ for ei in e:
+ assert(isinstance(ei, (complex, float, int, long)))
+ except AssertionError:
+ self._add_error_message('Expression "%s" is invalid for type complex vector.'%str(e))
+ raise Exception
+ return e
+ elif t == 'real_vector':
+ if not isinstance(e, (tuple, list, set)):
+ self._lisitify_flag = True
+ e = [e]
+ try:
+ for ei in e:
+ assert(isinstance(ei, (float, int, long)))
+ except AssertionError:
+ self._add_error_message('Expression "%s" is invalid for type real vector.'%str(e))
+ raise Exception
+ return e
+ elif t == 'int_vector':
+ if not isinstance(e, (tuple, list, set)):
+ self._lisitify_flag = True
+ e = [e]
+ try:
+ for ei in e:
+ assert(isinstance(ei, (int, long)))
+ except AssertionError:
+ self._add_error_message('Expression "%s" is invalid for type integer vector.'%str(e))
+ raise Exception
+ return e
+ elif t == 'hex':
+ return hex(e)
+ else: raise TypeError, 'Type "%s" not handled'%t
+ #########################
+ # String Types
+ #########################
+ elif t in ('string', 'file_open', 'file_save'):
+ #do not check if file/directory exists, that is a runtime issue
+ e = eval_string(v)
+ return str(e)
+ #########################
+ # Unique ID Type
+ #########################
+ elif t == 'id':
+ #can python use this as a variable?
+ try:
+ assert(len(v) > 0)
+ assert(v[0].isalpha())
+ for c in v: assert(c.isalnum() or c in ('_',))
+ except AssertionError:
+ self._add_error_message('ID "%s" must be alpha-numeric or underscored, and begin with a letter.'%v)
+ raise Exception
+ params = self.get_all_params('id')
+ keys = [param.get_value() for param in params]
+ try: assert(len(keys) == len(set(keys)))
+ except:
+ self._add_error_message('ID "%s" is not unique.'%v)
+ raise Exception
+ return v
+ #########################
+ # Grid Position Type
+ #########################
+ elif t == 'grid_pos':
+ if not v: return '' #allow for empty grid pos
+ e = self.get_parent().get_parent().evaluate(v)
+ try:
+ assert(isinstance(e, (list, tuple)) and len(e) == 4)
+ for ei in e: assert(isinstance(ei, int))
+ except AssertionError:
+ self._add_error_message('A grid position must be a list of 4 integers.')
+ raise Exception
+ row, col, row_span, col_span = e
+ #check row, col
+ try: assert(row >= 0 and col >= 0)
+ except AssertionError:
+ self._add_error_message('Row and column must be non-negative.')
+ raise Exception
+ #check row span, col span
+ try: assert(row_span > 0 and col_span > 0)
+ except AssertionError:
+ self._add_error_message('Row and column span must be greater than zero.')
+ raise Exception
+ #calculate hostage cells
+ for r in range(row_span):
+ for c in range(col_span):
+ self._hostage_cells.append((row+r, col+c))
+ #avoid collisions
+ params = filter(lambda p: p is not self, self.get_all_params('grid_pos'))
+ for param in params:
+ for cell in param._hostage_cells:
+ if cell in self._hostage_cells:
+ self._add_error_message('Another graphical element is using cell "%s".'%str(cell))
+ raise Exception
+ return e
+ #########################
+ # Import Type
+ #########################
+ elif t == 'import':
+ n = dict() #new namespace
+ try: exec v in n
+ except ImportError:
+ self._add_error_message('Import "%s" failed.'%v)
+ raise Exception
+ except Exception:
+ self._add_error_message('Bad import syntax: "%s".'%v)
+ raise Exception
+ return filter(lambda k: str(k) != '__builtins__', n.keys())
+ #########################
+ else: raise TypeError, 'Type "%s" not handled'%t
+
+ def to_code(self):
+ """
+ Convert the value to code.
+ @return a string representing the code
+ """
+ #run init tasks in evaluate
+ #such as setting flags
+ if not self._init:
+ self.evaluate()
+ self._init = True
+ v = self.get_value()
+ t = self.get_type()
+ if t in ('string', 'file_open', 'file_save'): #string types
+ if self._stringify_flag:
+ return '"%s"'%v.replace('"', '\"')
+ else:
+ return v
+ elif t in ('complex_vector', 'real_vector', 'int_vector'): #vector types
+ if self._lisitify_flag:
+ return '(%s, )'%v
+ else:
+ return '(%s)'%v
+ else:
+ return v
+
+ def get_all_params(self, type):
+ """
+ Get all the params from the flowgraph that have the given type.
+ @param type the specified type
+ @return a list of params
+ """
+ return sum([filter(lambda p: p.get_type() == type, block.get_params()) for block in self.get_parent().get_parent().get_blocks()], [])
diff --git a/grc/src/platforms/python/Platform.py b/grc/src/platforms/python/Platform.py
new file mode 100644
index 000000000..c31701e0e
--- /dev/null
+++ b/grc/src/platforms/python/Platform.py
@@ -0,0 +1,73 @@
+"""
+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 os
+from .. base.Constants import FLOW_GRAPH_FILE_EXTENSION
+from .. base.Platform import Platform as _Platform
+from FlowGraph import FlowGraph as _FlowGraph
+from Connection import Connection as _Connection
+from Block import Block as _Block
+from Port import Source,Sink
+from Param import Param as _Param
+from Generator import Generator
+from Constants import \
+ HIER_BLOCKS_LIB_DIR, BLOCK_DTD, \
+ BLOCK_TREE, DEFAULT_FLOW_GRAPH, \
+ BLOCKS_DIR
+
+class Platform(_Platform):
+
+ def __init__(self, block_paths_internal_only=[], block_paths_external=[]):
+ """
+ Make a platform for gnuradio.
+ The internal only list will replace the current block path.
+ @param block_paths_internal_only a list of blocks internal to this platform
+ @param block_paths_external a list of blocks to load in addition to the above blocks
+ """
+ #ensure hier dir
+ if not os.path.exists(HIER_BLOCKS_LIB_DIR): os.mkdir(HIER_BLOCKS_LIB_DIR)
+ #handle internal/only
+ if block_paths_internal_only:
+ block_paths = map(lambda b: os.path.join(BLOCKS_DIR, b), ['options.xml'] + block_paths_internal_only)
+ else: block_paths = [BLOCKS_DIR]
+ #handle external
+ block_paths.extend(block_paths_external)
+ #append custom hiers
+ block_paths.append(HIER_BLOCKS_LIB_DIR)
+ #init
+ _Platform.__init__(
+ self,
+ name='GNURadio Python',
+ key='gnuradio_python',
+ block_paths=block_paths,
+ block_dtd=BLOCK_DTD,
+ block_tree=BLOCK_TREE,
+ default_flow_graph=DEFAULT_FLOW_GRAPH,
+ generator=Generator,
+ )
+
+ ##############################################
+ # Constructors
+ ##############################################
+ FlowGraph = _FlowGraph
+ Connection = _Connection
+ Block = _Block
+ Source = Source
+ Sink = Sink
+ Param = _Param
diff --git a/grc/src/platforms/python/Port.py b/grc/src/platforms/python/Port.py
new file mode 100644
index 000000000..93fa087eb
--- /dev/null
+++ b/grc/src/platforms/python/Port.py
@@ -0,0 +1,131 @@
+"""
+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 .. base.Port import Port as _Port
+from ... import utils
+
+class Port(_Port):
+
+ ##possible port types
+ TYPES = ['complex', 'float', 'int', 'short', 'byte']
+
+ def __init__(self, block, n):
+ """
+ Make a new port from nested data.
+ @param block the parent element
+ @param n the nested odict
+ @return a new port
+ """
+ vlen = utils.exists_or_else(n, 'vlen', '1')
+ nports = utils.exists_or_else(n, 'nports', '')
+ optional = utils.exists_or_else(n, 'optional', '')
+ #build the port
+ _Port.__init__(
+ self,
+ block=block,
+ n=n,
+ )
+ self._nports = nports
+ self._vlen = vlen
+ self._optional = bool(optional)
+
+ def get_vlen(self):
+ """
+ Get the vector length.
+ If the evaluation of vlen cannot be cast to an integer, return 1.
+ @return the vector length or 1
+ """
+ vlen = self.get_parent().resolve_dependencies(self._vlen)
+ try: return int(self.get_parent().get_parent().evaluate(vlen))
+ except: return 1
+
+ def get_nports(self):
+ """
+ Get the number of ports.
+ If already blank, return a blank
+ If the evaluation of nports cannot be cast to an integer, return 1.
+ @return the number of ports or 1
+ """
+ nports = self.get_parent().resolve_dependencies(self._nports)
+ #return blank if nports is blank
+ if not nports: return ''
+ try:
+ nports = int(self.get_parent().get_parent().evaluate(nports))
+ assert 0 < nports
+ return nports
+ except: return 1
+
+ def get_optional(self): return bool(self._optional)
+
+ def get_color(self):
+ """
+ Get the color that represents this port's type.
+ Codes differ for ports where the vec length is 1 or greater than 1.
+ @return a hex color code.
+ """
+ try:
+ if self.get_vlen() == 1:
+ return {#vlen is 1
+ 'complex': '#3399FF',
+ 'float': '#FF8C69',
+ 'int': '#00FF99',
+ 'short': '#FFFF66',
+ 'byte': '#FF66FF',
+ }[self.get_type()]
+ return {#vlen is non 1
+ 'complex': '#3399AA',
+ 'float': '#CC8C69',
+ 'int': '#00CC99',
+ 'short': '#CCCC33',
+ 'byte': '#CC66CC',
+ }[self.get_type()]
+ except: return _Port.get_color(self)
+
+ def is_empty(self):
+ """
+ Is this port empty?
+ An empty port has no connections.
+ Not empty of optional is set.
+ @return true if empty
+ """
+ return not self.get_optional() and not self.get_connections()
+
+class Source(Port):
+
+ def __init__(self, block, n):
+ self._n = n #save n
+ #key is port index
+ n['key'] = str(block._source_count)
+ block._source_count = block._source_count + 1
+ Port.__init__(self, block, n)
+
+ def __del__(self):
+ self.get_parent()._source_count = self.get_parent()._source_count - 1
+
+class Sink(Port):
+
+ def __init__(self, block, n):
+ self._n = n #save n
+ #key is port index
+ n['key'] = str(block._sink_count)
+ block._sink_count = block._sink_count + 1
+ Port.__init__(self, block, n)
+
+ def __del__(self):
+ self.get_parent()._sink_count = self.get_parent()._sink_count - 1
diff --git a/grc/src/platforms/python/__init__.py b/grc/src/platforms/python/__init__.py
new file mode 100644
index 000000000..8b1378917
--- /dev/null
+++ b/grc/src/platforms/python/__init__.py
@@ -0,0 +1 @@
+
diff --git a/grc/src/platforms/python/utils/Makefile.am b/grc/src/platforms/python/utils/Makefile.am
new file mode 100644
index 000000000..b12e51d8e
--- /dev/null
+++ b/grc/src/platforms/python/utils/Makefile.am
@@ -0,0 +1,30 @@
+#
+# 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/python/utils
+
+ourpython_PYTHON = \
+ convert_hier.py \
+ expr_utils.py \
+ extract_docs.py \
+ __init__.py
diff --git a/grc/src/platforms/python/utils/__init__.py b/grc/src/platforms/python/utils/__init__.py
new file mode 100644
index 000000000..8b1378917
--- /dev/null
+++ b/grc/src/platforms/python/utils/__init__.py
@@ -0,0 +1 @@
+
diff --git a/grc/src/platforms/python/utils/convert_hier.py b/grc/src/platforms/python/utils/convert_hier.py
new file mode 100644
index 000000000..495358984
--- /dev/null
+++ b/grc/src/platforms/python/utils/convert_hier.py
@@ -0,0 +1,78 @@
+"""
+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 BLOCK_DTD
+from .... utils import ParseXML
+from .... utils import odict
+
+def convert_hier(flow_graph, python_file):
+ #extract info from the flow graph
+ input_sig = flow_graph.get_input_signature()
+ output_sig = flow_graph.get_output_signature()
+ parameters = flow_graph.get_parameters()
+ block_key = flow_graph.get_option('id')
+ block_name = flow_graph.get_option('title')
+ block_category = flow_graph.get_option('category')
+ block_desc = flow_graph.get_option('description')
+ block_author = flow_graph.get_option('author')
+ #build the nested data
+ block_n = odict()
+ block_n['name'] = block_name
+ block_n['key'] = block_key
+ block_n['category'] = block_category
+ block_n['import'] = 'execfile("%s")'%python_file
+ #make data
+ block_n['make'] = '%s(\n\t%s,\n)'%(
+ block_key,
+ ',\n\t'.join(['%s=$%s'%(param.get_id(), param.get_id()) for param in parameters]),
+ )
+ #callback data
+ block_n['callback'] = ['set_%s($%s)'%(param.get_id(), param.get_id()) for param in parameters]
+ #param data
+ params_n = list()
+ for param in parameters:
+ param_n = odict()
+ param_n['name'] = param.get_param('label').get_value() or param.get_id()
+ param_n['key'] = param.get_id()
+ param_n['value'] = param.get_param('value').get_value()
+ param_n['type'] = 'raw'
+ params_n.append(param_n)
+ block_n['param'] = params_n
+ #sink data
+ if int(input_sig['nports']):
+ sink_n = odict()
+ sink_n['name'] = 'in'
+ sink_n['type'] = input_sig['type']
+ sink_n['vlen'] = input_sig['vlen']
+ sink_n['nports'] = input_sig['nports']
+ block_n['sink'] = sink_n
+ #source data
+ if int(output_sig['nports']):
+ source_n = odict()
+ source_n['name'] = 'out'
+ source_n['type'] = output_sig['type']
+ source_n['vlen'] = output_sig['vlen']
+ source_n['nports'] = output_sig['nports']
+ block_n['source'] = source_n
+ #doc data
+ block_n['doc'] = "%s\n%s\n%s"%(block_author, block_desc, python_file)
+ #write the block_n to file
+ xml_file = python_file + '.xml'
+ ParseXML.to_file({'block': block_n}, xml_file)
+ ParseXML.validate_dtd(xml_file, BLOCK_DTD)
diff --git a/grc/src/platforms/python/utils/expr_utils.py b/grc/src/platforms/python/utils/expr_utils.py
new file mode 100644
index 000000000..40700993d
--- /dev/null
+++ b/grc/src/platforms/python/utils/expr_utils.py
@@ -0,0 +1,137 @@
+"""
+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 string
+VAR_CHARS = string.letters + string.digits + '_'
+
+class graph(object):
+ """
+ Simple graph structure held in a dictionary.
+ """
+
+ def __init__(self): self._graph = dict()
+
+ def __str__(self): return str(self._graph)
+
+ def add_node(self, node_key):
+ if self._graph.has_key(node_key): return
+ self._graph[node_key] = set()
+
+ def remove_node(self, node_key):
+ if not self._graph.has_key(node_key): return
+ for edges in self._graph.values():
+ if node_key in edges: edges.remove(node_key)
+ self._graph.pop(node_key)
+
+ def add_edge(self, src_node_key, dest_node_key):
+ self._graph[src_node_key].add(dest_node_key)
+
+ def remove_edge(self, src_node_key, dest_node_key):
+ self._graph[src_node_key].remove(dest_node_key)
+
+ def get_nodes(self): return self._graph.keys()
+
+ def get_edges(self, node_key): return self._graph[node_key]
+
+def expr_split(expr):
+ """
+ Split up an expression by non alphanumeric characters, including underscore.
+ Leave strings in-tact.
+ #TODO ignore escaped quotes, use raw strings.
+ @param expr an expression string
+ @return a list of string tokens that form expr
+ """
+ toks = list()
+ tok = ''
+ quote = ''
+ for char in expr:
+ if quote or char in VAR_CHARS:
+ if char == quote: quote = ''
+ tok += char
+ elif char in ("'", '"'):
+ toks.append(tok)
+ tok = char
+ quote = char
+ else:
+ toks.append(tok)
+ toks.append(char)
+ tok = ''
+ toks.append(tok)
+ return filter(lambda t: t, toks)
+
+def expr_prepend(expr, vars, prepend):
+ """
+ Search for vars in the expression and add the prepend.
+ @param expr an expression string
+ @param vars a list of variable names
+ @param prepend the prepend string
+ @return a new expression with the prepend
+ """
+ expr_splits = expr_split(expr)
+ for i, es in enumerate(expr_splits):
+ if es in vars: expr_splits[i] = prepend + es
+ return ''.join(expr_splits)
+
+def get_variable_dependencies(expr, vars):
+ """
+ Return a set of variables used in this expression.
+ @param expr an expression string
+ @param vars a list of variable names
+ @return a subset of vars used in the expression
+ """
+ expr_toks = expr_split(expr)
+ return set(filter(lambda v: v in expr_toks, vars))
+
+def get_graph(exprs):
+ """
+ Get a graph representing the variable dependencies
+ @param exprs a mapping of variable name to expression
+ @return a graph of variable deps
+ """
+ vars = exprs.keys()
+ #get dependencies for each expression, load into graph
+ var_graph = graph()
+ for var in vars: var_graph.add_node(var)
+ for var, expr in exprs.iteritems():
+ for dep in get_variable_dependencies(expr, vars):
+ var_graph.add_edge(dep, var)
+ return var_graph
+
+def sort_variables(exprs):
+ """
+ Get a list of variables in order of dependencies.
+ @param exprs a mapping of variable name to expression
+ @return a list of variable names
+ @throws AssertionError circular dependencies
+ """
+ var_graph = get_graph(exprs)
+ sorted_vars = list()
+ #determine dependency order
+ while var_graph.get_nodes():
+ #get a list of nodes with no edges
+ indep_vars = filter(lambda var: not var_graph.get_edges(var), var_graph.get_nodes())
+ assert indep_vars
+ #add the indep vars to the end of the list
+ sorted_vars.extend(sorted(indep_vars))
+ #remove each edge-less node from the graph
+ for var in indep_vars: var_graph.remove_node(var)
+ return reversed(sorted_vars)
+
+if __name__ == '__main__':
+ for i in sort_variables({'x':'1', 'y':'x+1', 'a':'x+y', 'b':'y+1', 'c':'a+b+x+y'}): print i
diff --git a/grc/src/platforms/python/utils/extract_docs.py b/grc/src/platforms/python/utils/extract_docs.py
new file mode 100644
index 000000000..dfc0b7e97
--- /dev/null
+++ b/grc/src/platforms/python/utils/extract_docs.py
@@ -0,0 +1,109 @@
+"""
+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 DOCS_DIR
+from lxml import etree
+import os
+
+DOXYGEN_NAME_XPATH = '/doxygen/compounddef/compoundname'
+DOXYGEN_BRIEFDESC_GR_XPATH = '/doxygen/compounddef/briefdescription'
+DOXYGEN_DETAILDESC_GR_XPATH = '/doxygen/compounddef/detaileddescription'
+DOXYGEN_BRIEFDESC_BLKS2_XPATH = '/doxygen/compounddef/sectiondef[@kind="public-func"]/memberdef/briefdescription'
+DOXYGEN_DETAILDESC_BLKS2_XPATH = '/doxygen/compounddef/sectiondef[@kind="public-func"]/memberdef/detaileddescription'
+
+def extract_txt(xml, parent_text=None):
+ """
+ Recursivly pull the text out of an xml tree.
+ @param xml the xml tree
+ @param parent_text the text of the parent element
+ @return a string
+ """
+ text = xml.text or ''
+ tail = parent_text and xml.tail or ''
+ return text + ''.join(
+ map(lambda x: extract_txt(x, text), xml)
+ ) + tail
+
+def is_match(key, file):
+ """
+ Is the block key a match for the given file name?
+ @param key block key
+ @param file the xml file name
+ @return true if matches
+ """
+ if not file.endswith('.xml'): return False
+ file = file.replace('.xml', '') #remove file ext
+ file = file.replace('__', '_') #doxygen xml files have 2 underscores
+ if key.startswith('gr_'):
+ if not file.startswith('classgr_'): return False
+ key = key.replace('gr_', 'classgr_')
+ elif key.startswith('trellis_'):
+ if not file.startswith('classtrellis_'): return False
+ key = key.replace('trellis_', 'classtrellis_')
+ elif key.startswith('blks2_'):
+ if not file.startswith('classgnuradio_'): return False
+ if 'blks2' not in file: return False
+ file = file.replace('_1_1', '_') #weird blks2 doxygen syntax
+ key = key.replace('blks2_', '')
+ else: return False
+ for k, f in zip(*map(reversed, map(lambda x: x.split('_'), [key, file]))):
+ if k == f: continue
+ ks = k.split('x')
+ if len(ks) == 2 and f.startswith(ks[0]) and f.endswith(ks[1]): continue
+ if len(ks) > 2 and all(ki in ('x', fi) for ki, fi in zip(k, f)): continue
+ return False
+ return True
+
+def extract(key):
+ """
+ Extract the documentation from the doxygen generated xml files.
+ If multiple files match, combine the docs.
+ @param key the block key
+ @return a string with documentation
+ """
+ #get potential xml file matches for the key
+ if os.path.exists(DOCS_DIR) and os.path.isdir(DOCS_DIR):
+ matches = filter(lambda f: is_match(key, f), os.listdir(DOCS_DIR))
+ else: matches = list()
+ #combine all matches
+ doc_strs = list()
+ for match in matches:
+ try:
+ xml_file = DOCS_DIR + '/' + match
+ xml = etree.parse(xml_file)
+ #extract descriptions
+ comp_name = extract_txt(xml.xpath(DOXYGEN_NAME_XPATH)[0]).strip('\n')
+ comp_name = ' --- ' + comp_name + ' --- '
+ if key.startswith('gr_') or key.startswith('trellis_'):
+ brief_desc = extract_txt(xml.xpath(DOXYGEN_BRIEFDESC_GR_XPATH)[0]).strip('\n')
+ detailed_desc = extract_txt(xml.xpath(DOXYGEN_DETAILDESC_GR_XPATH)[0]).strip('\n')
+ elif key.startswith('blks2_'):
+ brief_desc = extract_txt(xml.xpath(DOXYGEN_BRIEFDESC_BLKS2_XPATH)[0]).strip('\n')
+ detailed_desc = extract_txt(xml.xpath(DOXYGEN_DETAILDESC_BLKS2_XPATH)[0]).strip('\n')
+ else:
+ brief_desc = ''
+ detailed_desc = ''
+ #combine
+ doc_strs.append('\n'.join([comp_name, brief_desc, detailed_desc]).strip('\n'))
+ except IndexError: pass #bad format
+ return '\n\n'.join(doc_strs)
+
+if __name__ == '__main__':
+ import sys
+ print extract(sys.argv[1])