diff options
author | jblum | 2008-09-07 21:38:12 +0000 |
---|---|---|
committer | jblum | 2008-09-07 21:38:12 +0000 |
commit | c86f6c23c6883f73d953d64c28ab42cedb77e4d7 (patch) | |
tree | 0193b2a649eb0f7f1065912862de340a02848e16 /grc/src/platforms | |
parent | ddec4fc07744a6519086b1b111f29d551b7f19c6 (diff) | |
download | gnuradio-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')
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 ( + ('&', '&'), + ('<', '<'), + ('>', '>'), + ('"', '"'), + ("'", '''), + ): 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]) |