summaryrefslogtreecommitdiff
path: root/grc
diff options
context:
space:
mode:
Diffstat (limited to 'grc')
-rw-r--r--grc/CMakeLists.txt92
-rw-r--r--grc/blocks/CMakeLists.txt36
-rw-r--r--grc/blocks/block_tree.xml30
-rw-r--r--grc/blocks/bus_sink.xml27
-rw-r--r--grc/blocks/bus_source.xml27
-rw-r--r--grc/blocks/bus_structure_sink.xml18
-rw-r--r--grc/blocks/bus_structure_source.xml18
-rw-r--r--grc/blocks/dummy.xml11
-rw-r--r--grc/blocks/epy_block.xml58
-rw-r--r--grc/blocks/epy_module.xml32
-rw-r--r--grc/blocks/gr_message_domain.xml19
-rw-r--r--grc/blocks/gr_stream_domain.xml18
-rw-r--r--grc/blocks/grc.tree.yml15
-rw-r--r--grc/blocks/import.block.yml20
-rw-r--r--grc/blocks/import.xml26
-rw-r--r--grc/blocks/message.domain.yml10
-rw-r--r--grc/blocks/note.block.yml9
-rw-r--r--grc/blocks/note.xml17
-rw-r--r--grc/blocks/options.block.yml146
-rw-r--r--grc/blocks/options.xml250
-rw-r--r--grc/blocks/pad_sink.block.yml51
-rw-r--r--grc/blocks/pad_sink.xml103
-rw-r--r--grc/blocks/pad_source.block.yml51
-rw-r--r--grc/blocks/pad_source.xml104
-rw-r--r--grc/blocks/parameter.block.yml55
-rw-r--r--grc/blocks/parameter.xml118
-rw-r--r--grc/blocks/stream.domain.yml10
-rw-r--r--grc/blocks/variable.block.yml19
-rw-r--r--grc/blocks/variable.xml23
-rw-r--r--grc/blocks/variable_config.block.yml58
-rw-r--r--grc/blocks/variable_config.xml88
-rw-r--r--grc/blocks/variable_function_probe.block.yml54
-rw-r--r--grc/blocks/variable_function_probe.xml78
-rw-r--r--grc/blocks/variable_struct.block.yml.py105
-rw-r--r--grc/blocks/variable_struct.xml.py97
-rw-r--r--grc/blocks/virtual_sink.xml21
-rw-r--r--grc/blocks/virtual_source.xml21
-rwxr-xr-xgrc/compiler.py6
-rw-r--r--grc/converter/__init__.py20
-rw-r--r--grc/converter/__main__.py21
-rw-r--r--grc/converter/block.dtd (renamed from grc/core/block.dtd)0
-rw-r--r--grc/converter/block.py219
-rw-r--r--grc/converter/block_tree.dtd (renamed from grc/core/block_tree.dtd)0
-rw-r--r--grc/converter/block_tree.py56
-rw-r--r--grc/converter/cheetah_converter.py277
-rw-r--r--grc/converter/flow_graph.dtd38
-rw-r--r--grc/converter/flow_graph.py131
-rw-r--r--grc/converter/main.py165
-rw-r--r--grc/converter/xml.py82
-rw-r--r--grc/core/Block.py852
-rw-r--r--grc/core/CMakeLists.txt35
-rw-r--r--grc/core/Config.py38
-rw-r--r--grc/core/Connection.py155
-rw-r--r--grc/core/Constants.py109
-rw-r--r--grc/core/Element.py114
-rw-r--r--grc/core/FlowGraph.py450
-rw-r--r--grc/core/Messages.py7
-rw-r--r--grc/core/Param.py749
-rw-r--r--grc/core/ParseXML.py69
-rw-r--r--grc/core/Platform.py309
-rw-r--r--grc/core/Port.py414
-rw-r--r--grc/core/base.py164
-rw-r--r--grc/core/blocks/__init__.py37
-rw-r--r--grc/core/blocks/_build.py69
-rw-r--r--grc/core/blocks/_flags.py39
-rw-r--r--grc/core/blocks/_templates.py77
-rw-r--r--grc/core/blocks/block.py415
-rw-r--r--grc/core/blocks/dummy.py54
-rw-r--r--grc/core/blocks/embedded_python.py242
-rw-r--r--grc/core/blocks/virtual.py76
-rw-r--r--grc/core/default_flow_graph.grc70
-rw-r--r--grc/core/errors.py30
-rw-r--r--grc/core/generator/CMakeLists.txt30
-rw-r--r--grc/core/generator/FlowGraphProxy.py80
-rw-r--r--grc/core/generator/Generator.py354
-rw-r--r--grc/core/generator/__init__.py3
-rw-r--r--grc/core/generator/flow_graph.py.mako415
-rw-r--r--grc/core/generator/flow_graph.tmpl475
-rw-r--r--grc/core/generator/hier_block.py193
-rw-r--r--grc/core/generator/top_block.py284
-rw-r--r--grc/core/io/__init__.py16
-rw-r--r--grc/core/io/yaml.py91
-rw-r--r--grc/core/platform.py431
-rw-r--r--grc/core/ports/__init__.py (renamed from grc/core/domain.dtd)28
-rw-r--r--grc/core/ports/_virtual_connections.py126
-rw-r--r--grc/core/ports/clone.py38
-rw-r--r--grc/core/ports/port.py207
-rw-r--r--grc/core/schema_checker/__init__.py5
-rw-r--r--grc/core/schema_checker/block.py57
-rw-r--r--grc/core/schema_checker/domain.py16
-rw-r--r--grc/core/schema_checker/flow_graph.py23
-rw-r--r--grc/core/schema_checker/utils.py27
-rw-r--r--grc/core/schema_checker/validator.py102
-rw-r--r--grc/core/utils/__init__.py10
-rw-r--r--grc/core/utils/backports/__init__.py (renamed from grc/core/utils/CMakeLists.txt)12
-rw-r--r--grc/core/utils/backports/chainmap.py106
-rw-r--r--grc/core/utils/backports/shlex.py (renamed from grc/core/utils/shlex.py)0
-rw-r--r--grc/core/utils/complexity.py49
-rw-r--r--grc/core/utils/descriptors/__init__.py26
-rw-r--r--grc/core/utils/descriptors/_lazy.py39
-rw-r--r--grc/core/utils/descriptors/evaluated.py112
-rw-r--r--grc/core/utils/epy_block_io.py11
-rw-r--r--grc/core/utils/expr_utils.py175
-rw-r--r--grc/core/utils/extract_docs.py34
-rw-r--r--grc/core/utils/flow_graph_complexity.py54
-rw-r--r--grc/core/utils/hide_bokeh_gui_options_if_not_installed.py13
-rw-r--r--grc/core/utils/odict.py115
-rw-r--r--grc/gui/Actions.py644
-rw-r--r--grc/gui/Application.py (renamed from grc/gui/ActionHandler.py)651
-rw-r--r--grc/gui/Bars.py529
-rw-r--r--grc/gui/Block.py350
-rw-r--r--grc/gui/BlockTreeWindow.py219
-rw-r--r--grc/gui/CMakeLists.txt32
-rw-r--r--grc/gui/Colors.py49
-rw-r--r--grc/gui/Config.py156
-rw-r--r--grc/gui/Connection.py181
-rw-r--r--grc/gui/Console.py57
-rw-r--r--grc/gui/Constants.py37
-rw-r--r--grc/gui/Dialogs.py472
-rw-r--r--grc/gui/DrawingArea.py253
-rw-r--r--grc/gui/Element.py278
-rw-r--r--grc/gui/Executor.py24
-rw-r--r--grc/gui/FileDialogs.py293
-rw-r--r--grc/gui/MainWindow.py321
-rw-r--r--grc/gui/Notebook.py187
-rw-r--r--grc/gui/NotebookPage.py244
-rw-r--r--grc/gui/Param.py440
-rw-r--r--grc/gui/ParamWidgets.py330
-rw-r--r--grc/gui/ParserErrorsDialog.py36
-rw-r--r--grc/gui/Platform.py48
-rw-r--r--grc/gui/Port.py277
-rw-r--r--grc/gui/Preferences.py173
-rw-r--r--grc/gui/PropsDialog.py262
-rw-r--r--grc/gui/StateCache.py9
-rw-r--r--grc/gui/Utils.py141
-rw-r--r--grc/gui/VariableEditor.py183
-rw-r--r--grc/gui/canvas/__init__.py22
-rw-r--r--grc/gui/canvas/block.py400
-rw-r--r--grc/gui/canvas/colors.py78
-rw-r--r--grc/gui/canvas/connection.py253
-rw-r--r--grc/gui/canvas/drawable.py183
-rw-r--r--grc/gui/canvas/flowgraph.py (renamed from grc/gui/FlowGraph.py)746
-rw-r--r--grc/gui/canvas/param.py162
-rw-r--r--grc/gui/canvas/port.py227
-rw-r--r--grc/gui/external_editor.py9
-rwxr-xr-xgrc/main.py58
-rwxr-xr-xgrc/scripts/gnuradio-companion23
-rw-r--r--grc/tests/__init__.py0
-rw-r--r--grc/tests/resources/file1.block.yml38
-rw-r--r--grc/tests/resources/file2.block.yml31
-rw-r--r--grc/tests/resources/file3.block.yml66
-rw-r--r--grc/tests/test_block_flags.py26
-rw-r--r--grc/tests/test_block_templates.py45
-rw-r--r--grc/tests/test_cheetah_converter.py132
-rw-r--r--grc/tests/test_evaled_property.py104
-rw-r--r--grc/tests/test_expr_utils.py41
-rw-r--r--grc/tests/test_generator.py46
-rw-r--r--grc/tests/test_xml_parser.py (renamed from grc/core/Element.pyi)41
-rw-r--r--grc/tests/test_yaml_checker.py84
159 files changed, 11382 insertions, 9590 deletions
diff --git a/grc/CMakeLists.txt b/grc/CMakeLists.txt
index eed5202657..77c8d4c00d 100644
--- a/grc/CMakeLists.txt
+++ b/grc/CMakeLists.txt
@@ -1,4 +1,4 @@
-# Copyright 2011,2013 Free Software Foundation, Inc.
+# Copyright 2011,2013,2017 Free Software Foundation, Inc.
#
# This file is part of GNU Radio
#
@@ -22,11 +22,64 @@
########################################################################
include(GrPython)
-GR_PYTHON_CHECK_MODULE("python >= 2.5" sys "sys.version.split()[0] >= '2.5'" PYTHON_MIN_VER_FOUND)
-GR_PYTHON_CHECK_MODULE("Cheetah >= 2.0.0" Cheetah "Cheetah.Version >= '2.0.0'" CHEETAH_FOUND)
-GR_PYTHON_CHECK_MODULE("lxml >= 1.3.6" lxml.etree "lxml.etree.LXML_VERSION >= (1, 3, 6, 0)" LXML_FOUND)
-GR_PYTHON_CHECK_MODULE("pygtk >= 2.10.0" gtk "gtk.pygtk_version >= (2, 10, 0)" PYGTK_FOUND)
-GR_PYTHON_CHECK_MODULE("numpy" numpy True NUMPY_FOUND)
+message(STATUS "")
+
+GR_PYTHON_CHECK_MODULE_RAW(
+ "python2 >= 2.7.6 or python3 >= 3.4.0"
+ "import sys; \
+ requirement = (3, 4, 0) if sys.version_info.major >= 3 else (2, 7, 6); \
+ assert sys.version_info[:3] >= requirement"
+ PYTHON_MIN_VER_FOUND
+)
+
+GR_PYTHON_CHECK_MODULE_RAW(
+ "PyYAML >= 3.10"
+ "import yaml; assert yaml.__version__ >= '3.11'"
+ PYYAML_FOUND
+)
+
+GR_PYTHON_CHECK_MODULE_RAW(
+ "mako >= 0.9.1"
+ "import mako; assert mako.__version__ >= '0.9.1'"
+ MAKO_FOUND
+)
+
+GR_PYTHON_CHECK_MODULE_RAW(
+ "lxml >= 1.3.6"
+ "import lxml.etree; assert lxml.etree.LXML_VERSION >= (1, 3, 6, 0)"
+ LXML_FOUND
+)
+
+GR_PYTHON_CHECK_MODULE_RAW(
+ "pygobject >= 2.28.6"
+ "import gi; assert gi.version_info >= (2, 28, 6)"
+ PYGI_FOUND
+)
+
+GR_PYTHON_CHECK_MODULE_RAW(
+ "Gtk (GI) >= 3.10.8"
+ "import gi; gi.require_version('Gtk', '3.0'); \
+ from gi.repository import Gtk; Gtk.check_version(3, 10, 8)"
+ GTK_GI_FOUND
+)
+
+GR_PYTHON_CHECK_MODULE_RAW(
+ "Cairo (GI) >= 1.0"
+ "import gi; gi.require_foreign('cairo', 'Context')" # Cairo 1.13.0
+ CAIRO_GI_FOUND
+)
+
+GR_PYTHON_CHECK_MODULE_RAW(
+ "PangoCairo (GI) >= 1.0"
+ "import gi; gi.require_version('PangoCairo', '1.0')" # pangocairo 1.36.3
+ PANGOCAIRO_GI_FOUND
+)
+
+GR_PYTHON_CHECK_MODULE_RAW(
+ "numpy"
+ "import numpy"
+ NUMPY_FOUND
+)
########################################################################
# Register component
@@ -35,9 +88,13 @@ include(GrComponent)
if(NOT CMAKE_CROSSCOMPILING)
set(grc_python_deps
PYTHON_MIN_VER_FOUND
- CHEETAH_FOUND
+ PYYAML_FOUND
+ MAKO_FOUND
LXML_FOUND
- PYGTK_FOUND
+ PYGI_FOUND
+ GTK_GI_FOUND
+ CAIRO_GI_FOUND
+ PANGOCAIRO_GI_FOUND
NUMPY_FOUND
)
endif(NOT CMAKE_CROSSCOMPILING)
@@ -48,9 +105,6 @@ GR_REGISTER_COMPONENT("gnuradio-companion" ENABLE_GRC
${grc_python_deps}
)
-########################################################################
-# Begin conditional configuration
-########################################################################
if(ENABLE_GRC)
########################################################################
@@ -85,15 +139,23 @@ install(
DESTINATION ${GR_PREFSDIR}
)
+########################################################################
+# Install (+ compile) python sources and data files
+########################################################################
file(GLOB py_files "*.py")
-
GR_PYTHON_INSTALL(
FILES ${py_files}
- DESTINATION ${GR_PYTHON_DIR}/gnuradio/grc
+ DESTINATION "${GR_PYTHON_DIR}/gnuradio/grc"
+)
+
+GR_PYTHON_INSTALL(
+ DIRECTORY core gui converter
+ DESTINATION "${GR_PYTHON_DIR}/gnuradio/grc"
+ FILES_MATCHING REGEX "\\.(py|dtd|grc|tmpl|png|mako)$"
)
########################################################################
-# Appens NSIS commands to set environment variables
+# Append NSIS commands to set environment variables
########################################################################
if(WIN32)
@@ -112,8 +174,6 @@ endif(WIN32)
# Add subdirectories
########################################################################
add_subdirectory(blocks)
-add_subdirectory(gui)
-add_subdirectory(core)
add_subdirectory(scripts)
endif(ENABLE_GRC)
diff --git a/grc/blocks/CMakeLists.txt b/grc/blocks/CMakeLists.txt
index d46b1febbe..f5ec6dd214 100644
--- a/grc/blocks/CMakeLists.txt
+++ b/grc/blocks/CMakeLists.txt
@@ -20,40 +20,40 @@
########################################################################
include(GrPython)
-file(GLOB xml_files "*.xml")
+file(GLOB yml_files "*.yml")
-macro(REPLACE_IN_FILE _xml_block match replace)
- set(xml_block_src "${CMAKE_CURRENT_SOURCE_DIR}/${_xml_block}")
- set(xml_block "${CMAKE_CURRENT_BINARY_DIR}/${_xml_block}")
+macro(REPLACE_IN_FILE _yml_block match replace)
+ set(yml_block_src "${CMAKE_CURRENT_SOURCE_DIR}/${_yml_block}")
+ set(yml_block "${CMAKE_CURRENT_BINARY_DIR}/${_yml_block}")
- list(REMOVE_ITEM xml_files "${xml_block_src}")
- file(READ "${xml_block_src}" xml_block_src_text)
+ list(REMOVE_ITEM yml_files "${yml_block_src}")
+ file(READ "${yml_block_src}" yml_block_src_text)
string(REPLACE "${match}" "${replace}"
- xml_block_text "${xml_block_src_text}")
- file(WRITE "${xml_block}" "${xml_block_text}")
+ yml_block_text "${yml_block_src_text}")
+ file(WRITE "${yml_block}" "${yml_block_text}")
- list(APPEND generated_xml_files "${xml_block}")
+ list(APPEND generated_yml_files "${yml_block}")
endmacro()
-macro(GEN_BLOCK_XML _generator _xml_block)
+macro(GEN_BLOCK_YML _generator _yml_block)
set(generator ${CMAKE_CURRENT_SOURCE_DIR}/${_generator})
- set(xml_block ${CMAKE_CURRENT_BINARY_DIR}/${_xml_block})
- list(APPEND generated_xml_files ${xml_block})
+ set(yml_block ${CMAKE_CURRENT_BINARY_DIR}/${_yml_block})
+ list(APPEND generated_yml_files ${yml_block})
add_custom_command(
- DEPENDS ${generator} OUTPUT ${xml_block}
- COMMAND ${PYTHON_EXECUTABLE} ${generator} ${xml_block}
+ DEPENDS ${generator} OUTPUT ${yml_block}
+ COMMAND ${PYTHON_EXECUTABLE} ${generator} ${yml_block}
)
endmacro()
-GEN_BLOCK_XML(variable_struct.xml.py variable_struct.xml)
+GEN_BLOCK_YML(variable_struct.block.yml.py variable_struct.block.yml)
if(DESIRED_QT_VERSION EQUAL 4)
- REPLACE_IN_FILE(options.xml PyQt5 PyQt4)
+ REPLACE_IN_FILE(options.yml PyQt5 PyQt4)
endif()
-add_custom_target(grc_generated_xml ALL DEPENDS ${generated_xml_files})
+add_custom_target(grc_generated_yml ALL DEPENDS ${generated_yml_files})
install(
- FILES ${xml_files} ${generated_xml_files}
+ FILES ${yml_files} ${generated_yml_files}
DESTINATION ${GRC_BLOCKS_DIR}
)
diff --git a/grc/blocks/block_tree.xml b/grc/blocks/block_tree.xml
deleted file mode 100644
index 3125864d4d..0000000000
--- a/grc/blocks/block_tree.xml
+++ /dev/null
@@ -1,30 +0,0 @@
-<?xml version="1.0"?>
-<cat>
- <name>[Core]</name>
- <cat>
- <name>Misc</name>
- <block>pad_source</block>
- <block>pad_sink</block>
- <block>virtual_source</block>
- <block>virtual_sink</block>
-
- <block>bus_sink</block>
- <block>bus_source</block>
- <block>bus_structure_sink</block>
- <block>bus_structure_source</block>
-
- <block>epy_block</block>
- <block>epy_module</block>
-
- <block>note</block>
- <block>import</block>
- </cat>
- <cat>
- <name>Variables</name>
- <block>variable</block>
- <block>variable_struct</block>
- <block>variable_config</block>
- <block>variable_function_probe</block>
- <block>parameter</block>
- </cat>
-</cat>
diff --git a/grc/blocks/bus_sink.xml b/grc/blocks/bus_sink.xml
deleted file mode 100644
index 029820dc2c..0000000000
--- a/grc/blocks/bus_sink.xml
+++ /dev/null
@@ -1,27 +0,0 @@
-<?xml version="1.0"?>
-<!--
-###################################################
-##Bus Sink
-###################################################
- -->
-<block>
- <name>Bus Sink</name>
- <key>bus_sink</key>
- <make>$yesno.yesno</make>
-
- <param>
- <name>On/Off</name>
- <key>yesno</key>
- <type>enum</type>
- <option>
- <name>On</name>
- <key>on</key>
- <opt>yesno:True</opt>
- </option>
- <option>
- <name>Off</name>
- <key>off</key>
- <opt>yesno:False</opt>
- </option>
- </param>
-</block>
diff --git a/grc/blocks/bus_source.xml b/grc/blocks/bus_source.xml
deleted file mode 100644
index e5b5c2b9bf..0000000000
--- a/grc/blocks/bus_source.xml
+++ /dev/null
@@ -1,27 +0,0 @@
-<?xml version="1.0"?>
-<!--
-###################################################
-##Bus Sink
-###################################################
- -->
-<block>
- <name>Bus Source</name>
- <key>bus_source</key>
- <make>$yesno.yesno</make>
-
- <param>
- <name>On/Off</name>
- <key>yesno</key>
- <type>enum</type>
- <option>
- <name>On</name>
- <key>on</key>
- <opt>yesno:True</opt>
- </option>
- <option>
- <name>Off</name>
- <key>off</key>
- <opt>yesno:False</opt>
- </option>
- </param>
-</block>
diff --git a/grc/blocks/bus_structure_sink.xml b/grc/blocks/bus_structure_sink.xml
deleted file mode 100644
index 3adac92810..0000000000
--- a/grc/blocks/bus_structure_sink.xml
+++ /dev/null
@@ -1,18 +0,0 @@
-<?xml version="1.0"?>
-<!--
-###################################################
-##Bus Sink
-###################################################
- -->
-<block>
- <name>Bus Sink Structure</name>
- <key>bus_structure_sink</key>
- <make>None</make>
-
- <param>
- <name>Structure</name>
- <key>struct</key>
- <value></value>
- <type>raw</type>
- </param>
-</block>
diff --git a/grc/blocks/bus_structure_source.xml b/grc/blocks/bus_structure_source.xml
deleted file mode 100644
index 34e7c049a2..0000000000
--- a/grc/blocks/bus_structure_source.xml
+++ /dev/null
@@ -1,18 +0,0 @@
-<?xml version="1.0"?>
-<!--
-###################################################
-##Bus Sink
-###################################################
- -->
-<block>
- <name>Bus Source Structure</name>
- <key>bus_structure_source</key>
- <make>None</make>
-
- <param>
- <name>Structure</name>
- <key>struct</key>
- <value></value>
- <type>raw</type>
- </param>
-</block>
diff --git a/grc/blocks/dummy.xml b/grc/blocks/dummy.xml
deleted file mode 100644
index c0ca3b6698..0000000000
--- a/grc/blocks/dummy.xml
+++ /dev/null
@@ -1,11 +0,0 @@
-<?xml version="1.0"?>
-<!--
-###################################################
-##Dummy Block
-###################################################
--->
-<block>
- <name>Missing Block</name>
- <key>dummy_block</key>
- <make>raise NotImplementedError()</make>
-</block>
diff --git a/grc/blocks/epy_block.xml b/grc/blocks/epy_block.xml
deleted file mode 100644
index 65e78c4062..0000000000
--- a/grc/blocks/epy_block.xml
+++ /dev/null
@@ -1,58 +0,0 @@
-<?xml version="1.0"?>
-<block>
- <name>Python Block</name>
- <key>epy_block</key>
- <import></import>
- <make></make>
- <param><!-- Cache the last working block IO to keep FG sane -->
- <name>Block Io</name>
- <key>_io_cache</key>
- <type>string</type>
- <hide>all</hide>
- </param>
- <param>
- <name>Code</name>
- <key>_source_code</key>
- <value>"""
-Embedded Python Blocks:
-
-Each time this file is saved, GRC will instantiate the first class it finds
-to get ports and parameters of your block. The arguments to __init__ will
-be the parameters. All of them are required to have default values!
-"""
-
-import numpy as np
-from gnuradio import gr
-
-
-class blk(gr.sync_block): # other base classes are basic_block, decim_block, interp_block
- """Embedded Python Block example - a simple multiply const"""
-
- def __init__(self, example_param=1.0): # only default arguments here
- """arguments to this function show up as parameters in GRC"""
- gr.sync_block.__init__(
- self,
- name='Embedded Python Block', # will show up in GRC
- in_sig=[np.complex64],
- out_sig=[np.complex64]
- )
- # if an attribute with the same name as a parameter is found,
- # a callback is registered (properties work, too).
- self.example_param = example_param
-
- def work(self, input_items, output_items):
- """example: multiply with constant"""
- output_items[0][:] = input_items[0] * self.example_param
- return len(output_items[0])
-</value>
- <type>_multiline_python_external</type>
- <hide>part</hide>
- </param>
- <doc>This block represents an arbitrary GNU Radio Python Block.
-
-Its source code can be accessed through the parameter 'Code' which opens your editor. Each time you save changes in the editor, GRC will update the block. This includes the number, names and defaults of the parameters, the ports (stream and message) and the block name and documentation.
-
-Block Documentation:
-(will be replaced the docstring of your block class)
-</doc>
-</block>
diff --git a/grc/blocks/epy_module.xml b/grc/blocks/epy_module.xml
deleted file mode 100644
index fa3e5f91f4..0000000000
--- a/grc/blocks/epy_module.xml
+++ /dev/null
@@ -1,32 +0,0 @@
-<?xml version="1.0"?>
-<block>
- <name>Python Module</name>
- <key>epy_module</key>
- <import>import $id # embedded python module</import>
- <make></make>
- <param>
- <name>Code</name>
- <key>source_code</key>
- <value># this module will be imported in the into your flowgraph</value>
- <type>_multiline_python_external</type>
- <hide>part</hide>
- </param>
- <doc>This block lets you embed a python module in your flowgraph.
-
-Code you put in this module is accessible in other blocks using the ID of this
-block. Example:
-
-If you put
-
- a = 2
-
- def double(arg):
- return 2 * arg
-
-in a Python Module Block with the ID 'stuff' you can use code like
-
- stuff.a # evals to 2
- stuff.double(3) # evals to 6
-
-to set parameters of other blocks in your flowgraph.</doc>
-</block>
diff --git a/grc/blocks/gr_message_domain.xml b/grc/blocks/gr_message_domain.xml
deleted file mode 100644
index bc8add99ab..0000000000
--- a/grc/blocks/gr_message_domain.xml
+++ /dev/null
@@ -1,19 +0,0 @@
-<?xml version="1.0"?>
-<!--
-###################################################
-##GNU Radio default domain 'gr_message'
-###################################################
- -->
- <domain>
- <name>GR Message</name>
- <key>gr_message</key>
- <color>#000</color>
- <multiple_sources>True</multiple_sources>
- <connection>
- <source_domain>gr_message</source_domain>
- <sink_domain>gr_message</sink_domain>
- <make>#slurp
- self.msg_connect($make_port_sig($source), $make_port_sig($sink))#slurp
- </make>
- </connection>
-</domain>
diff --git a/grc/blocks/gr_stream_domain.xml b/grc/blocks/gr_stream_domain.xml
deleted file mode 100644
index 66026448ca..0000000000
--- a/grc/blocks/gr_stream_domain.xml
+++ /dev/null
@@ -1,18 +0,0 @@
-<?xml version="1.0"?>
-<!--
-###################################################
-##GNU Radio default domain 'gr_stream'
-###################################################
- -->
- <domain>
- <name>GR Stream</name>
- <key>gr_stream</key>
- <color>#000</color>
- <connection>
- <source_domain>gr_stream</source_domain>
- <sink_domain>gr_stream</sink_domain>
- <make>#slurp
- self.connect($make_port_sig($source), $make_port_sig($sink))#slurp
- </make>
- </connection>
-</domain>
diff --git a/grc/blocks/grc.tree.yml b/grc/blocks/grc.tree.yml
new file mode 100644
index 0000000000..c84a6dc478
--- /dev/null
+++ b/grc/blocks/grc.tree.yml
@@ -0,0 +1,15 @@
+'[Core]':
+- Misc:
+ - pad_source
+ - pad_sink
+ - virtual_source
+ - virtual_sink
+ - epy_module
+ - note
+ - import
+- Variables:
+ - variable
+ - variable_struct
+ - variable_config
+ - variable_function_probe
+ - parameter
diff --git a/grc/blocks/import.block.yml b/grc/blocks/import.block.yml
new file mode 100644
index 0000000000..2d36b7396d
--- /dev/null
+++ b/grc/blocks/import.block.yml
@@ -0,0 +1,20 @@
+id: import_
+label: Import
+
+parameters:
+- id: imports
+ label: Import
+ dtype: import
+
+templates:
+ imports: ${imports}
+
+documentation: |-
+ Import additional python modules into the namespace.
+
+ Examples:
+ from gnuradio.filter import firdes
+ import math,cmath
+ from math import pi
+
+file_format: 1
diff --git a/grc/blocks/import.xml b/grc/blocks/import.xml
deleted file mode 100644
index 59f807bacb..0000000000
--- a/grc/blocks/import.xml
+++ /dev/null
@@ -1,26 +0,0 @@
-<?xml version="1.0"?>
-<!--
-###################################################
-##Import python modules into the namespace
-###################################################
- -->
-<block>
- <name>Import</name>
- <key>import</key>
- <import>$import</import>
- <make></make>
- <param>
- <name>Import</name>
- <key>import</key>
- <value></value>
- <type>import</type>
- </param>
- <doc>
-Import additional python modules into the namespace.
-
-Examples:
-from gnuradio.filter import firdes
-import math,cmath
-from math import pi
- </doc>
-</block>
diff --git a/grc/blocks/message.domain.yml b/grc/blocks/message.domain.yml
new file mode 100644
index 0000000000..7e6cc529d9
--- /dev/null
+++ b/grc/blocks/message.domain.yml
@@ -0,0 +1,10 @@
+id: message
+label: Message
+color: "#FFFFFF"
+
+multiple_connections_per_input: true
+multiple_connections_per_output: true
+
+templates:
+- type: [message, message]
+ connect: self.msg_connect(${ make_port_sig(source) }, ${ make_port_sig(sink) })
diff --git a/grc/blocks/note.block.yml b/grc/blocks/note.block.yml
new file mode 100644
index 0000000000..3f21a75ceb
--- /dev/null
+++ b/grc/blocks/note.block.yml
@@ -0,0 +1,9 @@
+id: note
+label: Note
+
+parameters:
+- id: note
+ label: Note
+ dtype: string
+
+file_format: 1
diff --git a/grc/blocks/note.xml b/grc/blocks/note.xml
deleted file mode 100644
index db6687c033..0000000000
--- a/grc/blocks/note.xml
+++ /dev/null
@@ -1,17 +0,0 @@
-<?xml version="1.0"?>
-<!--
-###################################################
-##Note Block (dummy)
-###################################################
- -->
-<block>
- <name>Note</name>
- <key>note</key>
- <make></make>
- <param>
- <name>Note</name>
- <key>note</key>
- <value></value>
- <type>string</type>
- </param>
-</block>
diff --git a/grc/blocks/options.block.yml b/grc/blocks/options.block.yml
new file mode 100644
index 0000000000..ab18f8ae5f
--- /dev/null
+++ b/grc/blocks/options.block.yml
@@ -0,0 +1,146 @@
+id: options
+label: Options
+
+parameters:
+- id: title
+ label: Title
+ dtype: string
+ hide: ${ ('none' if title else 'part') }
+- id: author
+ label: Author
+ dtype: string
+ hide: ${ ('none' if author else 'part') }
+- id: description
+ label: Description
+ dtype: string
+ hide: ${ ('none' if description else 'part') }
+- id: window_size
+ label: Canvas Size
+ dtype: int_vector
+ hide: part
+- id: generate_options
+ label: Generate Options
+ dtype: enum
+ default: qt_gui
+ options: [qt_gui, bokeh_gui, no_gui, hb, hb_qt_gui]
+ option_labels: [QT GUI, Bokeh GUI, No GUI, Hier Block, Hier Block (QT GUI)]
+- id: category
+ label: Category
+ dtype: string
+ default: '[GRC Hier Blocks]'
+ hide: ${ ('none' if generate_options.startswith('hb') else 'all') }
+- id: run_options
+ label: Run Options
+ dtype: enum
+ default: prompt
+ options: [run, prompt]
+ option_labels: [Run to Completion, Prompt for Exit]
+ hide: ${ ('none' if generate_options == 'no_gui' else 'all') }
+- id: placement
+ label: Widget Placement
+ dtype: int_vector
+ default: (0,0)
+ hide: ${ ('part' if generate_options == 'bokeh_gui' else 'all') }
+- id: sizing_mode
+ label: Sizing Mode
+ dtype: enum
+ default: fixed
+ options: [fixed, stretch_both, scale_width, scale_height, scale_both]
+ option_labels: [Fixed, Stretch Both, Scale Width, Scale Height, Scale Both]
+ hide: ${ ('part' if generate_options == 'bokeh_gui' else 'all') }
+- id: run
+ label: Run
+ dtype: bool
+ default: 'True'
+ options: ['True', 'False']
+ option_labels: [Autostart, 'Off']
+ hide: ${ ('all' if generate_options not in ('qt_gui', 'bokeh_gui') else ('part'
+ if run else 'none')) }
+- id: max_nouts
+ label: Max Number of Output
+ dtype: int
+ default: '0'
+ hide: ${ ('all' if generate_options.startswith('hb') else ('none' if max_nouts
+ else 'part')) }
+- id: realtime_scheduling
+ label: Realtime Scheduling
+ dtype: enum
+ options: ['', '1']
+ option_labels: ['Off', 'On']
+ hide: ${ ('all' if generate_options.startswith('hb') else ('none' if realtime_scheduling
+ else 'part')) }
+- id: qt_qss_theme
+ label: QSS Theme
+ dtype: file_open
+ hide: ${ ('all' if generate_options != 'qt_gui' else ('none' if qt_qss_theme else
+ 'part')) }
+- id: thread_safe_setters
+ label: Thread-safe setters
+ category: Advanced
+ dtype: enum
+ options: ['', '1']
+ option_labels: ['Off', 'On']
+ hide: part
+- id: run_command
+ label: Run Command
+ category: Advanced
+ dtype: string
+ default: '{python} -u {filename}'
+ hide: ${ ('all' if generate_options.startswith('hb') else 'part') }
+- id: hier_block_src_path
+ label: Hier Block Source Path
+ category: Advanced
+ dtype: string
+ default: '.:'
+ hide: part
+
+asserts:
+- ${ not window_size or len(window_size) == 2 }
+- ${ not window_size or 300 <= window_size[0] <= 4096 }
+- ${ not window_size or 300 <= window_size[1] <= 4096 }
+- ${ len(placement) == 4 or len(placement) == 2 }
+- ${ all(i >= 0 for i in placement) }
+
+templates:
+ imports: |-
+ from gnuradio import gr
+ from gnuradio.filter import firdes
+ % if generate_options == 'qt_gui':
+ from PyQt5 import Qt
+ import sys
+ % endif
+ % if generate_options == 'bokeh_gui':
+ import time
+ import signal
+ import functools
+ from bokeh.client import push_session
+ from bokeh.plotting import curdoc
+ % endif
+ % if not generate_options.startswith('hb'):
+ from argparse import ArgumentParser
+ from gnuradio.eng_arg import eng_float, intx
+ from gnuradio import eng_notation
+ % endif
+ callbacks:
+ - 'if ${run}: self.start()
+
+ else: self.stop(); self.wait()'
+
+documentation: |-
+ The options block sets special parameters for the flow graph. Only one option block is allowed per flow graph.
+
+ Title, author, and description parameters are for identification purposes.
+
+ The window size controls the dimensions of the flow graph editor. The window size (width, height) must be between (300, 300) and (4096, 4096).
+
+ The generate options controls the type of code generated. Non-graphical flow graphs should avoid using graphical sinks or graphical variable controls.
+
+ In a graphical application, run can be controlled by a variable to start and stop the flowgraph at runtime.
+
+ The id of this block determines the name of the generated file and the name of the class. For example, an id of my_block will generate the file my_block.py and class my_block(gr....
+
+ The category parameter determines the placement of the block in the block selection window. The category only applies when creating hier blocks. To put hier blocks into the root category, enter / for the category.
+
+ The Max Number of Output is the maximum number of output items allowed for any block in the flowgraph; to disable this set the max_nouts equal to 0.Use this to adjust the maximum latency a flowgraph can exhibit.
+
+file_format: 1
diff --git a/grc/blocks/options.xml b/grc/blocks/options.xml
deleted file mode 100644
index bff88cec85..0000000000
--- a/grc/blocks/options.xml
+++ /dev/null
@@ -1,250 +0,0 @@
-<?xml version="1.0"?>
-<!--
-###################################################
-##Options Block:
-## options for window size,
-## and flow graph building.
-###################################################
- -->
-<block>
- <name>Options</name>
- <key>options</key>
- <import>from gnuradio import gr</import>
- <import>from gnuradio.filter import firdes</import>
- <import>#if $generate_options() == 'qt_gui'
-from PyQt5 import Qt
-import sys
-#end if
-#if $generate_options() == 'bokeh_gui'
-import time
-import signal
-import functools
-from bokeh.client import push_session
-from bokeh.plotting import curdoc
-#end if
-#if not $generate_options().startswith('hb')
-from argparse import ArgumentParser
-from gnuradio.eng_arg import eng_float, intx
-from gnuradio import eng_notation
-#end if</import>
- <make></make>
- <callback>if $run: self.start()
-else: self.stop(); self.wait()</callback>
- <param>
- <name>Title</name>
- <key>title</key>
- <value></value>
- <type>string</type>
- <hide>#if $title() then 'none' else 'part'#</hide>
- </param>
- <param>
- <name>Author</name>
- <key>author</key>
- <value></value>
- <type>string</type>
- <hide>#if $author() then 'none' else 'part'#</hide>
- </param>
- <param>
- <name>Description</name>
- <key>description</key>
- <value></value>
- <type>string</type>
- <hide>#if $description() then 'none' else 'part'#</hide>
- </param>
- <param>
- <name>Canvas Size</name>
- <key>window_size</key>
- <value></value>
- <type>int_vector</type>
- <hide>part</hide>
- </param>
- <param>
- <name>Generate Options</name>
- <key>generate_options</key>
- <value>qt_gui</value>
- <type>enum</type>
- <option>
- <name>Bokeh GUI</name>
- <key>bokeh_gui</key>
- </option>
- <option>
- <name>QT GUI</name>
- <key>qt_gui</key>
- </option>
- <option>
- <name>No GUI</name>
- <key>no_gui</key>
- </option>
- <option>
- <name>Hier Block</name>
- <key>hb</key>
- </option>
- <option>
- <name>Hier Block (QT GUI)</name>
- <key>hb_qt_gui</key>
- </option>
- </param>
- <param>
- <name>Category</name>
- <key>category</key>
- <value>[GRC Hier Blocks]</value>
- <type>string</type>
- <hide>#if $generate_options().startswith('hb') then 'none' else 'all'#</hide>
- </param>
- <param>
- <name>Run Options</name>
- <key>run_options</key>
- <value>prompt</value>
- <type>enum</type>
- <hide>#if $generate_options() == 'no_gui' then 'none' else 'all'#</hide>
- <option>
- <name>Run to Completion</name>
- <key>run</key>
- </option>
- <option>
- <name>Prompt for Exit</name>
- <key>prompt</key>
- </option>
- </param>
- <param>
- <name>Widget Placement</name>
- <key>placement</key>
- <value>(0,0)</value>
- <type>int_vector</type>
- <hide>#if $generate_options() == 'bokeh_gui' then 'part' else 'all'#</hide>
- </param>
- <param>
- <name>Sizing Mode</name>
- <key>sizing_mode</key>
- <value>fixed</value>
- <type>enum</type>
- <hide>#if $generate_options() == 'bokeh_gui' then 'part' else 'all'#</hide>
- <option>
- <name>Fixed</name>
- <key>fixed</key>
- </option>
- <option>
- <name>Stretch Both</name>
- <key>stretch_both</key>
- </option>
- <option>
- <name>Scale Width</name>
- <key>scale_width</key>
- </option>
- <option>
- <name>Scale Height</name>
- <key>scale_height</key>
- </option>
- <option>
- <name>Scale Both</name>
- <key>scale_both</key>
- </option>
- </param>
- <param>
- <name>Run</name>
- <key>run</key>
- <value>True</value>
- <type>bool</type>
- <hide>#if $generate_options() in ('qt_gui', 'bokeh_gui') then ('part' if $run() else 'none') else 'all'#</hide>
- <option>
- <name>Autostart</name>
- <key>True</key>
- </option>
- <option>
- <name>Off</name>
- <key>False</key>
- </option>
- </param>
- <param>
- <name>Max Number of Output</name>
- <key>max_nouts</key>
- <value>0</value>
- <type>int</type>
- <hide>#if $generate_options().startswith('hb') then 'all' else ('none' if $max_nouts() else 'part')#</hide>
- </param>
- <param>
- <name>Realtime Scheduling</name>
- <key>realtime_scheduling</key>
- <value></value>
- <type>enum</type>
- <hide>#if $generate_options().startswith('hb') then 'all' else ('none' if $realtime_scheduling() else 'part')#</hide>
- <option>
- <name>Off</name>
- <key></key>
- </option>
- <option>
- <name>On</name>
- <key>1</key>
- </option>
- </param>
- <param>
- <name>QSS Theme</name>
- <key>qt_qss_theme</key>
- <value></value>
- <type>file_open</type>
- <hide>#if $generate_options() == 'qt_gui' then ('none' if $qt_qss_theme() else 'part') else 'all'#</hide>
- </param>
- <param>
- <name>Thread-safe setters</name>
- <key>thread_safe_setters</key>
- <value></value>
- <type>enum</type>
- <hide>part</hide>
- <option>
- <name>Off</name>
- <key></key>
- </option>
- <option>
- <name>On</name>
- <key>1</key>
- </option>
- <tab>Advanced</tab>
- </param>
- <param>
- <name>Run Command</name>
- <key>run_command</key>
- <value>{python} -u {filename}</value>
- <type>string</type>
- <hide>#if $generate_options().startswith('hb') then 'all' else 'part'</hide>
- <tab>Advanced</tab>
- </param>
- <param>
- <name>Hier Block Source Path</name>
- <key>hier_block_src_path</key>
- <value>.:</value>
- <type>string</type>
- <hide>part</hide>
- <tab>Advanced</tab>
- </param>
- <check>not $window_size or len($window_size) == 2</check>
- <check>not $window_size or 300 &lt;= $(window_size)[0] &lt;= 4096</check>
- <check>not $window_size or 300 &lt;= $(window_size)[1] &lt;= 4096</check>
- <check>len($placement) == 4 or len($placement) == 2</check>
- <check>all(i &gt;= 0 for i in $(placement))</check>
- <doc>
-The options block sets special parameters for the flow graph. \
-Only one option block is allowed per flow graph.
-
-Title, author, and description parameters are for identification purposes.
-
-The window size controls the dimensions of the flow graph editor. \
-The window size (width, height) must be between (300, 300) and (4096, 4096).
-
-The generate options controls the type of code generated. \
-Non-graphical flow graphs should avoid using graphical sinks or graphical variable controls.
-
-In a graphical application, \
-run can be controlled by a variable to start and stop the flowgraph at runtime.
-
-The id of this block determines the name of the generated file and the name of the class. \
-For example, an id of my_block will generate the file my_block.py and class my_block(gr....
-
-The category parameter determines the placement of the block in the block selection window. \
-The category only applies when creating hier blocks. \
-To put hier blocks into the root category, enter / for the category.
-
-The Max Number of Output is the maximum number of output items allowed for any block \
-in the flowgraph; to disable this set the max_nouts equal to 0.\
-Use this to adjust the maximum latency a flowgraph can exhibit.
- </doc>
-</block>
diff --git a/grc/blocks/pad_sink.block.yml b/grc/blocks/pad_sink.block.yml
new file mode 100644
index 0000000000..d304a998b4
--- /dev/null
+++ b/grc/blocks/pad_sink.block.yml
@@ -0,0 +1,51 @@
+id: pad_sink
+label: Pad Sink
+
+parameters:
+- id: label
+ label: Label
+ dtype: string
+ default: out
+- id: type
+ label: Input Type
+ dtype: enum
+ options: [complex, float, int, short, byte, bit, message, '']
+ option_labels: [Complex, Float, Int, Short, Byte, Bits, Message, Wildcard]
+ option_attributes:
+ size: [gr.sizeof_gr_complex, gr.sizeof_float, gr.sizeof_int, gr.sizeof_short,
+ gr.sizeof_char, gr.sizeof_char, '0', '0']
+ hide: part
+- id: vlen
+ label: Vec Length
+ dtype: int
+ default: '1'
+ hide: ${ 'part' if vlen == 1 else 'none' }
+- id: num_streams
+ label: Num Streams
+ dtype: int
+ default: '1'
+ hide: part
+- id: optional
+ label: Optional
+ dtype: bool
+ default: 'False'
+ options: ['True', 'False']
+ option_labels: [Optional, Required]
+ hide: part
+
+inputs:
+- domain: stream
+ dtype: ${ type }
+ vlen: ${ vlen }
+ multiplicity: ${ num_streams }
+
+asserts:
+- ${ vlen > 0 }
+- ${ num_streams > 0 }
+
+documentation: |-
+ The inputs of this block will become the outputs to this flow graph when it is instantiated as a hierarchical block.
+
+ Pad sink will be ordered alphabetically by their ids. The first pad sink will have an index of 0.
+
+file_format: 1
diff --git a/grc/blocks/pad_sink.xml b/grc/blocks/pad_sink.xml
deleted file mode 100644
index 8ea8871d2e..0000000000
--- a/grc/blocks/pad_sink.xml
+++ /dev/null
@@ -1,103 +0,0 @@
-<?xml version="1.0"?>
-<!--
-###################################################
-##Pad Sink: IO Pads
-###################################################
- -->
-<block>
- <name>Pad Sink</name>
- <key>pad_sink</key>
- <make></make>
- <param>
- <name>Label</name>
- <key>label</key>
- <value>out</value>
- <type>string</type>
- </param>
- <param>
- <name>Input Type</name>
- <key>type</key>
- <type>enum</type>
- <option>
- <name>Complex</name>
- <key>complex</key>
- <opt>size:gr.sizeof_gr_complex</opt>
- </option>
- <option>
- <name>Float</name>
- <key>float</key>
- <opt>size:gr.sizeof_float</opt>
- </option>
- <option>
- <name>Int</name>
- <key>int</key>
- <opt>size:gr.sizeof_int</opt>
- </option>
- <option>
- <name>Short</name>
- <key>short</key>
- <opt>size:gr.sizeof_short</opt>
- </option>
- <option>
- <name>Byte</name>
- <key>byte</key>
- <opt>size:gr.sizeof_char</opt>
- </option>
- <option>
- <name>Bits</name>
- <key>bit</key>
- <opt>size:gr.sizeof_char</opt>
- </option>
- <option>
- <name>Message</name>
- <key>message</key>
- <opt>size:0</opt>
- </option>
- <option>
- <name>Wildcard</name>
- <key></key>
- <opt>size:0</opt>
- </option>
- </param>
- <param>
- <name>Vec Length</name>
- <key>vlen</key>
- <value>1</value>
- <type>int</type>
- </param>
-
- <param>
- <name>Num Streams</name>
- <key>num_streams</key>
- <value>1</value>
- <type>int</type>
- </param>
- <param>
- <name>Optional</name>
- <key>optional</key>
- <value>False</value>
- <type>bool</type>
- <hide>part</hide>
- <option>
- <name>Optional</name>
- <key>True</key>
- </option>
- <option>
- <name>Required</name>
- <key>False</key>
- </option>
- </param>
- <check>$vlen &gt; 0</check>
- <check>$num_streams &gt; 0</check>
- <sink>
- <name>in</name>
- <type>$type</type>
- <vlen>$vlen</vlen>
- <nports>$num_streams</nports>
- </sink>
- <doc>
-The inputs of this block will become the outputs to this flow graph when it is instantiated as a hierarchical block.
-
-Pad sink will be ordered alphabetically by their ids. The first pad sink will have an index of 0.
- </doc>
-</block>
diff --git a/grc/blocks/pad_source.block.yml b/grc/blocks/pad_source.block.yml
new file mode 100644
index 0000000000..92f7a8b822
--- /dev/null
+++ b/grc/blocks/pad_source.block.yml
@@ -0,0 +1,51 @@
+id: pad_source
+label: Pad Source
+
+parameters:
+- id: label
+ label: Label
+ dtype: string
+ default: in
+- id: type
+ label: Output Type
+ dtype: enum
+ options: [complex, float, int, short, byte, bit, message, '']
+ option_labels: [Complex, Float, Int, Short, Byte, Bits, Message, Wildcard]
+ option_attributes:
+ size: [gr.sizeof_gr_complex, gr.sizeof_float, gr.sizeof_int, gr.sizeof_short,
+ gr.sizeof_char, gr.sizeof_char, '0', '0']
+ hide: part
+- id: vlen
+ label: Vec Length
+ dtype: int
+ default: '1'
+ hide: ${ 'part' if vlen == 1 else 'none' }
+- id: num_streams
+ label: Num Streams
+ dtype: int
+ default: '1'
+ hide: part
+- id: optional
+ label: Optional
+ dtype: bool
+ default: 'False'
+ options: ['True', 'False']
+ option_labels: [Optional, Required]
+ hide: part
+
+outputs:
+- domain: stream
+ dtype: ${ type }
+ vlen: ${ vlen }
+ multiplicity: ${ num_streams }
+
+asserts:
+- ${ vlen > 0 }
+- ${ num_streams > 0 }
+
+documentation: |-
+ The outputs of this block will become the inputs to this flow graph when it is instantiated as a hierarchical block.
+
+ Pad sources will be ordered alphabetically by their ids. The first pad source will have an index of 0.
+
+file_format: 1
diff --git a/grc/blocks/pad_source.xml b/grc/blocks/pad_source.xml
deleted file mode 100644
index 3d8ccbed6a..0000000000
--- a/grc/blocks/pad_source.xml
+++ /dev/null
@@ -1,104 +0,0 @@
-<?xml version="1.0"?>
-<!--
-###################################################
-##Pad Source: IO Pads
-###################################################
- -->
-<block>
- <name>Pad Source</name>
- <key>pad_source</key>
- <make></make>
- <param>
- <name>Label</name>
- <key>label</key>
- <value>in</value>
- <type>string</type>
- </param>
- <param>
- <name>Output Type</name>
- <key>type</key>
- <type>enum</type>
- <option>
- <name>Complex</name>
- <key>complex</key>
- <opt>size:gr.sizeof_gr_complex</opt>
- </option>
- <option>
- <name>Float</name>
- <key>float</key>
- <opt>size:gr.sizeof_float</opt>
- </option>
- <option>
- <name>Int</name>
- <key>int</key>
- <opt>size:gr.sizeof_int</opt>
- </option>
- <option>
- <name>Short</name>
- <key>short</key>
- <opt>size:gr.sizeof_short</opt>
- </option>
- <option>
- <name>Byte</name>
- <key>byte</key>
- <opt>size:gr.sizeof_char</opt>
- </option>
- <option>
- <name>Bits</name>
- <key>bit</key>
- <opt>size:gr.sizeof_char</opt>
- </option>
- <option>
- <name>Message</name>
- <key>message</key>
- <opt>size:0</opt>
- </option>
- <option>
- <name>Wildcard</name>
- <key></key>
- <opt>size:0</opt>
- </option>
- </param>
- <param>
- <name>Vec Length</name>
- <key>vlen</key>
- <value>1</value>
- <type>int</type>
- </param>
-
- <param>
- <name>Num Streams</name>
- <key>num_streams</key>
- <value>1</value>
- <type>int</type>
- </param>
-
- <param>
- <name>Optional</name>
- <key>optional</key>
- <value>False</value>
- <type>bool</type>
- <hide>part</hide>
- <option>
- <name>Optional</name>
- <key>True</key>
- </option>
- <option>
- <name>Required</name>
- <key>False</key>
- </option>
- </param>
- <check>$vlen &gt; 0</check>
- <check>$num_streams &gt; 0</check>
- <source>
- <name>out</name>
- <type>$type</type>
- <vlen>$vlen</vlen>
- <nports>$num_streams</nports>
- </source>
- <doc>
-The outputs of this block will become the inputs to this flow graph when it is instantiated as a hierarchical block.
-
-Pad sources will be ordered alphabetically by their ids. The first pad source will have an index of 0.
- </doc>
-</block>
diff --git a/grc/blocks/parameter.block.yml b/grc/blocks/parameter.block.yml
new file mode 100644
index 0000000000..ac97c7d319
--- /dev/null
+++ b/grc/blocks/parameter.block.yml
@@ -0,0 +1,55 @@
+id: parameter
+label: Parameter
+
+parameters:
+- id: label
+ label: Label
+ dtype: string
+ hide: ${ ('none' if label else 'part') }
+- id: value
+ label: Value
+ dtype: ${ type.type }
+ default: '0'
+- id: type
+ label: Type
+ dtype: enum
+ options: ['', complex, eng_float, intx, long, str]
+ option_labels: [None, Complex, Float, Int, Long, String]
+ option_attributes:
+ type: [raw, complex, real, int, int, string]
+ hide: ${ ('none' if type else 'part') }
+- id: short_id
+ label: Short ID
+ dtype: string
+ hide: ${ 'all' if not type else ('none' if short_id else 'part') }
+- id: hide
+ label: Show
+ dtype: enum
+ options: [none, part]
+ option_labels: [Always, Only in Properties]
+ hide: part
+
+asserts:
+- ${ len(short_id) in (0, 1) }
+- ${ short_id == '' or short_id.isalpha() }
+
+templates:
+ var_make: self.${id} = ${id}
+ make: ${value}
+
+documentation: |-
+ This block represents a parameter to the flow graph. A parameter can be used to pass command line arguments into a top block. Or, parameters can pass arguments into an instantiated hierarchical block.
+
+ The paramater value cannot depend on any variables.
+
+ Leave the label blank to use the parameter id as the label.
+
+ When type is not None, this parameter also becomes a command line option of the form:
+
+ -[short_id] --[id] [value]
+
+ The Short ID field may be left blank.
+
+ To disable showing the parameter on the hierarchical block in GRC, use Only in Properties option in the Show field.
+
+file_format: 1
diff --git a/grc/blocks/parameter.xml b/grc/blocks/parameter.xml
deleted file mode 100644
index f01527acb0..0000000000
--- a/grc/blocks/parameter.xml
+++ /dev/null
@@ -1,118 +0,0 @@
-<?xml version="1.0"?>
-<!--
-###################################################
-##Parameter block: a grc variable with key, value
-###################################################
- -->
-<block>
- <name>Parameter</name>
- <key>parameter</key>
- <var_make>self.$(id) = $(id)</var_make>
- <make>$value</make>
- <param>
- <name>Label</name>
- <key>label</key>
- <value></value>
- <type>string</type>
- <hide>#if $label() then 'none' else 'part'#</hide>
- </param>
- <param>
- <name>Value</name>
- <key>value</key>
- <value>0</value>
- <type>$type.type</type>
- </param>
- <param>
- <name>Type</name>
- <key>type</key>
- <value></value>
- <type>enum</type>
- <hide>#if $type() then 'none' else 'part'#</hide>
- <option>
- <name>None</name>
- <key></key>
- <opt>type:raw</opt>
- </option>
- <option>
- <name>Complex</name>
- <key>complex</key>
- <opt>type:complex</opt>
- </option>
- <option>
- <name>Float</name>
- <key>eng_float</key>
- <opt>type:real</opt>
- </option>
- <option>
- <name>Int</name>
- <key>intx</key>
- <opt>type:int</opt>
- </option>
- <option>
- <name>Long</name>
- <key>long</key>
- <opt>type:int</opt>
- </option>
- <option>
- <name>String</name>
- <key>str</key>
- <opt>type:string</opt>
- </option>
- <!-- Do not forget to add option value type handler import into
- grc/python/flow_graph.tmpl for each new type. -->
- <!-- not supported yet in tmpl
- <option>
- <name>Boolean</name>
- <key>bool</key>
- <opt>type:bool</opt>
- </option>
- -->
- </param>
- <param>
- <name>Short ID</name>
- <key>short_id</key>
- <value></value>
- <type>string</type>
- <hide>#if not $type()
-all#slurp
-#elif $short_id()
-none#slurp
-#else
-part#slurp
-#end if</hide>
- </param>
- <param>
- <name>Show</name>
- <key>hide</key>
- <value></value>
- <type>enum</type>
- <hide>part</hide>
- <option>
- <name>Always</name>
- <key>none</key> <!--## Do not hide the parameter value-->
- </option>
- <option>
- <name>Only in Properties</name>
- <key>part</key> <!--## Partially hide the parameter value-->
- </option>
- </param>
- <check>len($short_id) in (0, 1)</check>
- <check>$short_id == '' or $(short_id).isalpha()</check>
- <doc>
-This block represents a parameter to the flow graph. \
-A parameter can be used to pass command line arguments into a top block. \
-Or, parameters can pass arguments into an instantiated hierarchical block.
-
-The paramater value cannot depend on any variables.
-
-Leave the label blank to use the parameter id as the label.
-
-When type is not None, this parameter also becomes a command line option of the form:
-
--[short_id] --[id] [value]
-
-The Short ID field may be left blank.
-
-To disable showing the parameter on the hierarchical block in GRC, use Only in Properties option in the Show field.
- </doc>
-</block>
diff --git a/grc/blocks/stream.domain.yml b/grc/blocks/stream.domain.yml
new file mode 100644
index 0000000000..a4d786f8b4
--- /dev/null
+++ b/grc/blocks/stream.domain.yml
@@ -0,0 +1,10 @@
+id: stream
+label: Stream
+color: "#000000"
+
+multiple_connections_per_input: false
+multiple_connections_per_output: true
+
+templates:
+- type: [stream, stream]
+ connect: self.connect(${ make_port_sig(source) }, ${ make_port_sig(sink) })
diff --git a/grc/blocks/variable.block.yml b/grc/blocks/variable.block.yml
new file mode 100644
index 0000000000..fa62dabe87
--- /dev/null
+++ b/grc/blocks/variable.block.yml
@@ -0,0 +1,19 @@
+id: variable
+label: Variable
+
+parameters:
+- id: value
+ label: Value
+ dtype: raw
+ default: '0'
+value: ${ value }
+
+templates:
+ var_make: self.${id} = ${id} = ${value}
+ callbacks:
+ - self.set_${id}(${value})
+
+documentation: |-
+ This block maps a value to a unique variable. This variable block has no graphical representation.
+
+file_format: 1
diff --git a/grc/blocks/variable.xml b/grc/blocks/variable.xml
deleted file mode 100644
index afee0f5d4a..0000000000
--- a/grc/blocks/variable.xml
+++ /dev/null
@@ -1,23 +0,0 @@
-<?xml version="1.0"?>
-<!--
-###################################################
-##Variable block: a grc variable with key, value
-###################################################
- -->
-<block>
- <name>Variable</name>
- <key>variable</key>
- <var_make>self.$(id) = $(id) = $value</var_make>
- <make></make>
- <callback>self.set_$(id)($value)</callback>
- <param>
- <name>Value</name>
- <key>value</key>
- <value>0</value>
- <type>raw</type>
- </param>
- <doc>
-This block maps a value to a unique variable. \
-This variable block has no graphical representation.
- </doc>
-</block>
diff --git a/grc/blocks/variable_config.block.yml b/grc/blocks/variable_config.block.yml
new file mode 100644
index 0000000000..bb64ea2a8f
--- /dev/null
+++ b/grc/blocks/variable_config.block.yml
@@ -0,0 +1,58 @@
+id: variable_config
+label: Variable Config
+
+parameters:
+- id: value
+ label: Default Value
+ dtype: ${ type }
+ default: '0'
+- id: type
+ label: Type
+ dtype: enum
+ default: real
+ options: [real, int, bool, string]
+ option_labels: [Float, Int, Bool, String]
+ option_attributes:
+ get: [getfloat, getint, getboolean, get]
+- id: config_file
+ label: Config File
+ dtype: file_open
+ default: default
+- id: section
+ label: Section
+ dtype: string
+ default: main
+- id: option
+ label: Option
+ dtype: string
+ default: key
+- id: writeback
+ label: WriteBack
+ dtype: raw
+ default: None
+value: ${ value }
+
+templates:
+ imports: import ConfigParser
+ var_make: 'self._${id}_config = ConfigParser.ConfigParser()
+
+ self._${id}_config.read(${config_file})
+
+ try: ${id} = self._${id}_config.${type.get}(${section}, ${option})
+
+ except: ${id} = ${value}
+
+ self.${id} = ${id}'
+ callbacks:
+ - self.set_${id}(${value})
+ - "self._${id}_config = ConfigParser.ConfigParser()\nself._${id}_config.read(${config_file})\n\
+ if not self._${id}_config.has_section(${section}):\n\tself._${id}_config.add_section(${section})\n\
+ self._${id}_config.set(${section}, ${option}, str(${writeback}))\nself._${id}_config.write(open(${config_file},\
+ \ 'w'))"
+
+documentation: |-
+ This block represents a variable that can be read from a config file.
+
+ To save the value back into the config file: enter the name of another variable into the writeback param. When the other variable is changed at runtime, the config file will be re-written.
+
+file_format: 1
diff --git a/grc/blocks/variable_config.xml b/grc/blocks/variable_config.xml
deleted file mode 100644
index 11bff9edc2..0000000000
--- a/grc/blocks/variable_config.xml
+++ /dev/null
@@ -1,88 +0,0 @@
-<?xml version="1.0"?>
-<!--
-###################################################
-##Variable Config block:
-## a variable that reads and writes to a config file
-###################################################
- -->
-<block>
- <name>Variable Config</name>
- <key>variable_config</key>
- <import>import ConfigParser</import>
- <var_make>self._$(id)_config = ConfigParser.ConfigParser()
-self._$(id)_config.read($config_file)
-try: $(id) = self._$(id)_config.$(type.get)($section, $option)
-except: $(id) = $value
-self.$(id) = $(id)</var_make>
- <make></make>
- <callback>self.set_$(id)($value)</callback>
- <callback>self._$(id)_config = ConfigParser.ConfigParser()
-self._$(id)_config.read($config_file)
-if not self._$(id)_config.has_section($section):
- self._$(id)_config.add_section($section)
-self._$(id)_config.set($section, $option, str($writeback))
-self._$(id)_config.write(open($config_file, 'w'))</callback>
- <param>
- <name>Default Value</name>
- <key>value</key>
- <value>0</value>
- <type>$type</type>
- </param>
- <param>
- <name>Type</name>
- <key>type</key>
- <value>real</value>
- <type>enum</type>
- <option>
- <name>Float</name>
- <key>real</key>
- <opt>get:getfloat</opt>
- </option>
- <option>
- <name>Int</name>
- <key>int</key>
- <opt>get:getint</opt>
- </option>
- <option>
- <name>Bool</name>
- <key>bool</key>
- <opt>get:getboolean</opt>
- </option>
- <option>
- <name>String</name>
- <key>string</key>
- <opt>get:get</opt>
- </option>
- </param>
- <param>
- <name>Config File</name>
- <key>config_file</key>
- <value>default</value>
- <type>file_open</type>
- </param>
- <param>
- <name>Section</name>
- <key>section</key>
- <value>main</value>
- <type>string</type>
- </param>
- <param>
- <name>Option</name>
- <key>option</key>
- <value>key</value>
- <type>string</type>
- </param>
- <param>
- <name>WriteBack</name>
- <key>writeback</key>
- <value>None</value>
- <type>raw</type>
- </param>
- <doc>
-This block represents a variable that can be read from a config file.
-
-To save the value back into the config file: \
-enter the name of another variable into the writeback param. \
-When the other variable is changed at runtime, the config file will be re-written.
- </doc>
-</block>
diff --git a/grc/blocks/variable_function_probe.block.yml b/grc/blocks/variable_function_probe.block.yml
new file mode 100644
index 0000000000..702ab5d60e
--- /dev/null
+++ b/grc/blocks/variable_function_probe.block.yml
@@ -0,0 +1,54 @@
+id: variable_function_probe
+label: Function Probe
+
+parameters:
+- id: block_id
+ label: Block ID
+ dtype: string
+ default: my_block_0
+- id: function_name
+ label: Function Name
+ dtype: string
+ default: get_number
+- id: function_args
+ label: Function Args
+ dtype: string
+ hide: ${ ('none' if function_args else 'part') }
+- id: poll_rate
+ label: Poll Rate (Hz)
+ dtype: real
+ default: '10'
+- id: value
+ label: Initial Value
+ dtype: raw
+ default: '0'
+ hide: part
+value: ${ value }
+
+templates:
+ imports: |-
+ import time
+ import threading
+ var_make: self.${id} = ${id} = ${value}
+ make: "\ndef _${id}_probe():\n while True:\n <% obj = 'self' + ('.'\
+ \ + block_id if block_id else '') %>\n val = ${obj}.${function_name}(${function_args})\n\
+ \ try:\n self.set_${id}(val)\n except AttributeError:\n\
+ \ pass\n time.sleep(1.0 / (${poll_rate}))\n_${id}_thread\
+ \ = threading.Thread(target=_${id}_probe)\n_${id}_thread.daemon = True\n_${id}_thread.start()\n\
+ \ "
+ callbacks:
+ - self.set_${id}(${value})
+
+documentation: |-
+ Periodically probe a function and set its value to this variable.
+
+ Set the values for block ID, function name, and function args appropriately: Block ID should be the ID of another block in this flow graph. An empty Block ID references the flow graph itself. Function name should be the name of a class method on that block. Function args are the parameters passed into that function. For a function with no arguments, leave function args blank. When passing a string for the function arguments, quote the string literal: '"arg"'.
+
+ The values will used literally, and generated into the following form:
+ self.block_id.function_name(function_args)
+ or, if the Block ID is empty,
+ self.function_name(function_args)
+
+ To poll a stream for a level, use this with the probe signal block.
+
+file_format: 1
diff --git a/grc/blocks/variable_function_probe.xml b/grc/blocks/variable_function_probe.xml
deleted file mode 100644
index 47c11b29fe..0000000000
--- a/grc/blocks/variable_function_probe.xml
+++ /dev/null
@@ -1,78 +0,0 @@
-<?xml version="1.0"?>
-<!--
-###################################################
-##Variable function probe
-###################################################
- -->
-<block>
- <name>Function Probe</name>
- <key>variable_function_probe</key>
- <import>import time</import>
- <import>import threading</import>
- <var_make>self.$(id) = $(id) = $value</var_make>
- <make>
-def _$(id)_probe():
- while True:
- #set $obj = 'self' + ('.' + $block_id() if $block_id() else '')
- val = $(obj).$(function_name())($(function_args()))
- try:
- self.set_$(id)(val)
- except AttributeError:
- pass
- time.sleep(1.0 / ($poll_rate))
-_$(id)_thread = threading.Thread(target=_$(id)_probe)
-_$(id)_thread.daemon = True
-_$(id)_thread.start()
- </make>
- <callback>self.set_$(id)($value)</callback>
- <param>
- <name>Block ID</name>
- <key>block_id</key>
- <value>my_block_0</value>
- <type>string</type>
- </param>
- <param>
- <name>Function Name</name>
- <key>function_name</key>
- <value>get_number</value>
- <type>string</type>
- </param>
- <param>
- <name>Function Args</name>
- <key>function_args</key>
- <value></value>
- <type>string</type>
- <hide>#if $function_args() then 'none' else 'part'#</hide>
- </param>
- <param>
- <name>Poll Rate (Hz)</name>
- <key>poll_rate</key>
- <value>10</value>
- <type>real</type>
- </param>
- <param>
- <name>Initial Value</name>
- <key>value</key>
- <value>0</value>
- <type>raw</type>
- <hide>part</hide>
- </param>
- <doc>
-Periodically probe a function and set its value to this variable.
-
-Set the values for block ID, function name, and function args appropriately: \
-Block ID should be the ID of another block in this flow graph. \
-An empty Block ID references the flow graph itself. \
-Function name should be the name of a class method on that block. \
-Function args are the parameters passed into that function. \
-For a function with no arguments, leave function args blank. \
-When passing a string for the function arguments, quote the string literal: '"arg"'.
-
-The values will used literally, and generated into the following form:
- self.block_id.function_name(function_args)
-or, if the Block ID is empty,
- self.function_name(function_args)
-
-To poll a stream for a level, use this with the probe signal block.
- </doc>
-</block>
diff --git a/grc/blocks/variable_struct.block.yml.py b/grc/blocks/variable_struct.block.yml.py
new file mode 100644
index 0000000000..19b29982e7
--- /dev/null
+++ b/grc/blocks/variable_struct.block.yml.py
@@ -0,0 +1,105 @@
+#!/usr/bin/env python
+
+MAX_NUM_FIELDS = 20
+
+HEADER = """\
+id: variable_struct
+label: Struct Variable
+
+parameters:
+"""
+
+TEMPLATES = """\
+
+templates:
+ imports: "def struct(data): return type('Struct', (object,), data)()"
+ var_make: |-
+ self.${{id}} = ${{id}} = struct({{
+ % for i in range({0}):
+ <%
+ field = getVar('field' + str(i))
+ %>
+ % if len(str(field)) > 2:
+ ${{field}}: getVar('value' + str(i)),
+ % endif
+ % endfor
+ }})
+ var_value: |-
+ struct({{
+ % for i in range({0}):
+ <%
+ field = getVar('field' + str(i))
+ %>
+ % if len(str(field)) > 2:
+ ${{field}}: getVar('field' + str(i)),
+ % endif
+ % endfor
+ }})
+"""
+
+FIELD0 = """\
+- id: field0
+ label: Field 0
+ category: Fields
+ dtype: string
+ default: ${field0}
+ hide: part
+"""
+
+FIELDS = """\
+- id: field{0}
+ label: Field {0}
+ category: Fields
+ dtype: string
+ hide: part
+"""
+
+VALUES = """\
+- id: value{0}
+ label: ${{field{0}}}
+ dtype: raw
+ default: '0'
+ hide: ${{ 'none' if field{0} else 'all' }}
+"""
+
+ASSERTS = """\
+- ${{ (str(field{0}) or "a")[0].isalpha() }}
+- ${{ (str(field{0}) or "a").isalnum() }}
+"""
+
+FOOTER = """\
+
+documentation: |-
+ This is a simple struct/record like variable.
+
+ Attribute/field names can be specified in the tab 'Fields'.
+ For each non-empty field a parameter with type raw is shown.
+ Value access via the dot operator, e.g. "variable_struct_0.field0"
+
+file_format: 1
+"""
+
+
+def make_yml(num_fields):
+ return ''.join((
+ HEADER.format(num_fields),
+ FIELD0, ''.join(FIELDS.format(i) for i in range(1, num_fields)),
+ ''.join(VALUES.format(i) for i in range(num_fields)),
+ 'value: ${value}\n\nasserts:\n',
+ ''.join(ASSERTS.format(i) for i in range(num_fields)),
+ ''.join(TEMPLATES.format(num_fields)),
+ FOOTER
+ ))
+
+
+if __name__ == '__main__':
+ import sys
+ try:
+ filename = sys.argv[1]
+ except IndexError:
+ filename = __file__[:-3]
+
+ data = make_yml(MAX_NUM_FIELDS)
+
+ with open(filename, 'wb') as fp:
+ fp.write(data.encode())
diff --git a/grc/blocks/variable_struct.xml.py b/grc/blocks/variable_struct.xml.py
deleted file mode 100644
index de4411e975..0000000000
--- a/grc/blocks/variable_struct.xml.py
+++ /dev/null
@@ -1,97 +0,0 @@
-#!/usr/bin/env python
-
-MAX_NUM_FIELDS = 20
-
-HEADER = """\
-<block>
- <name>Struct Variable</name>
- <key>variable_struct</key>
- <import>def struct(data): return type('Struct', (object,), data)()</import>
- <var_make>self.$id = $id = struct({{#slurp
-#for $i in range({0}):
-#set $field = $getVar('field' + str(i))
-#if len(str($field)) > 2
-$field: $getVar('value' + str(i)), #slurp
-#end if
-#end for
-}})</var_make>
- <var_value>struct({{#slurp
-#for $i in range({0}):
-#set $field = $getVar('field' + str(i))
-#if len(str($field)) > 2
-$field: $getVar('value' + str(i)), #slurp
-#end if
-#end for
-}})</var_value>
- <make></make>
-"""
-
-FIELD0 = """\
- <param>
- <name>Field 0</name>
- <key>field0</key>
- <value>field0</value>
- <type>string</type>
- <hide>part</hide>
- <tab>Fields</tab>
- </param>
-"""
-
-FIELDS = """\
- <param>
- <name>Field {0}</name>
- <key>field{0}</key>
- <value></value>
- <type>string</type>
- <hide>part</hide>
- <tab>Fields</tab>
- </param>
-"""
-
-VALUES = """\
- <param>
- <name>$field{0}()</name>
- <key>value{0}</key>
- <value>0</value>
- <type>raw</type>
- <hide>#if $field{0}() then 'none' else 'all'#</hide>
- </param>
-"""
-
-CHECKS = """\
- <check>($str($field{0}) or "a")[0].isalpha()</check>
- <check>($str($field{0}) or "a").isalnum()</check>
-"""
-
-FOOTER = """\
- <doc>This is a simple struct/record like variable.
-
-Attribute/field names can be specified in the tab 'Fields'.
-For each non-empty field a parameter with type raw is shown.
-Value access via the dot operator, e.g. "variable_struct_0.field0"
- </doc>
-</block>
-"""
-
-
-def make_xml(num_fields):
- return ''.join((
- HEADER.format(num_fields),
- FIELD0, ''.join(FIELDS.format(i) for i in range(1, num_fields)),
- ''.join(VALUES.format(i) for i in range(num_fields)),
- ''.join(CHECKS.format(i) for i in range(num_fields)),
- FOOTER
- ))
-
-
-if __name__ == '__main__':
- import sys
- try:
- filename = sys.argv[1]
- except IndexError:
- filename = __file__[:-3]
-
- data = make_xml(MAX_NUM_FIELDS)
-
- with open(filename, 'w') as fp:
- fp.write(data.encode())
diff --git a/grc/blocks/virtual_sink.xml b/grc/blocks/virtual_sink.xml
deleted file mode 100644
index 35fb27e67c..0000000000
--- a/grc/blocks/virtual_sink.xml
+++ /dev/null
@@ -1,21 +0,0 @@
-<?xml version="1.0"?>
-<!--
-###################################################
-##Virtual Sink
-###################################################
- -->
-<block>
- <name>Virtual Sink</name>
- <key>virtual_sink</key>
- <make></make>
- <param>
- <name>Stream ID</name>
- <key>stream_id</key>
- <value></value>
- <type>stream_id</type>
- </param>
- <sink>
- <name>in</name>
- <type></type>
- </sink>
-</block>
diff --git a/grc/blocks/virtual_source.xml b/grc/blocks/virtual_source.xml
deleted file mode 100644
index e0c7754492..0000000000
--- a/grc/blocks/virtual_source.xml
+++ /dev/null
@@ -1,21 +0,0 @@
-<?xml version="1.0"?>
-<!--
-###################################################
-##Virtual Source
-###################################################
- -->
-<block>
- <name>Virtual Source</name>
- <key>virtual_source</key>
- <make></make>
- <param>
- <name>Stream ID</name>
- <key>stream_id</key>
- <value></value>
- <type>stream_id</type>
- </param>
- <source>
- <name>out</name>
- <type></type>
- </source>
-</block>
diff --git a/grc/compiler.py b/grc/compiler.py
index 0cda0d946d..a5f6c3edcd 100755
--- a/grc/compiler.py
+++ b/grc/compiler.py
@@ -26,7 +26,7 @@ import subprocess
from gnuradio import gr
from .core import Messages
-from .core.Platform import Platform
+from .core.platform import Platform
def argument_parser():
@@ -49,10 +49,12 @@ def main(args=None):
platform = Platform(
name='GNU Radio Companion Compiler',
- prefs_file=gr.prefs(),
+ prefs=gr.prefs(),
version=gr.version(),
version_parts=(gr.major_version(), gr.api_version(), gr.minor_version())
)
+ platform.build_library()
+
out_dir = args.output if not args.user_lib_dir else platform.config.hier_block_lib_dir
if os.path.exists(out_dir):
pass # all is well
diff --git a/grc/converter/__init__.py b/grc/converter/__init__.py
new file mode 100644
index 0000000000..224f2e9afc
--- /dev/null
+++ b/grc/converter/__init__.py
@@ -0,0 +1,20 @@
+# Copyright 2016 Free Software Foundation, Inc.
+# This file is part of GNU Radio
+#
+# GNU Radio Companion is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by the
+# Free Software Foundation; either version 2 of the License, or (at your
+# option) any later version.
+#
+# GNU Radio Companion is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+# details.
+#
+# You should have received a copy of the GNU General Public License along with
+# this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
+
+from __future__ import absolute_import
+
+from .main import Converter
diff --git a/grc/converter/__main__.py b/grc/converter/__main__.py
new file mode 100644
index 0000000000..6efc2d7c59
--- /dev/null
+++ b/grc/converter/__main__.py
@@ -0,0 +1,21 @@
+# Copyright 2016 Free Software Foundation, Inc.
+# This file is part of GNU Radio
+#
+# GNU Radio Companion is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by the
+# Free Software Foundation; either version 2 of the License, or (at your
+# option) any later version.
+#
+# GNU Radio Companion is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+# details.
+#
+# You should have received a copy of the GNU General Public License along with
+# this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
+
+from __future__ import absolute_import
+
+# TODO: implement cli
+
diff --git a/grc/core/block.dtd b/grc/converter/block.dtd
index 145f4d8610..145f4d8610 100644
--- a/grc/core/block.dtd
+++ b/grc/converter/block.dtd
diff --git a/grc/converter/block.py b/grc/converter/block.py
new file mode 100644
index 0000000000..0e362d97c0
--- /dev/null
+++ b/grc/converter/block.py
@@ -0,0 +1,219 @@
+# Copyright 2016 Free Software Foundation, Inc.
+# This file is part of GNU Radio
+#
+# GNU Radio Companion is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by the
+# Free Software Foundation; either version 2 of the License, or (at your
+# option) any later version.
+#
+# GNU Radio Companion is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+# details.
+#
+# You should have received a copy of the GNU General Public License along with
+# this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
+"""
+Converter for legacy block definitions in XML format
+
+- Cheetah expressions that can not be converted are passed to Cheetah for now
+- Instead of generating a Block subclass directly a string representation is
+ used and evaluated. This is slower / lamer but allows us to show the user
+ how a converted definition would look like
+"""
+
+from __future__ import absolute_import, division, print_function
+
+from collections import OrderedDict, defaultdict
+from itertools import chain
+
+from ..core.io import yaml
+from . import cheetah_converter, xml
+
+current_file_format = 1
+reserved_block_keys = ('import', ) # todo: add more keys
+
+
+def from_xml(filename):
+ """Load block description from xml file"""
+ element, version_info = xml.load(filename, 'block.dtd')
+
+ try:
+ data = convert_block_xml(element)
+ except NameError:
+ raise ValueError('Conversion failed', filename)
+
+ return data
+
+
+def dump(data, stream):
+ out = yaml.dump(data)
+
+ replace = [
+ ('parameters:', '\nparameters:'),
+ ('inputs:', '\ninputs:'),
+ ('outputs:', '\noutputs:'),
+ ('templates:', '\ntemplates:'),
+ ('documentation:', '\ndocumentation:'),
+ ('file_format:', '\nfile_format:'),
+ ]
+ for r in replace:
+ out = out.replace(*r)
+ prefix = '# auto-generated by grc.converter\n\n'
+ stream.write(prefix + out)
+
+
+no_value = object()
+dummy = cheetah_converter.DummyConverter()
+
+
+def convert_block_xml(node):
+ converter = cheetah_converter.Converter(names={
+ param_node.findtext('key'): {
+ opt_node.text.split(':')[0]
+ for opt_node in next(param_node.iterfind('option'), param_node).iterfind('opt')
+ } for param_node in node.iterfind('param')
+ })
+
+ block_id = node.findtext('key')
+ if block_id in reserved_block_keys:
+ block_id += '_'
+
+ data = OrderedDict()
+ data['id'] = block_id
+ data['label'] = node.findtext('name') or no_value
+ data['category'] = node.findtext('category') or no_value
+ data['flags'] = node.findtext('flags') or no_value
+
+ data['parameters'] = [convert_param_xml(param_node, converter.to_python_dec)
+ for param_node in node.iterfind('param')] or no_value
+ # data['params'] = {p.pop('key'): p for p in data['params']}
+
+ data['inputs'] = [convert_port_xml(port_node, converter.to_python_dec)
+ for port_node in node.iterfind('sink')] or no_value
+
+ data['outputs'] = [convert_port_xml(port_node, converter.to_python_dec)
+ for port_node in node.iterfind('source')] or no_value
+ data['value'] = (
+ converter.to_python_dec(node.findtext('var_value')) or
+ ('${ value }' if block_id.startswith('variable') else no_value)
+ )
+
+ data['asserts'] = [converter.to_python_dec(check_node.text)
+ for check_node in node.iterfind('check')] or no_value
+
+ data['templates'] = convert_templates(node, converter.to_mako, block_id) or no_value
+
+ docs = node.findtext('doc')
+ if docs:
+ docs = docs.strip().replace('\\\n', '')
+ data['documentation'] = yaml.MultiLineString(docs)
+
+ data['file_format'] = current_file_format
+
+ data = OrderedDict((key, value) for key, value in data.items() if value is not no_value)
+ auto_hide_params_for_item_sizes(data)
+
+ return data
+
+
+def auto_hide_params_for_item_sizes(data):
+ item_size_templates = []
+ vlen_templates = []
+ for port in chain(*[data.get(direction, []) for direction in ['inputs', 'outputs']]):
+ for key in ['dtype', 'multiplicity']:
+ item_size_templates.append(str(port.get(key, '')))
+ vlen_templates.append(str(port.get('vlen', '')))
+ item_size_templates = ' '.join(value for value in item_size_templates if '${' in value)
+ vlen_templates = ' '.join(value for value in vlen_templates if '${' in value)
+
+ for param in data.get('parameters', []):
+ if param['id'] in item_size_templates:
+ param.setdefault('hide', 'part')
+ if param['id'] in vlen_templates:
+ param.setdefault('hide', "${ 'part' if vlen == 1 else 'none' }")
+
+
+def convert_templates(node, convert, block_id=''):
+ templates = OrderedDict()
+
+ imports = '\n'.join(convert(import_node.text)
+ for import_node in node.iterfind('import'))
+ if '\n' in imports:
+ imports = yaml.MultiLineString(imports)
+ templates['imports'] = imports or no_value
+
+ templates['var_make'] = convert(node.findtext('var_make') or '') or no_value
+
+ make = convert(node.findtext('make') or '')
+ if make:
+ check_mako_template(block_id, make)
+ if '\n' in make:
+ make = yaml.MultiLineString(make)
+ templates['make'] = make or no_value
+
+ templates['callbacks'] = [
+ convert(cb_node.text) for cb_node in node.iterfind('callback')
+ ] or no_value
+
+ return OrderedDict((key, value) for key, value in templates.items() if value is not no_value)
+
+
+def convert_param_xml(node, convert):
+ param = OrderedDict()
+ param['id'] = node.findtext('key').strip()
+ param['label'] = node.findtext('name').strip()
+ param['category'] = node.findtext('tab') or no_value
+
+ param['dtype'] = convert(node.findtext('type') or '')
+ param['default'] = node.findtext('value') or no_value
+
+ options = yaml.ListFlowing(on.findtext('key') for on in node.iterfind('option'))
+ option_labels = yaml.ListFlowing(on.findtext('name') for on in node.iterfind('option'))
+ param['options'] = options or no_value
+ if not all(str(o).title() == l for o, l in zip(options, option_labels)):
+ param['option_labels'] = option_labels
+
+ attributes = defaultdict(yaml.ListFlowing)
+ for option_n in node.iterfind('option'):
+ for opt_n in option_n.iterfind('opt'):
+ key, value = opt_n.text.split(':', 2)
+ attributes[key].append(value)
+ param['option_attributes'] = dict(attributes) or no_value
+
+ param['hide'] = convert(node.findtext('hide')) or no_value
+
+ return OrderedDict((key, value) for key, value in param.items() if value is not no_value)
+
+
+def convert_port_xml(node, convert):
+ port = OrderedDict()
+ label = node.findtext('name')
+ # default values:
+ port['label'] = label if label not in ('in', 'out') else no_value
+
+ dtype = convert(node.findtext('type'))
+ # TODO: detect dyn message ports
+ port['domain'] = domain = 'message' if dtype == 'message' else 'stream'
+ if domain == 'message':
+ port['id'], port['label'] = label, no_value
+ else:
+ port['dtype'] = dtype
+ vlen = node.findtext('vlen')
+ port['vlen'] = int(vlen) if vlen and vlen.isdigit() else convert(vlen) or no_value
+
+ port['multiplicity'] = convert(node.findtext('nports')) or no_value
+ port['optional'] = bool(node.findtext('optional')) or no_value
+ port['hide'] = convert(node.findtext('hide')) or no_value
+
+ return OrderedDict((key, value) for key, value in port.items() if value is not no_value)
+
+
+def check_mako_template(block_id, expr):
+ import sys
+ from mako.template import Template
+ try:
+ Template(expr)
+ except Exception as error:
+ print(block_id, expr, type(error), error, '', sep='\n', file=sys.stderr)
diff --git a/grc/core/block_tree.dtd b/grc/converter/block_tree.dtd
index 9e23576477..9e23576477 100644
--- a/grc/core/block_tree.dtd
+++ b/grc/converter/block_tree.dtd
diff --git a/grc/converter/block_tree.py b/grc/converter/block_tree.py
new file mode 100644
index 0000000000..dee9adba49
--- /dev/null
+++ b/grc/converter/block_tree.py
@@ -0,0 +1,56 @@
+# Copyright 2016 Free Software Foundation, Inc.
+# This file is part of GNU Radio
+#
+# GNU Radio Companion is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by the
+# Free Software Foundation; either version 2 of the License, or (at your
+# option) any later version.
+#
+# GNU Radio Companion is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+# details.
+#
+# You should have received a copy of the GNU General Public License along with
+# this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
+"""
+Converter for legacy block tree definitions in XML format
+"""
+
+from __future__ import absolute_import, print_function
+
+from ..core.io import yaml
+from . import xml
+
+
+def from_xml(filename):
+ """Load block tree description from xml file"""
+ element, version_info = xml.load(filename, 'block_tree.dtd')
+
+ try:
+ data = convert_category_node(element)
+ except NameError:
+ raise ValueError('Conversion failed', filename)
+
+ return data
+
+
+def dump(data, stream):
+ out = yaml.dump(data, indent=2)
+ prefix = '# auto-generated by grc.converter\n\n'
+ stream.write(prefix + out)
+
+
+def convert_category_node(node):
+ """convert nested <cat> tags to nested lists dicts"""
+ assert node.tag == 'cat'
+ name, elements = '', []
+ for child in node:
+ if child.tag == 'name':
+ name = child.text.strip()
+ elif child.tag == 'block':
+ elements.append(child.text.strip())
+ elif child.tag == 'cat':
+ elements.append(convert_category_node(child))
+ return {name: elements}
diff --git a/grc/converter/cheetah_converter.py b/grc/converter/cheetah_converter.py
new file mode 100644
index 0000000000..16fea32c99
--- /dev/null
+++ b/grc/converter/cheetah_converter.py
@@ -0,0 +1,277 @@
+# Copyright 2016 Free Software Foundation, Inc.
+# This file is part of GNU Radio
+#
+# GNU Radio Companion is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# GNU Radio Companion is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
+
+from __future__ import absolute_import, print_function
+
+import collections
+import re
+import string
+
+delims = {'(': ')', '[': ']', '{': '}', '': ', #\\*:'}
+identifier_start = '_' + string.ascii_letters + ''.join(delims.keys())
+string_delims = '"\''
+
+cheetah_substitution = re.compile(
+ r'^\$((?P<d1>\()|(?P<d2>\{)|(?P<d3>\[)|)'
+ r'(?P<arg>[_a-zA-Z][_a-zA-Z0-9]*(?:\.[_a-zA-Z][_a-zA-Z0-9]*)?)(?P<eval>\(\))?'
+ r'(?(d1)\)|(?(d2)\}|(?(d3)\]|)))$'
+)
+cheetah_inline_if = re.compile(r'#if (?P<cond>.*) then (?P<then>.*?) ?else (?P<else>.*?) ?(#|$)')
+
+
+class Python(object):
+ start = ''
+ end = ''
+ nested_start = ''
+ nested_end = ''
+ eval = ''
+ type = str # yaml_output.Eval
+
+
+class FormatString(Python):
+ start = '{'
+ end = '}'
+ nested_start = '{'
+ nested_end = '}'
+ eval = ':eval'
+ type = str
+
+
+class Mako(Python):
+ start = '${'
+ end = '}'
+ nested_start = ''
+ nested_end = ''
+ type = str
+
+
+class Converter(object):
+
+ def __init__(self, names):
+ self.stats = collections.defaultdict(int)
+ self.names = set(names)
+ self.extended = set(self._iter_identifiers(names))
+
+ @staticmethod
+ def _iter_identifiers(names):
+ if not isinstance(names, dict):
+ names = {name: {} for name in names}
+ for key, sub_keys in names.items():
+ yield key
+ for sub_key in sub_keys:
+ yield '{}.{}'.format(key, sub_key)
+
+ def to_python(self, expr):
+ return self.convert(expr=expr, spec=Python)
+
+ def to_python_dec(self, expr):
+ converted = self.convert(expr=expr, spec=Python)
+ if converted and converted != expr:
+ converted = '${ ' + converted.strip() + ' }'
+ return converted
+
+ def to_format_string(self, expr):
+ return self.convert(expr=expr, spec=FormatString)
+
+ def to_mako(self, expr):
+ return self.convert(expr=expr, spec=Mako)
+
+ def convert(self, expr, spec=Python):
+ if not expr:
+ return ''
+
+ elif '$' not in expr:
+ return expr
+
+ try:
+ return self.convert_simple(expr, spec)
+ except ValueError:
+ pass
+
+ try:
+ if '#if' in expr and '\n' not in expr:
+ expr = self.convert_inline_conditional(expr, spec)
+ return self.convert_hard(expr, spec)
+ except ValueError:
+ return 'Cheetah! ' + expr
+
+ def convert_simple(self, expr, spec=Python):
+ match = cheetah_substitution.match(expr)
+ if not match:
+ raise ValueError('Not a simple substitution: ' + expr)
+
+ identifier = match.group('arg')
+ if identifier not in self.extended:
+ raise NameError('Unknown substitution {!r}'.format(identifier))
+ if match.group('eval'):
+ identifier += spec.eval
+
+ out = spec.start + identifier + spec.end
+ if '$' in out or '#' in out:
+ raise ValueError('Failed to convert: ' + expr)
+
+ self.stats['simple'] += 1
+ return spec.type(out)
+
+ def convert_hard(self, expr, spec=Python):
+ lines = '\n'.join(self.convert_hard_line(line, spec) for line in expr.split('\n'))
+ if spec == Mako:
+ # no line-continuation before a mako control structure
+ lines = re.sub(r'\\\n(\s*%)', r'\n\1', lines)
+ return lines
+
+ def convert_hard_line(self, expr, spec=Python):
+ if spec == Mako:
+ if '#set' in expr:
+ ws, set_, statement = expr.partition('#set ')
+ return ws + '<% ' + self.to_python(statement) + ' %>'
+
+ if '#if' in expr:
+ ws, if_, condition = expr.partition('#if ')
+ return ws + '% if ' + self.to_python(condition) + ':'
+ if '#else if' in expr:
+ ws, elif_, condition = expr.partition('#else if ')
+ return ws + '% elif ' + self.to_python(condition) + ':'
+ if '#else' in expr:
+ return expr.replace('#else', '% else:')
+ if '#end if' in expr:
+ return expr.replace('#end if', '% endif')
+
+ if '#slurp' in expr:
+ expr = expr.split('#slurp', 1)[0] + '\\'
+ return self.convert_hard_replace(expr, spec)
+
+ def convert_hard_replace(self, expr, spec=Python):
+ counts = collections.Counter()
+
+ def all_delims_closed():
+ for opener_, closer_ in delims.items():
+ if counts[opener_] != counts[closer_]:
+ return False
+ return True
+
+ def extra_close():
+ for opener_, closer_ in delims.items():
+ if counts[opener_] < counts[closer_]:
+ return True
+ return False
+
+ out = []
+ delim_to_find = False
+
+ pos = 0
+ char = ''
+ in_string = None
+ while pos < len(expr):
+ prev, char = char, expr[pos]
+ counts.update(char)
+
+ if char in string_delims:
+ if not in_string:
+ in_string = char
+ elif char == in_string:
+ in_string = None
+ out.append(char)
+ pos += 1
+ continue
+ if in_string:
+ out.append(char)
+ pos += 1
+ continue
+
+ if char == '$':
+ pass # no output
+
+ elif prev == '$':
+ if char not in identifier_start: # not a substitution
+ out.append('$' + char) # now print the $ we skipped over
+
+ elif not delim_to_find: # start of a substitution
+ try:
+ delim_to_find = delims[char]
+ out.append(spec.start)
+ except KeyError:
+ if char in identifier_start:
+ delim_to_find = delims['']
+ out.append(spec.start)
+ out.append(char)
+
+ counts.clear()
+ counts.update(char)
+
+ else: # nested substitution: simply match known variable names
+ found = False
+ for known_identifier in self.names:
+ if expr[pos:].startswith(known_identifier):
+ found = True
+ break
+ if found:
+ out.append(spec.nested_start)
+ out.append(known_identifier)
+ out.append(spec.nested_end)
+ pos += len(known_identifier)
+ continue
+
+ elif delim_to_find and char in delim_to_find and all_delims_closed(): # end of substitution
+ out.append(spec.end)
+ if char in delims['']:
+ out.append(char)
+ delim_to_find = False
+
+ elif delim_to_find and char in ')]}' and extra_close(): # end of substitution
+ out.append(spec.end)
+ out.append(char)
+ delim_to_find = False
+
+ else:
+ out.append(char)
+
+ pos += 1
+
+ if delim_to_find == delims['']:
+ out.append(spec.end)
+
+ out = ''.join(out)
+ # fix: eval stuff
+ out = re.sub(r'(?P<arg>' + r'|'.join(self.extended) + r')\(\)', '\g<arg>', out)
+
+ self.stats['hard'] += 1
+ return spec.type(out)
+
+ def convert_inline_conditional(self, expr, spec=Python):
+ if spec == FormatString:
+ raise ValueError('No conditionals in format strings: ' + expr)
+ matcher = r'\g<then> if \g<cond> else \g<else>'
+ if spec == Python:
+ matcher = '(' + matcher + ')'
+ expr = cheetah_inline_if.sub(matcher, expr)
+ return spec.type(self.convert_hard(expr, spec))
+
+
+class DummyConverter(object):
+
+ def __init__(self, names={}):
+ pass
+
+ def to_python(self, expr):
+ return expr
+
+ def to_format_string(self, expr):
+ return expr
+
+ def to_mako(self, expr):
+ return expr
diff --git a/grc/converter/flow_graph.dtd b/grc/converter/flow_graph.dtd
new file mode 100644
index 0000000000..bdfe1dc059
--- /dev/null
+++ b/grc/converter/flow_graph.dtd
@@ -0,0 +1,38 @@
+<!--
+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
+-->
+<!--
+ flow_graph.dtd
+ Josh Blum
+ The document type definition for flow graph xml files.
+ -->
+<!ELEMENT flow_graph (timestamp?, block*, connection*)> <!-- optional timestamp -->
+<!ELEMENT timestamp (#PCDATA)>
+<!-- Block -->
+<!ELEMENT block (key, param*, bus_sink?, bus_source?)>
+<!ELEMENT param (key, value)>
+<!ELEMENT key (#PCDATA)>
+<!ELEMENT value (#PCDATA)>
+<!ELEMENT bus_sink (#PCDATA)>
+<!ELEMENT bus_source (#PCDATA)>
+<!-- Connection -->
+<!ELEMENT connection (source_block_id, sink_block_id, source_key, sink_key)>
+<!ELEMENT source_block_id (#PCDATA)>
+<!ELEMENT sink_block_id (#PCDATA)>
+<!ELEMENT source_key (#PCDATA)>
+<!ELEMENT sink_key (#PCDATA)>
diff --git a/grc/converter/flow_graph.py b/grc/converter/flow_graph.py
new file mode 100644
index 0000000000..d20c67703c
--- /dev/null
+++ b/grc/converter/flow_graph.py
@@ -0,0 +1,131 @@
+# Copyright 2017 Free Software Foundation, Inc.
+# This file is part of GNU Radio
+#
+# GNU Radio Companion is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# GNU Radio Companion is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
+
+from __future__ import absolute_import, division
+
+import ast
+from collections import OrderedDict
+
+from ..core.io import yaml
+from . import xml
+
+
+def from_xml(filename):
+ """Load flow graph from xml file"""
+ element, version_info = xml.load(filename, 'flow_graph.dtd')
+
+ data = convert_flow_graph_xml(element)
+ try:
+ file_format = int(version_info['format'])
+ except KeyError:
+ file_format = _guess_file_format_1(data)
+
+ data['metadata'] = {'file_format': file_format}
+
+ return data
+
+
+def dump(data, stream):
+ out = yaml.dump(data, indent=2)
+
+ replace = [
+ ('blocks:', '\nblocks:'),
+ ('connections:', '\nconnections:'),
+ ('metadata:', '\nmetadata:'),
+ ]
+ for r in replace:
+ out = out.replace(*r)
+ prefix = '# auto-generated by grc.converter\n\n'
+ stream.write(prefix + out)
+
+
+def convert_flow_graph_xml(node):
+ blocks = [
+ convert_block(block_data)
+ for block_data in node.findall('block')
+ ]
+
+ options = next(b for b in blocks if b['id'] == 'options')
+ blocks.remove(options)
+ options.pop('id')
+
+ connections = [
+ convert_connection(connection)
+ for connection in node.findall('connection')
+ ]
+
+ flow_graph = OrderedDict()
+ flow_graph['options'] = options
+ flow_graph['blocks'] = blocks
+ flow_graph['connections'] = connections
+ return flow_graph
+
+
+def convert_block(data):
+ block_id = data.findtext('key')
+
+ params = OrderedDict(sorted(
+ (param.findtext('key'), param.findtext('value'))
+ for param in data.findall('param')
+ ))
+ states = OrderedDict()
+ x, y = ast.literal_eval(params.pop('_coordinate', '(10, 10)'))
+ states['coordinate'] = yaml.ListFlowing([x, y])
+ states['rotation'] = int(params.pop('_rotation', '0'))
+ enabled = params.pop('_enabled', 'True')
+ states['state'] = (
+ 'enabled' if enabled in ('1', 'True') else
+ 'bypassed' if enabled == '2' else
+ 'disabled'
+ )
+
+ block = OrderedDict()
+ if block_id != 'options':
+ block['name'] = params.pop('id')
+ block['id'] = block_id
+ block['parameters'] = params
+ block['states'] = states
+
+ return block
+
+
+def convert_connection(data):
+ src_blk_id = data.findtext('source_block_id')
+ src_port_id = data.findtext('source_key')
+ snk_blk_id = data.findtext('sink_block_id')
+ snk_port_id = data.findtext('sink_key')
+
+ if src_port_id.isdigit():
+ src_port_id = 'out' + src_port_id
+ if snk_port_id.isdigit():
+ snk_port_id = 'in' + snk_port_id
+
+ return yaml.ListFlowing([src_blk_id, src_port_id, snk_blk_id, snk_port_id])
+
+
+def _guess_file_format_1(data):
+ """Try to guess the file format for flow-graph files without version tag"""
+
+ def has_numeric_port_ids(src_id, src_port_id, snk_id, snk_port_id):
+ return src_port_id.isdigit() and snk_port_id.is_digit()
+
+ try:
+ if any(not has_numeric_port_ids(*con) for con in data['connections']):
+ return 1
+ except:
+ pass
+ return 0
diff --git a/grc/converter/main.py b/grc/converter/main.py
new file mode 100644
index 0000000000..5a18471fc7
--- /dev/null
+++ b/grc/converter/main.py
@@ -0,0 +1,165 @@
+# Copyright 2016 Free Software Foundation, Inc.
+# This file is part of GNU Radio
+#
+# GNU Radio Companion is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by the
+# Free Software Foundation; either version 2 of the License, or (at your
+# option) any later version.
+#
+# GNU Radio Companion is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+# details.
+#
+# You should have received a copy of the GNU General Public License along with
+# this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
+
+from __future__ import absolute_import
+
+from codecs import open
+import json
+import logging
+import os
+
+import six
+
+from . import block_tree, block
+
+path = os.path
+logger = logging.getLogger(__name__)
+
+excludes = [
+ 'qtgui_',
+ '.grc_gnuradio/',
+ 'blks2',
+ 'wxgui',
+ 'epy_block.xml',
+ 'virtual_sink.xml',
+ 'virtual_source.xml',
+ 'dummy.xml',
+ 'variable_struct.xml', # todo: re-implement as class
+ 'digital_constellation', # todo: fix template
+]
+
+
+class Converter(object):
+
+ def __init__(self, search_path, output_dir='~/.cache/grc_gnuradio'):
+ self.search_path = search_path
+ self.output_dir = os.path.expanduser(output_dir)
+ logger.info("Saving converted files to {}".format(self.output_dir))
+
+ self._force = False
+
+ converter_module_path = path.dirname(__file__)
+ self._converter_mtime = max(path.getmtime(path.join(converter_module_path, module))
+ for module in os.listdir(converter_module_path)
+ if not module.endswith('flow_graph.py'))
+
+ self.cache_file = os.path.join(self.output_dir, '_cache.json')
+ self.cache = {}
+
+ def run(self, force=False):
+ self._force = force
+
+ try:
+ logger.debug("Loading block cache from: {}".format(self.cache_file))
+ with open(self.cache_file, encoding='utf-8') as cache_file:
+ self.cache = byteify(json.load(cache_file))
+ except (IOError, ValueError):
+ self.cache = {}
+ self._force = True
+ need_cache_write = False
+
+ if not path.isdir(self.output_dir):
+ os.makedirs(self.output_dir)
+ if self._force:
+ for name in os.listdir(self.output_dir):
+ os.remove(os.path.join(self.output_dir, name))
+
+ for xml_file in self.iter_files_in_block_path():
+ if xml_file.endswith("block_tree.xml"):
+ changed = self.load_category_tree_xml(xml_file)
+ elif xml_file.endswith('domain.xml'):
+ continue
+ else:
+ changed = self.load_block_xml(xml_file)
+
+ if changed:
+ need_cache_write = True
+
+ if need_cache_write:
+ logger.info('Saving %d entries to json cache', len(self.cache))
+ with open(self.cache_file, 'w', encoding='utf-8') as cache_file:
+ json.dump(self.cache, cache_file)
+
+ def load_block_xml(self, xml_file):
+ """Load block description from xml file"""
+ if any(part in xml_file for part in excludes):
+ return
+
+ block_id_from_xml = path.basename(xml_file)[:-4]
+ yml_file = path.join(self.output_dir, block_id_from_xml + '.block.yml')
+
+ if not self.needs_conversion(xml_file, yml_file):
+ return # yml file up-to-date
+
+ logger.info('Converting block %s', path.basename(xml_file))
+ data = block.from_xml(xml_file)
+ if block_id_from_xml != data['id']:
+ logger.warning('block_id and filename differ')
+ self.cache[yml_file] = data
+
+ with open(yml_file, 'w', encoding='utf-8') as yml_file:
+ block.dump(data, yml_file)
+ return True
+
+ def load_category_tree_xml(self, xml_file):
+ """Validate and parse category tree file and add it to list"""
+ module_name = path.basename(xml_file)[:-len('block_tree.xml')].rstrip('._-')
+ yml_file = path.join(self.output_dir, module_name + '.tree.yml')
+
+ if not self.needs_conversion(xml_file, yml_file):
+ return # yml file up-to-date
+
+ logger.info('Converting module %s', path.basename(xml_file))
+ data = block_tree.from_xml(xml_file)
+ self.cache[yml_file] = data
+
+ with open(yml_file, 'w', encoding='utf-8') as yml_file:
+ block_tree.dump(data, yml_file)
+ return True
+
+ def needs_conversion(self, source, destination):
+ """Check if source has already been converted and destination is up-to-date"""
+ if self._force or not path.exists(destination):
+ return True
+ xml_time = path.getmtime(source)
+ yml_time = path.getmtime(destination)
+
+ return yml_time < xml_time or yml_time < self._converter_mtime
+
+ def iter_files_in_block_path(self, suffix='.xml'):
+ """Iterator for block descriptions and category trees"""
+ for block_path in self.search_path:
+ if path.isfile(block_path):
+ yield block_path
+ elif path.isdir(block_path):
+ for root, _, files in os.walk(block_path, followlinks=True):
+ for name in files:
+ if name.endswith(suffix):
+ yield path.join(root, name)
+ else:
+ logger.warning('Invalid entry in search path: {}'.format(block_path))
+
+
+def byteify(data):
+ if isinstance(data, dict):
+ return {byteify(key): byteify(value) for key, value in six.iteritems(data)}
+ elif isinstance(data, list):
+ return [byteify(element) for element in data]
+ elif isinstance(data, six.text_type) and six.PY2:
+ return data.encode('utf-8')
+ else:
+ return data
diff --git a/grc/converter/xml.py b/grc/converter/xml.py
new file mode 100644
index 0000000000..2eda786c0f
--- /dev/null
+++ b/grc/converter/xml.py
@@ -0,0 +1,82 @@
+# Copyright 2017 Free Software Foundation, Inc.
+# This file is part of GNU Radio
+#
+# GNU Radio Companion is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# GNU Radio Companion is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
+
+from __future__ import absolute_import, division
+
+import re
+from os import path
+
+try:
+ # raise ImportError()
+ from lxml import etree
+ HAVE_LXML = True
+except ImportError:
+ import xml.etree.ElementTree as etree
+ HAVE_LXML = False
+
+
+_validator_cache = {None: lambda xml: True}
+
+
+def _get_validator(dtd=None):
+ validator = _validator_cache.get(dtd)
+ if not validator:
+ if not path.isabs(dtd):
+ dtd = path.join(path.dirname(__file__), dtd)
+ validator = _validator_cache[dtd] = etree.DTD(dtd).validate
+ return validator
+
+
+def load_lxml(filename, document_type_def=None):
+ """Load block description from xml file"""
+
+ try:
+ xml_tree = etree.parse(filename)
+ _get_validator(document_type_def)
+ element = xml_tree.getroot()
+ except etree.LxmlError:
+ raise ValueError("Failed to parse or validate {}".format(filename))
+
+ version_info = {}
+ for inst in xml_tree.xpath('/processing-instruction()'):
+ if inst.target == 'grc':
+ version_info.update(inst.attrib)
+
+ return element, version_info
+
+
+def load_stdlib(filename, document_type_def=None):
+ """Load block description from xml file"""
+
+ with open(filename, 'rb') as xml_file:
+ data = xml_file.read().decode('utf-8')
+
+ try:
+ element = etree.fromstring(data)
+ except etree.ParseError:
+ raise ValueError("Failed to parse {}".format(filename))
+
+ version_info = {}
+ for body in re.findall(r'<\?(.*?)\?>', data):
+ element = etree.fromstring('<' + body + '/>')
+ if element.tag == 'grc':
+ version_info.update(element.attrib)
+
+ return element, version_info
+
+
+load = load_lxml if HAVE_LXML else load_stdlib
diff --git a/grc/core/Block.py b/grc/core/Block.py
deleted file mode 100644
index fba9371963..0000000000
--- a/grc/core/Block.py
+++ /dev/null
@@ -1,852 +0,0 @@
-"""
-Copyright 2008-2015 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 collections
-import itertools
-
-from Cheetah.Template import Template
-
-from .utils import epy_block_io, odict
-from . Constants import (
- BLOCK_FLAG_NEED_QT_GUI,
- ADVANCED_PARAM_TAB, DEFAULT_PARAM_TAB,
- BLOCK_FLAG_THROTTLE, BLOCK_FLAG_DISABLE_BYPASS,
- BLOCK_FLAG_DEPRECATED,
- BLOCK_ENABLED, BLOCK_BYPASSED, BLOCK_DISABLED
-)
-from . Element import Element
-
-
-def _get_keys(lst):
- return [elem.get_key() for elem in lst]
-
-
-def _get_elem(lst, key):
- try:
- return lst[_get_keys(lst).index(key)]
- except ValueError:
- raise ValueError('Key "{}" not found in {}.'.format(key, _get_keys(lst)))
-
-
-class Block(Element):
-
- is_block = True
-
- def __init__(self, flow_graph, n):
- """
- Make a new block from nested data.
-
- Args:
- flow: graph the parent element
- n: the nested odict
-
- Returns:
- block a new block
- """
- # Grab the data
- self._doc = (n.find('doc') or '').strip('\n').replace('\\\n', '')
- self._imports = map(lambda i: i.strip(), n.findall('import'))
- self._make = n.find('make')
- self._var_make = n.find('var_make')
- self._checks = n.findall('check')
- self._callbacks = n.findall('callback')
- self._bus_structure_source = n.find('bus_structure_source') or ''
- self._bus_structure_sink = n.find('bus_structure_sink') or ''
- self.port_counters = [itertools.count(), itertools.count()]
-
- # Build the block
- Element.__init__(self, flow_graph)
-
- # Grab the data
- params = n.findall('param')
- sources = n.findall('source')
- sinks = n.findall('sink')
- self._name = n.find('name')
- self._key = n.find('key')
- category = (n.find('category') or '').split('/')
- self.category = [cat.strip() for cat in category if cat.strip()]
- self._flags = n.find('flags') or ''
- # Backwards compatibility
- if n.find('throttle') and BLOCK_FLAG_THROTTLE not in self._flags:
- self._flags += BLOCK_FLAG_THROTTLE
- self._grc_source = n.find('grc_source') or ''
- self._block_wrapper_path = n.find('block_wrapper_path')
- self._bussify_sink = n.find('bus_sink')
- self._bussify_source = n.find('bus_source')
- self._var_value = n.find('var_value') or '$value'
-
- # Get list of param tabs
- n_tabs = n.find('param_tab_order') or None
- self._param_tab_labels = n_tabs.findall('tab') if n_tabs is not None else [DEFAULT_PARAM_TAB]
-
- # Create the param objects
- self._params = list()
-
- # Add the id param
- self.get_params().append(self.get_parent().get_parent().Param(
- block=self,
- n=odict({
- 'name': 'ID',
- 'key': 'id',
- 'type': 'id',
- })
- ))
- self.get_params().append(self.get_parent().get_parent().Param(
- block=self,
- n=odict({
- 'name': 'Enabled',
- 'key': '_enabled',
- 'type': 'raw',
- 'value': 'True',
- 'hide': 'all',
- })
- ))
- for param in itertools.imap(lambda n: self.get_parent().get_parent().Param(block=self, n=n), params):
- key = param.get_key()
- # Test against repeated keys
- if key in self.get_param_keys():
- raise Exception('Key "{}" already exists in params'.format(key))
- # Store the param
- self.get_params().append(param)
- # Create the source objects
- self._sources = list()
- for source in map(lambda n: self.get_parent().get_parent().Port(block=self, n=n, dir='source'), sources):
- key = source.get_key()
- # Test against repeated keys
- if key in self.get_source_keys():
- raise Exception('Key "{}" already exists in sources'.format(key))
- # Store the port
- self.get_sources().append(source)
- self.back_ofthe_bus(self.get_sources())
- # Create the sink objects
- self._sinks = list()
- for sink in map(lambda n: self.get_parent().get_parent().Port(block=self, n=n, dir='sink'), sinks):
- key = sink.get_key()
- # Test against repeated keys
- if key in self.get_sink_keys():
- raise Exception('Key "{}" already exists in sinks'.format(key))
- # Store the port
- self.get_sinks().append(sink)
- self.back_ofthe_bus(self.get_sinks())
- self.current_bus_structure = {'source': '', 'sink': ''}
-
- # Virtual source/sink and pad source/sink blocks are
- # indistinguishable from normal GR blocks. Make explicit
- # checks for them here since they have no work function or
- # buffers to manage.
- self.is_virtual_or_pad = self._key in (
- "virtual_source", "virtual_sink", "pad_source", "pad_sink")
- self.is_variable = self._key.startswith('variable')
- self.is_import = (self._key == 'import')
-
- # Disable blocks that are virtual/pads or variables
- if self.is_virtual_or_pad or self.is_variable:
- self._flags += BLOCK_FLAG_DISABLE_BYPASS
-
- if not (self.is_virtual_or_pad or self.is_variable or self._key == 'options'):
- self.get_params().append(self.get_parent().get_parent().Param(
- block=self,
- n=odict({'name': 'Block Alias',
- 'key': 'alias',
- 'type': 'string',
- 'hide': 'part',
- 'tab': ADVANCED_PARAM_TAB
- })
- ))
-
- if (len(sources) or len(sinks)) and not self.is_virtual_or_pad:
- self.get_params().append(self.get_parent().get_parent().Param(
- block=self,
- n=odict({'name': 'Core Affinity',
- 'key': 'affinity',
- 'type': 'int_vector',
- 'hide': 'part',
- 'tab': ADVANCED_PARAM_TAB
- })
- ))
- if len(sources) and not self.is_virtual_or_pad:
- self.get_params().append(self.get_parent().get_parent().Param(
- block=self,
- n=odict({'name': 'Min Output Buffer',
- 'key': 'minoutbuf',
- 'type': 'int',
- 'hide': 'part',
- 'value': '0',
- 'tab': ADVANCED_PARAM_TAB
- })
- ))
- self.get_params().append(self.get_parent().get_parent().Param(
- block=self,
- n=odict({'name': 'Max Output Buffer',
- 'key': 'maxoutbuf',
- 'type': 'int',
- 'hide': 'part',
- 'value': '0',
- 'tab': ADVANCED_PARAM_TAB
- })
- ))
-
- self.get_params().append(self.get_parent().get_parent().Param(
- block=self,
- n=odict({'name': 'Comment',
- 'key': 'comment',
- 'type': '_multiline',
- 'hide': 'part',
- 'value': '',
- 'tab': ADVANCED_PARAM_TAB
- })
- ))
-
- self._epy_source_hash = -1 # for epy blocks
- self._epy_reload_error = None
-
- if self._bussify_sink:
- self.bussify({'name': 'bus', 'type': 'bus'}, 'sink')
- if self._bussify_source:
- self.bussify({'name': 'bus', 'type': 'bus'}, 'source')
-
- def get_bus_structure(self, direction):
- if direction == 'source':
- bus_structure = self._bus_structure_source
- else:
- bus_structure = self._bus_structure_sink
-
- bus_structure = self.resolve_dependencies(bus_structure)
-
- if not bus_structure:
- return '' # TODO: Don't like empty strings. should change this to None eventually
-
- try:
- clean_bus_structure = self.get_parent().evaluate(bus_structure)
- return clean_bus_structure
- except:
- return ''
-
- def validate(self):
- """
- Validate this block.
- Call the base class validate.
- Evaluate the checks: each check must evaluate to True.
- """
- Element.validate(self)
- # Evaluate the checks
- for check in self._checks:
- check_res = self.resolve_dependencies(check)
- try:
- if not self.get_parent().evaluate(check_res):
- self.add_error_message('Check "{}" failed.'.format(check))
- except:
- self.add_error_message('Check "{}" did not evaluate.'.format(check))
-
- # For variables check the value (only if var_value is used
- if self.is_variable and self._var_value != '$value':
- value = self._var_value
- try:
- value = self.get_var_value()
- self.get_parent().evaluate(value)
- except Exception as err:
- self.add_error_message('Value "{}" cannot be evaluated:\n{}'.format(value, err))
-
- # check if this is a GUI block and matches the selected generate option
- current_generate_option = self.get_parent().get_option('generate_options')
-
- def check_generate_mode(label, flag, valid_options):
- block_requires_mode = (
- flag in self.get_flags() or
- self.get_name().upper().startswith(label)
- )
- if block_requires_mode and current_generate_option not in valid_options:
- self.add_error_message("Can't generate this block in mode: {} ".format(
- repr(current_generate_option)))
-
- check_generate_mode('QT GUI', BLOCK_FLAG_NEED_QT_GUI, ('qt_gui', 'hb_qt_gui'))
- if self._epy_reload_error:
- self.get_param('_source_code').add_error_message(str(self._epy_reload_error))
-
- def rewrite(self):
- """
- Add and remove ports to adjust for the nports.
- """
- Element.rewrite(self)
- # Check and run any custom rewrite function for this block
- getattr(self, 'rewrite_' + self._key, lambda: None)()
-
- # Adjust nports, disconnect hidden ports
- for ports in (self.get_sources(), self.get_sinks()):
- for i, master_port in enumerate(ports):
- nports = master_port.get_nports() or 1
- num_ports = 1 + len(master_port.get_clones())
- if master_port.get_hide():
- for connection in master_port.get_connections():
- self.get_parent().remove_element(connection)
- if not nports and num_ports == 1: # Not a master port and no left-over clones
- continue
- # Remove excess cloned ports
- for port in master_port.get_clones()[nports-1:]:
- # Remove excess connections
- for connection in port.get_connections():
- self.get_parent().remove_element(connection)
- master_port.remove_clone(port)
- ports.remove(port)
- # Add more cloned ports
- for j in range(num_ports, nports):
- port = master_port.add_clone()
- ports.insert(ports.index(master_port) + j, port)
-
- self.back_ofthe_bus(ports)
- # Renumber non-message/message ports
- domain_specific_port_index = collections.defaultdict(int)
- for port in filter(lambda p: p.get_key().isdigit(), ports):
- domain = port.get_domain()
- port._key = str(domain_specific_port_index[domain])
- domain_specific_port_index[domain] += 1
-
- def port_controller_modify(self, direction):
- """
- Change the port controller.
-
- Args:
- direction: +1 or -1
-
- Returns:
- true for change
- """
- changed = False
- # Concat the nports string from the private nports settings of all ports
- nports_str = ' '.join([port._nports for port in self.get_ports()])
- # Modify all params whose keys appear in the nports string
- for param in self.get_params():
- if param.is_enum() or param.get_key() not in nports_str:
- continue
- # Try to increment the port controller by direction
- try:
- value = param.get_evaluated()
- value = value + direction
- if 0 < value:
- param.set_value(value)
- changed = True
- except:
- pass
- return changed
-
- def get_doc(self):
- platform = self.get_parent().get_parent()
- documentation = platform.block_docstrings.get(self._key, {})
- from_xml = self._doc.strip()
- if from_xml:
- documentation[''] = from_xml
- return documentation
-
- def get_imports(self, raw=False):
- """
- Resolve all import statements.
- Split each import statement at newlines.
- Combine all import statements into a list.
- Filter empty imports.
-
- Returns:
- a list of import statements
- """
- if raw:
- return self._imports
- return filter(lambda i: i, sum(map(lambda i: self.resolve_dependencies(i).split('\n'), self._imports), []))
-
- def get_make(self, raw=False):
- if raw:
- return self._make
- return self.resolve_dependencies(self._make)
-
- def get_var_make(self):
- return self.resolve_dependencies(self._var_make)
-
- def get_var_value(self):
- return self.resolve_dependencies(self._var_value)
-
- def get_callbacks(self):
- """
- Get a list of function callbacks for this block.
-
- Returns:
- a list of strings
- """
- def make_callback(callback):
- callback = self.resolve_dependencies(callback)
- if 'self.' in callback:
- return callback
- return 'self.{}.{}'.format(self.get_id(), callback)
- return map(make_callback, self._callbacks)
-
- def is_virtual_sink(self):
- return self.get_key() == 'virtual_sink'
-
- def is_virtual_source(self):
- return self.get_key() == 'virtual_source'
-
- ###########################################################################
- # Custom rewrite functions
- ###########################################################################
-
- def rewrite_epy_block(self):
- flowgraph = self.get_parent()
- platform = flowgraph.get_parent()
- param_blk = self.get_param('_io_cache')
- param_src = self.get_param('_source_code')
-
- src = param_src.get_value()
- src_hash = hash((self.get_id(), src))
- if src_hash == self._epy_source_hash:
- return
-
- try:
- blk_io = epy_block_io.extract(src)
-
- except Exception as e:
- self._epy_reload_error = ValueError(str(e))
- try: # Load last working block io
- blk_io_args = eval(param_blk.get_value())
- if len(blk_io_args) == 6:
- blk_io_args += ([],) # add empty callbacks
- blk_io = epy_block_io.BlockIO(*blk_io_args)
- except Exception:
- return
- else:
- self._epy_reload_error = None # Clear previous errors
- param_blk.set_value(repr(tuple(blk_io)))
-
- # print "Rewriting embedded python block {!r}".format(self.get_id())
-
- self._epy_source_hash = src_hash
- self._name = blk_io.name or blk_io.cls
- self._doc = blk_io.doc
- self._imports[0] = 'import ' + self.get_id()
- self._make = '{0}.{1}({2})'.format(self.get_id(), blk_io.cls, ', '.join(
- '{0}=${{ {0} }}'.format(key) for key, _ in blk_io.params))
- self._callbacks = ['{0} = ${{ {0} }}'.format(attr) for attr in blk_io.callbacks]
-
- params = {}
- for param in list(self._params):
- if hasattr(param, '__epy_param__'):
- params[param.get_key()] = param
- self._params.remove(param)
-
- for key, value in blk_io.params:
- try:
- param = params[key]
- param.set_default(value)
- except KeyError: # need to make a new param
- name = key.replace('_', ' ').title()
- n = odict(dict(name=name, key=key, type='raw', value=value))
- param = platform.Param(block=self, n=n)
- setattr(param, '__epy_param__', True)
- self._params.append(param)
-
- def update_ports(label, ports, port_specs, direction):
- ports_to_remove = list(ports)
- iter_ports = iter(ports)
- ports_new = []
- port_current = next(iter_ports, None)
- for key, port_type, vlen in port_specs:
- reuse_port = (
- port_current is not None and
- port_current.get_type() == port_type and
- port_current.get_vlen() == vlen and
- (key.isdigit() or port_current.get_key() == key)
- )
- if reuse_port:
- ports_to_remove.remove(port_current)
- port, port_current = port_current, next(iter_ports, None)
- else:
- n = odict(dict(name=label + str(key), type=port_type, key=key))
- if port_type == 'message':
- n['name'] = key
- n['optional'] = '1'
- if vlen > 1:
- n['vlen'] = str(vlen)
- port = platform.Port(block=self, n=n, dir=direction)
- ports_new.append(port)
- # replace old port list with new one
- del ports[:]
- ports.extend(ports_new)
- # remove excess port connections
- for port in ports_to_remove:
- for connection in port.get_connections():
- flowgraph.remove_element(connection)
-
- update_ports('in', self.get_sinks(), blk_io.sinks, 'sink')
- update_ports('out', self.get_sources(), blk_io.sources, 'source')
- self.rewrite()
-
- def back_ofthe_bus(self, portlist):
- portlist.sort(key=lambda p: p._type == 'bus')
-
- def filter_bus_port(self, ports):
- buslist = [p for p in ports if p._type == 'bus']
- return buslist or ports
-
- # Main functions to get and set the block state
- # Also kept get_enabled and set_enabled to keep compatibility
- def get_state(self):
- """
- Gets the block's current state.
-
- Returns:
- ENABLED - 0
- BYPASSED - 1
- DISABLED - 2
- """
- try:
- return int(eval(self.get_param('_enabled').get_value()))
- except:
- return BLOCK_ENABLED
-
- def set_state(self, state):
- """
- Sets the state for the block.
-
- Args:
- ENABLED - 0
- BYPASSED - 1
- DISABLED - 2
- """
- if state in [BLOCK_ENABLED, BLOCK_BYPASSED, BLOCK_DISABLED]:
- self.get_param('_enabled').set_value(str(state))
- else:
- self.get_param('_enabled').set_value(str(BLOCK_ENABLED))
-
- # Enable/Disable Aliases
- def get_enabled(self):
- """
- Get the enabled state of the block.
-
- Returns:
- true for enabled
- """
- return not (self.get_state() == BLOCK_DISABLED)
-
- def set_enabled(self, enabled):
- """
- Set the enabled state of the block.
-
- Args:
- enabled: true for enabled
-
- Returns:
- True if block changed state
- """
- old_state = self.get_state()
- new_state = BLOCK_ENABLED if enabled else BLOCK_DISABLED
- self.set_state(new_state)
- return old_state != new_state
-
- # Block bypassing
- def get_bypassed(self):
- """
- Check if the block is bypassed
- """
- return self.get_state() == BLOCK_BYPASSED
-
- def set_bypassed(self):
- """
- Bypass the block
-
- Returns:
- True if block chagnes state
- """
- if self.get_state() != BLOCK_BYPASSED and self.can_bypass():
- self.set_state(BLOCK_BYPASSED)
- return True
- return False
-
- def can_bypass(self):
- """ Check the number of sinks and sources and see if this block can be bypassed """
- # Check to make sure this is a single path block
- # Could possibly support 1 to many blocks
- if len(self.get_sources()) != 1 or len(self.get_sinks()) != 1:
- return False
- if not (self.get_sources()[0].get_type() == self.get_sinks()[0].get_type()):
- return False
- if self.bypass_disabled():
- return False
- return True
-
- def __str__(self):
- return 'Block - {} - {}({})'.format(self.get_id(), self.get_name(), self.get_key())
-
- def get_id(self):
- return self.get_param('id').get_value()
-
- def get_name(self):
- return self._name
-
- def get_key(self):
- return self._key
-
- def get_ports(self):
- return self.get_sources() + self.get_sinks()
-
- def get_ports_gui(self):
- return self.filter_bus_port(self.get_sources()) + self.filter_bus_port(self.get_sinks())
-
- def get_children(self):
- return self.get_ports() + self.get_params()
-
- def get_children_gui(self):
- return self.get_ports_gui() + self.get_params()
-
- def get_block_wrapper_path(self):
- return self._block_wrapper_path
-
- def get_comment(self):
- return self.get_param('comment').get_value()
-
- def get_flags(self):
- return self._flags
-
- def throtteling(self):
- return BLOCK_FLAG_THROTTLE in self._flags
-
- def bypass_disabled(self):
- return BLOCK_FLAG_DISABLE_BYPASS in self._flags
-
- @property
- def is_deprecated(self):
- return BLOCK_FLAG_DEPRECATED in self._flags
-
- ##############################################
- # Access Params
- ##############################################
- def get_param_tab_labels(self):
- return self._param_tab_labels
-
- def get_param_keys(self):
- return _get_keys(self._params)
-
- def get_param(self, key):
- return _get_elem(self._params, key)
-
- def get_params(self):
- return self._params
-
- def has_param(self, key):
- try:
- _get_elem(self._params, key)
- return True
- except:
- return False
-
- ##############################################
- # Access Sinks
- ##############################################
- def get_sink_keys(self):
- return _get_keys(self._sinks)
-
- def get_sink(self, key):
- return _get_elem(self._sinks, key)
-
- def get_sinks(self):
- return self._sinks
-
- def get_sinks_gui(self):
- return self.filter_bus_port(self.get_sinks())
-
- ##############################################
- # Access Sources
- ##############################################
- def get_source_keys(self):
- return _get_keys(self._sources)
-
- def get_source(self, key):
- return _get_elem(self._sources, key)
-
- def get_sources(self):
- return self._sources
-
- def get_sources_gui(self):
- return self.filter_bus_port(self.get_sources())
-
- 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.
-
- Args:
- tmpl: the string with dependencies
-
- Returns:
- the resolved value
- """
- tmpl = str(tmpl)
- if '$' not in tmpl:
- return tmpl
- n = dict((param.get_key(), param.template_arg)
- for param in self.get_params()) # TODO: cache that
- try:
- return str(Template(tmpl, n))
- except Exception as err:
- return "Template error: {}\n {}".format(tmpl, err)
-
- ##############################################
- # Controller Modify
- ##############################################
- def type_controller_modify(self, direction):
- """
- Change the type controller.
-
- Args:
- direction: +1 or -1
-
- Returns:
- true for change
- """
- changed = False
- type_param = None
- for param in filter(lambda p: p.is_enum(), self.get_params()):
- children = self.get_ports() + self.get_params()
- # Priority to the type controller
- if param.get_key() in ' '.join(map(lambda p: p._type, children)): type_param = param
- # Use param if type param is unset
- if not type_param:
- 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 form_bus_structure(self, direc):
- if direc == 'source':
- get_p = self.get_sources
- get_p_gui = self.get_sources_gui
- bus_structure = self.get_bus_structure('source')
- else:
- get_p = self.get_sinks
- get_p_gui = self.get_sinks_gui
- bus_structure = self.get_bus_structure('sink')
-
- struct = [range(len(get_p()))]
- if True in map(lambda a: isinstance(a.get_nports(), int), get_p()):
- structlet = []
- last = 0
- for j in [i.get_nports() for i in get_p() if isinstance(i.get_nports(), int)]:
- structlet.extend(map(lambda a: a+last, range(j)))
- last = structlet[-1] + 1
- struct = [structlet]
- if bus_structure:
-
- struct = bus_structure
-
- self.current_bus_structure[direc] = struct
- return struct
-
- def bussify(self, n, direc):
- if direc == 'source':
- get_p = self.get_sources
- get_p_gui = self.get_sources_gui
- bus_structure = self.get_bus_structure('source')
- else:
- get_p = self.get_sinks
- get_p_gui = self.get_sinks_gui
- bus_structure = self.get_bus_structure('sink')
-
- for elt in get_p():
- for connect in elt.get_connections():
- self.get_parent().remove_element(connect)
-
- if ('bus' not in map(lambda a: a.get_type(), get_p())) and len(get_p()) > 0:
- struct = self.form_bus_structure(direc)
- self.current_bus_structure[direc] = struct
- if get_p()[0].get_nports():
- n['nports'] = str(1)
-
- for i in range(len(struct)):
- n['key'] = str(len(get_p()))
- n = odict(n)
- port = self.get_parent().get_parent().Port(block=self, n=n, dir=direc)
- get_p().append(port)
- elif 'bus' in map(lambda a: a.get_type(), get_p()):
- for elt in get_p_gui():
- get_p().remove(elt)
- self.current_bus_structure[direc] = ''
-
- ##############################################
- # Import/Export Methods
- ##############################################
- def export_data(self):
- """
- Export this block's params to nested data.
-
- Returns:
- a nested data odict
- """
- n = odict()
- n['key'] = self.get_key()
- n['param'] = map(lambda p: p.export_data(), sorted(self.get_params(), key=str))
- if 'bus' in map(lambda a: a.get_type(), self.get_sinks()):
- n['bus_sink'] = str(1)
- if 'bus' in map(lambda a: a.get_type(), self.get_sources()):
- n['bus_source'] = str(1)
- return n
-
- def get_hash(self):
- return hash(tuple(map(hash, self.get_params())))
-
- def import_data(self, n):
- """
- Import this block's params from nested data.
- Any param keys that do not exist will be ignored.
- Since params can be dynamically created based another param,
- call rewrite, and repeat the load until the params stick.
- This call to rewrite will also create any dynamic ports
- that are needed for the connections creation phase.
-
- Args:
- n: the nested data odict
- """
- my_hash = 0
- while self.get_hash() != my_hash:
- params_n = n.findall('param')
- for param_n in params_n:
- key = param_n.find('key')
- value = param_n.find('value')
- # The key must exist in this block's params
- if key in self.get_param_keys():
- self.get_param(key).set_value(value)
- # Store hash and call rewrite
- my_hash = self.get_hash()
- self.rewrite()
- bussinks = n.findall('bus_sink')
- if len(bussinks) > 0 and not self._bussify_sink:
- self.bussify({'name': 'bus', 'type': 'bus'}, 'sink')
- elif len(bussinks) > 0:
- self.bussify({'name': 'bus', 'type': 'bus'}, 'sink')
- self.bussify({'name': 'bus', 'type': 'bus'}, 'sink')
- bussrcs = n.findall('bus_source')
- if len(bussrcs) > 0 and not self._bussify_source:
- self.bussify({'name': 'bus', 'type': 'bus'}, 'source')
- elif len(bussrcs) > 0:
- self.bussify({'name': 'bus', 'type': 'bus'}, 'source')
- self.bussify({'name': 'bus', 'type': 'bus'}, 'source')
diff --git a/grc/core/CMakeLists.txt b/grc/core/CMakeLists.txt
deleted file mode 100644
index f340127873..0000000000
--- a/grc/core/CMakeLists.txt
+++ /dev/null
@@ -1,35 +0,0 @@
-# Copyright 2011 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.
-
-file(GLOB py_files "*.py")
-
-GR_PYTHON_INSTALL(
- FILES ${py_files}
- DESTINATION ${GR_PYTHON_DIR}/gnuradio/grc/core
-)
-
-file(GLOB dtd_files "*.dtd")
-
-install(
- FILES ${dtd_files} default_flow_graph.grc
- DESTINATION ${GR_PYTHON_DIR}/gnuradio/grc/core
-)
-
-add_subdirectory(generator)
-add_subdirectory(utils)
diff --git a/grc/core/Config.py b/grc/core/Config.py
index 744ad06ba9..eb53e1751d 100644
--- a/grc/core/Config.py
+++ b/grc/core/Config.py
@@ -1,5 +1,4 @@
-"""
-Copyright 2016 Free Software Foundation, Inc.
+"""Copyright 2016 Free Software Foundation, Inc.
This file is part of GNU Radio
GNU Radio Companion is free software; you can redistribute it and/or
@@ -17,6 +16,8 @@ along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
"""
+from __future__ import absolute_import
+
import os
from os.path import expanduser, normpath, expandvars, exists
@@ -24,21 +25,24 @@ from . import Constants
class Config(object):
-
- key = 'grc'
name = 'GNU Radio Companion (no gui)'
license = __doc__.strip()
website = 'http://gnuradio.org'
hier_block_lib_dir = os.environ.get('GRC_HIER_PATH', Constants.DEFAULT_HIER_BLOCK_LIB_DIR)
- def __init__(self, prefs_file, version, version_parts=None, name=None):
- self.prefs = prefs_file
+ yml_block_cache = os.path.expanduser('~/.cache/grc_gnuradio') # FIXME: remove this as soon as converter is stable
+
+ def __init__(self, version, version_parts=None, name=None, prefs=None):
+ self._gr_prefs = prefs if prefs else DummyPrefs()
self.version = version
self.version_parts = version_parts or version[1:].split('-', 1)[0].split('.')[:3]
if name:
self.name = name
+ if not os.path.exists(self.yml_block_cache):
+ os.mkdir(self.yml_block_cache)
+
@property
def block_paths(self):
path_list_sep = {'/': ':', '\\': ';'}[os.path.sep]
@@ -46,8 +50,9 @@ class Config(object):
paths_sources = (
self.hier_block_lib_dir,
os.environ.get('GRC_BLOCKS_PATH', ''),
- self.prefs.get_string('grc', 'local_blocks_path', ''),
- self.prefs.get_string('grc', 'global_blocks_path', ''),
+ self.yml_block_cache,
+ self._gr_prefs.get_string('grc', 'local_blocks_path', ''),
+ self._gr_prefs.get_string('grc', 'global_blocks_path', ''),
)
collected_paths = sum((paths.split(path_list_sep)
@@ -62,7 +67,22 @@ class Config(object):
def default_flow_graph(self):
user_default = (
os.environ.get('GRC_DEFAULT_FLOW_GRAPH') or
- self.prefs.get_string('grc', 'default_flow_graph', '') or
+ self._gr_prefs.get_string('grc', 'default_flow_graph', '') or
os.path.join(self.hier_block_lib_dir, 'default_flow_graph.grc')
)
return user_default if exists(user_default) else Constants.DEFAULT_FLOW_GRAPH
+
+
+class DummyPrefs(object):
+
+ def get_string(self, category, item, default):
+ return str(default)
+
+ def set_string(self, category, item, value):
+ pass
+
+ def get_long(self, category, item, default):
+ return int(default)
+
+ def save(self):
+ pass
diff --git a/grc/core/Connection.py b/grc/core/Connection.py
index c028d89ddc..01baaaf8fc 100644
--- a/grc/core/Connection.py
+++ b/grc/core/Connection.py
@@ -17,128 +17,95 @@ 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 Constants
-from .Element import Element
-from .utils import odict
+from __future__ import absolute_import
+
+from .base import Element
+from .utils.descriptors import lazy_property
class Connection(Element):
is_connection = True
- def __init__(self, flow_graph, porta, portb):
+ def __init__(self, parent, source, sink):
"""
Make a new connection given the parent and 2 ports.
Args:
flow_graph: the parent of this element
- porta: a port (any direction)
- portb: a port (any direction)
+ source: a port (any direction)
+ sink: a port (any direction)
@throws Error cannot make connection
Returns:
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
- else:
- sink = port
- if not source:
+ Element.__init__(self, parent)
+
+ if not source.is_source:
+ source, sink = sink, source
+ if not source.is_source:
raise ValueError('Connection could not isolate source')
- if not sink:
+ if not sink.is_sink:
raise ValueError('Connection could not isolate sink')
- busses = len(filter(lambda a: a.get_type() == 'bus', [source, sink])) % 2
- if not busses == 0:
- raise ValueError('busses must get with busses')
-
- if not len(source.get_associated_ports()) == len(sink.get_associated_ports()):
- raise ValueError('port connections must have same cardinality')
- # Ensure that this connection (source -> sink) is unique
- for connection in flow_graph.connections:
- if connection.get_source() is source and connection.get_sink() is sink:
- raise LookupError('This connection between source and sink is not unique.')
- self._source = source
- self._sink = sink
- if source.get_type() == 'bus':
-
- sources = source.get_associated_ports()
- sinks = sink.get_associated_ports()
-
- for i in range(len(sources)):
- try:
- flow_graph.connect(sources[i], sinks[i])
- except:
- pass
+
+ self.source_port = source
+ self.sink_port = sink
def __str__(self):
return 'Connection (\n\t{}\n\t\t{}\n\t{}\n\t\t{}\n)'.format(
- self.get_source().get_parent(),
- self.get_source(),
- self.get_sink().get_parent(),
- self.get_sink(),
+ self.source_block, self.source_port, self.sink_block, self.sink_port,
)
- def is_bus(self):
- return self.get_source().get_type() == self.get_sink().get_type() == 'bus'
+ def __eq__(self, other):
+ if not isinstance(other, self.__class__):
+ return NotImplemented
+ return self.source_port == other.source_port and self.sink_port == other.sink_port
- def validate(self):
- """
- Validate the connections.
- The ports must match in io size.
- """
- """
- Validate the connections.
- The ports must match in type.
- """
- Element.validate(self)
- platform = self.get_parent().get_parent()
- source_domain = self.get_source().get_domain()
- sink_domain = self.get_sink().get_domain()
- if (source_domain, sink_domain) not in platform.connection_templates:
- self.add_error_message('No connection known for domains "{}", "{}"'.format(
- source_domain, sink_domain))
- too_many_other_sinks = (
- not platform.domains.get(source_domain, []).get('multiple_sinks', False) and
- len(self.get_source().get_enabled_connections()) > 1
- )
- too_many_other_sources = (
- not platform.domains.get(sink_domain, []).get('multiple_sources', False) and
- len(self.get_sink().get_enabled_connections()) > 1
- )
- if too_many_other_sinks:
- self.add_error_message(
- 'Domain "{}" can have only one downstream block'.format(source_domain))
- if too_many_other_sources:
- self.add_error_message(
- 'Domain "{}" can have only one upstream block'.format(sink_domain))
-
- source_size = Constants.TYPE_TO_SIZEOF[self.get_source().get_type()] * self.get_source().get_vlen()
- sink_size = Constants.TYPE_TO_SIZEOF[self.get_sink().get_type()] * self.get_sink().get_vlen()
- if source_size != sink_size:
- self.add_error_message('Source IO size "{}" does not match sink IO size "{}".'.format(source_size, sink_size))
+ def __hash__(self):
+ return hash((self.source_port, self.sink_port))
+
+ def __iter__(self):
+ return iter((self.source_port, self.sink_port))
+
+ @lazy_property
+ def source_block(self):
+ return self.source_port.parent_block
+
+ @lazy_property
+ def sink_block(self):
+ return self.sink_port.parent_block
- def get_enabled(self):
+ @lazy_property
+ def type(self):
+ return self.source_port.domain, self.sink_port.domain
+
+ @property
+ def enabled(self):
"""
Get the enabled state of this connection.
Returns:
true if source and sink blocks are enabled
"""
- return self.get_source().get_parent().get_enabled() and \
- self.get_sink().get_parent().get_enabled()
+ return self.source_block.enabled and self.sink_block.enabled
- #############################
- # Access Ports
- #############################
- def get_sink(self):
- return self._sink
+ def validate(self):
+ """
+ Validate the connections.
+ The ports must match in io size.
+ """
+ Element.validate(self)
+ platform = self.parent_platform
- def get_source(self):
- return self._source
+ if self.type not in platform.connection_templates:
+ self.add_error_message('No connection known between domains "{}" and "{}"'
+ ''.format(*self.type))
+
+ source_size = self.source_port.item_size
+ sink_size = self.sink_port.item_size
+ if source_size != sink_size:
+ self.add_error_message('Source IO size "{}" does not match sink IO size "{}".'.format(source_size, sink_size))
##############################################
# Import/Export Methods
@@ -150,9 +117,7 @@ class Connection(Element):
Returns:
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
+ return (
+ self.source_block.name, self.source_port.key,
+ self.sink_block.name, self.sink_port.key
+ )
diff --git a/grc/core/Constants.py b/grc/core/Constants.py
index edd3442a94..fc5383378c 100644
--- a/grc/core/Constants.py
+++ b/grc/core/Constants.py
@@ -17,19 +17,21 @@ along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
"""
+from __future__ import absolute_import
+
import os
-import numpy
import stat
+import numpy
+
+
# Data files
DATA_DIR = os.path.dirname(__file__)
-FLOW_GRAPH_DTD = os.path.join(DATA_DIR, 'flow_graph.dtd')
-BLOCK_TREE_DTD = os.path.join(DATA_DIR, 'block_tree.dtd')
BLOCK_DTD = os.path.join(DATA_DIR, 'block.dtd')
DEFAULT_FLOW_GRAPH = os.path.join(DATA_DIR, 'default_flow_graph.grc')
DEFAULT_HIER_BLOCK_LIB_DIR = os.path.expanduser('~/.grc_gnuradio')
-DOMAIN_DTD = os.path.join(DATA_DIR, 'domain.dtd')
+BLOCK_DESCRIPTION_FILE_FORMAT_VERSION = 1
# File format versions:
# 0: undefined / legacy
# 1: non-numeric message port keys (label is used instead)
@@ -41,38 +43,39 @@ ADVANCED_PARAM_TAB = "Advanced"
DEFAULT_BLOCK_MODULE_NAME = '(no module specified)'
# Port domains
-GR_STREAM_DOMAIN = "gr_stream"
-GR_MESSAGE_DOMAIN = "gr_message"
+GR_STREAM_DOMAIN = "stream"
+GR_MESSAGE_DOMAIN = "message"
DEFAULT_DOMAIN = GR_STREAM_DOMAIN
-BLOCK_FLAG_THROTTLE = 'throttle'
-BLOCK_FLAG_DISABLE_BYPASS = 'disable_bypass'
-BLOCK_FLAG_NEED_QT_GUI = 'need_qt_gui'
-BLOCK_FLAG_DEPRECATED = 'deprecated'
-
-# Block States
-BLOCK_DISABLED = 0
-BLOCK_ENABLED = 1
-BLOCK_BYPASSED = 2
-
# 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
+PARAM_TYPE_NAMES = (
+ 'raw', 'enum',
+ 'complex', 'real', 'float', 'int',
+ 'complex_vector', 'real_vector', 'float_vector', 'int_vector',
+ 'hex', 'string', 'bool',
+ 'file_open', 'file_save', '_multiline', '_multiline_python_external',
+ 'id', 'stream_id',
+ 'gui_hint',
+ 'import',
+)
+
# Define types, native python + numpy
VECTOR_TYPES = (tuple, list, set, numpy.ndarray)
COMPLEX_TYPES = [complex, numpy.complex, numpy.complex64, numpy.complex128]
REAL_TYPES = [float, numpy.float, numpy.float32, numpy.float64]
-INT_TYPES = [int, long, numpy.int, numpy.int8, numpy.int16, numpy.int32, numpy.uint64,
+INT_TYPES = [int, numpy.int, numpy.int8, numpy.int16, numpy.int32, numpy.uint64,
numpy.uint, numpy.uint8, numpy.uint16, numpy.uint32, numpy.uint64]
# Cast to tuple for isinstance, concat subtypes
COMPLEX_TYPES = tuple(COMPLEX_TYPES + REAL_TYPES + INT_TYPES)
REAL_TYPES = tuple(REAL_TYPES + INT_TYPES)
INT_TYPES = tuple(INT_TYPES)
-# Updating colors. Using the standard color pallette from:
-# http://www.google.com/design/spec/style/color.html#color-color-palette
+# Updating colors. Using the standard color palette from:
+# http://www.google.com/design/spec/style/color.html#color-color-palette
# Most are based on the main, primary color standard. Some are within
# that color's spectrum when it was deemed necessary.
GRC_COLOR_BROWN = '#795548'
@@ -95,54 +98,32 @@ GRC_COLOR_GREY = '#BDBDBD'
GRC_COLOR_WHITE = '#FFFFFF'
CORE_TYPES = ( # name, key, sizeof, color
- ('Complex Float 64', 'fc64', 16, GRC_COLOR_BROWN),
- ('Complex Float 32', 'fc32', 8, GRC_COLOR_BLUE),
- ('Complex Integer 64', 'sc64', 16, GRC_COLOR_LIGHT_GREEN),
- ('Complex Integer 32', 'sc32', 8, GRC_COLOR_GREEN),
- ('Complex Integer 16', 'sc16', 4, GRC_COLOR_AMBER),
- ('Complex Integer 8', 'sc8', 2, GRC_COLOR_PURPLE),
- ('Float 64', 'f64', 8, GRC_COLOR_CYAN),
- ('Float 32', 'f32', 4, GRC_COLOR_ORANGE),
- ('Integer 64', 's64', 8, GRC_COLOR_LIME),
- ('Integer 32', 's32', 4, GRC_COLOR_TEAL),
- ('Integer 16', 's16', 2, GRC_COLOR_YELLOW),
- ('Integer 8', 's8', 1, GRC_COLOR_PURPLE_A400),
- ('Bits (unpacked byte)', 'bit', 1, GRC_COLOR_PURPLE_A100),
- ('Async Message', 'message', 0, GRC_COLOR_GREY),
- ('Bus Connection', 'bus', 0, GRC_COLOR_WHITE),
- ('Wildcard', '', 0, GRC_COLOR_WHITE),
+ ('Complex Float 64', 'fc64', 16, GRC_COLOR_BROWN),
+ ('Complex Float 32', 'fc32', 8, GRC_COLOR_BLUE),
+ ('Complex Integer 64', 'sc64', 16, GRC_COLOR_LIGHT_GREEN),
+ ('Complex Integer 32', 'sc32', 8, GRC_COLOR_GREEN),
+ ('Complex Integer 16', 'sc16', 4, GRC_COLOR_AMBER),
+ ('Complex Integer 8', 'sc8', 2, GRC_COLOR_PURPLE),
+ ('Float 64', 'f64', 8, GRC_COLOR_CYAN),
+ ('Float 32', 'f32', 4, GRC_COLOR_ORANGE),
+ ('Integer 64', 's64', 8, GRC_COLOR_LIME),
+ ('Integer 32', 's32', 4, GRC_COLOR_TEAL),
+ ('Integer 16', 's16', 2, GRC_COLOR_YELLOW),
+ ('Integer 8', 's8', 1, GRC_COLOR_PURPLE_A400),
+ ('Bits (unpacked byte)', 'bit', 1, GRC_COLOR_PURPLE_A100),
+ ('Async Message', 'message', 0, GRC_COLOR_GREY),
+ ('Bus Connection', 'bus', 0, GRC_COLOR_WHITE),
+ ('Wildcard', '', 0, GRC_COLOR_WHITE),
)
ALIAS_TYPES = {
'complex': (8, GRC_COLOR_BLUE),
- 'float': (4, GRC_COLOR_ORANGE),
- 'int': (4, GRC_COLOR_TEAL),
- 'short': (2, GRC_COLOR_YELLOW),
- 'byte': (1, GRC_COLOR_PURPLE_A400),
- 'bits': (1, GRC_COLOR_PURPLE_A100),
+ 'float': (4, GRC_COLOR_ORANGE),
+ 'int': (4, GRC_COLOR_TEAL),
+ 'short': (2, GRC_COLOR_YELLOW),
+ 'byte': (1, GRC_COLOR_PURPLE_A400),
+ 'bits': (1, GRC_COLOR_PURPLE_A100),
}
-TYPE_TO_COLOR = dict()
-TYPE_TO_SIZEOF = dict()
-
-for name, key, sizeof, color in CORE_TYPES:
- TYPE_TO_COLOR[key] = color
- TYPE_TO_SIZEOF[key] = sizeof
-
-for key, (sizeof, color) in ALIAS_TYPES.iteritems():
- TYPE_TO_COLOR[key] = color
- TYPE_TO_SIZEOF[key] = sizeof
-
-# Coloring
-COMPLEX_COLOR_SPEC = '#3399FF'
-FLOAT_COLOR_SPEC = '#FF8C69'
-INT_COLOR_SPEC = '#00FF99'
-SHORT_COLOR_SPEC = '#FFFF66'
-BYTE_COLOR_SPEC = '#FF66FF'
-COMPLEX_VECTOR_COLOR_SPEC = '#3399AA'
-FLOAT_VECTOR_COLOR_SPEC = '#CC8C69'
-INT_VECTOR_COLOR_SPEC = '#00CC99'
-SHORT_VECTOR_COLOR_SPEC = '#CCCC33'
-BYTE_VECTOR_COLOR_SPEC = '#CC66CC'
-ID_COLOR_SPEC = '#DDDDDD'
-WILDCARD_COLOR_SPEC = '#FFFFFF'
+TYPE_TO_SIZEOF = {key: sizeof for name, key, sizeof, color in CORE_TYPES}
+TYPE_TO_SIZEOF.update((key, sizeof) for key, (sizeof, _) in ALIAS_TYPES.items())
diff --git a/grc/core/Element.py b/grc/core/Element.py
deleted file mode 100644
index 67c36e12b4..0000000000
--- a/grc/core/Element.py
+++ /dev/null
@@ -1,114 +0,0 @@
-"""
-Copyright 2008, 2009, 2015 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 = list()
-
- ##################################################
- # Element Validation API
- ##################################################
- def validate(self):
- """
- Validate this element and call validate on all children.
- Call this base method before adding error messages in the subclass.
- """
- del self._error_messages[:]
- for child in self.get_children():
- child.validate()
-
- def is_valid(self):
- """
- Is this element valid?
-
- Returns:
- true when the element is enabled and has no error messages or is bypassed
- """
- return (not self.get_error_messages() or not self.get_enabled()) or self.get_bypassed()
-
- def add_error_message(self, msg):
- """
- Add an error message to the list of errors.
-
- Args:
- msg: the error message string
- """
- self._error_messages.append(msg)
-
- def get_error_messages(self):
- """
- Get the list of error messages from this element and all of its children.
- Do not include the error messages from disabled or bypassed children.
- Cleverly indent the children error messages for printing purposes.
-
- Returns:
- a list of error message strings
- """
- error_messages = list(self._error_messages) # Make a copy
- for child in filter(lambda c: c.get_enabled() and not c.get_bypassed(), self.get_children()):
- for msg in child.get_error_messages():
- error_messages.append("{}:\n\t{}".format(child, msg.replace("\n", "\n\t")))
- return error_messages
-
- def rewrite(self):
- """
- Rewrite this element and call rewrite on all children.
- Call this base method before rewriting the element.
- """
- for child in self.get_children():
- child.rewrite()
-
- def get_enabled(self):
- return True
-
- def get_bypassed(self):
- return False
-
- ##############################################
- # Tree-like API
- ##############################################
- def get_parent(self):
- return self._parent
-
- def get_children(self):
- return list()
-
- ##############################################
- # Type testing
- ##############################################
- is_platform = False
-
- is_flow_graph = False
-
- is_block = False
-
- is_dummy_block = False
-
- is_connection = False
-
- is_port = False
-
- is_param = False
-
- is_variable = False
-
- is_import = False
diff --git a/grc/core/FlowGraph.py b/grc/core/FlowGraph.py
index ecae11cf1a..3f21ec6a9c 100644
--- a/grc/core/FlowGraph.py
+++ b/grc/core/FlowGraph.py
@@ -15,69 +15,57 @@
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
+from __future__ import absolute_import, print_function
+
+import collections
import imp
-from itertools import ifilter, chain
-from operator import methodcaller, attrgetter
-import re
+import itertools
import sys
-import time
+from operator import methodcaller, attrgetter
-from . import Messages
+from . import Messages, blocks
from .Constants import FLOW_GRAPH_FILE_FORMAT_VERSION
-from .Element import Element
-from .utils import odict, expr_utils, shlex
-
-_parameter_matcher = re.compile('^(parameter)$')
-_monitors_searcher = re.compile('(ctrlport_monitor)')
-_bussink_searcher = re.compile('^(bus_sink)$')
-_bussrc_searcher = re.compile('^(bus_source)$')
-_bus_struct_sink_searcher = re.compile('^(bus_structure_sink)$')
-_bus_struct_src_searcher = re.compile('^(bus_structure_source)$')
+from .base import Element
+from .utils import expr_utils
+from .utils.backports import shlex
class FlowGraph(Element):
is_flow_graph = True
- def __init__(self, platform):
+ def __init__(self, parent):
"""
Make a flow graph from the arguments.
Args:
- platform: a platforms with blocks and contrcutors
+ parent: a platforms with blocks and element factories
Returns:
the flow graph object
"""
- Element.__init__(self, platform)
- self._elements = []
- self._timestamp = time.ctime()
+ Element.__init__(self, parent)
+ self._options_block = self.parent_platform.make_block(self, 'options')
- self.platform = platform # todo: make this a lazy prop
- self.blocks = []
- self.connections = []
+ self.blocks = [self._options_block]
+ self.connections = set()
self._eval_cache = {}
self.namespace = {}
self.grc_file_path = ''
- self._options_block = self.new_block('options')
def __str__(self):
return 'FlowGraph - {}({})'.format(self.get_option('title'), self.get_option('id'))
- ##############################################
- # TODO: Move these to new generator package
- ##############################################
- def get_imports(self):
+ def imports(self):
"""
Get a set of all import statements in this flow graph namespace.
Returns:
- a set of import statements
+ a list of import statements
"""
- imports = sum([block.get_imports() for block in self.iter_enabled_blocks()], [])
- return sorted(set(imports))
+ return [block.templates.render('imports') for block in self.iter_enabled_blocks()]
def get_variables(self):
"""
@@ -87,8 +75,8 @@ class FlowGraph(Element):
Returns:
a sorted list of variable blocks in order of dependency (indep -> dep)
"""
- variables = filter(attrgetter('is_variable'), self.iter_enabled_blocks())
- return expr_utils.sort_objects(variables, methodcaller('get_id'), methodcaller('get_var_make'))
+ variables = [block for block in self.iter_enabled_blocks() if block.is_variable]
+ return expr_utils.sort_objects(variables, attrgetter('name'), methodcaller('get_var_make'))
def get_parameters(self):
"""
@@ -97,54 +85,27 @@ class FlowGraph(Element):
Returns:
a list of parameterized variables
"""
- parameters = filter(lambda b: _parameter_matcher.match(b.get_key()), self.iter_enabled_blocks())
+ parameters = [b for b in self.iter_enabled_blocks() if b.key == 'parameter']
return parameters
def get_monitors(self):
"""
Get a list of all ControlPort monitors
"""
- monitors = filter(lambda b: _monitors_searcher.search(b.get_key()),
- self.iter_enabled_blocks())
+ monitors = [b for b in self.iter_enabled_blocks() if 'ctrlport_monitor' in b.key]
return monitors
def get_python_modules(self):
"""Iterate over custom code block ID and Source"""
for block in self.iter_enabled_blocks():
- if block.get_key() == 'epy_module':
- yield block.get_id(), block.get_param('source_code').get_value()
-
- def get_bussink(self):
- bussink = filter(lambda b: _bussink_searcher.search(b.get_key()), self.get_enabled_blocks())
-
- for i in bussink:
- for j in i.get_params():
- if j.get_name() == 'On/Off' and j.get_value() == 'on':
- return True
- return False
-
- def get_bussrc(self):
- bussrc = filter(lambda b: _bussrc_searcher.search(b.get_key()), self.get_enabled_blocks())
-
- for i in bussrc:
- for j in i.get_params():
- if j.get_name() == 'On/Off' and j.get_value() == 'on':
- return True
- return False
-
- def get_bus_structure_sink(self):
- bussink = filter(lambda b: _bus_struct_sink_searcher.search(b.get_key()), self.get_enabled_blocks())
- return bussink
-
- def get_bus_structure_src(self):
- bussrc = filter(lambda b: _bus_struct_src_searcher.search(b.get_key()), self.get_enabled_blocks())
- return bussrc
+ if block.key == 'epy_module':
+ yield block.name, block.params[1].get_value()
def iter_enabled_blocks(self):
"""
Get an iterator of all blocks that are enabled and not bypassed.
"""
- return ifilter(methodcaller('get_enabled'), self.blocks)
+ return (block for block in self.blocks if block.enabled)
def get_enabled_blocks(self):
"""
@@ -162,7 +123,7 @@ class FlowGraph(Element):
Returns:
a list of blocks
"""
- return filter(methodcaller('get_bypassed'), self.blocks)
+ return [block for block in self.blocks if block.get_bypassed()]
def get_enabled_connections(self):
"""
@@ -171,7 +132,7 @@ class FlowGraph(Element):
Returns:
a list of connections
"""
- return filter(methodcaller('get_enabled'), self.connections)
+ return [connection for connection in self.connections if connection.enabled]
def get_option(self, key):
"""
@@ -184,7 +145,7 @@ class FlowGraph(Element):
Returns:
the value held by that param
"""
- return self._options_block.get_param(key).get_evaluated()
+ return self._options_block.params[key].get_evaluated()
def get_run_command(self, file_path, split=False):
run_command = self.get_option('run_command')
@@ -199,73 +160,59 @@ class FlowGraph(Element):
##############################################
# Access Elements
##############################################
- def get_block(self, id):
+ def get_block(self, name):
for block in self.blocks:
- if block.get_id() == id:
+ if block.name == name:
return block
- raise KeyError('No block with ID {!r}'.format(id))
+ raise KeyError('No block with name {!r}'.format(name))
def get_elements(self):
- """
- Get a list of all the elements.
- Always ensure that the options block is in the list (only once).
+ elements = list(self.blocks)
+ elements.extend(self.connections)
+ return elements
- Returns:
- the element list
- """
- options_block_count = self.blocks.count(self._options_block)
- if not options_block_count:
- self.blocks.append(self._options_block)
- for i in range(options_block_count-1):
- self.blocks.remove(self._options_block)
-
- return self.blocks + self.connections
-
- get_children = get_elements
+ def children(self):
+ return itertools.chain(self.blocks, self.connections)
def rewrite(self):
"""
Flag the namespace to be renewed.
"""
-
self.renew_namespace()
- for child in chain(self.blocks, self.connections):
- child.rewrite()
-
- self.bus_ports_rewrite()
+ Element.rewrite(self)
def renew_namespace(self):
namespace = {}
# Load imports
- for expr in self.get_imports():
+ for expr in self.imports():
try:
- exec expr in namespace
+ exec(expr, namespace)
except:
pass
for id, expr in self.get_python_modules():
try:
module = imp.new_module(id)
- exec expr in module.__dict__
+ exec(expr, module.__dict__)
namespace[id] = module
except:
pass
# Load parameters
np = {} # params don't know each other
- for parameter in self.get_parameters():
+ for parameter_block in self.get_parameters():
try:
- value = eval(parameter.get_param('value').to_code(), namespace)
- np[parameter.get_id()] = value
+ value = eval(parameter_block.params['value'].to_code(), namespace)
+ np[parameter_block.name] = value
except:
pass
namespace.update(np) # Merge param namespace
# Load variables
- for variable in self.get_variables():
+ for variable_block in self.get_variables():
try:
- value = eval(variable.get_var_value(), namespace)
- namespace[variable.get_id()] = value
+ value = eval(variable_block.value, namespace, variable_block.namespace)
+ namespace[variable_block.name] = value
except:
pass
@@ -273,39 +220,37 @@ class FlowGraph(Element):
self._eval_cache.clear()
self.namespace.update(namespace)
- def evaluate(self, expr):
+ def evaluate(self, expr, namespace=None, local_namespace=None):
"""
Evaluate the expression.
-
- Args:
- expr: the string expression
- @throw Exception bad expression
-
- Returns:
- the evaluated data
"""
# Evaluate
if not expr:
raise Exception('Cannot evaluate empty statement.')
- return self._eval_cache.setdefault(expr, eval(expr, self.namespace))
+ if namespace is not None:
+ return eval(expr, namespace, local_namespace)
+ else:
+ return self._eval_cache.setdefault(expr, eval(expr, self.namespace))
##############################################
# Add/remove stuff
##############################################
- def new_block(self, key):
+ def new_block(self, block_id, **kwargs):
"""
Get a new block of the specified key.
Add the block to the list of elements.
Args:
- key: the block key
+ block_id: the block key
Returns:
the new block or None if not found
"""
+ if block_id == 'options':
+ return self._options_block
try:
- block = self.platform.get_new_block(self, key)
+ block = self.parent_platform.make_block(self, block_id, **kwargs)
self.blocks.append(block)
except KeyError:
block = None
@@ -323,12 +268,17 @@ class FlowGraph(Element):
Returns:
the new connection
"""
-
- connection = self.platform.Connection(
- flow_graph=self, porta=porta, portb=portb)
- self.connections.append(connection)
+ connection = self.parent_platform.Connection(
+ parent=self, source=porta, sink=portb)
+ self.connections.add(connection)
return connection
+ def disconnect(self, *ports):
+ to_be_removed = [con for con in self.connections
+ if any(port in con for port in ports)]
+ for con in to_be_removed:
+ self.remove_element(con)
+
def remove_element(self, element):
"""
Remove the element from the list of elements.
@@ -336,22 +286,18 @@ class FlowGraph(Element):
If the element is a block, remove its connections.
If the element is a connection, just remove the connection.
"""
+ if element is self._options_block:
+ return
+
if element.is_port:
- # Found a port, set to parent signal block
- element = element.get_parent()
+ element = element.parent_block # remove parent block
if element in self.blocks:
# Remove block, remove all involved connections
- for port in element.get_ports():
- map(self.remove_element, port.get_connections())
+ self.disconnect(*element.ports())
self.blocks.remove(element)
elif element in self.connections:
- if element.is_bus():
- cons_list = []
- for i in map(lambda a: a.get_connections(), element.get_source().get_associated_ports()):
- cons_list.extend(i)
- map(self.remove_element, cons_list)
self.connections.remove(element)
##############################################
@@ -365,173 +311,107 @@ class FlowGraph(Element):
Returns:
a nested data odict
"""
- # sort blocks and connections for nicer diffs
- blocks = sorted(self.blocks, key=lambda b: (
- b.get_key() != 'options', # options to the front
- not b.get_key().startswith('variable'), # then vars
- str(b)
- ))
- connections = sorted(self.connections, key=str)
- n = odict()
- n['timestamp'] = self._timestamp
- n['block'] = [b.export_data() for b in blocks]
- n['connection'] = [c.export_data() for c in connections]
- instructions = odict({
- 'created': '.'.join(self.get_parent().config.version_parts),
- 'format': FLOW_GRAPH_FILE_FORMAT_VERSION,
- })
- return odict({'flow_graph': n, '_instructions': instructions})
-
- def import_data(self, n):
+ def block_order(b):
+ return not b.key.startswith('variable'), b.name # todo: vars still first ?!?
+
+ data = collections.OrderedDict()
+ data['options'] = self._options_block.export_data()
+ data['blocks'] = [b.export_data() for b in sorted(self.blocks, key=block_order)
+ if b is not self._options_block]
+ data['connections'] = sorted(c.export_data() for c in self.connections)
+ data['metadata'] = {'file_format': FLOW_GRAPH_FILE_FORMAT_VERSION}
+ return data
+
+ def _build_depending_hier_block(self, block_id):
+ # we're before the initial fg update(), so no evaluated values!
+ # --> use raw value instead
+ path_param = self._options_block.params['hier_block_src_path']
+ file_path = self.parent_platform.find_file_in_paths(
+ filename=block_id + '.grc',
+ paths=path_param.get_value(),
+ cwd=self.grc_file_path
+ )
+ if file_path: # grc file found. load and get block
+ self.parent_platform.load_and_generate_flow_graph(file_path, hier_only=True)
+ return self.new_block(block_id) # can be None
+
+ def import_data(self, data):
"""
Import blocks and connections into this flow graph.
- Clear this flowgraph of all previous blocks and connections.
+ Clear this flow graph of all previous blocks and connections.
Any blocks or connections in error will be ignored.
Args:
- n: the nested data odict
+ data: the nested data odict
"""
# Remove previous elements
del self.blocks[:]
- del self.connections[:]
- # set file format
- try:
- instructions = n.find('_instructions') or {}
- file_format = int(instructions.get('format', '0')) or _guess_file_format_1(n)
- except:
- file_format = 0
+ self.connections.clear()
- fg_n = n and n.find('flow_graph') or odict() # use blank data if none provided
- self._timestamp = fg_n.find('timestamp') or time.ctime()
+ file_format = data['metadata']['file_format']
# build the blocks
- self._options_block = self.new_block('options')
- for block_n in fg_n.findall('block'):
- key = block_n.find('key')
- block = self._options_block if key == 'options' else self.new_block(key)
-
- if not block:
- # we're before the initial fg update(), so no evaluated values!
- # --> use raw value instead
- path_param = self._options_block.get_param('hier_block_src_path')
- file_path = self.platform.find_file_in_paths(
- filename=key + '.grc',
- paths=path_param.get_value(),
- cwd=self.grc_file_path
- )
- if file_path: # grc file found. load and get block
- self.platform.load_and_generate_flow_graph(file_path, hier_only=True)
- block = self.new_block(key) # can be None
-
- if not block: # looks like this block key cannot be found
- # create a dummy block instead
- block = self.new_block('dummy_block')
- # Ugly ugly ugly
- _initialize_dummy_block(block, block_n)
- print('Block key "%s" not found' % key)
-
- block.import_data(block_n)
+ self._options_block.import_data(name='', **data.get('options', {}))
+ self.blocks.append(self._options_block)
+
+ for block_data in data.get('blocks', []):
+ block_id = block_data['id']
+ block = (
+ self.new_block(block_id) or
+ self._build_depending_hier_block(block_id) or
+ self.new_block(block_id='_dummy', missing_block_id=block_id, **block_data)
+ )
+
+ if isinstance(block, blocks.DummyBlock):
+ print('Block id "%s" not found' % block_id)
+
+ block.import_data(**block_data)
self.rewrite() # evaluate stuff like nports before adding connections
# build the connections
def verify_and_get_port(key, block, dir):
- ports = block.get_sinks() if dir == 'sink' else block.get_sources()
+ ports = block.sinks if dir == 'sink' else block.sources
for port in ports:
- if key == port.get_key():
+ if key == port.key or key + '0' == port.key:
break
- if not key.isdigit() and port.get_type() == '' and key == port.get_name():
+ if not key.isdigit() and port.dtype == '' and key == port.name:
break
else:
if block.is_dummy_block:
- port = _dummy_block_add_port(block, key, dir)
+ port = block.add_missing_port(key, dir)
else:
raise LookupError('%s key %r not in %s block keys' % (dir, key, dir))
return port
- errors = False
- for connection_n in fg_n.findall('connection'):
- # get the block ids and port keys
- source_block_id = connection_n.find('source_block_id')
- sink_block_id = connection_n.find('sink_block_id')
- source_key = connection_n.find('source_key')
- sink_key = connection_n.find('sink_key')
- try:
- source_block = self.get_block(source_block_id)
- sink_block = self.get_block(sink_block_id)
+ had_connect_errors = False
+ _blocks = {block.name: block for block in self.blocks}
+
+ try:
+ # TODO: Add better error handling if no connections exist in the flowgraph file.
+ for src_blk_id, src_port_id, snk_blk_id, snk_port_id in data.get('connections', []):
+ source_block = _blocks[src_blk_id]
+ sink_block = _blocks[snk_blk_id]
# fix old, numeric message ports keys
if file_format < 1:
- source_key, sink_key = _update_old_message_port_keys(
- source_key, sink_key, source_block, sink_block)
+ src_port_id, snk_port_id = _update_old_message_port_keys(
+ src_port_id, snk_port_id, source_block, sink_block)
# build the connection
- source_port = verify_and_get_port(source_key, source_block, 'source')
- sink_port = verify_and_get_port(sink_key, sink_block, 'sink')
+ source_port = verify_and_get_port(src_port_id, source_block, 'source')
+ sink_port = verify_and_get_port(snk_port_id, sink_block, 'sink')
+
self.connect(source_port, sink_port)
- except LookupError as e:
- Messages.send_error_load(
- 'Connection between {}({}) and {}({}) could not be made.\n\t{}'.format(
- source_block_id, source_key, sink_block_id, sink_key, e))
- errors = True
- self.rewrite() # global rewrite
- return errors
+ except (KeyError, LookupError) as e:
+ Messages.send_error_load(
+ 'Connection between {}({}) and {}({}) could not be made.\n\t{}'.format(
+ src_blk_id, src_port_id, snk_blk_id, snk_port_id, e))
+ had_connect_errors = True
- ##############################################
- # Needs to go
- ##############################################
- def bus_ports_rewrite(self):
- # todo: move to block.rewrite()
- for block in self.blocks:
- for direc in ['source', 'sink']:
- if direc == 'source':
- get_p = block.get_sources
- get_p_gui = block.get_sources_gui
- bus_structure = block.form_bus_structure('source')
- else:
- get_p = block.get_sinks
- get_p_gui = block.get_sinks_gui
- bus_structure = block.form_bus_structure('sink')
-
- if 'bus' in map(lambda a: a.get_type(), get_p_gui()):
- if len(get_p_gui()) > len(bus_structure):
- times = range(len(bus_structure), len(get_p_gui()))
- for i in times:
- for connect in get_p_gui()[-1].get_connections():
- block.get_parent().remove_element(connect)
- get_p().remove(get_p_gui()[-1])
- elif len(get_p_gui()) < len(bus_structure):
- n = {'name': 'bus', 'type': 'bus'}
- if True in map(
- lambda a: isinstance(a.get_nports(), int),
- get_p()):
- n['nports'] = str(1)
-
- times = range(len(get_p_gui()), len(bus_structure))
-
- for i in times:
- n['key'] = str(len(get_p()))
- n = odict(n)
- port = block.get_parent().get_parent().Port(
- block=block, n=n, dir=direc)
- get_p().append(port)
-
- if 'bus' in map(lambda a: a.get_type(),
- block.get_sources_gui()):
- for i in range(len(block.get_sources_gui())):
- if len(block.get_sources_gui()[
- i].get_connections()) > 0:
- source = block.get_sources_gui()[i]
- sink = []
-
- for j in range(len(source.get_connections())):
- sink.append(
- source.get_connections()[j].get_sink())
- for elt in source.get_connections():
- self.remove_element(elt)
- for j in sink:
- self.connect(source, j)
+ self.rewrite() # global rewrite
+ return had_connect_errors
def _update_old_message_port_keys(source_key, sink_key, source_block, sink_block):
@@ -548,55 +428,11 @@ def _update_old_message_port_keys(source_key, sink_key, source_block, sink_block
message port.
"""
try:
- # get ports using the "old way" (assuming liner indexed keys)
- source_port = source_block.get_sources()[int(source_key)]
- sink_port = sink_block.get_sinks()[int(sink_key)]
- if source_port.get_type() == "message" and sink_port.get_type() == "message":
- source_key, sink_key = source_port.get_key(), sink_port.get_key()
+ # get ports using the "old way" (assuming linear indexed keys)
+ source_port = source_block.sources[int(source_key)]
+ sink_port = sink_block.sinks[int(sink_key)]
+ if source_port.dtype == "message" and sink_port.dtype == "message":
+ source_key, sink_key = source_port.key, sink_port.key
except (ValueError, IndexError):
pass
return source_key, sink_key # do nothing
-
-
-def _guess_file_format_1(n):
- """
- Try to guess the file format for flow-graph files without version tag
- """
- try:
- has_non_numeric_message_keys = any(not (
- connection_n.find('source_key').isdigit() and
- connection_n.find('sink_key').isdigit()
- ) for connection_n in n.find('flow_graph').findall('connection'))
- if has_non_numeric_message_keys:
- return 1
- except:
- pass
- return 0
-
-
-def _initialize_dummy_block(block, block_n):
- """
- This is so ugly... dummy-fy a block
- Modify block object to get the behaviour for a missing block
- """
-
- block._key = block_n.find('key')
- block.is_dummy_block = lambda: True
- block.is_valid = lambda: False
- block.get_enabled = lambda: False
- for param_n in block_n.findall('param'):
- if param_n['key'] not in block.get_param_keys():
- new_param_n = odict({'key': param_n['key'], 'name': param_n['key'], 'type': 'string'})
- params = block.get_parent().get_parent().Param(block=block, n=new_param_n)
- block.get_params().append(params)
-
-
-def _dummy_block_add_port(block, key, dir):
- """ This is so ugly... Add a port to a dummy-field block """
- port_n = odict({'name': '?', 'key': key, 'type': ''})
- port = block.get_parent().get_parent().Port(block=block, n=port_n, dir=dir)
- if port.is_source:
- block.get_sources().append(port)
- else:
- block.get_sinks().append(port)
- return port
diff --git a/grc/core/Messages.py b/grc/core/Messages.py
index 8daa12c33f..f546c3b62e 100644
--- a/grc/core/Messages.py
+++ b/grc/core/Messages.py
@@ -16,9 +16,10 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
+from __future__ import absolute_import
+
import traceback
import sys
-import os
# A list of functions that can receive a message.
MESSENGERS_LIST = list()
@@ -124,8 +125,8 @@ def send_fail_save(file_path):
send('>>> Error: Cannot save: %s\n' % file_path)
-def send_fail_connection():
- send('>>> Error: Cannot create connection.\n')
+def send_fail_connection(msg=''):
+ send('>>> Error: Cannot create connection.\n' + ('\t{}\n'.format(msg) if msg else ''))
def send_fail_load_preferences(prefs_file_path):
diff --git a/grc/core/Param.py b/grc/core/Param.py
index 00b306ea98..13439f43b4 100644
--- a/grc/core/Param.py
+++ b/grc/core/Param.py
@@ -17,112 +17,31 @@ along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
"""
+from __future__ import absolute_import
+
import ast
-import weakref
+import numbers
import re
+import collections
import textwrap
-from . import Constants
-from .Constants import VECTOR_TYPES, COMPLEX_TYPES, REAL_TYPES, INT_TYPES
-from .Element import Element
-from .utils import odict
-
-# Blacklist certain ids, its not complete, but should help
-import __builtin__
+import six
+from six.moves import builtins, range
+from . import Constants, blocks
+from .base import Element
+from .utils.descriptors import Evaluated, EvaluatedEnum, setup_names
-ID_BLACKLIST = ['self', 'options', 'gr', 'math', 'firdes'] + dir(__builtin__)
+# Blacklist certain ids, its not complete, but should help
+ID_BLACKLIST = ['self', 'options', 'gr', 'math', 'firdes'] + dir(builtins)
try:
from gnuradio import gr
ID_BLACKLIST.extend(attr for attr in dir(gr.top_block()) if not attr.startswith('_'))
-except ImportError:
+except (ImportError, AttributeError):
pass
-_check_id_matcher = re.compile('^[a-z|A-Z]\w*$')
-_show_id_matcher = re.compile('^(variable\w*|parameter|options|notebook)$')
-
-
-def _get_keys(lst):
- return [elem.get_key() for elem in lst]
-
-
-def _get_elem(lst, key):
- try:
- return lst[_get_keys(lst).index(key)]
- except ValueError:
- raise ValueError('Key "{}" not found in {}.'.format(key, _get_keys(lst)))
-
-
-def num_to_str(num):
- """ Display logic for numbers """
- def eng_notation(value, fmt='g'):
- """Convert a number to a string in engineering notation. E.g., 5e-9 -> 5n"""
- template = '{:' + fmt + '}{}'
- magnitude = abs(value)
- for exp, symbol in zip(range(9, -15-1, -3), 'GMk munpf'):
- factor = 10 ** exp
- if magnitude >= factor:
- return template.format(value / factor, symbol.strip())
- return template.format(value, '')
-
- if isinstance(num, COMPLEX_TYPES):
- num = complex(num) # Cast to python complex
- if num == 0:
- return '0'
- output = eng_notation(num.real) if num.real else ''
- output += eng_notation(num.imag, '+g' if output else 'g') + 'j' if num.imag else ''
- return output
- else:
- return str(num)
-
-
-class Option(Element):
-
- def __init__(self, param, n):
- Element.__init__(self, param)
- self._name = n.find('name')
- self._key = n.find('key')
- self._opts = dict()
- opts = n.findall('opt')
- # Test against opts when non enum
- if not self.get_parent().is_enum() and opts:
- raise Exception('Options for non-enum types cannot have sub-options')
- # Extract opts
- for opt in opts:
- # Separate the key:value
- try:
- key, value = opt.split(':')
- except:
- raise Exception('Error separating "{}" into key:value'.format(opt))
- # Test against repeated keys
- if key in self._opts:
- raise Exception('Key "{}" already exists in option'.format(key))
- # Store the option
- self._opts[key] = value
-
- def __str__(self):
- return 'Option {}({})'.format(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()
-
-
-class TemplateArg(object):
+class TemplateArg(str):
"""
A cheetah template argument created from a param.
The str of this class evaluates to the param's to code method.
@@ -130,8 +49,11 @@ class TemplateArg(object):
The __call__ or () method can return the param evaluated to a raw python data type.
"""
- def __init__(self, param):
- self._param = weakref.proxy(param)
+ def __new__(cls, param):
+ value = param.to_code()
+ instance = str.__new__(cls, value)
+ setattr(instance, '_param', param)
+ return instance
def __getitem__(self, item):
return str(self._param.get_opt(item)) if self._param.is_enum() else NotImplemented
@@ -151,217 +73,111 @@ class TemplateArg(object):
return self._param.get_evaluated()
+@setup_names
class Param(Element):
is_param = True
- def __init__(self, block, n):
- """
- Make a new param from nested data.
+ name = Evaluated(str, default='no name')
+ dtype = EvaluatedEnum(Constants.PARAM_TYPE_NAMES, default='raw')
+ hide = EvaluatedEnum('none all part')
+
+ # region init
+ def __init__(self, parent, id, label='', dtype='raw', default='',
+ options=None, option_labels=None, option_attributes=None,
+ category='', hide='none', **_):
+ """Make a new param from nested data"""
+ super(Param, self).__init__(parent)
+ self.key = id
+ self.name = label.strip() or id.title()
+ self.category = category or Constants.DEFAULT_PARAM_TAB
+
+ self.dtype = dtype
+ self.value = self.default = str(default)
+
+ self.options = self._init_options(options or [], option_labels or [],
+ option_attributes or {})
+ self.hide = hide or 'none'
+ # end of args ########################################################
- Args:
- block: the parent element
- n: the nested odict
- """
- # If the base key is a valid param key, copy its data and overlay this params data
- base_key = n.find('base_key')
- if base_key and base_key in block.get_param_keys():
- n_expanded = block.get_param(base_key)._n.copy()
- n_expanded.update(n)
- n = n_expanded
- # Save odict in case this param will be base for another
- self._n = n
- # Parse the data
- self._name = n.find('name')
- self._key = n.find('key')
- value = n.find('value') or ''
- self._type = n.find('type') or 'raw'
- self._hide = n.find('hide') or ''
- self._tab_label = n.find('tab') or block.get_param_tab_labels()[0]
- if self._tab_label not in block.get_param_tab_labels():
- block.get_param_tab_labels().append(self._tab_label)
- # Build the param
- Element.__init__(self, block)
- # Create the Option objects from the n data
- self._options = list()
self._evaluated = None
- for option in map(lambda o: Option(param=self, n=o), n.findall('option')):
- key = option.get_key()
- # Test against repeated keys
- if key in self.get_option_keys():
- raise Exception('Key "{}" already exists in options'.format(key))
- # Store the option
- self.get_options().append(option)
- # Test the enum options
- if self.is_enum():
- # Test against options with identical keys
- if len(set(self.get_option_keys())) != len(self.get_options()):
- raise Exception('Options keys "{}" are not unique.'.format(self.get_option_keys()))
- # Test against inconsistent keys in options
- opt_keys = self.get_options()[0].get_opt_keys()
- for option in self.get_options():
- if set(opt_keys) != set(option.get_opt_keys()):
- raise Exception('Opt keys "{}" are not identical across all options.'.format(opt_keys))
- # If a value is specified, it must be in the options keys
- if value or value in self.get_option_keys():
- self._value = value
- else:
- self._value = self.get_option_keys()[0]
- if self.get_value() not in self.get_option_keys():
- raise Exception('The value "{}" is not in the possible values of "{}".'.format(self.get_value(), self.get_option_keys()))
- else:
- self._value = value or ''
- self._default = value
- self._init = False
+ self._stringify_flag = False
+ self._lisitify_flag = False
self.hostage_cells = set()
- self.template_arg = TemplateArg(self)
-
- def get_types(self):
- return (
- 'raw', 'enum',
- 'complex', 'real', 'float', 'int',
- 'complex_vector', 'real_vector', 'float_vector', 'int_vector',
- 'hex', 'string', 'bool',
- 'file_open', 'file_save', '_multiline', '_multiline_python_external',
- 'id', 'stream_id',
- 'gui_hint',
- 'import',
- )
+ self._init = False
- def __repr__(self):
- """
- Get the repr (nice string format) for this param.
+ @property
+ def template_arg(self):
+ return TemplateArg(self)
- Returns:
- the string representation
- """
- ##################################################
- # Truncate helper method
- ##################################################
- def _truncate(string, style=0):
- max_len = max(27 - len(self.get_name()), 3)
- if len(string) > max_len:
- if style < 0: # Front truncate
- string = '...' + string[3-max_len:]
- elif style == 0: # Center truncate
- string = string[:max_len/2 - 3] + '...' + string[-max_len/2:]
- elif style > 0: # Rear truncate
- string = string[:max_len-3] + '...'
- return string
-
- ##################################################
- # Simple conditions
- ##################################################
- if not self.is_valid():
- return _truncate(self.get_value())
- if self.get_value() in self.get_option_keys():
- return self.get_option(self.get_value()).get_name()
-
- ##################################################
- # Split up formatting by type
- ##################################################
- # Default center truncate
- truncate = 0
- e = self.get_evaluated()
- t = self.get_type()
- if isinstance(e, bool):
- return str(e)
- elif isinstance(e, COMPLEX_TYPES):
- dt_str = num_to_str(e)
- elif isinstance(e, VECTOR_TYPES):
- # Vector types
- if len(e) > 8:
- # Large vectors use code
- dt_str = self.get_value()
- truncate = 1
- else:
- # Small vectors use eval
- dt_str = ', '.join(map(num_to_str, e))
- elif t in ('file_open', 'file_save'):
- dt_str = self.get_value()
- truncate = -1
- else:
- # Other types
- dt_str = str(e)
+ def _init_options(self, values, labels, attributes):
+ """parse option and option attributes"""
+ options = collections.OrderedDict()
+ options.attributes = collections.defaultdict(dict)
- # Done
- return _truncate(dt_str, truncate)
+ padding = [''] * max(len(values), len(labels))
+ attributes = {key: value + padding for key, value in six.iteritems(attributes)}
- def __repr2__(self):
- """
- Get the repr (nice string format) for this param.
+ for i, option in enumerate(values):
+ # Test against repeated keys
+ if option in options:
+ raise KeyError('Value "{}" already exists in options'.format(option))
+ # get label
+ try:
+ label = str(labels[i])
+ except IndexError:
+ label = str(option)
+ # Store the option
+ options[option] = label
+ options.attributes[option] = {attrib: values[i] for attrib, values in six.iteritems(attributes)}
- Returns:
- the string representation
- """
- if self.is_enum():
- return self.get_option(self.get_value()).get_name()
- return self.get_value()
+ default = next(iter(options)) if options else ''
+ if not self.value:
+ self.value = self.default = default
+
+ if self.is_enum() and self.value not in options:
+ self.value = self.default = default # TODO: warn
+ # raise ValueError('The value {!r} is not in the possible values of {}.'
+ # ''.format(self.get_value(), ', '.join(self.options)))
+ return options
+ # endregion
def __str__(self):
- return 'Param - {}({})'.format(self.get_name(), self.get_key())
+ return 'Param - {}({})'.format(self.name, self.key)
- def get_color(self):
- """
- Get the color that represents this param's type.
+ def __repr__(self):
+ return '{!r}.param[{}]'.format(self.parent, self.key)
- Returns:
- a hex color code.
- """
- try:
- return {
- # Number types
- 'complex': Constants.COMPLEX_COLOR_SPEC,
- 'real': Constants.FLOAT_COLOR_SPEC,
- 'float': Constants.FLOAT_COLOR_SPEC,
- 'int': Constants.INT_COLOR_SPEC,
- # Vector types
- 'complex_vector': Constants.COMPLEX_VECTOR_COLOR_SPEC,
- 'real_vector': Constants.FLOAT_VECTOR_COLOR_SPEC,
- 'float_vector': Constants.FLOAT_VECTOR_COLOR_SPEC,
- 'int_vector': Constants.INT_VECTOR_COLOR_SPEC,
- # Special
- 'bool': Constants.INT_COLOR_SPEC,
- 'hex': Constants.INT_COLOR_SPEC,
- 'string': Constants.BYTE_VECTOR_COLOR_SPEC,
- 'id': Constants.ID_COLOR_SPEC,
- 'stream_id': Constants.ID_COLOR_SPEC,
- 'raw': Constants.WILDCARD_COLOR_SPEC,
- }[self.get_type()]
- except:
- return '#FFFFFF'
-
- def get_hide(self):
- """
- Get the hide value from the base class.
- Hide the ID parameter for most blocks. Exceptions below.
- If the parameter controls a port type, vlen, or nports, return part.
- If the parameter is an empty grid position, return part.
- These parameters are redundant to display in the flow graph view.
+ def is_enum(self):
+ return self.get_raw('dtype') == 'enum'
- Returns:
- hide the hide property string
- """
- hide = self.get_parent().resolve_dependencies(self._hide).strip()
- if hide:
- return hide
- # Hide ID in non variable blocks
- if self.get_key() == 'id' and not _show_id_matcher.match(self.get_parent().get_key()):
- return 'part'
- # Hide port controllers for type and nports
- if self.get_key() in ' '.join(map(lambda p: ' '.join([p._type, p._nports]),
- self.get_parent().get_ports())):
- return 'part'
- # Hide port controllers for vlen, when == 1
- if self.get_key() in ' '.join(map(
- lambda p: p._vlen, self.get_parent().get_ports())
- ):
- try:
- if int(self.get_evaluated()) == 1:
- return 'part'
- except:
- pass
- return hide
+ def get_value(self):
+ value = self.value
+ if self.is_enum() and value not in self.options:
+ value = self.default
+ self.set_value(value)
+ return value
+
+ def set_value(self, value):
+ # Must be a string
+ self.value = str(value)
+
+ def set_default(self, value):
+ if self.default == self.value:
+ self.set_value(value)
+ self.default = str(value)
+
+ def rewrite(self):
+ Element.rewrite(self)
+ del self.name
+ del self.dtype
+ del self.hide
+
+ self._evaluated = None
+ try:
+ self._evaluated = self.evaluate()
+ except Exception as e:
+ self.add_error_message(str(e))
def validate(self):
"""
@@ -369,14 +185,8 @@ class Param(Element):
The value must be evaluated and type must a possible type.
"""
Element.validate(self)
- if self.get_type() not in self.get_types():
- self.add_error_message('Type "{}" is not a possible type.'.format(self.get_type()))
-
- self._evaluated = None
- try:
- self._evaluated = self.evaluate()
- except Exception, e:
- self.add_error_message(str(e))
+ if self.dtype not in Constants.PARAM_TYPE_NAMES:
+ self.add_error_message('Type "{}" is not a possible type.'.format(self.dtype))
def get_evaluated(self):
return self._evaluated
@@ -391,165 +201,159 @@ class Param(Element):
self._init = True
self._lisitify_flag = False
self._stringify_flag = False
- t = self.get_type()
- v = self.get_value()
+ dtype = self.dtype
+ expr = self.get_value()
#########################
# Enum Type
#########################
if self.is_enum():
- return v
+ return expr
#########################
# Numeric Types
#########################
- elif t in ('raw', 'complex', 'real', 'float', 'int', 'hex', 'bool'):
+ elif dtype in ('raw', 'complex', 'real', 'float', 'int', 'hex', 'bool'):
# Raise exception if python cannot evaluate this value
try:
- e = self.get_parent().get_parent().evaluate(v)
- except Exception, e:
- raise Exception('Value "{}" cannot be evaluated:\n{}'.format(v, e))
+ value = self.parent_flowgraph.evaluate(expr)
+ except Exception as value:
+ raise Exception('Value "{}" cannot be evaluated:\n{}'.format(expr, value))
# Raise an exception if the data is invalid
- if t == 'raw':
- return e
- elif t == 'complex':
- if not isinstance(e, COMPLEX_TYPES):
- raise Exception('Expression "{}" is invalid for type complex.'.format(str(e)))
- return e
- elif t == 'real' or t == 'float':
- if not isinstance(e, REAL_TYPES):
- raise Exception('Expression "{}" is invalid for type float.'.format(str(e)))
- return e
- elif t == 'int':
- if not isinstance(e, INT_TYPES):
- raise Exception('Expression "{}" is invalid for type integer.'.format(str(e)))
- return e
- elif t == 'hex':
- return hex(e)
- elif t == 'bool':
- if not isinstance(e, bool):
- raise Exception('Expression "{}" is invalid for type bool.'.format(str(e)))
- return e
+ if dtype == 'raw':
+ return value
+ elif dtype == 'complex':
+ if not isinstance(value, Constants.COMPLEX_TYPES):
+ raise Exception('Expression "{}" is invalid for type complex.'.format(str(value)))
+ return value
+ elif dtype in ('real', 'float'):
+ if not isinstance(value, Constants.REAL_TYPES):
+ raise Exception('Expression "{}" is invalid for type float.'.format(str(value)))
+ return value
+ elif dtype == 'int':
+ if not isinstance(value, Constants.INT_TYPES):
+ raise Exception('Expression "{}" is invalid for type integer.'.format(str(value)))
+ return value
+ elif dtype == 'hex':
+ return hex(value)
+ elif dtype == 'bool':
+ if not isinstance(value, bool):
+ raise Exception('Expression "{}" is invalid for type bool.'.format(str(value)))
+ return value
else:
- raise TypeError('Type "{}" not handled'.format(t))
+ raise TypeError('Type "{}" not handled'.format(dtype))
#########################
# Numeric Vector Types
#########################
- elif t in ('complex_vector', 'real_vector', 'float_vector', 'int_vector'):
- if not v:
- # Turn a blank string into an empty list, so it will eval
- v = '()'
- # Raise exception if python cannot evaluate this value
+ elif dtype in ('complex_vector', 'real_vector', 'float_vector', 'int_vector'):
+ default = []
+
+ if not expr:
+ return default # Turn a blank string into an empty list, so it will eval
+
try:
- e = self.get_parent().get_parent().evaluate(v)
- except Exception, e:
- raise Exception('Value "{}" cannot be evaluated:\n{}'.format(v, e))
+ value = self.parent.parent.evaluate(expr)
+ except Exception as value:
+ raise Exception('Value "{}" cannot be evaluated:\n{}'.format(expr, value))
+
+ if not isinstance(value, Constants.VECTOR_TYPES):
+ self._lisitify_flag = True
+ value = [value]
+
# Raise an exception if the data is invalid
- if t == 'complex_vector':
- if not isinstance(e, VECTOR_TYPES):
- self._lisitify_flag = True
- e = [e]
- if not all([isinstance(ei, COMPLEX_TYPES) for ei in e]):
- raise Exception('Expression "{}" is invalid for type complex vector.'.format(str(e)))
- return e
- elif t == 'real_vector' or t == 'float_vector':
- if not isinstance(e, VECTOR_TYPES):
- self._lisitify_flag = True
- e = [e]
- if not all([isinstance(ei, REAL_TYPES) for ei in e]):
- raise Exception('Expression "{}" is invalid for type float vector.'.format(str(e)))
- return e
- elif t == 'int_vector':
- if not isinstance(e, VECTOR_TYPES):
- self._lisitify_flag = True
- e = [e]
- if not all([isinstance(ei, INT_TYPES) for ei in e]):
- raise Exception('Expression "{}" is invalid for type integer vector.'.format(str(e)))
- return e
+ if dtype == 'complex_vector' and not all(isinstance(item, numbers.Complex) for item in value):
+ raise Exception('Expression "{}" is invalid for type complex vector.'.format(value))
+ elif dtype in ('real_vector', 'float_vector') and not all(isinstance(item, numbers.Real) for item in value):
+ raise Exception('Expression "{}" is invalid for type float vector.'.format(value))
+ elif dtype == 'int_vector' and not all(isinstance(item, Constants.INT_TYPES) for item in value):
+ raise Exception('Expression "{}" is invalid for type integer vector.'.format(str(value)))
+ return value
#########################
# String Types
#########################
- elif t in ('string', 'file_open', 'file_save', '_multiline', '_multiline_python_external'):
+ elif dtype in ('string', 'file_open', 'file_save', '_multiline', '_multiline_python_external'):
# Do not check if file/directory exists, that is a runtime issue
try:
- e = self.get_parent().get_parent().evaluate(v)
- if not isinstance(e, str):
+ value = self.parent.parent.evaluate(expr)
+ if not isinstance(value, str):
raise Exception()
except:
self._stringify_flag = True
- e = str(v)
- if t == '_multiline_python_external':
- ast.parse(e) # Raises SyntaxError
- return e
+ value = str(expr)
+ if dtype == '_multiline_python_external':
+ ast.parse(value) # Raises SyntaxError
+ return value
#########################
# Unique ID Type
#########################
- elif t == 'id':
- # Can python use this as a variable?
- if not _check_id_matcher.match(v):
- raise Exception('ID "{}" must begin with a letter and may contain letters, numbers, and underscores.'.format(v))
- ids = [param.get_value() for param in self.get_all_params(t, 'id')]
-
- if v in ID_BLACKLIST:
- raise Exception('ID "{}" is blacklisted.'.format(v))
-
- if self._key == 'id':
- # Id should only appear once, or zero times if block is disabled
- if ids.count(v) > 1:
- raise Exception('ID "{}" is not unique.'.format(v))
- else:
- # Id should exist to be a reference
- if ids.count(v) < 1:
- raise Exception('ID "{}" does not exist.'.format(v))
-
- return v
+ elif dtype == 'id':
+ self.validate_block_id()
+ return expr
#########################
# Stream ID Type
#########################
- elif t == 'stream_id':
- # Get a list of all stream ids used in the virtual sinks
- ids = [param.get_value() for param in filter(
- lambda p: p.get_parent().is_virtual_sink(),
- self.get_all_params(t),
- )]
- # Check that the virtual sink's stream id is unique
- if self.get_parent().is_virtual_sink():
- # Id should only appear once, or zero times if block is disabled
- if ids.count(v) > 1:
- raise Exception('Stream ID "{}" is not unique.'.format(v))
- # Check that the virtual source's steam id is found
- if self.get_parent().is_virtual_source():
- if v not in ids:
- raise Exception('Stream ID "{}" is not found.'.format(v))
- return v
+ elif dtype == 'stream_id':
+ self.validate_stream_id()
+ return expr
#########################
# GUI Position/Hint
#########################
- elif t == 'gui_hint':
- if self.get_parent().get_state() == Constants.BLOCK_DISABLED:
+ elif dtype == 'gui_hint':
+ if self.parent_block.state == 'disabled':
return ''
else:
- return self.parse_gui_hint(v)
+ return self.parse_gui_hint(expr)
#########################
# Import Type
#########################
- elif t == 'import':
+ elif dtype == 'import':
# New namespace
n = dict()
try:
- exec v in n
+ exec(expr, n)
except ImportError:
- raise Exception('Import "{}" failed.'.format(v))
+ raise Exception('Import "{}" failed.'.format(expr))
except Exception:
- raise Exception('Bad import syntax: "{}".'.format(v))
- return filter(lambda k: str(k) != '__builtins__', n.keys())
+ raise Exception('Bad import syntax: "{}".'.format(expr))
+ return [k for k in list(n.keys()) if str(k) != '__builtins__']
#########################
else:
- raise TypeError('Type "{}" not handled'.format(t))
+ raise TypeError('Type "{}" not handled'.format(dtype))
+
+ def validate_block_id(self):
+ value = self.value
+ # Can python use this as a variable?
+ if not re.match(r'^[a-z|A-Z]\w*$', value):
+ raise Exception('ID "{}" must begin with a letter and may contain letters, numbers, '
+ 'and underscores.'.format(value))
+ if value in ID_BLACKLIST:
+ raise Exception('ID "{}" is blacklisted.'.format(value))
+ block_names = [block.name for block in self.parent_flowgraph.iter_enabled_blocks()]
+ # Id should only appear once, or zero times if block is disabled
+ if self.key == 'id' and block_names.count(value) > 1:
+ raise Exception('ID "{}" is not unique.'.format(value))
+ elif value not in block_names:
+ raise Exception('ID "{}" does not exist.'.format(value))
+ return value
+
+ def validate_stream_id(self):
+ value = self.value
+ stream_ids = [
+ block.params['stream_id'].value
+ for block in self.parent_flowgraph.iter_enabled_blocks()
+ if isinstance(block, blocks.VirtualSink)
+ ]
+ # Check that the virtual sink's stream id is unique
+ if isinstance(self.parent_block, blocks.VirtualSink) and stream_ids.count(value) >= 2:
+ # Id should only appear once, or zero times if block is disabled
+ raise Exception('Stream ID "{}" is not unique.'.format(value))
+ # Check that the virtual source's steam id is found
+ elif isinstance(self.parent_block, blocks.VirtualSource) and value not in stream_ids:
+ raise Exception('Stream ID "{}" is not found.'.format(value))
def to_code(self):
"""
@@ -560,8 +364,9 @@ class Param(Element):
Returns:
a string representing the code
"""
+ self._init = True
v = self.get_value()
- t = self.get_type()
+ t = self.dtype
# String types
if t in ('string', 'file_open', 'file_save', '_multiline', '_multiline_python_external'):
if not self._init:
@@ -579,99 +384,18 @@ class Param(Element):
else:
return v
- def get_all_params(self, type, key=None):
- """
- Get all the params from the flowgraph that have the given type and
- optionally a given key
-
- Args:
- type: the specified type
- key: the key to match against
-
- Returns:
- a list of params
- """
- return sum([filter(lambda p: ((p.get_type() == type) and ((key is None) or (p.get_key() == key))), block.get_params()) for block in self.get_parent().get_parent().get_enabled_blocks()], [])
-
- def is_enum(self):
- return self._type == 'enum'
-
- 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):
- # Must be a string
- self._value = str(value)
-
- def set_default(self, value):
- if self._default == self._value:
- self.set_value(value)
- self._default = str(value)
-
- def get_type(self):
- return self.get_parent().resolve_dependencies(self._type)
-
- def get_tab_label(self):
- return self._tab_label
-
- def get_name(self):
- return self.get_parent().resolve_dependencies(self._name).strip()
-
- def get_key(self):
- return self._key
-
- ##############################################
- # Access Options
- ##############################################
- def get_option_keys(self):
- return _get_keys(self.get_options())
-
- def get_option(self, key):
- return _get_elem(self.get_options(), key)
-
- def get_options(self):
- return self._options
-
- ##############################################
- # Access Opts
- ##############################################
- def get_opt_keys(self):
- return self.get_option(self.get_value()).get_opt_keys()
-
- def get_opt(self, key):
- return self.get_option(self.get_value()).get_opt(key)
-
- def get_opts(self):
- return self.get_option(self.get_value()).get_opts()
-
- ##############################################
- # Import/Export Methods
- ##############################################
- def export_data(self):
- """
- Export this param's key/value.
-
- Returns:
- a nested data odict
- """
- n = odict()
- n['key'] = self.get_key()
- n['value'] = self.get_value()
- return n
+ def get_opt(self, item):
+ return self.options.attributes[self.get_value()][item]
##############################################
# GUI Hint
##############################################
- def parse_gui_hint(self, v):
+ def parse_gui_hint(self, expr):
"""
Parse/validate gui hint value.
Args:
- v: gui_hint string from a block's 'gui_hint' param
+ expr: gui_hint string from a block's 'gui_hint' param
Returns:
string of python code for positioning GUI elements in pyQT
@@ -679,12 +403,12 @@ class Param(Element):
self.hostage_cells.clear()
# Parsing
- if ':' in v:
- tab, pos = v.split(':')
- elif ',' in v:
- tab, pos = '', v
+ if ':' in expr:
+ tab, pos = expr.split(':')
+ elif ',' in expr:
+ tab, pos = '', expr
else:
- tab, pos = v, ''
+ tab, pos = expr, ''
if '@' in tab:
tab, index = tab.split('@')
@@ -694,7 +418,7 @@ class Param(Element):
# Validation
def parse_pos():
- e = self.get_parent().get_parent().evaluate(pos)
+ e = self.parent_flowgraph.evaluate(pos)
if not isinstance(e, (list, tuple)) or len(e) not in (2, 4) or not all(isinstance(ei, int) for ei in e):
raise Exception('Invalid GUI Hint entered: {e!r} (Must be a list of {{2,4}} non-negative integers).'.format(e=e))
@@ -714,14 +438,13 @@ class Param(Element):
return row, col, row_span, col_span
def validate_tab():
- enabled_blocks = self.get_parent().get_parent().iter_enabled_blocks()
- tabs = (block for block in enabled_blocks
- if block.get_key() == 'qtgui_tab_widget' and block.get_id() == tab)
+ tabs = (block for block in self.parent_flowgraph.iter_enabled_blocks()
+ if block.key == 'qtgui_tab_widget' and block.name == tab)
tab_block = next(iter(tabs), None)
if not tab_block:
raise Exception('Invalid tab name entered: {tab} (Tab name not found).'.format(tab=tab))
- tab_index_size = int(tab_block.get_param('num_tabs').get_value())
+ tab_index_size = int(tab_block.params['num_tabs'].value)
if index >= tab_index_size:
raise Exception('Invalid tab index entered: {tab}@{index} (Index out of range).'.format(
tab=tab, index=index))
@@ -740,7 +463,7 @@ class Param(Element):
collision = next(iter(self.hostage_cells & other.hostage_cells), None)
if collision:
raise Exception('Block {block!r} is also using parent {parent!r}, cell {cell!r}.'.format(
- block=other.get_parent().get_id(), parent=collision[0], cell=collision[1]
+ block=other.parent_block.name, parent=collision[0], cell=collision[1]
))
# Code Generation
@@ -772,3 +495,23 @@ class Param(Element):
widget_str = 'self.{layout}.addWidget({widget})'.format(layout=layout, widget=widget)
return widget_str
+
+ def get_all_params(self, dtype, key=None):
+ """
+ Get all the params from the flowgraph that have the given type and
+ optionally a given key
+
+ Args:
+ type: the specified type
+ key: the key to match against
+
+ Returns:
+ a list of params
+ """
+ params = []
+ for block in self.parent_flowgraph.iter_enabled_blocks():
+ params.extend(
+ param for param in block.params.values()
+ if param.dtype == dtype and (key is None or key == param.name)
+ )
+ return params
diff --git a/grc/core/ParseXML.py b/grc/core/ParseXML.py
index c9f6541ee7..430ba5b474 100644
--- a/grc/core/ParseXML.py
+++ b/grc/core/ParseXML.py
@@ -17,9 +17,13 @@ along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
"""
+from __future__ import absolute_import
+
from lxml import etree
-from .utils import odict
+import six
+from six.moves import map
+
xml_failures = {}
etree.set_default_parser(etree.XMLParser(remove_comments=True))
@@ -75,17 +79,35 @@ def from_file(xml_file):
the nested data with grc version information
"""
xml = etree.parse(xml_file)
- nested_data = _from_file(xml.getroot())
+
+ tag, nested_data = _from_file(xml.getroot())
+ nested_data = {tag: nested_data, '_instructions': {}}
# Get the embedded instructions and build a dictionary item
- nested_data['_instructions'] = {}
xml_instructions = xml.xpath('/processing-instruction()')
- for inst in filter(lambda i: i.target == 'grc', xml_instructions):
- nested_data['_instructions'] = odict(inst.attrib)
+ for inst in xml_instructions:
+ if inst.target != 'grc':
+ continue
+ nested_data['_instructions'] = dict(inst.attrib)
return nested_data
-def _from_file(xml):
+WANT_A_LIST = {
+ '/block': 'import callback param check sink source'.split(),
+ '/block/param_tab_order': 'tab'.split(),
+ '/block/param': 'option'.split(),
+ '/block/param/option': 'opt'.split(),
+ '/flow_graph': 'block connection'.split(),
+ '/flow_graph/block': 'param'.split(),
+ '/cat': 'cat block'.split(),
+ '/cat/cat': 'cat block'.split(),
+ '/cat/cat/cat': 'cat block'.split(),
+ '/cat/cat/cat/cat': 'cat block'.split(),
+ '/domain': 'connection'.split(),
+}
+
+
+def _from_file(xml, parent_tag=''):
"""
Recursively parse the xml tree into nested data format.
@@ -96,21 +118,24 @@ def _from_file(xml):
the nested data
"""
tag = xml.tag
+ tag_path = parent_tag + '/' + tag
+
if not len(xml):
- return odict({tag: xml.text or ''}) # store empty tags (text is None) as empty string
- nested_data = odict()
+ return tag, xml.text or '' # store empty tags (text is None) as empty string
+
+ nested_data = {}
for elem in xml:
- key, value = _from_file(elem).items()[0]
- if key in nested_data:
- nested_data[key].append(value)
+ key, value = _from_file(elem, tag_path)
+
+ if key in WANT_A_LIST.get(tag_path, []):
+ try:
+ nested_data[key].append(value)
+ except KeyError:
+ nested_data[key] = [value]
else:
- nested_data[key] = [value]
- # Delistify if the length of values is 1
- for key, values in nested_data.iteritems():
- if len(values) == 1:
- nested_data[key] = values[0]
+ nested_data[key] = value
- return odict({tag: nested_data})
+ return tag, nested_data
def to_file(nested_data, xml_file):
@@ -127,11 +152,11 @@ def to_file(nested_data, xml_file):
if instructions:
xml_data += etree.tostring(etree.ProcessingInstruction(
'grc', ' '.join(
- "{0}='{1}'".format(*item) for item in instructions.iteritems())
+ "{0}='{1}'".format(*item) for item in six.iteritems(instructions))
), xml_declaration=True, pretty_print=True, encoding='utf-8')
xml_data += etree.tostring(_to_file(nested_data)[0],
pretty_print=True, encoding='utf-8')
- with open(xml_file, 'w') as fp:
+ with open(xml_file, 'wb') as fp:
fp.write(xml_data)
@@ -146,14 +171,14 @@ def _to_file(nested_data):
the xml tree filled with child nodes
"""
nodes = list()
- for key, values in nested_data.iteritems():
+ for key, values in six.iteritems(nested_data):
# Listify the values if not a list
if not isinstance(values, (list, set, tuple)):
values = [values]
for value in values:
node = etree.Element(key)
- if isinstance(value, (str, unicode)):
- node.text = unicode(value)
+ if isinstance(value, (str, six.text_type)):
+ node.text = six.text_type(value)
else:
node.extend(_to_file(value))
nodes.append(node)
diff --git a/grc/core/Platform.py b/grc/core/Platform.py
deleted file mode 100644
index 3ff3700a6b..0000000000
--- a/grc/core/Platform.py
+++ /dev/null
@@ -1,309 +0,0 @@
-"""
-Copyright 2008-2016 Free Software Foundation, Inc.
-This file is part of GNU Radio
-
-GNU Radio Companion is free software; you can redistribute it and/or
-modify it under the terms of the GNU General Public License
-as published by the Free Software Foundation; either version 2
-of the License, or (at your option) any later version.
-
-GNU Radio Companion is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-GNU General Public License for more details.
-
-You should have received a copy of the GNU General Public License
-along with this program; if not, write to the Free Software
-Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
-"""
-
-import os
-import sys
-
-from . import ParseXML, Messages, Constants
-
-from .Config import Config
-from .Element import Element
-from .generator import Generator
-from .FlowGraph import FlowGraph
-from .Connection import Connection
-from .Block import Block
-from .Port import Port
-from .Param import Param
-
-from .utils import odict, extract_docs, hide_bokeh_gui_options_if_not_installed
-
-
-class Platform(Element):
-
- Config = Config
- Generator = Generator
- FlowGraph = FlowGraph
- Connection = Connection
- Block = Block
- Port = Port
- Param = Param
-
- is_platform = True
-
- def __init__(self, *args, **kwargs):
- """ Make a platform for GNU Radio """
- Element.__init__(self)
-
- self.config = self.Config(*args, **kwargs)
-
- self.block_docstrings = {}
- self.block_docstrings_loaded_callback = lambda: None # dummy to be replaced by BlockTreeWindow
-
- self._docstring_extractor = extract_docs.SubprocessLoader(
- callback_query_result=self._save_docstring_extraction_result,
- callback_finished=lambda: self.block_docstrings_loaded_callback()
- )
-
- # Create a dummy flow graph for the blocks
- self._flow_graph = Element(self)
- self._flow_graph.connections = []
-
- self.blocks = odict()
- self._blocks_n = odict()
- self._block_categories = {}
- self.domains = {}
- self.connection_templates = {}
-
- self._auto_hier_block_generate_chain = set()
-
- self.build_block_library()
-
- def __str__(self):
- return 'Platform - {}({})'.format(self.config.key, self.config.name)
-
- @staticmethod
- def find_file_in_paths(filename, paths, cwd):
- """Checks the provided paths relative to cwd for a certain filename"""
- if not os.path.isdir(cwd):
- cwd = os.path.dirname(cwd)
- if isinstance(paths, str):
- paths = (p for p in paths.split(':') if p)
-
- for path in paths:
- path = os.path.expanduser(path)
- if not os.path.isabs(path):
- path = os.path.normpath(os.path.join(cwd, path))
- file_path = os.path.join(path, filename)
- if os.path.exists(os.path.normpath(file_path)):
- return file_path
-
- def load_and_generate_flow_graph(self, file_path, out_path=None, hier_only=False):
- """Loads a flow graph from file and generates it"""
- Messages.set_indent(len(self._auto_hier_block_generate_chain))
- Messages.send('>>> Loading: {}\n'.format(file_path))
- if file_path in self._auto_hier_block_generate_chain:
- Messages.send(' >>> Warning: cyclic hier_block dependency\n')
- return None, None
- self._auto_hier_block_generate_chain.add(file_path)
- try:
- flow_graph = self.get_new_flow_graph()
- flow_graph.grc_file_path = file_path
- # Other, nested hier_blocks might be auto-loaded here
- flow_graph.import_data(self.parse_flow_graph(file_path))
- flow_graph.rewrite()
- flow_graph.validate()
- if not flow_graph.is_valid():
- raise Exception('Flowgraph invalid')
- if hier_only and not flow_graph.get_option('generate_options').startswith('hb'):
- raise Exception('Not a hier block')
- except Exception as e:
- Messages.send('>>> Load Error: {}: {}\n'.format(file_path, str(e)))
- return None, None
- finally:
- self._auto_hier_block_generate_chain.discard(file_path)
- Messages.set_indent(len(self._auto_hier_block_generate_chain))
-
- try:
- generator = self.Generator(flow_graph, out_path or file_path)
- Messages.send('>>> Generating: {}\n'.format(generator.file_path))
- generator.write()
- except Exception as e:
- Messages.send('>>> Generate Error: {}: {}\n'.format(file_path, str(e)))
- return None, None
-
- if flow_graph.get_option('generate_options').startswith('hb'):
- self.load_block_xml(generator.get_file_path_xml())
- return flow_graph, generator.file_path
-
- def build_block_library(self):
- """load the blocks and block tree from the search paths"""
- self._docstring_extractor.start()
- # Reset
- self.blocks.clear()
- self._blocks_n.clear()
- self._block_categories.clear()
- self.domains.clear()
- self.connection_templates.clear()
- ParseXML.xml_failures.clear()
-
- # Try to parse and load blocks
- for xml_file in self.iter_xml_files():
- try:
- if xml_file.endswith("block_tree.xml"):
- self.load_category_tree_xml(xml_file)
- elif xml_file.endswith('domain.xml'):
- self.load_domain_xml(xml_file)
- else:
- self.load_block_xml(xml_file)
- except ParseXML.XMLSyntaxError as e:
- # print >> sys.stderr, 'Warning: Block validation failed:\n\t%s\n\tIgnoring: %s' % (e, xml_file)
- pass
- except Exception as e:
- print >> sys.stderr, 'Warning: XML parsing failed:\n\t%r\n\tIgnoring: %s' % (e, xml_file)
-
- # Add blocks to block tree
- for key, block in self.blocks.iteritems():
- category = self._block_categories.get(key, block.category)
- # Blocks with empty categories are hidden
- if not category:
- continue
- root = category[0]
- if root.startswith('[') and root.endswith(']'):
- category[0] = root[1:-1]
- else:
- category.insert(0, Constants.DEFAULT_BLOCK_MODULE_NAME)
- block.category = category
-
- self._docstring_extractor.finish()
- # self._docstring_extractor.wait()
-
- hide_bokeh_gui_options_if_not_installed(self.blocks['options'])
-
- def iter_xml_files(self):
- """Iterator for block descriptions and category trees"""
- for block_path in self.config.block_paths:
- if os.path.isfile(block_path):
- yield block_path
- elif os.path.isdir(block_path):
- for dirpath, dirnames, filenames in os.walk(block_path):
- for filename in sorted(filter(lambda f: f.endswith('.xml'), filenames)):
- yield os.path.join(dirpath, filename)
-
- def load_block_xml(self, xml_file):
- """Load block description from xml file"""
- # Validate and import
- ParseXML.validate_dtd(xml_file, Constants.BLOCK_DTD)
- n = ParseXML.from_file(xml_file).find('block')
- n['block_wrapper_path'] = xml_file # inject block wrapper path
- # Get block instance and add it to the list of blocks
- block = self.Block(self._flow_graph, n)
- key = block.get_key()
- if key in self.blocks:
- print >> sys.stderr, 'Warning: Block with key "{}" already exists.\n\tIgnoring: {}'.format(key, xml_file)
- else: # Store the block
- self.blocks[key] = block
- self._blocks_n[key] = n
-
- self._docstring_extractor.query(
- block.get_key(),
- block.get_imports(raw=True),
- block.get_make(raw=True)
- )
-
- def load_category_tree_xml(self, xml_file):
- """Validate and parse category tree file and add it to list"""
- ParseXML.validate_dtd(xml_file, Constants.BLOCK_TREE_DTD)
- xml = ParseXML.from_file(xml_file)
- path = []
-
- def load_category(cat_n):
- path.append(cat_n.find('name').strip())
- for block_key in cat_n.findall('block'):
- if block_key not in self._block_categories:
- self._block_categories[block_key] = list(path)
- for sub_cat_n in cat_n.findall('cat'):
- load_category(sub_cat_n)
- path.pop()
-
- load_category(xml.find('cat'))
-
- def load_domain_xml(self, xml_file):
- """Load a domain properties and connection templates from XML"""
- ParseXML.validate_dtd(xml_file, Constants.DOMAIN_DTD)
- n = ParseXML.from_file(xml_file).find('domain')
-
- key = n.find('key')
- if not key:
- print >> sys.stderr, 'Warning: Domain with emtpy key.\n\tIgnoring: {}'.format(xml_file)
- return
- if key in self.domains: # test against repeated keys
- print >> sys.stderr, 'Warning: Domain with key "{}" already exists.\n\tIgnoring: {}'.format(key, xml_file)
- return
-
- #to_bool = lambda s, d: d if s is None else s.lower() not in ('false', 'off', '0', '')
- def to_bool(s, d):
- if s is not None:
- return s.lower() not in ('false', 'off', '0', '')
- return d
-
- color = n.find('color') or ''
- try:
- import gtk # ugly but handy
- gtk.gdk.color_parse(color)
- except (ValueError, ImportError):
- if color: # no color is okay, default set in GUI
- print >> sys.stderr, 'Warning: Can\'t parse color code "{}" for domain "{}" '.format(color, key)
- color = None
-
- self.domains[key] = dict(
- name=n.find('name') or key,
- multiple_sinks=to_bool(n.find('multiple_sinks'), True),
- multiple_sources=to_bool(n.find('multiple_sources'), False),
- color=color
- )
- for connection_n in n.findall('connection'):
- key = (connection_n.find('source_domain'), connection_n.find('sink_domain'))
- if not all(key):
- print >> sys.stderr, 'Warning: Empty domain key(s) in connection template.\n\t{}'.format(xml_file)
- elif key in self.connection_templates:
- print >> sys.stderr, 'Warning: Connection template "{}" already exists.\n\t{}'.format(key, xml_file)
- else:
- self.connection_templates[key] = connection_n.find('make') or ''
-
- def _save_docstring_extraction_result(self, key, docstrings):
- docs = {}
- for match, docstring in docstrings.iteritems():
- if not docstring or match.endswith('_sptr'):
- continue
- docstring = docstring.replace('\n\n', '\n').strip()
- docs[match] = docstring
- self.block_docstrings[key] = docs
-
- ##############################################
- # Access
- ##############################################
-
- def parse_flow_graph(self, flow_graph_file):
- """
- Parse a saved flow graph file.
- Ensure that the file exists, and passes the dtd check.
-
- Args:
- flow_graph_file: the flow graph file
-
- Returns:
- nested data
- @throws exception if the validation fails
- """
- flow_graph_file = flow_graph_file or self.config.default_flow_graph
- open(flow_graph_file, 'r').close() # Test open
- ParseXML.validate_dtd(flow_graph_file, Constants.FLOW_GRAPH_DTD)
- return ParseXML.from_file(flow_graph_file)
-
- def get_new_flow_graph(self):
- return self.FlowGraph(platform=self)
-
- 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_colors(self):
- return [(name, color) for name, key, sizeof, color in Constants.CORE_TYPES]
diff --git a/grc/core/Port.py b/grc/core/Port.py
deleted file mode 100644
index 8549656c9b..0000000000
--- a/grc/core/Port.py
+++ /dev/null
@@ -1,414 +0,0 @@
-"""
-Copyright 2008-2017 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 itertools import chain
-
-from .Constants import DEFAULT_DOMAIN, GR_STREAM_DOMAIN, GR_MESSAGE_DOMAIN
-from .Element import Element
-
-from . import Constants
-
-
-class LoopError(Exception):
- pass
-
-
-def _upstream_ports(port):
- if port.is_sink:
- return _sources_from_virtual_sink_port(port)
- else:
- return _sources_from_virtual_source_port(port)
-
-
-def _sources_from_virtual_sink_port(sink_port, _traversed=None):
- """
- Resolve the source port that is connected to the given virtual sink port.
- Use the get source from virtual source to recursively resolve subsequent ports.
- """
- source_ports_per_virtual_connection = (
- # there can be multiple ports per virtual connection
- _sources_from_virtual_source_port(c.get_source(), _traversed) # type: list
- for c in sink_port.get_enabled_connections()
- )
- return list(chain(*source_ports_per_virtual_connection)) # concatenate generated lists of ports
-
-
-def _sources_from_virtual_source_port(source_port, _traversed=None):
- """
- Recursively resolve source ports over the virtual connections.
- Keep track of traversed sources to avoid recursive loops.
- """
- _traversed = set(_traversed or []) # a new set!
- if source_port in _traversed:
- raise LoopError('Loop found when resolving port type')
- _traversed.add(source_port)
-
- block = source_port.get_parent()
- flow_graph = block.get_parent()
-
- if not block.is_virtual_source():
- return [source_port] # nothing to resolve, we're done
-
- stream_id = block.get_param('stream_id').get_value()
-
- # currently the validation does not allow multiple virtual sinks and one virtual source
- # but in the future it may...
- connected_virtual_sink_blocks = (
- b for b in flow_graph.get_enabled_blocks()
- if b.is_virtual_sink() and b.get_param('stream_id').get_value() == stream_id
- )
- source_ports_per_virtual_connection = (
- _sources_from_virtual_sink_port(b.get_sinks()[0], _traversed) # type: list
- for b in connected_virtual_sink_blocks
- )
- return list(chain(*source_ports_per_virtual_connection)) # concatenate generated lists of ports
-
-
-def _downstream_ports(port):
- if port.is_source:
- return _sinks_from_virtual_source_port(port)
- else:
- return _sinks_from_virtual_sink_port(port)
-
-
-def _sinks_from_virtual_source_port(source_port, _traversed=None):
- """
- Resolve the sink port that is connected to the given virtual source port.
- Use the get sink from virtual sink to recursively resolve subsequent ports.
- """
- sink_ports_per_virtual_connection = (
- # there can be multiple ports per virtual connection
- _sinks_from_virtual_sink_port(c.get_sink(), _traversed) # type: list
- for c in source_port.get_enabled_connections()
- )
- return list(chain(*sink_ports_per_virtual_connection)) # concatenate generated lists of ports
-
-
-def _sinks_from_virtual_sink_port(sink_port, _traversed=None):
- """
- Recursively resolve sink ports over the virtual connections.
- Keep track of traversed sinks to avoid recursive loops.
- """
- _traversed = set(_traversed or []) # a new set!
- if sink_port in _traversed:
- raise LoopError('Loop found when resolving port type')
- _traversed.add(sink_port)
-
- block = sink_port.get_parent()
- flow_graph = block.get_parent()
-
- if not block.is_virtual_sink():
- return [sink_port]
-
- stream_id = block.get_param('stream_id').get_value()
-
- connected_virtual_source_blocks = (
- b for b in flow_graph.get_enabled_blocks()
- if b.is_virtual_source() and b.get_param('stream_id').get_value() == stream_id
- )
- sink_ports_per_virtual_connection = (
- _sinks_from_virtual_source_port(b.get_sources()[0], _traversed) # type: list
- for b in connected_virtual_source_blocks
- )
- return list(chain(*sink_ports_per_virtual_connection)) # concatenate generated lists of ports
-
-
-class Port(Element):
-
- is_port = True
-
- def __init__(self, block, n, dir):
- """
- Make a new port from nested data.
-
- Args:
- block: the parent element
- n: the nested odict
- dir: the direction
- """
- self._n = n
- if n['type'] == 'message':
- n['domain'] = GR_MESSAGE_DOMAIN
- if 'domain' not in n:
- n['domain'] = DEFAULT_DOMAIN
- elif n['domain'] == GR_MESSAGE_DOMAIN:
- n['key'] = n['name']
- n['type'] = 'message' # For port color
- if not n.find('key'):
- n['key'] = str(next(block.port_counters[dir == 'source']))
-
- # Build the port
- Element.__init__(self, block)
- # Grab the data
- self._name = n['name']
- self._key = n['key']
- self._type = n['type'] or ''
- self._domain = n['domain']
- self._hide = n.find('hide') or ''
- self._dir = dir
- self._hide_evaluated = False # Updated on rewrite()
-
- self._nports = n.find('nports') or ''
- self._vlen = n.find('vlen') or ''
- self._optional = n.find('optional') or ''
- self._optional_evaluated = False # Updated on rewrite()
- self._clones = [] # References to cloned ports (for nports > 1)
-
- def __str__(self):
- if self.is_source:
- return 'Source - {}({})'.format(self.get_name(), self.get_key())
- if self.is_sink:
- return 'Sink - {}({})'.format(self.get_name(), self.get_key())
-
- def get_types(self):
- return Constants.TYPE_TO_SIZEOF.keys()
-
- def is_type_empty(self):
- return not self._n['type'] or not self.get_parent().resolve_dependencies(self._n['type'])
-
- def validate(self):
- if self.get_type() not in self.get_types():
- self.add_error_message('Type "{}" is not a possible type.'.format(self.get_type()))
- platform = self.get_parent().get_parent().get_parent()
- if self.get_domain() not in platform.domains:
- self.add_error_message('Domain key "{}" is not registered.'.format(self.get_domain()))
- if not self.get_enabled_connections() and not self.get_optional():
- self.add_error_message('Port is not connected.')
-
- def rewrite(self):
- """
- Handle the port cloning for virtual blocks.
- """
- del self._error_messages[:]
- if self.is_type_empty():
- self.resolve_empty_type()
-
- hide = self.get_parent().resolve_dependencies(self._hide).strip().lower()
- self._hide_evaluated = False if hide in ('false', 'off', '0') else bool(hide)
- optional = self.get_parent().resolve_dependencies(self._optional).strip().lower()
- self._optional_evaluated = False if optional in ('false', 'off', '0') else bool(optional)
-
- # Update domain if was deduced from (dynamic) port type
- type_ = self.get_type()
- if self._domain == GR_STREAM_DOMAIN and type_ == "message":
- self._domain = GR_MESSAGE_DOMAIN
- self._key = self._name
- if self._domain == GR_MESSAGE_DOMAIN and type_ != "message":
- self._domain = GR_STREAM_DOMAIN
- self._key = '0' # Is rectified in rewrite()
-
- def resolve_virtual_source(self):
- """Only used by Generator after validation is passed"""
- return _upstream_ports(self)
-
- def resolve_empty_type(self):
- def find_port(finder):
- try:
- return next((p for p in finder(self) if not p.is_type_empty()), None)
- except LoopError as error:
- self.add_error_message(str(error))
- except (StopIteration, Exception) as error:
- pass
-
- try:
- port = find_port(_upstream_ports) or find_port(_downstream_ports)
- self._type = str(port.get_type())
- self._vlen = str(port.get_vlen())
- except Exception:
- # Reset type and vlen
- self._type = self._vlen = ''
-
- def get_vlen(self):
- """
- Get the vector length.
- If the evaluation of vlen cannot be cast to an integer, return 1.
-
- Returns:
- 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 a positive integer, return 1.
-
- Returns:
- the number of ports or 1
- """
- if self._nports == '':
- return ''
-
- nports = self.get_parent().resolve_dependencies(self._nports)
- try:
- return max(1, int(self.get_parent().get_parent().evaluate(nports)))
- except:
- return 1
-
- def get_optional(self):
- return self._optional_evaluated
-
- 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.
-
- Returns:
- a hex color code.
- """
- try:
- color = Constants.TYPE_TO_COLOR[self.get_type()]
- vlen = self.get_vlen()
- if vlen == 1:
- return color
- color_val = int(color[1:], 16)
- r = (color_val >> 16) & 0xff
- g = (color_val >> 8) & 0xff
- b = (color_val >> 0) & 0xff
- dark = (0, 0, 30, 50, 70)[min(4, vlen)]
- r = max(r-dark, 0)
- g = max(g-dark, 0)
- b = max(b-dark, 0)
- # TODO: Change this to .format()
- return '#%.2x%.2x%.2x' % (r, g, b)
- except:
- return '#FFFFFF'
-
- def get_clones(self):
- """
- Get the clones of this master port (nports > 1)
-
- Returns:
- a list of ports
- """
- return self._clones
-
- def add_clone(self):
- """
- Create a clone of this (master) port and store a reference in self._clones.
-
- The new port name (and key for message ports) will have index 1... appended.
- If this is the first clone, this (master) port will get a 0 appended to its name (and key)
-
- Returns:
- the cloned port
- """
- # Add index to master port name if there are no clones yet
- if not self._clones:
- self._name = self._n['name'] + '0'
- # Also update key for none stream ports
- if not self._key.isdigit():
- self._key = self._name
-
- # Prepare a copy of the odict for the clone
- n = self._n.copy()
- # Remove nports from the key so the copy cannot be a duplicator
- if 'nports' in n:
- n.pop('nports')
- n['name'] = self._n['name'] + str(len(self._clones) + 1)
- # Dummy value 99999 will be fixed later
- n['key'] = '99999' if self._key.isdigit() else n['name']
-
- # Clone
- port = self.__class__(self.get_parent(), n, self._dir)
- self._clones.append(port)
- return port
-
- def remove_clone(self, port):
- """
- Remove a cloned port (from the list of clones only)
- Remove the index 0 of the master port name (and key9 if there are no more clones left
- """
- self._clones.remove(port)
- # Remove index from master port name if there are no more clones
- if not self._clones:
- self._name = self._n['name']
- # Also update key for none stream ports
- if not self._key.isdigit():
- self._key = self._name
-
- def get_name(self):
- number = ''
- if self.get_type() == 'bus':
- busses = filter(lambda a: a._dir == self._dir, self.get_parent().get_ports_gui())
- number = str(busses.index(self)) + '#' + str(len(self.get_associated_ports()))
- return self._name + number
-
- def get_key(self):
- return self._key
-
- @property
- def is_sink(self):
- return self._dir == 'sink'
-
- @property
- def is_source(self):
- return self._dir == 'source'
-
- def get_type(self):
- return self.get_parent().resolve_dependencies(self._type)
-
- def get_domain(self):
- return self._domain
-
- def get_hide(self):
- return self._hide_evaluated
-
- def get_connections(self):
- """
- Get all connections that use this port.
-
- Returns:
- a list of connection objects
- """
- connections = self.get_parent().get_parent().connections
- connections = filter(lambda c: c.get_source() is self or c.get_sink() is self, connections)
- return connections
-
- def get_enabled_connections(self):
- """
- Get all enabled connections that use this port.
-
- Returns:
- a list of connection objects
- """
- return filter(lambda c: c.get_enabled(), self.get_connections())
-
- def get_associated_ports(self):
- if not self.get_type() == 'bus':
- return [self]
- else:
- if self.is_source:
- get_ports = self.get_parent().get_sources
- bus_structure = self.get_parent().current_bus_structure['source']
- else:
- get_ports = self.get_parent().get_sinks
- bus_structure = self.get_parent().current_bus_structure['sink']
-
- ports = [i for i in get_ports() if not i.get_type() == 'bus']
- if bus_structure:
- busses = [i for i in get_ports() if i.get_type() == 'bus']
- bus_index = busses.index(self)
- ports = filter(lambda a: ports.index(a) in bus_structure[bus_index], ports)
- return ports
diff --git a/grc/core/base.py b/grc/core/base.py
new file mode 100644
index 0000000000..e5ff657d85
--- /dev/null
+++ b/grc/core/base.py
@@ -0,0 +1,164 @@
+# Copyright 2008, 2009, 2015, 2016 Free Software Foundation, Inc.
+# This file is part of GNU Radio
+#
+# GNU Radio Companion is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# GNU Radio Companion is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
+
+import weakref
+
+from .utils.descriptors import lazy_property
+
+
+class Element(object):
+
+ def __init__(self, parent=None):
+ self._parent = weakref.ref(parent) if parent else lambda: None
+ self._error_messages = []
+
+ ##################################################
+ # Element Validation API
+ ##################################################
+ def validate(self):
+ """
+ Validate this element and call validate on all children.
+ Call this base method before adding error messages in the subclass.
+ """
+ del self._error_messages[:]
+
+ for child in self.children():
+ child.validate()
+
+ def is_valid(self):
+ """
+ Is this element valid?
+
+ Returns:
+ true when the element is enabled and has no error messages or is bypassed
+ """
+ if not self.enabled or self.get_bypassed():
+ return True
+ return not next(self.iter_error_messages(), False)
+
+ def add_error_message(self, msg):
+ """
+ Add an error message to the list of errors.
+
+ Args:
+ msg: the error message string
+ """
+ self._error_messages.append(msg)
+
+ def get_error_messages(self):
+ """
+ Get the list of error messages from this element and all of its children.
+ Do not include the error messages from disabled or bypassed children.
+ Cleverly indent the children error messages for printing purposes.
+
+ Returns:
+ a list of error message strings
+ """
+ return [msg if elem is self else "{}:\n\t{}".format(elem, msg.replace("\n", "\n\t"))
+ for elem, msg in self.iter_error_messages()]
+
+ def iter_error_messages(self):
+ """
+ Iterate over error messages. Yields tuples of (element, message)
+ """
+ for msg in self._error_messages:
+ yield self, msg
+ for child in self.children():
+ if not child.enabled or child.get_bypassed():
+ continue
+ for element_msg in child.iter_error_messages():
+ yield element_msg
+
+ def rewrite(self):
+ """
+ Rewrite this element and call rewrite on all children.
+ Call this base method before rewriting the element.
+ """
+ for child in self.children():
+ child.rewrite()
+
+ @property
+ def enabled(self):
+ return True
+
+ def get_bypassed(self):
+ return False
+
+ ##############################################
+ # Tree-like API
+ ##############################################
+ @property
+ def parent(self):
+ return self._parent()
+
+ def get_parent_by_type(self, cls):
+ parent = self.parent
+ if parent is None:
+ return None
+ elif isinstance(parent, cls):
+ return parent
+ else:
+ return parent.get_parent_by_type(cls)
+
+ @lazy_property
+ def parent_platform(self):
+ from .platform import Platform
+ return self.get_parent_by_type(Platform)
+
+ @lazy_property
+ def parent_flowgraph(self):
+ from .FlowGraph import FlowGraph
+ return self.get_parent_by_type(FlowGraph)
+
+ @lazy_property
+ def parent_block(self):
+ from .blocks import Block
+ return self.get_parent_by_type(Block)
+
+ def reset_parents_by_type(self):
+ """Reset all lazy properties"""
+ for name, obj in vars(Element): # explicitly only in Element, not subclasses
+ if isinstance(obj, lazy_property):
+ delattr(self, name)
+
+ def children(self):
+ return
+ yield # empty generator
+
+ ##############################################
+ # Type testing
+ ##############################################
+ is_flow_graph = False
+ is_block = False
+ is_dummy_block = False
+ is_connection = False
+ is_port = False
+ is_param = False
+ is_variable = False
+ is_import = False
+
+ def get_raw(self, name):
+ descriptor = getattr(self.__class__, name, None)
+ if not descriptor:
+ raise ValueError("No evaluated property '{}' found".format(name))
+ return getattr(self, descriptor.name_raw, None) or getattr(self, descriptor.name, None)
+
+ def set_evaluated(self, name, value):
+ descriptor = getattr(self.__class__, name, None)
+ if not descriptor:
+ raise ValueError("No evaluated property '{}' found".format(name))
+ self.__dict__[descriptor.name] = value
diff --git a/grc/core/blocks/__init__.py b/grc/core/blocks/__init__.py
new file mode 100644
index 0000000000..e4a085d477
--- /dev/null
+++ b/grc/core/blocks/__init__.py
@@ -0,0 +1,37 @@
+# Copyright 2016 Free Software Foundation, Inc.
+# This file is part of GNU Radio
+#
+# GNU Radio Companion is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# GNU Radio Companion is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
+
+from __future__ import absolute_import
+
+from ._flags import Flags
+from ._templates import MakoTemplates
+
+from .block import Block
+
+from ._build import build
+
+
+build_ins = {}
+
+
+def register_build_in(cls):
+ build_ins[cls.key] = cls
+ return cls
+
+from .dummy import DummyBlock
+from .embedded_python import EPyBlock, EPyModule
+from .virtual import VirtualSink, VirtualSource
diff --git a/grc/core/blocks/_build.py b/grc/core/blocks/_build.py
new file mode 100644
index 0000000000..9221433387
--- /dev/null
+++ b/grc/core/blocks/_build.py
@@ -0,0 +1,69 @@
+# Copyright 2016 Free Software Foundation, Inc.
+# This file is part of GNU Radio
+#
+# GNU Radio Companion is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# GNU Radio Companion is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
+
+from __future__ import absolute_import
+
+import re
+
+from .block import Block
+from ._flags import Flags
+from ._templates import MakoTemplates
+
+
+def build(id, label='', category='', flags='', documentation='',
+ value=None, asserts=None,
+ parameters=None, inputs=None, outputs=None, templates=None, **kwargs):
+ block_id = id
+
+ cls = type(block_id, (Block,), {})
+ cls.key = block_id
+
+ cls.label = label or block_id.title()
+ cls.category = [cat.strip() for cat in category.split('/') if cat.strip()]
+
+ cls.flags = Flags(flags)
+ if re.match(r'options$|variable|virtual', block_id):
+ cls.flags += Flags.NOT_DSP + Flags.DISABLE_BYPASS
+
+ cls.documentation = {'': documentation.strip('\n\t ').replace('\\\n', '')}
+
+ cls.asserts = [_single_mako_expr(a, block_id) for a in (asserts or [])]
+
+ cls.parameters_data = parameters or []
+ cls.inputs_data = inputs or []
+ cls.outputs_data = outputs or []
+ cls.extra_data = kwargs
+
+ templates = templates or {}
+ cls.templates = MakoTemplates(
+ imports=templates.get('imports', ''),
+ make=templates.get('make', ''),
+ callbacks=templates.get('callbacks', []),
+ var_make=templates.get('var_make', ''),
+ )
+ # todo: MakoTemplates.compile() to check for errors
+
+ cls.value = _single_mako_expr(value, block_id)
+
+ return cls
+
+
+def _single_mako_expr(value, block_id):
+ match = re.match(r'\s*\$\{\s*(.*?)\s*\}\s*', str(value))
+ if value and not match:
+ raise ValueError('{} is not a mako substitution in {}'.format(value, block_id))
+ return match.group(1) if match else None
diff --git a/grc/core/blocks/_flags.py b/grc/core/blocks/_flags.py
new file mode 100644
index 0000000000..ffea2ad569
--- /dev/null
+++ b/grc/core/blocks/_flags.py
@@ -0,0 +1,39 @@
+# Copyright 2016 Free Software Foundation, Inc.
+# This file is part of GNU Radio
+#
+# GNU Radio Companion is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# GNU Radio Companion is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
+
+from __future__ import absolute_import
+
+import six
+
+
+class Flags(six.text_type):
+
+ THROTTLE = 'throttle'
+ DISABLE_BYPASS = 'disable_bypass'
+ NEED_QT_GUI = 'need_qt_gui'
+ DEPRECATED = 'deprecated'
+ NOT_DSP = 'not_dsp'
+
+ def __getattr__(self, item):
+ return item in self
+
+ def __add__(self, other):
+ if not isinstance(other, six.string_types):
+ return NotImplemented
+ return self.__class__(str(self) + other)
+
+ __iadd__ = __add__
diff --git a/grc/core/blocks/_templates.py b/grc/core/blocks/_templates.py
new file mode 100644
index 0000000000..0b15166423
--- /dev/null
+++ b/grc/core/blocks/_templates.py
@@ -0,0 +1,77 @@
+# Copyright 2016 Free Software Foundation, Inc.
+# This file is part of GNU Radio
+#
+# GNU Radio Companion is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# GNU Radio Companion is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
+"""
+This dict class holds a (shared) cache of compiled mako templates.
+These
+
+"""
+from __future__ import absolute_import, print_function
+
+from mako.template import Template
+from mako.exceptions import SyntaxException
+
+from ..errors import TemplateError
+
+
+class MakoTemplates(dict):
+
+ _template_cache = {}
+
+ def __init__(self, _bind_to=None, *args, **kwargs):
+ self.instance = _bind_to
+ dict.__init__(self, *args, **kwargs)
+
+ def __get__(self, instance, owner):
+ if instance is None or self.instance is not None:
+ return self
+ copy = self.__class__(_bind_to=instance, **self)
+ if getattr(instance.__class__, 'templates', None) is self:
+ setattr(instance, 'templates', copy)
+ return copy
+
+ @classmethod
+ def compile(cls, text):
+ text = str(text)
+ try:
+ template = Template(text)
+ except SyntaxException as error:
+ raise TemplateError(text, *error.args)
+
+ cls._template_cache[text] = template
+ return template
+
+ def _get_template(self, text):
+ try:
+ return self._template_cache[str(text)]
+ except KeyError:
+ return self.compile(text)
+
+ def render(self, item):
+ text = self.get(item)
+ if not text:
+ return ''
+ namespace = self.instance.namespace_templates
+
+ try:
+ if isinstance(text, list):
+ templates = (self._get_template(t) for t in text)
+ return [template.render(**namespace) for template in templates]
+ else:
+ template = self._get_template(text)
+ return template.render(**namespace)
+ except Exception as error:
+ raise TemplateError(error, text)
diff --git a/grc/core/blocks/block.py b/grc/core/blocks/block.py
new file mode 100644
index 0000000000..adc046936d
--- /dev/null
+++ b/grc/core/blocks/block.py
@@ -0,0 +1,415 @@
+"""
+Copyright 2008-2015 Free Software Foundation, Inc.
+This file is part of GNU Radio
+
+GNU Radio Companion is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation; either version 2
+of the License, or (at your option) any later version.
+
+GNU Radio Companion is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
+"""
+
+from __future__ import absolute_import
+
+import ast
+import collections
+import itertools
+
+import six
+from six.moves import range
+
+from ._templates import MakoTemplates
+from ._flags import Flags
+
+from ..Constants import ADVANCED_PARAM_TAB
+from ..base import Element
+from ..utils.descriptors import lazy_property
+
+
+def _get_elem(iterable, key):
+ items = list(iterable)
+ for item in items:
+ if item.key == key:
+ return item
+ return ValueError('Key "{}" not found in {}.'.format(key, items))
+
+
+class Block(Element):
+
+ is_block = True
+
+ STATE_LABELS = ['disabled', 'enabled', 'bypassed']
+
+ key = ''
+ label = ''
+ category = ''
+ flags = Flags('')
+ documentation = {'': ''}
+
+ value = None
+ asserts = []
+
+ templates = MakoTemplates()
+ parameters_data = []
+ inputs_data = []
+ outputs_data = []
+
+ extra_data = {}
+
+ # region Init
+ def __init__(self, parent):
+ """Make a new block from nested data."""
+ super(Block, self).__init__(parent)
+ self.params = self._init_params()
+ self.sinks = self._init_ports(self.inputs_data, direction='sink')
+ self.sources = self._init_ports(self.outputs_data, direction='source')
+
+ self.active_sources = [] # on rewrite
+ self.active_sinks = [] # on rewrite
+
+ self.states = {'state': True}
+
+ def _init_params(self):
+ is_dsp_block = not self.flags.not_dsp
+ has_inputs = bool(self.inputs_data)
+ has_outputs = bool(self.outputs_data)
+
+ params = collections.OrderedDict()
+ param_factory = self.parent_platform.make_param
+
+ def add_param(id, **kwargs):
+ params[id] = param_factory(self, id=id, **kwargs)
+
+ add_param(id='id', name='ID', dtype='id',
+ hide='none' if (self.key == 'options' or self.is_variable) else 'part')
+
+ if is_dsp_block:
+ add_param(id='alias', name='Block Alias', dtype='string',
+ hide='part', category=ADVANCED_PARAM_TAB)
+
+ if has_outputs or has_inputs:
+ add_param(id='affinity', name='Core Affinity', dtype='int_vector',
+ hide='part', category=ADVANCED_PARAM_TAB)
+
+ if has_outputs:
+ add_param(id='minoutbuf', name='Min Output Buffer', dtype='int',
+ hide='part', value='0', category=ADVANCED_PARAM_TAB)
+ add_param(id='maxoutbuf', name='Max Output Buffer', dtype='int',
+ hide='part', value='0', category=ADVANCED_PARAM_TAB)
+
+ base_params_n = {}
+ for param_data in self.parameters_data:
+ param_id = param_data['id']
+ if param_id in params:
+ raise Exception('Param id "{}" is not unique'.format(param_id))
+
+ base_key = param_data.get('base_key', None)
+ param_data_ext = base_params_n.get(base_key, {}).copy()
+ param_data_ext.update(param_data)
+
+ add_param(**param_data_ext)
+ base_params_n[param_id] = param_data_ext
+
+ add_param(id='comment', name='Comment', dtype='_multiline', hide='part',
+ value='', category=ADVANCED_PARAM_TAB)
+ return params
+
+ def _init_ports(self, ports_n, direction):
+ ports = []
+ port_factory = self.parent_platform.make_port
+ port_ids = set()
+
+ stream_port_ids = itertools.count()
+
+ for i, port_data in enumerate(ports_n):
+ port_id = port_data.setdefault('id', str(next(stream_port_ids)))
+ if port_id in port_ids:
+ raise Exception('Port id "{}" already exists in {}s'.format(port_id, direction))
+ port_ids.add(port_id)
+
+ port = port_factory(parent=self, direction=direction, **port_data)
+ ports.append(port)
+ return ports
+ # endregion
+
+ # region Rewrite_and_Validation
+ def rewrite(self):
+ """
+ Add and remove ports to adjust for the nports.
+ """
+ Element.rewrite(self)
+
+ def rekey(ports):
+ """Renumber non-message/message ports"""
+ domain_specific_port_index = collections.defaultdict(int)
+ for port in ports:
+ if not port.key.isdigit():
+ continue
+ domain = port.domain
+ port.key = str(domain_specific_port_index[domain])
+ domain_specific_port_index[domain] += 1
+
+ # Adjust nports
+ for ports in (self.sources, self.sinks):
+ self._rewrite_nports(ports)
+ rekey(ports)
+
+ # disconnect hidden ports
+ self.parent_flowgraph.disconnect(*[p for p in self.ports() if p.hidden])
+
+ self.active_sources = [p for p in self.sources if not p.hidden]
+ self.active_sinks = [p for p in self.sinks if not p.hidden]
+
+ def _rewrite_nports(self, ports):
+ for port in ports:
+ if hasattr(port, 'master_port'): # Not a master port and no left-over clones
+ continue
+ nports = port.multiplicity
+ for clone in port.clones[nports-1:]:
+ # Remove excess connections
+ self.parent_flowgraph.disconnect(clone)
+ port.remove_clone(clone)
+ ports.remove(clone)
+ # Add more cloned ports
+ for j in range(1 + len(port.clones), nports):
+ clone = port.add_clone()
+ ports.insert(ports.index(port) + j, clone)
+
+ def validate(self):
+ """
+ Validate this block.
+ Call the base class validate.
+ Evaluate the checks: each check must evaluate to True.
+ """
+ Element.validate(self)
+ self._run_asserts()
+ self._validate_generate_mode_compat()
+ self._validate_var_value()
+
+ def _run_asserts(self):
+ """Evaluate the checks"""
+ for expr in self.asserts:
+ try:
+ if not self.evaluate(expr):
+ self.add_error_message('Assertion "{}" failed.'.format(expr))
+ except:
+ self.add_error_message('Assertion "{}" did not evaluate.'.format(expr))
+
+ def _validate_generate_mode_compat(self):
+ """check if this is a GUI block and matches the selected generate option"""
+ current_generate_option = self.parent.get_option('generate_options')
+
+ def check_generate_mode(label, flag, valid_options):
+ block_requires_mode = (
+ flag in self.flags or self.label.upper().startswith(label)
+ )
+ if block_requires_mode and current_generate_option not in valid_options:
+ self.add_error_message("Can't generate this block in mode: {} ".format(
+ repr(current_generate_option)))
+
+ check_generate_mode('QT GUI', Flags.NEED_QT_GUI, ('qt_gui', 'hb_qt_gui'))
+
+ def _validate_var_value(self):
+ """or variables check the value (only if var_value is used)"""
+ if self.is_variable and self.value != 'value':
+ try:
+ self.parent_flowgraph.evaluate(self.value, local_namespace=self.namespace)
+ except Exception as err:
+ self.add_error_message('Value "{}" cannot be evaluated:\n{}'.format(self.value, err))
+ # endregion
+
+ # region Properties
+
+ def __str__(self):
+ return 'Block - {} - {}({})'.format(self.name, self.label, self.key)
+
+ def __repr__(self):
+ try:
+ name = self.name
+ except Exception:
+ name = self.key
+ return 'block[' + name + ']'
+
+ @property
+ def name(self):
+ return self.params['id'].value
+
+ @lazy_property
+ def is_virtual_or_pad(self):
+ return self.key in ("virtual_source", "virtual_sink", "pad_source", "pad_sink")
+
+ @lazy_property
+ def is_variable(self):
+ return bool(self.value)
+
+ @lazy_property
+ def is_import(self):
+ return self.key == 'import'
+
+ @property
+ def comment(self):
+ return self.params['comment'].value
+
+ @property
+ def state(self):
+ """Gets the block's current state."""
+ state = self.states['state']
+ return state if state in self.STATE_LABELS else 'enabled'
+
+ @state.setter
+ def state(self, value):
+ """Sets the state for the block."""
+ self.states['state'] = value
+
+ # Enable/Disable Aliases
+ @property
+ def enabled(self):
+ """Get the enabled state of the block"""
+ return self.state != 'disabled'
+
+ # endregion
+
+ ##############################################
+ # Getters (old)
+ ##############################################
+ def get_var_make(self):
+ return self.templates.render('var_make')
+
+ def get_var_value(self):
+ return self.templates.render('var_value')
+
+ def get_callbacks(self):
+ """
+ Get a list of function callbacks for this block.
+
+ Returns:
+ a list of strings
+ """
+ def make_callback(callback):
+ if 'self.' in callback:
+ return callback
+ return 'self.{}.{}'.format(self.name, callback)
+ return [make_callback(c) for c in self.templates.render('callbacks')]
+
+ def is_virtual_sink(self):
+ return self.key == 'virtual_sink'
+
+ def is_virtual_source(self):
+ return self.key == 'virtual_source'
+
+ # Block bypassing
+ def get_bypassed(self):
+ """
+ Check if the block is bypassed
+ """
+ return self.state == 'bypassed'
+
+ def set_bypassed(self):
+ """
+ Bypass the block
+
+ Returns:
+ True if block chagnes state
+ """
+ if self.state != 'bypassed' and self.can_bypass():
+ self.state = 'bypassed'
+ return True
+ return False
+
+ def can_bypass(self):
+ """ Check the number of sinks and sources and see if this block can be bypassed """
+ # Check to make sure this is a single path block
+ # Could possibly support 1 to many blocks
+ if len(self.sources) != 1 or len(self.sinks) != 1:
+ return False
+ if not (self.sources[0].dtype == self.sinks[0].dtype):
+ return False
+ if self.flags.disable_bypass:
+ return False
+ return True
+
+ def ports(self):
+ return itertools.chain(self.sources, self.sinks)
+
+ def active_ports(self):
+ return itertools.chain(self.active_sources, self.active_sinks)
+
+ def children(self):
+ return itertools.chain(six.itervalues(self.params), self.ports())
+
+ ##############################################
+ # Access
+ ##############################################
+
+ def get_sink(self, key):
+ return _get_elem(self.sinks, key)
+
+ def get_source(self, key):
+ return _get_elem(self.sources, key)
+
+ ##############################################
+ # Resolve
+ ##############################################
+ @property
+ def namespace(self):
+ return {key: param.get_evaluated() for key, param in six.iteritems(self.params)}
+
+ @property
+ def namespace_templates(self):
+ return {key: param.template_arg for key, param in six.iteritems(self.params)}
+
+ def evaluate(self, expr):
+ return self.parent_flowgraph.evaluate(expr, self.namespace)
+
+ ##############################################
+ # Import/Export Methods
+ ##############################################
+ def export_data(self):
+ """
+ Export this block's params to nested data.
+
+ Returns:
+ a nested data odict
+ """
+ data = collections.OrderedDict()
+ if self.key != 'options':
+ data['name'] = self.name
+ data['id'] = self.key
+ data['parameters'] = collections.OrderedDict(sorted(
+ (param_id, param.value) for param_id, param in self.params.items()
+ if (param_id != 'id' or self.key == 'options')
+ ))
+ data['states'] = collections.OrderedDict(sorted(self.states.items()))
+ return data
+
+ def import_data(self, name, states, parameters, **_):
+ """
+ Import this block's params from nested data.
+ Any param keys that do not exist will be ignored.
+ Since params can be dynamically created based another param,
+ call rewrite, and repeat the load until the params stick.
+ """
+ self.params['id'].value = name
+ self.states.update(states)
+
+ def get_hash():
+ return hash(tuple(hash(v) for v in self.params.values()))
+
+ pre_rewrite_hash = -1
+ while pre_rewrite_hash != get_hash():
+ for key, value in six.iteritems(parameters):
+ try:
+ self.params[key].set_value(value)
+ except KeyError:
+ continue
+ # Store hash and call rewrite
+ pre_rewrite_hash = get_hash()
+ self.rewrite()
diff --git a/grc/core/blocks/dummy.py b/grc/core/blocks/dummy.py
new file mode 100644
index 0000000000..6a5ec2fa72
--- /dev/null
+++ b/grc/core/blocks/dummy.py
@@ -0,0 +1,54 @@
+# Copyright 2016 Free Software Foundation, Inc.
+# This file is part of GNU Radio
+#
+# GNU Radio Companion is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# GNU Radio Companion is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
+
+from __future__ import absolute_import
+
+from . import Block, register_build_in
+
+
+@register_build_in
+class DummyBlock(Block):
+
+ is_dummy_block = True
+
+ label = 'Missing Block'
+ key = '_dummy'
+
+ def __init__(self, parent, missing_block_id, parameters, **_):
+ self.key = missing_block_id
+ super(DummyBlock, self).__init__(parent=parent)
+
+ param_factory = self.parent_platform.make_param
+ for param_id in parameters:
+ self.params.setdefault(param_id, param_factory(parent=self, id=param_id, dtype='string'))
+
+ def is_valid(self):
+ return False
+
+ @property
+ def enabled(self):
+ return False
+
+ def add_missing_port(self, port_id, direction):
+ port = self.parent_platform.make_port(
+ parent=self, direction=direction, id=port_id, name='?', dtype='',
+ )
+ if port.is_source:
+ self.sources.append(port)
+ else:
+ self.sinks.append(port)
+ return port
diff --git a/grc/core/blocks/embedded_python.py b/grc/core/blocks/embedded_python.py
new file mode 100644
index 0000000000..0b5a7a21c5
--- /dev/null
+++ b/grc/core/blocks/embedded_python.py
@@ -0,0 +1,242 @@
+# Copyright 2015-16 Free Software Foundation, Inc.
+# This file is part of GNU Radio
+#
+# GNU Radio Companion is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# GNU Radio Companion is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
+
+from __future__ import absolute_import
+
+from ast import literal_eval
+from textwrap import dedent
+
+from . import Block, register_build_in
+from ._templates import MakoTemplates
+
+from .. import utils
+from ..base import Element
+
+
+DEFAULT_CODE = '''\
+"""
+Embedded Python Blocks:
+
+Each time this file is saved, GRC will instantiate the first class it finds
+to get ports and parameters of your block. The arguments to __init__ will
+be the parameters. All of them are required to have default values!
+"""
+
+import numpy as np
+from gnuradio import gr
+
+
+class blk(gr.sync_block): # other base classes are basic_block, decim_block, interp_block
+ """Embedded Python Block example - a simple multiply const"""
+
+ def __init__(self, example_param=1.0): # only default arguments here
+ """arguments to this function show up as parameters in GRC"""
+ gr.sync_block.__init__(
+ self,
+ name='Embedded Python Block', # will show up in GRC
+ in_sig=[np.complex64],
+ out_sig=[np.complex64]
+ )
+ # if an attribute with the same name as a parameter is found,
+ # a callback is registered (properties work, too).
+ self.example_param = example_param
+
+ def work(self, input_items, output_items):
+ """example: multiply with constant"""
+ output_items[0][:] = input_items[0] * self.example_param
+ return len(output_items[0])
+'''
+
+DOC = """
+This block represents an arbitrary GNU Radio Python Block.
+
+Its source code can be accessed through the parameter 'Code' which opens your editor. \
+Each time you save changes in the editor, GRC will update the block. \
+This includes the number, names and defaults of the parameters, \
+the ports (stream and message) and the block name and documentation.
+
+Block Documentation:
+(will be replaced the docstring of your block class)
+"""
+
+
+@register_build_in
+class EPyBlock(Block):
+
+ key = 'epy_block'
+ label = 'Python Block'
+ documentation = {'': DOC}
+
+ parameters_data = [dict(
+ label='Code',
+ id='_source_code',
+ dtype='_multiline_python_external',
+ value=DEFAULT_CODE,
+ hide='part',
+ )]
+ inputs_data = []
+ outputs_data = []
+
+ def __init__(self, flow_graph, **kwargs):
+ super(EPyBlock, self).__init__(flow_graph, **kwargs)
+ self.states['_io_cache'] = ''
+
+ self._epy_source_hash = -1
+ self._epy_reload_error = None
+
+ def rewrite(self):
+ Element.rewrite(self)
+
+ param_src = self.params['_source_code']
+
+ src = param_src.get_value()
+ src_hash = hash((self.name, src))
+ if src_hash == self._epy_source_hash:
+ return
+
+ try:
+ blk_io = utils.epy_block_io.extract(src)
+
+ except Exception as e:
+ self._epy_reload_error = ValueError(str(e))
+ try: # Load last working block io
+ blk_io_args = literal_eval(self.states['_io_cache'])
+ if len(blk_io_args) == 6:
+ blk_io_args += ([],) # add empty callbacks
+ blk_io = utils.epy_block_io.BlockIO(*blk_io_args)
+ except Exception:
+ return
+ else:
+ self._epy_reload_error = None # Clear previous errors
+ self.states['_io_cache'] = repr(tuple(blk_io))
+
+ # print "Rewriting embedded python block {!r}".format(self.name)
+ self._epy_source_hash = src_hash
+
+ self.label = blk_io.name or blk_io.cls
+ self.documentation = {'': blk_io.doc}
+
+ self.templates['imports'] = 'import ' + self.name
+ self.templates['make'] = '{mod}.{cls}({args})'.format(
+ mod=self.name,
+ cls=blk_io.cls,
+ args=', '.join('{0}=${{ {0} }}'.format(key) for key, _ in blk_io.params))
+ self.templates['callbacks'] = [
+ '{0} = ${{ {0} }}'.format(attr) for attr in blk_io.callbacks
+ ]
+
+ self._update_params(blk_io.params)
+ self._update_ports('in', self.sinks, blk_io.sinks, 'sink')
+ self._update_ports('out', self.sources, blk_io.sources, 'source')
+
+ super(EPyBlock, self).rewrite()
+
+ def _update_params(self, params_in_src):
+ param_factory = self.parent_platform.make_param
+ params = {}
+ for param in list(self.params):
+ if hasattr(param, '__epy_param__'):
+ params[param.key] = param
+ del self.params[param.key]
+
+ for id_, value in params_in_src:
+ try:
+ param = params[id_]
+ if param.default == param.value:
+ param.set_value(value)
+ param.default = str(value)
+ except KeyError: # need to make a new param
+ param = param_factory(
+ parent=self, id=id_, dtype='raw', value=value,
+ name=id_.replace('_', ' ').title(),
+ )
+ setattr(param, '__epy_param__', True)
+ self.params[id_] = param
+
+ def _update_ports(self, label, ports, port_specs, direction):
+ port_factory = self.parent_platform.make_port
+ ports_to_remove = list(ports)
+ iter_ports = iter(ports)
+ ports_new = []
+ port_current = next(iter_ports, None)
+ for key, port_type, vlen in port_specs:
+ reuse_port = (
+ port_current is not None and
+ port_current.dtype == port_type and
+ port_current.vlen == vlen and
+ (key.isdigit() or port_current.key == key)
+ )
+ if reuse_port:
+ ports_to_remove.remove(port_current)
+ port, port_current = port_current, next(iter_ports, None)
+ else:
+ n = dict(name=label + str(key), dtype=port_type, id=key)
+ if port_type == 'message':
+ n['name'] = key
+ n['optional'] = '1'
+ if vlen > 1:
+ n['vlen'] = str(vlen)
+ port = port_factory(self, direction=direction, **n)
+ ports_new.append(port)
+ # replace old port list with new one
+ del ports[:]
+ ports.extend(ports_new)
+ # remove excess port connections
+ self.parent_flowgraph.disconnect(*ports_to_remove)
+
+ def validate(self):
+ super(EPyBlock, self).validate()
+ if self._epy_reload_error:
+ self.params['_source_code'].add_error_message(str(self._epy_reload_error))
+
+
+@register_build_in
+class EPyModule(Block):
+ key = 'epy_module'
+ label = 'Python Module'
+ documentation = {'': dedent("""
+ This block lets you embed a python module in your flowgraph.
+
+ Code you put in this module is accessible in other blocks using the ID of this
+ block. Example:
+
+ If you put
+
+ a = 2
+
+ def double(arg):
+ return 2 * arg
+
+ in a Python Module Block with the ID 'stuff' you can use code like
+
+ stuff.a # evals to 2
+ stuff.double(3) # evals to 6
+
+ to set parameters of other blocks in your flowgraph.
+ """)}
+
+ parameters_data = [dict(
+ label='Code',
+ id='source_code',
+ dtype='_multiline_python_external',
+ value='# this module will be imported in the into your flowgraph',
+ hide='part',
+ )]
+
+ templates = MakoTemplates(
+ imports='import ${ id } # embedded python module',
+ )
diff --git a/grc/core/blocks/virtual.py b/grc/core/blocks/virtual.py
new file mode 100644
index 0000000000..a10853ad1b
--- /dev/null
+++ b/grc/core/blocks/virtual.py
@@ -0,0 +1,76 @@
+# Copyright 2016 Free Software Foundation, Inc.
+# This file is part of GNU Radio
+#
+# GNU Radio Companion is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# GNU Radio Companion is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
+
+from __future__ import absolute_import
+
+import itertools
+
+from . import Block, register_build_in
+
+
+@register_build_in
+class VirtualSink(Block):
+ count = itertools.count()
+
+ key = 'virtual_sink'
+ label = 'Virtual Sink'
+ documentation = {'': ''}
+
+ parameters_data = [dict(
+ label='Stream ID',
+ id='stream_id',
+ dtype='stream_id',
+ )]
+ inputs_data = [dict(
+ domain='stream',
+ dtype=''
+ )]
+
+ def __init__(self, parent, **kwargs):
+ super(VirtualSink, self).__init__(parent, **kwargs)
+ self.params['id'].hide = 'all'
+
+ @property
+ def stream_id(self):
+ return self.params['stream_id'].value
+
+
+@register_build_in
+class VirtualSource(Block):
+ count = itertools.count()
+
+ key = 'virtual_source'
+ label = 'Virtual Source'
+ documentation = {'': ''}
+
+ parameters_data = [dict(
+ label='Stream ID',
+ id='stream_id',
+ dtype='stream_id',
+ )]
+ outputs_data = [dict(
+ domain='stream',
+ dtype=''
+ )]
+
+ def __init__(self, parent, **kwargs):
+ super(VirtualSource, self).__init__(parent, **kwargs)
+ self.params['id'].hide = 'all'
+
+ @property
+ def stream_id(self):
+ return self.params['stream_id'].value
diff --git a/grc/core/default_flow_graph.grc b/grc/core/default_flow_graph.grc
index 059509d34b..9df289f327 100644
--- a/grc/core/default_flow_graph.grc
+++ b/grc/core/default_flow_graph.grc
@@ -1,43 +1,31 @@
-<?xml version="1.0"?>
-<!--
###################################################
-##Default Flow Graph:
-## include an options block and a variable for sample rate
+# Default Flow Graph:
+# Include an options block and a variable for sample rate
###################################################
- -->
-<flow_graph>
- <block>
- <key>options</key>
- <param>
- <key>id</key>
- <value>top_block</value>
- </param>
- <param>
- <key>_coordinate</key>
- <value>(8, 8)</value>
- </param>
- <param>
- <key>_rotation</key>
- <value>0</value>
- </param>
- </block>
- <block>
- <key>variable</key>
- <param>
- <key>id</key>
- <value>samp_rate</value>
- </param>
- <param>
- <key>value</key>
- <value>32000</value>
- </param>
- <param>
- <key>_coordinate</key>
- <value>(8, 160)</value>
- </param>
- <param>
- <key>_rotation</key>
- <value>0</value>
- </param>
- </block>
-</flow_graph>
+
+options:
+ parameters:
+ title: 'top_block'
+ states:
+ coordinate:
+ - 8
+ - 8
+ rotation: 0
+ state: enabled
+
+blocks:
+- name: samp_rate
+ id: variable
+ parameters:
+ comment: ''
+ value: '32000'
+ states:
+ coordinate:
+ - 184
+ - 12
+ rotation: 0
+ state: enabled
+
+metadata:
+ file_format: 1
+ grc_version: 3.8.0
diff --git a/grc/core/errors.py b/grc/core/errors.py
new file mode 100644
index 0000000000..6437cc4fa1
--- /dev/null
+++ b/grc/core/errors.py
@@ -0,0 +1,30 @@
+# Copyright 2016 Free Software Foundation, Inc.
+# This file is part of GNU Radio
+#
+# GNU Radio Companion is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# GNU Radio Companion is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
+
+from __future__ import absolute_import, print_function
+
+
+class GRCError(Exception):
+ """Generic error class"""
+
+
+class BlockLoadError(GRCError):
+ """Error during block loading"""
+
+
+class TemplateError(BlockLoadError):
+ """Mako Template Error"""
diff --git a/grc/core/generator/CMakeLists.txt b/grc/core/generator/CMakeLists.txt
deleted file mode 100644
index 492ad7c4ad..0000000000
--- a/grc/core/generator/CMakeLists.txt
+++ /dev/null
@@ -1,30 +0,0 @@
-# Copyright 2011 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.
-
-file(GLOB py_files "*.py")
-
-GR_PYTHON_INSTALL(
- FILES ${py_files}
- DESTINATION ${GR_PYTHON_DIR}/gnuradio/grc/core/generator
-)
-
-install(FILES
- flow_graph.tmpl
- DESTINATION ${GR_PYTHON_DIR}/gnuradio/grc/core/generator
-)
diff --git a/grc/core/generator/FlowGraphProxy.py b/grc/core/generator/FlowGraphProxy.py
index 3723005576..f438fa0d39 100644
--- a/grc/core/generator/FlowGraphProxy.py
+++ b/grc/core/generator/FlowGraphProxy.py
@@ -16,13 +16,17 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
-class FlowGraphProxy(object):
+from __future__ import absolute_import
+from six.moves import range
+
+
+class FlowGraphProxy(object): # TODO: move this in a refactored Generator
def __init__(self, fg):
- self._fg = fg
+ self.orignal_flowgraph = fg
def __getattr__(self, item):
- return getattr(self._fg, item)
+ return getattr(self.orignal_flowgraph, item)
def get_hier_block_stream_io(self, direction):
"""
@@ -34,7 +38,7 @@ class FlowGraphProxy(object):
Returns:
a list of dicts with: type, label, vlen, size, optional
"""
- return filter(lambda p: p['type'] != "message", self.get_hier_block_io(direction))
+ return [p for p in self.get_hier_block_io(direction) if p['type'] != "message"]
def get_hier_block_message_io(self, direction):
"""
@@ -46,7 +50,7 @@ class FlowGraphProxy(object):
Returns:
a list of dicts with: type, label, vlen, size, optional
"""
- return filter(lambda p: p['type'] == "message", self.get_hier_block_io(direction))
+ return [p for p in self.get_hier_block_io(direction) if p['type'] == "message"]
def get_hier_block_io(self, direction):
"""
@@ -62,16 +66,17 @@ class FlowGraphProxy(object):
self.get_pad_sinks() if direction in ('source', 'out') else []
ports = []
for pad in pads:
+ type_param = pad.params['type']
master = {
- 'label': str(pad.get_param('label').get_evaluated()),
- 'type': str(pad.get_param('type').get_evaluated()),
- 'vlen': str(pad.get_param('vlen').get_value()),
- 'size': pad.get_param('type').get_opt('size'),
- 'optional': bool(pad.get_param('optional').get_evaluated()),
+ 'label': str(pad.params['label'].get_evaluated()),
+ 'type': str(pad.params['type'].get_evaluated()),
+ 'vlen': str(pad.params['vlen'].get_value()),
+ 'size': type_param.options.attributes[type_param.get_value()]['size'],
+ 'optional': bool(pad.params['optional'].get_evaluated()),
}
- num_ports = pad.get_param('num_streams').get_evaluated()
+ num_ports = pad.params['num_streams'].get_evaluated()
if num_ports > 1:
- for i in xrange(num_ports):
+ for i in range(num_ports):
clone = master.copy()
clone['label'] += str(i)
ports.append(clone)
@@ -86,8 +91,8 @@ class FlowGraphProxy(object):
Returns:
a list of pad source blocks in this flow graph
"""
- pads = filter(lambda b: b.get_key() == 'pad_source', self.get_enabled_blocks())
- return sorted(pads, lambda x, y: cmp(x.get_id(), y.get_id()))
+ pads = [b for b in self.get_enabled_blocks() if b.key == 'pad_source']
+ return sorted(pads, key=lambda x: x.name)
def get_pad_sinks(self):
"""
@@ -96,8 +101,8 @@ class FlowGraphProxy(object):
Returns:
a list of pad sink blocks in this flow graph
"""
- pads = filter(lambda b: b.get_key() == 'pad_sink', self.get_enabled_blocks())
- return sorted(pads, lambda x, y: cmp(x.get_id(), y.get_id()))
+ pads = [b for b in self.get_enabled_blocks() if b.key == 'pad_sink']
+ return sorted(pads, key=lambda x: x.name)
def get_pad_port_global_key(self, port):
"""
@@ -112,15 +117,46 @@ class FlowGraphProxy(object):
for pad in pads:
# using the block param 'type' instead of the port domain here
# to emphasize that hier block generation is domain agnostic
- is_message_pad = pad.get_param('type').get_evaluated() == "message"
- if port.get_parent() == pad:
+ is_message_pad = pad.params['type'].get_evaluated() == "message"
+ if port.parent == pad:
if is_message_pad:
- key = pad.get_param('label').get_value()
+ key = pad.params['label'].get_value()
else:
- key = str(key_offset + int(port.get_key()))
+ key = str(key_offset + int(port.key))
return key
else:
# assuming we have either only sources or sinks
if not is_message_pad:
- key_offset += len(pad.get_ports())
- return -1 \ No newline at end of file
+ key_offset += len(pad.sinks) + len(pad.sources)
+ return -1
+
+
+def get_hier_block_io(flow_graph, direction, domain=None):
+ """
+ Get a list of io ports for this flow graph.
+
+ Returns a list of dicts with: type, label, vlen, size, optional
+ """
+ pads = flow_graph.get_pad_sources() if direction in ('sink', 'in') else \
+ flow_graph.get_pad_sinks() if direction in ('source', 'out') else []
+ ports = []
+ for pad in pads:
+ type_param = pad.params['type']
+ master = {
+ 'label': str(pad.params['label'].get_evaluated()),
+ 'type': str(pad.params['type'].get_evaluated()),
+ 'vlen': str(pad.params['vlen'].get_value()),
+ 'size': type_param.options.attributes[type_param.get_value()]['size'],
+ 'optional': bool(pad.params['optional'].get_evaluated()),
+ }
+ num_ports = pad.params['num_streams'].get_evaluated()
+ if num_ports > 1:
+ for i in range(num_ports):
+ clone = master.copy()
+ clone['label'] += str(i)
+ ports.append(clone)
+ else:
+ ports.append(master)
+ if domain is not None:
+ ports = [p for p in ports if p.domain == domain]
+ return ports
diff --git a/grc/core/generator/Generator.py b/grc/core/generator/Generator.py
index c94e90a321..62dc26b8a8 100644
--- a/grc/core/generator/Generator.py
+++ b/grc/core/generator/Generator.py
@@ -16,22 +16,18 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
-import codecs
+from __future__ import absolute_import
+
import os
-import tempfile
-from Cheetah.Template import Template
+from mako.template import Template
-from .FlowGraphProxy import FlowGraphProxy
-from .. import ParseXML, Messages
-from ..Constants import (
- TOP_BLOCK_FILE_MODE, BLOCK_FLAG_NEED_QT_GUI,
- HIER_BLOCK_FILE_MODE, BLOCK_DTD
-)
-from ..utils import expr_utils, odict
+from .hier_block import HierBlockGenerator, QtHierBlockGenerator
+from .top_block import TopBlockGenerator
DATA_DIR = os.path.dirname(__file__)
-FLOW_GRAPH_TEMPLATE = os.path.join(DATA_DIR, 'flow_graph.tmpl')
+FLOW_GRAPH_TEMPLATE = os.path.join(DATA_DIR, 'flow_graph.py.mako')
+flow_graph_template = Template(filename=FLOW_GRAPH_TEMPLATE)
class Generator(object):
@@ -59,339 +55,3 @@ class Generator(object):
def __getattr__(self, item):
"""get all other attrib from actual generator object"""
return getattr(self._generator, item)
-
-
-class TopBlockGenerator(object):
-
- def __init__(self, flow_graph, file_path):
- """
- Initialize the top block generator object.
-
- Args:
- flow_graph: the flow graph object
- file_path: the path to write the file to
- """
- self._flow_graph = FlowGraphProxy(flow_graph)
- self._generate_options = self._flow_graph.get_option('generate_options')
- self._mode = TOP_BLOCK_FILE_MODE
- dirname = os.path.dirname(file_path)
- # Handle the case where the directory is read-only
- # In this case, use the system's temp directory
- if not os.access(dirname, os.W_OK):
- dirname = tempfile.gettempdir()
- filename = self._flow_graph.get_option('id') + '.py'
- self.file_path = os.path.join(dirname, filename)
- self._dirname = dirname
-
- def get_file_path(self):
- return self.file_path
-
- def write(self):
- """generate output and write it to files"""
- # Do throttle warning
- throttling_blocks = filter(lambda b: b.throtteling(), self._flow_graph.get_enabled_blocks())
- if not throttling_blocks and not self._generate_options.startswith('hb'):
- Messages.send_warning("This flow graph may not have flow control: "
- "no audio or RF hardware blocks found. "
- "Add a Misc->Throttle block to your flow "
- "graph to avoid CPU congestion.")
- if len(throttling_blocks) > 1:
- keys = set(map(lambda b: b.get_key(), throttling_blocks))
- if len(keys) > 1 and 'blocks_throttle' in keys:
- Messages.send_warning("This flow graph contains a throttle "
- "block and another rate limiting block, "
- "e.g. a hardware source or sink. "
- "This is usually undesired. Consider "
- "removing the throttle block.")
- # Generate
- for filename, data in self._build_python_code_from_template():
- with codecs.open(filename, 'w', encoding='utf-8') as fp:
- fp.write(data)
- if filename == self.file_path:
- try:
- os.chmod(filename, self._mode)
- except:
- pass
-
- def _build_python_code_from_template(self):
- """
- Convert the flow graph to python code.
-
- Returns:
- a string of python code
- """
- output = list()
-
- fg = self._flow_graph
- title = fg.get_option('title') or fg.get_option('id').replace('_', ' ').title()
- imports = fg.get_imports()
- variables = fg.get_variables()
- parameters = fg.get_parameters()
- monitors = fg.get_monitors()
-
- # List of blocks not including variables and imports and parameters and disabled
- def _get_block_sort_text(block):
- code = block.get_make().replace(block.get_id(), ' ')
- try:
- code += block.get_param('gui_hint').get_value() # Newer gui markup w/ qtgui
- except:
- pass
- return code
-
- blocks_all = expr_utils.sort_objects(
- filter(lambda b: b.get_enabled() and not b.get_bypassed(), fg.blocks),
- lambda b: b.get_id(), _get_block_sort_text
- )
- deprecated_block_keys = set(block.get_name() for block in blocks_all if block.is_deprecated)
- for key in deprecated_block_keys:
- Messages.send_warning("The block {!r} is deprecated.".format(key))
-
- # List of regular blocks (all blocks minus the special ones)
- blocks = filter(lambda b: b not in (imports + parameters), blocks_all)
-
- for block in blocks:
- key = block.get_key()
- file_path = os.path.join(self._dirname, block.get_id() + '.py')
- if key == 'epy_block':
- src = block.get_param('_source_code').get_value()
- output.append((file_path, src))
- elif key == 'epy_module':
- src = block.get_param('source_code').get_value()
- output.append((file_path, src))
-
- # Filter out virtual sink connections
- def cf(c):
- return not (c.is_bus() or c.get_sink().get_parent().is_virtual_sink())
- connections = filter(cf, fg.get_enabled_connections())
-
- # Get the virtual blocks and resolve their connections
- virtual = filter(lambda c: c.get_source().get_parent().is_virtual_source(), connections)
- for connection in virtual:
- sink = connection.get_sink()
- for source in connection.get_source().resolve_virtual_source():
- resolved = fg.get_parent().Connection(flow_graph=fg, porta=source, portb=sink)
- connections.append(resolved)
- # Remove the virtual connection
- connections.remove(connection)
-
- # Bypassing blocks: Need to find all the enabled connections for the block using
- # the *connections* object rather than get_connections(). Create new connections
- # that bypass the selected block and remove the existing ones. This allows adjacent
- # bypassed blocks to see the newly created connections to downstream blocks,
- # allowing them to correctly construct bypass connections.
- bypassed_blocks = fg.get_bypassed_blocks()
- for block in bypassed_blocks:
- # Get the upstream connection (off of the sink ports)
- # Use *connections* not get_connections()
- source_connection = filter(lambda c: c.get_sink() == block.get_sinks()[0], connections)
- # The source connection should never have more than one element.
- assert (len(source_connection) == 1)
-
- # Get the source of the connection.
- source_port = source_connection[0].get_source()
-
- # Loop through all the downstream connections
- for sink in filter(lambda c: c.get_source() == block.get_sources()[0], connections):
- if not sink.get_enabled():
- # Ignore disabled connections
- continue
- sink_port = sink.get_sink()
- connection = fg.get_parent().Connection(flow_graph=fg, porta=source_port, portb=sink_port)
- connections.append(connection)
- # Remove this sink connection
- connections.remove(sink)
- # Remove the source connection
- connections.remove(source_connection[0])
-
- # List of connections where each endpoint is enabled (sorted by domains, block names)
- connections.sort(key=lambda c: (
- c.get_source().get_domain(), c.get_sink().get_domain(),
- c.get_source().get_parent().get_id(), c.get_sink().get_parent().get_id()
- ))
-
- connection_templates = fg.get_parent().connection_templates
-
- # List of variable names
- var_ids = [var.get_id() for var in parameters + variables]
- replace_dict = dict((var_id, 'self.' + var_id) for var_id in var_ids)
- callbacks_all = []
- for block in blocks_all:
- callbacks_all.extend(expr_utils.expr_replace(cb, replace_dict) for cb in block.get_callbacks())
-
- # Map var id to callbacks
- def uses_var_id():
- used = expr_utils.get_variable_dependencies(callback, [var_id])
- return used and 'self.' + var_id in callback # callback might contain var_id itself
-
- callbacks = {}
- for var_id in var_ids:
- callbacks[var_id] = [callback for callback in callbacks_all if uses_var_id()]
-
- # Load the namespace
- namespace = {
- 'title': title,
- 'imports': imports,
- 'flow_graph': fg,
- 'variables': variables,
- 'parameters': parameters,
- 'monitors': monitors,
- 'blocks': blocks,
- 'connections': connections,
- 'connection_templates': connection_templates,
- 'generate_options': self._generate_options,
- 'callbacks': callbacks,
- }
- # Build the template
- t = Template(open(FLOW_GRAPH_TEMPLATE, 'r').read(), namespace)
- output.append((self.file_path, "\n".join(line.rstrip() for line in str(t).split("\n"))))
- return output
-
-
-class HierBlockGenerator(TopBlockGenerator):
- """Extends the top block generator to also generate a block XML file"""
-
- def __init__(self, flow_graph, file_path):
- """
- Initialize the hier block generator object.
-
- Args:
- flow_graph: the flow graph object
- file_path: where to write the py file (the xml goes into HIER_BLOCK_LIB_DIR)
- """
- TopBlockGenerator.__init__(self, flow_graph, file_path)
- platform = flow_graph.get_parent()
-
- hier_block_lib_dir = platform.config.hier_block_lib_dir
- if not os.path.exists(hier_block_lib_dir):
- os.mkdir(hier_block_lib_dir)
-
- self._mode = HIER_BLOCK_FILE_MODE
- self.file_path = os.path.join(hier_block_lib_dir, self._flow_graph.get_option('id') + '.py')
- self._file_path_xml = self.file_path + '.xml'
-
- def get_file_path_xml(self):
- return self._file_path_xml
-
- def write(self):
- """generate output and write it to files"""
- TopBlockGenerator.write(self)
- ParseXML.to_file(self._build_block_n_from_flow_graph_io(), self.get_file_path_xml())
- ParseXML.validate_dtd(self.get_file_path_xml(), BLOCK_DTD)
- try:
- os.chmod(self.get_file_path_xml(), self._mode)
- except:
- pass
-
- def _build_block_n_from_flow_graph_io(self):
- """
- Generate a block XML nested data from the flow graph IO
-
- Returns:
- a xml node tree
- """
- # Extract info from the flow graph
- block_key = self._flow_graph.get_option('id')
- parameters = self._flow_graph.get_parameters()
-
- def var_or_value(name):
- if name in map(lambda p: p.get_id(), parameters):
- return "$"+name
- return name
-
- # Build the nested data
- block_n = odict()
- block_n['name'] = self._flow_graph.get_option('title') or \
- self._flow_graph.get_option('id').replace('_', ' ').title()
- block_n['key'] = block_key
- block_n['category'] = self._flow_graph.get_option('category')
- block_n['import'] = "from {0} import {0} # grc-generated hier_block".format(
- self._flow_graph.get_option('id'))
- # Make data
- if parameters:
- block_n['make'] = '{cls}(\n {kwargs},\n)'.format(
- cls=block_key,
- kwargs=',\n '.join(
- '{key}=${key}'.format(key=param.get_id()) for param in parameters
- ),
- )
- else:
- block_n['make'] = '{cls}()'.format(cls=block_key)
- # Callback data
- block_n['callback'] = [
- 'set_{key}(${key})'.format(key=param.get_id()) for param in parameters
- ]
-
- # Parameters
- block_n['param'] = 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'
- param_n['hide'] = param.get_param('hide').get_value()
- block_n['param'].append(param_n)
-
- # Bus stuff
- if self._flow_graph.get_bussink():
- block_n['bus_sink'] = '1'
- if self._flow_graph.get_bussrc():
- block_n['bus_source'] = '1'
-
- # Sink/source ports
- for direction in ('sink', 'source'):
- block_n[direction] = list()
- for port in self._flow_graph.get_hier_block_io(direction):
- port_n = odict()
- port_n['name'] = port['label']
- port_n['type'] = port['type']
- if port['type'] != "message":
- port_n['vlen'] = var_or_value(port['vlen'])
- if port['optional']:
- port_n['optional'] = '1'
- block_n[direction].append(port_n)
-
- # More bus stuff
- bus_struct_sink = self._flow_graph.get_bus_structure_sink()
- if bus_struct_sink:
- block_n['bus_structure_sink'] = bus_struct_sink[0].get_param('struct').get_value()
- bus_struct_src = self._flow_graph.get_bus_structure_src()
- if bus_struct_src:
- block_n['bus_structure_source'] = bus_struct_src[0].get_param('struct').get_value()
-
- # Documentation
- block_n['doc'] = "\n".join(field for field in (
- self._flow_graph.get_option('author'),
- self._flow_graph.get_option('description'),
- self.file_path
- ) if field)
- block_n['grc_source'] = str(self._flow_graph.grc_file_path)
-
- n = {'block': block_n}
- return n
-
-
-class QtHierBlockGenerator(HierBlockGenerator):
-
- def _build_block_n_from_flow_graph_io(self):
- n = HierBlockGenerator._build_block_n_from_flow_graph_io(self)
- block_n = n['block']
-
- if not block_n['name'].upper().startswith('QT GUI'):
- block_n['name'] = 'QT GUI ' + block_n['name']
-
- block_n.insert_after('category', 'flags', BLOCK_FLAG_NEED_QT_GUI)
-
- gui_hint_param = odict()
- gui_hint_param['name'] = 'GUI Hint'
- gui_hint_param['key'] = 'gui_hint'
- gui_hint_param['value'] = ''
- gui_hint_param['type'] = 'gui_hint'
- gui_hint_param['hide'] = 'part'
- block_n['param'].append(gui_hint_param)
-
- block_n['make'] += (
- "\n#set $win = 'self.%s' % $id"
- "\n${gui_hint() % $win}"
- )
- return n
diff --git a/grc/core/generator/__init__.py b/grc/core/generator/__init__.py
index f44b94a85d..98f410c8d4 100644
--- a/grc/core/generator/__init__.py
+++ b/grc/core/generator/__init__.py
@@ -15,4 +15,5 @@
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
-from Generator import Generator
+from __future__ import absolute_import
+from .Generator import Generator
diff --git a/grc/core/generator/flow_graph.py.mako b/grc/core/generator/flow_graph.py.mako
new file mode 100644
index 0000000000..877c9eee9d
--- /dev/null
+++ b/grc/core/generator/flow_graph.py.mako
@@ -0,0 +1,415 @@
+% if not generate_options.startswith('hb'):
+<%
+from sys import version_info
+python_version = version_info.major
+%>\
+% if python_version == 2:
+#!/usr/bin/env python2
+% elif python_version == 3:
+#!/usr/bin/env python3
+% endif
+% endif
+# -*- coding: utf-8 -*-
+<%def name="indent(code)">${ '\n '.join(str(code).splitlines()) }</%def>
+"""
+GNU Radio Python Flow Graph
+
+Title: ${title}
+% if flow_graph.get_option('author'):
+Author: ${flow_graph.get_option('author')}
+% endif
+% if flow_graph.get_option('description'):
+Description: ${flow_graph.get_option('description')}
+% endif
+Generated: ${ generated_time }
+"""
+
+% if generate_options == 'qt_gui':
+from distutils.version import StrictVersion
+
+if __name__ == '__main__':
+ import ctypes
+ import sys
+ if sys.platform.startswith('linux'):
+ try:
+ x11 = ctypes.cdll.LoadLibrary('libX11.so')
+ x11.XInitThreads()
+ except:
+ print("Warning: failed to XInitThreads()")
+
+% endif
+########################################################
+##Create Imports
+########################################################
+% for imp in imports:
+##${imp.replace(" # grc-generated hier_block", "")}
+${imp}
+% endfor
+########################################################
+##Create Class
+## Write the class declaration for a top or hier block.
+## The parameter names are the arguments to __init__.
+## Setup the IO signature (hier block only).
+########################################################
+<%
+ class_name = flow_graph.get_option('id')
+ param_str = ', '.join(['self'] + ['%s=%s'%(param.name, param.templates.render('make')) for param in parameters])
+%>\
+% if generate_options == 'qt_gui':
+from gnuradio import qtgui
+
+class ${class_name}(gr.top_block, Qt.QWidget):
+
+ def __init__(${param_str}):
+ gr.top_block.__init__(self, "${title}")
+ Qt.QWidget.__init__(self)
+ self.setWindowTitle("${title}")
+ qtgui.util.check_set_qss()
+ try:
+ self.setWindowIcon(Qt.QIcon.fromTheme('gnuradio-grc'))
+ except:
+ pass
+ self.top_scroll_layout = Qt.QVBoxLayout()
+ self.setLayout(self.top_scroll_layout)
+ self.top_scroll = Qt.QScrollArea()
+ self.top_scroll.setFrameStyle(Qt.QFrame.NoFrame)
+ self.top_scroll_layout.addWidget(self.top_scroll)
+ self.top_scroll.setWidgetResizable(True)
+ self.top_widget = Qt.QWidget()
+ self.top_scroll.setWidget(self.top_widget)
+ self.top_layout = Qt.QVBoxLayout(self.top_widget)
+ self.top_grid_layout = Qt.QGridLayout()
+ self.top_layout.addLayout(self.top_grid_layout)
+
+ self.settings = Qt.QSettings("GNU Radio", "${class_name}")
+
+ try:
+ if StrictVersion(Qt.qVersion()) < StrictVersion("5.0.0"):
+ self.restoreGeometry(self.settings.value("geometry").toByteArray())
+ else:
+ self.restoreGeometry(self.settings.value("geometry"))
+ except:
+ pass
+% elif generate_options == 'bokeh_gui':
+
+class ${class_name}(gr.top_block):
+ def __init__(self, doc):
+ gr.top_block.__init__(self, "${title}")
+ self.doc = doc
+ self.plot_lst = []
+ self.widget_lst = []
+% elif generate_options == 'no_gui':
+
+class ${class_name}(gr.top_block):
+
+ def __init__(${param_str}):
+ gr.top_block.__init__(self, "${title}")
+% elif generate_options.startswith('hb'):
+ <% in_sigs = flow_graph.get_hier_block_stream_io('in') %>
+ <% out_sigs = flow_graph.get_hier_block_stream_io('out') %>
+
+
+% if generate_options == 'hb_qt_gui':
+class ${class_name}(gr.hier_block2, Qt.QWidget):
+% else:
+class ${class_name}(gr.hier_block2):
+% endif
+<%def name="make_io_sig(io_sigs)">
+ <% size_strs = ['%s*%s'%(io_sig['size'], io_sig['vlen']) for io_sig in io_sigs] %>
+ % if len(io_sigs) == 0:
+gr.io_signature(0, 0, 0)
+ % elif len(io_sigs) == 1:
+gr.io_signature(1, 1, ${size_strs[0]})
+ % else:
+gr.io_signaturev(${len(io_sigs)}, ${len(io_sigs)}, [${', '.join(ize_strs)}])
+ % endif
+</%def>
+
+ def __init__(${param_str}):
+ gr.hier_block2.__init__(
+ self, "${ title }",
+ ${make_io_sig(in_sigs)},
+ ${make_io_sig(out_sigs)},
+ )
+ % for pad in flow_graph.get_hier_block_message_io('in'):
+ self.message_port_register_hier_in("${ pad['label'] }")
+ % endfor
+ % for pad in flow_graph.get_hier_block_message_io('out'):
+ self.message_port_register_hier_out("${ pad['label'] }")
+ % endfor
+ % if generate_options == 'hb_qt_gui':
+
+ Qt.QWidget.__init__(self)
+ self.top_layout = Qt.QVBoxLayout()
+ self.top_grid_layout = Qt.QGridLayout()
+ self.top_layout.addLayout(self.top_grid_layout)
+ self.setLayout(self.top_layout)
+ % endif
+% endif
+% if flow_graph.get_option('thread_safe_setters'):
+
+ self._lock = threading.RLock()
+% endif
+########################################################
+##Create Parameters
+## Set the parameter to a property of self.
+########################################################
+% if parameters:
+
+ ${'##################################################'}
+ # Parameters
+ ${'##################################################'}
+% endif
+% for param in parameters:
+ ${indent(param.get_var_make())}
+% endfor
+########################################################
+##Create Variables
+########################################################
+% if variables:
+
+ ${'##################################################'}
+ # Variables
+ ${'##################################################'}
+% endif
+% for var in variables:
+ ${indent(var.templates.render('var_make'))}
+% endfor
+ % if blocks:
+
+ ${'##################################################'}
+ # Blocks
+ ${'##################################################'}
+ % endif
+ % for blk, blk_make in blocks:
+ ${ indent(blk_make.strip('\n')) }
+## % if 'alias' in blk.params and blk.params['alias'].get_evaluated():
+## (self.${blk.name}).set_block_alias("${blk.params['alias'].get_evaluated()}")
+## % endif
+## % if 'affinity' in blk.params and blk.params['affinity'].get_evaluated():
+## (self.${blk.name}).set_processor_affinity(${blk.params['affinity'].get_evaluated()})
+## % endif
+## % if len(blk.sources) > 0 and 'minoutbuf' in blk.params and int(blk.params['minoutbuf'].get_evaluated()) > 0:
+## (self.${blk.name}).set_min_output_buffer(${blk.params['minoutbuf'].get_evaluated()})
+## % endif
+## % if len(blk.sources) > 0 and 'maxoutbuf' in blk.params and int(blk.params['maxoutbuf'].get_evaluated()) > 0:
+## (self.${blk.name}).set_max_output_buffer(${blk.params['maxoutbuf'].get_evaluated()})
+## % endif
+ % endfor
+
+##########################################################
+## Create a layout entry if not manually done for BokehGUI
+##########################################################
+% if generate_options == 'bokeh_gui':
+ if self.widget_lst:
+ input_t = bokehgui.BokehLayout.widgetbox(self.widget_lst)
+ widgetbox = bokehgui.BokehLayout.WidgetLayout(input_t)
+ widgetbox.set_layout(*(${flow_graph.get_option('placement')}))
+ list_obj = [widgetbox] + self.plot_lst
+ else:
+ list_obj = self.plot_lst
+ layout_t = bokehgui.BokehLayout.create_layout(list_obj, "${flow_graph.get_option('sizing_mode')}")
+ self.doc.add_root(layout_t)
+% endif
+
+ % if connections:
+
+ ${'##################################################'}
+ # Connections
+ ${'##################################################'}
+ % for connection in connections:
+ ${ connection.rstrip() }
+ % endfor
+ % endif
+########################################################
+## QT sink close method reimplementation
+########################################################
+% if generate_options == 'qt_gui':
+
+ def closeEvent(self, event):
+ self.settings = Qt.QSettings("GNU Radio", "${class_name}")
+ self.settings.setValue("geometry", self.saveGeometry())
+ event.accept()
+ % if flow_graph.get_option('qt_qss_theme'):
+
+ def setStyleSheetFromFile(self, filename):
+ try:
+ if not os.path.exists(filename):
+ filename = os.path.join(
+ gr.prefix(), "share", "gnuradio", "themes", filename)
+ with open(filename) as ss:
+ self.setStyleSheet(ss.read())
+ except Exception as e:
+ print >> sys.stderr, e
+ % endif
+% endif
+##
+##
+##
+## Create Callbacks
+## Write a set method for this variable that calls the callbacks
+########################################################
+ % for var in parameters + variables:
+
+ def get_${ var.name }(self):
+ return self.${ var.name }
+
+ def set_${ var.name }(self, ${ var.name }):
+ % if flow_graph.get_option('thread_safe_setters'):
+ with self._lock:
+ self.${ var.name } = ${ var.name }
+ % for callback in callbacks[var.name]:
+ ${ indent(callback) }
+ % endfor
+ % else:
+ self.${ var.name } = ${ var.name }
+ % for callback in callbacks[var.name]:
+ ${ indent(callback) }
+ % endfor
+ % endif
+ % endfor
+########################################################
+##Create Main
+## For top block code, generate a main routine.
+## Instantiate the top block and run as gui or cli.
+########################################################
+<%def name="make_default(type_, param)">
+ % if type_ == 'eng_float':
+eng_notation.num_to_str(${param.templates.render('make')})
+ % else:
+${param.templates.render('make')}
+ % endif
+</%def>\
+% if not generate_options.startswith('hb'):
+<% params_eq_list = list() %>
+% if parameters:
+
+<% arg_parser_args = '' %>\
+def argument_parser():
+ % if flow_graph.get_option('description'):
+ <%
+ arg_parser_args = 'description=description'
+ %>description = ${repr(flow_graph.get_option('description'))}
+ % endif
+ parser = ArgumentParser(${arg_parser_args})
+ % for param in parameters:
+<%
+ switches = ['"--{}"'.format(param.name.replace('_', '-'))]
+ short_id = param.params['short_id'].get_value()
+ if short_id:
+ switches.insert(0, '"-{}"'.format(short_id))
+
+ type_ = param.params['type'].get_value()
+ if type_:
+ params_eq_list.append('%s=options.%s' % (param.name, param.name))
+
+ default = param.templates.render('make')
+ if type_ == 'eng_float':
+ default = eng_notation.num_to_str(default)
+ # FIXME:
+ if type_ == 'string':
+ type_ = 'str'
+ %>\
+ % if type_:
+ parser.add_argument(
+ ${ ', '.join(switches) }, dest="${param.name}", type=${type_}, default=${ default },
+ help="Set ${param.params['label'].get_evaluated() or param.name} [default=%(default)r]")
+ % endif
+ % endfor
+ return parser
+% endif
+
+
+def main(top_block_cls=${class_name}, options=None):
+ % if parameters:
+ if options is None:
+ options = argument_parser().parse_args()
+ % endif
+ % if flow_graph.get_option('realtime_scheduling'):
+ if gr.enable_realtime_scheduling() != gr.RT_OK:
+ print "Error: failed to enable real-time scheduling."
+ % endif
+ % if generate_options == 'qt_gui':
+
+ if StrictVersion("4.5.0") <= StrictVersion(Qt.qVersion()) < StrictVersion("5.0.0"):
+ style = gr.prefs().get_string('qtgui', 'style', 'raster')
+ Qt.QApplication.setGraphicsSystem(style)
+ qapp = Qt.QApplication(sys.argv)
+
+ tb = top_block_cls(${ ', '.join(params_eq_list) })
+ % if flow_graph.get_option('run'):
+ tb.start(${flow_graph.get_option('max_nouts') or ''})
+ % endif
+ % if flow_graph.get_option('qt_qss_theme'):
+ tb.setStyleSheetFromFile(${ flow_graph.get_option('qt_qss_theme') })
+ % endif
+ tb.show()
+
+ def quitting():
+ tb.stop()
+ tb.wait()
+ qapp.aboutToQuit.connect(quitting)
+ % for m in monitors:
+ if 'en' in m.params:
+ if m.params['en'].get_value():
+ (tb.${m.name}).start()
+ else:
+ sys.stderr.write("Monitor '{0}' does not have an enable ('en') parameter.".format("tb.${m.name}"))
+ % endfor
+ qapp.exec_()
+ % elif generate_options == 'bokeh_gui':
+ serverProc, port = bokehgui.utils.create_server()
+ def killProc(signum, frame, tb):
+ tb.stop()
+ tb.wait()
+ serverProc.terminate()
+ serverProc.kill()
+ time.sleep(1)
+ try:
+ # Define the document instance
+ doc = curdoc()
+ % if flow_graph.get_option('author'):
+ doc.title = "${title} - ${flow_graph.get_option('author')}"
+ % else:
+ doc.title = "${title}"
+ % endif
+ session = push_session(doc, session_id="${flow_graph.get_option('id')}",
+ url = "http://localhost:" + port + "/bokehgui")
+ # Create Top Block instance
+ tb = top_block_cls(doc)
+ try:
+ tb.start()
+ signal.signal(signal.SIGTERM, functools.partial(killProc, tb=tb))
+ session.loop_until_closed()
+ finally:
+ print("Exiting the simulation. Stopping Bokeh Server")
+ tb.stop()
+ tb.wait()
+ finally:
+ serverProc.terminate()
+ serverProc.kill()
+ % elif generate_options == 'no_gui':
+ tb = top_block_cls(${ ', '.join(params_eq_list) })
+ % if flow_graph.get_option('run_options') == 'prompt':
+ tb.start(${ flow_graph.get_option('max_nouts') or '' })
+ % for m in monitors:
+ (tb.${m.name}).start()
+ % endfor
+ try:
+ input('Press Enter to quit: ')
+ except EOFError:
+ pass
+ tb.stop()
+ % elif flow_graph.get_option('run_options') == 'run':
+ tb.start(${flow_graph.get_option('max_nouts') or ''})
+ % endif
+ % for m in monitors:
+ (tb.${m.name}).start()
+ % endfor
+ tb.wait()
+ % endif
+
+
+if __name__ == '__main__':
+ main()
+% endif
diff --git a/grc/core/generator/flow_graph.tmpl b/grc/core/generator/flow_graph.tmpl
deleted file mode 100644
index 3bcf54eee4..0000000000
--- a/grc/core/generator/flow_graph.tmpl
+++ /dev/null
@@ -1,475 +0,0 @@
-#if not $generate_options.startswith('hb')
-#!/usr/bin/env python2
-#end if
-# -*- coding: utf-8 -*-
-########################################################
-##Cheetah template - gnuradio_python
-##
-##@param imports the import statements
-##@param flow_graph the flow_graph
-##@param variables the variable blocks
-##@param parameters the parameter blocks
-##@param blocks the signal blocks
-##@param connections the connections
-##@param generate_options the type of flow graph
-##@param callbacks variable id map to callback strings
-########################################################
-#def indent($code)
-#set $code = '\n '.join(str($code).splitlines())
-$code#slurp
-#end def
-#import time
-#set $DIVIDER = '#'*50
-$DIVIDER
-# GNU Radio Python Flow Graph
-# Title: $title
-#if $flow_graph.get_option('author')
-# Author: $flow_graph.get_option('author')
-#end if
-#if $flow_graph.get_option('description')
-# Description: $flow_graph.get_option('description')
-#end if
-# Generated: $time.ctime()
-$DIVIDER
-#if $flow_graph.get_option('thread_safe_setters')
-import threading
-#end if
-
-#if $generate_options == 'qt_gui'
-from distutils.version import StrictVersion
-#end if
-
-## Call XInitThreads as the _very_ first thing.
-## After some Qt import, it's too late
-#if $generate_options == 'qt_gui'
-if __name__ == '__main__':
- import ctypes
- import sys
- if sys.platform.startswith('linux'):
- try:
- x11 = ctypes.cdll.LoadLibrary('libX11.so')
- x11.XInitThreads()
- except:
- print "Warning: failed to XInitThreads()"
-
-#end if
-#
-########################################################
-##Create Imports
-########################################################
-#if $flow_graph.get_option('qt_qss_theme')
-#set imports = $sorted(set($imports + ["import os", "import sys"]))
-#end if
-#if any(imp.endswith("# grc-generated hier_block") for imp in $imports)
-import os
-import sys
-#set imports = $filter(lambda i: i not in ("import os", "import sys"), $imports)
-sys.path.append(os.environ.get('GRC_HIER_PATH', os.path.expanduser('~/.grc_gnuradio')))
-
-#end if
-#for $imp in $imports
-##$(imp.replace(" # grc-generated hier_block", ""))
-$imp
-#end for
-########################################################
-##Create Class
-## Write the class declaration for a top or hier block.
-## The parameter names are the arguments to __init__.
-## Setup the IO signature (hier block only).
-########################################################
-#set $class_name = $flow_graph.get_option('id')
-#set $param_str = ', '.join(['self'] + ['%s=%s'%(param.get_id(), param.get_make()) for param in $parameters])
-#if $generate_options == 'qt_gui'
-from gnuradio import qtgui
-
-class $(class_name)(gr.top_block, Qt.QWidget):
-
- def __init__($param_str):
- gr.top_block.__init__(self, "$title")
- Qt.QWidget.__init__(self)
- self.setWindowTitle("$title")
- qtgui.util.check_set_qss()
- try:
- self.setWindowIcon(Qt.QIcon.fromTheme('gnuradio-grc'))
- except:
- pass
- self.top_scroll_layout = Qt.QVBoxLayout()
- self.setLayout(self.top_scroll_layout)
- self.top_scroll = Qt.QScrollArea()
- self.top_scroll.setFrameStyle(Qt.QFrame.NoFrame)
- self.top_scroll_layout.addWidget(self.top_scroll)
- self.top_scroll.setWidgetResizable(True)
- self.top_widget = Qt.QWidget()
- self.top_scroll.setWidget(self.top_widget)
- self.top_layout = Qt.QVBoxLayout(self.top_widget)
- self.top_grid_layout = Qt.QGridLayout()
- self.top_layout.addLayout(self.top_grid_layout)
-
- self.settings = Qt.QSettings("GNU Radio", "$class_name")
-
- try:
- if StrictVersion(Qt.qVersion()) < StrictVersion("5.0.0"):
- self.restoreGeometry(self.settings.value("geometry").toByteArray())
- else:
- self.restoreGeometry(self.settings.value("geometry"))
- except:
- pass
-#elif $generate_options == 'bokeh_gui'
-
-class $(class_name)(gr.top_block):
- def __init__(self, doc):
- gr.top_block.__init__(self, "$title")
- self.doc = doc
- self.plot_lst = []
- self.widget_lst = []
-#elif $generate_options == 'no_gui'
-
-
-class $(class_name)(gr.top_block):
-
- def __init__($param_str):
- gr.top_block.__init__(self, "$title")
-#elif $generate_options.startswith('hb')
- #set $in_sigs = $flow_graph.get_hier_block_stream_io('in')
- #set $out_sigs = $flow_graph.get_hier_block_stream_io('out')
-
-
-#if $generate_options == 'hb_qt_gui'
-class $(class_name)(gr.hier_block2, Qt.QWidget):
-#else
-class $(class_name)(gr.hier_block2):
-#end if
-#def make_io_sig($io_sigs)
- #set $size_strs = ['%s*%s'%(io_sig['size'], io_sig['vlen']) for io_sig in $io_sigs]
- #if len($io_sigs) == 0
-gr.io_signature(0, 0, 0)#slurp
- #elif len($io_sigs) == 1
-gr.io_signature(1, 1, $size_strs[0])#slurp
- #else
-gr.io_signaturev($(len($io_sigs)), $(len($io_sigs)), [$(', '.join($size_strs))])#slurp
- #end if
-#end def
-
- def __init__($param_str):
- gr.hier_block2.__init__(
- self, "$title",
- $make_io_sig($in_sigs),
- $make_io_sig($out_sigs),
- )
- #for $pad in $flow_graph.get_hier_block_message_io('in')
- self.message_port_register_hier_in("$pad['label']")
- #end for
- #for $pad in $flow_graph.get_hier_block_message_io('out')
- self.message_port_register_hier_out("$pad['label']")
- #end for
- #if $generate_options == 'hb_qt_gui'
-
- Qt.QWidget.__init__(self)
- self.top_layout = Qt.QVBoxLayout()
- self.top_grid_layout = Qt.QGridLayout()
- self.top_layout.addLayout(self.top_grid_layout)
- self.setLayout(self.top_layout)
- #end if
-#end if
-#if $flow_graph.get_option('thread_safe_setters')
-
- self._lock = threading.RLock()
-#end if
-########################################################
-##Create Parameters
-## Set the parameter to a property of self.
-########################################################
-#if $parameters
-
- $DIVIDER
- # Parameters
- $DIVIDER
-#end if
-#for $param in $parameters
- $indent($param.get_var_make())
-#end for
-########################################################
-##Create Variables
-########################################################
-#if $variables
-
- $DIVIDER
- # Variables
- $DIVIDER
-#end if
-#for $var in $variables
- $indent($var.get_var_make())
-#end for
-########################################################
-##Create Blocks
-########################################################
-#if $blocks
-
- $DIVIDER
- # Blocks
- $DIVIDER
-#end if
-#for $blk in filter(lambda b: b.get_make(), $blocks)
- #if $blk in $variables
- $indent($blk.get_make())
- #else
- self.$blk.get_id() = $indent($blk.get_make())
- #if $blk.has_param('alias') and $blk.get_param('alias').get_evaluated()
- (self.$blk.get_id()).set_block_alias("$blk.get_param('alias').get_evaluated()")
- #end if
- #if $blk.has_param('affinity') and $blk.get_param('affinity').get_evaluated()
- (self.$blk.get_id()).set_processor_affinity($blk.get_param('affinity').get_evaluated())
- #end if
- #if (len($blk.get_sources())>0) and $blk.has_param('minoutbuf') and (int($blk.get_param('minoutbuf').get_evaluated()) > 0)
- (self.$blk.get_id()).set_min_output_buffer($blk.get_param('minoutbuf').get_evaluated())
- #end if
- #if (len($blk.get_sources())>0) and $blk.has_param('maxoutbuf') and (int($blk.get_param('maxoutbuf').get_evaluated()) > 0)
- (self.$blk.get_id()).set_max_output_buffer($blk.get_param('maxoutbuf').get_evaluated())
- #end if
- #end if
-#end for
-
-##########################################################
-## Create a layout entry if not manually done for BokehGUI
-##########################################################
-#if $generate_options == 'bokeh_gui'
- if self.widget_lst:
- input_t = bokehgui.BokehLayout.widgetbox(self.widget_lst)
- widgetbox = bokehgui.BokehLayout.WidgetLayout(input_t)
- widgetbox.set_layout(*($flow_graph.get_option('placement')))
- list_obj = [widgetbox] + self.plot_lst
- else:
- list_obj = self.plot_lst
- layout_t = bokehgui.BokehLayout.create_layout(list_obj, "$flow_graph.get_option('sizing_mode')")
- self.doc.add_root(layout_t)
-#end if
-
-########################################################
-##Create Connections
-## The port name should be the id of the parent block.
-## However, port names for IO pads should be self.
-########################################################
-#def make_port_sig($port)
- #if $port.get_parent().get_key() in ('pad_source', 'pad_sink')
- #set block = 'self'
- #set key = $flow_graph.get_pad_port_global_key($port)
- #else
- #set block = 'self.' + $port.get_parent().get_id()
- #set key = $port.get_key()
- #end if
- #if not $key.isdigit()
- #set key = repr($key)
- #end if
-($block, $key)#slurp
-#end def
-#if $connections
-
- $DIVIDER
- # Connections
- $DIVIDER
-#end if
-#for $con in $connections
- #set global $source = $con.get_source()
- #set global $sink = $con.get_sink()
- #include source=$connection_templates[($source.get_domain(), $sink.get_domain())]
-
-#end for
-########################################################
-## QT sink close method reimplementation
-########################################################
-#if $generate_options == 'qt_gui'
-
- def closeEvent(self, event):
- self.settings = Qt.QSettings("GNU Radio", "$class_name")
- self.settings.setValue("geometry", self.saveGeometry())
- event.accept()
- #if $flow_graph.get_option('qt_qss_theme')
-
- def setStyleSheetFromFile(self, filename):
- try:
- if not os.path.exists(filename):
- filename = os.path.join(
- gr.prefix(), "share", "gnuradio", "themes", filename)
- with open(filename) as ss:
- self.setStyleSheet(ss.read())
- except Exception as e:
- print >> sys.stderr, e
- #end if
-#end if
-########################################################
-##Create Callbacks
-## Write a set method for this variable that calls the callbacks
-########################################################
-#for $var in $parameters + $variables
-
- #set $id = $var.get_id()
- def get_$(id)(self):
- return self.$id
-
- def set_$(id)(self, $id):
- #if $flow_graph.get_option('thread_safe_setters')
- with self._lock:
- self.$id = $id
- #for $callback in $callbacks[$id]
- $indent($callback)
- #end for
- #else
- self.$id = $id
- #for $callback in $callbacks[$id]
- $indent($callback)
- #end for
- #end if
-#end for
-########################################################
-##Create Main
-## For top block code, generate a main routine.
-## Instantiate the top block and run as gui or cli.
-########################################################
-#def make_default($type, $param)
- #if $type == 'eng_float'
-eng_notation.num_to_str($param.get_make())#slurp
- #else
-$param.get_make()#slurp
- #end if
-#end def
-#def make_short_id($param)
- #set $short_id = $param.get_param('short_id').get_evaluated()
- #if $short_id
- #set $short_id = '-' + $short_id
- #end if
-$short_id#slurp
-#end def
-#if not $generate_options.startswith('hb')
-#set $params_eq_list = list()
-#if $parameters
-
-
-def argument_parser():
- #set $arg_parser_args = ''
- #if $flow_graph.get_option('description')
- #set $arg_parser_args = 'description=description'
- description = $repr($flow_graph.get_option('description'))
- #end if
- parser = ArgumentParser($arg_parser_args)
- #for $param in $parameters
- #set $type = $param.get_param('type').get_value()
- #if $type
- #silent $params_eq_list.append('%s=options.%s'%($param.get_id(), $param.get_id()))
- parser.add_argument(
- #if $make_short_id($param)
- "$make_short_id($param)", #slurp
- #end if
- "--$param.get_id().replace('_', '-')", dest="$param.get_id()", type=$type, default=$make_default($type, $param),
- help="Set $($param.get_param('label').get_evaluated() or $param.get_id()) [default=%(default)r]")
- #end if
- #end for
- return parser
-#end if
-
-
-def main(top_block_cls=$(class_name), options=None):
- #if $parameters
- if options is None:
- options = argument_parser().parse_args()
- #end if
- #if $flow_graph.get_option('realtime_scheduling')
- if gr.enable_realtime_scheduling() != gr.RT_OK:
- print "Error: failed to enable real-time scheduling."
- #end if
-
- #if $generate_options == 'qt_gui'
- if StrictVersion("4.5.0") <= StrictVersion(Qt.qVersion()) < StrictVersion("5.0.0"):
- style = gr.prefs().get_string('qtgui', 'style', 'raster')
- Qt.QApplication.setGraphicsSystem(style)
- qapp = Qt.QApplication(sys.argv)
-
- tb = top_block_cls($(', '.join($params_eq_list)))
- #if $flow_graph.get_option('run')
- #if $flow_graph.get_option('max_nouts')
- tb.start($flow_graph.get_option('max_nouts'))
- #else
- tb.start()
- #end if
- #end if
- #if $flow_graph.get_option('qt_qss_theme')
- tb.setStyleSheetFromFile($repr($flow_graph.get_option('qt_qss_theme')))
- #end if
- tb.show()
-
- def quitting():
- tb.stop()
- tb.wait()
- qapp.aboutToQuit.connect(quitting)
- #for $m in $monitors
- if $m.has_param('en'):
- if $m.get_param('en').get_value():
- (tb.$m.get_id()).start()
- else:
- sys.stderr.write("Monitor '{0}' does not have an enable ('en') parameter.".format("tb.$m.get_id()"))
- #end for
- qapp.exec_()
- #elif $generate_options == 'bokeh_gui'
- serverProc, port = bokehgui.utils.create_server()
- def killProc(signum, frame, tb):
- tb.stop()
- tb.wait()
- serverProc.terminate()
- serverProc.kill()
- time.sleep(1)
- try:
- \# Define the document instance
- doc = curdoc()
- #if $flow_graph.get_option('author')
- doc.title = "$title - $flow_graph.get_option('author')"
- #else
- doc.title = "$title"
- #end if
- session = push_session(doc, session_id="$flow_graph.get_option('id')",
- url = "http://localhost:" + port + "/bokehgui")
- \# Create Top Block instance
- tb = top_block_cls(doc)
- try:
- tb.start()
- signal.signal(signal.SIGTERM, functools.partial(killProc, tb=tb))
- session.loop_until_closed()
- finally:
- print "Exiting the simulation. Stopping Bokeh Server"
- tb.stop()
- tb.wait()
- finally:
- serverProc.terminate()
- serverProc.kill()
- #elif $generate_options == 'no_gui'
- tb = top_block_cls($(', '.join($params_eq_list)))
- #set $run_options = $flow_graph.get_option('run_options')
- #if $run_options == 'prompt'
- #if $flow_graph.get_option('max_nouts')
- tb.start($flow_graph.get_option('max_nouts'))
- #else
- tb.start()
- #end if
- #for $m in $monitors
- (tb.$m.get_id()).start()
- #end for
- try:
- raw_input('Press Enter to quit: ')
- except EOFError:
- pass
- tb.stop()
- #elif $run_options == 'run'
- #if $flow_graph.get_option('max_nouts')
- tb.start($flow_graph.get_option('max_nouts'))
- #else
- tb.start()
- #end if
- #end if
- #for $m in $monitors
- (tb.$m.get_id()).start()
- #end for
- tb.wait()
- #end if
-
-
-if __name__ == '__main__':
- main()
-#end if
diff --git a/grc/core/generator/hier_block.py b/grc/core/generator/hier_block.py
new file mode 100644
index 0000000000..31cd198c01
--- /dev/null
+++ b/grc/core/generator/hier_block.py
@@ -0,0 +1,193 @@
+import collections
+import os
+
+import six
+
+from .top_block import TopBlockGenerator
+
+from .. import ParseXML, Constants
+
+
+class HierBlockGenerator(TopBlockGenerator):
+ """Extends the top block generator to also generate a block XML file"""
+
+ def __init__(self, flow_graph, file_path):
+ """
+ Initialize the hier block generator object.
+
+ Args:
+ flow_graph: the flow graph object
+ file_path: where to write the py file (the xml goes into HIER_BLOCK_LIB_DIR)
+ """
+ TopBlockGenerator.__init__(self, flow_graph, file_path)
+ platform = flow_graph.parent
+
+ hier_block_lib_dir = platform.config.hier_block_lib_dir
+ if not os.path.exists(hier_block_lib_dir):
+ os.mkdir(hier_block_lib_dir)
+
+ self._mode = Constants.HIER_BLOCK_FILE_MODE
+ self.file_path = os.path.join(hier_block_lib_dir, self._flow_graph.get_option('id') + '.py')
+ self.file_path_xml = self.file_path + '.xml'
+
+ def write(self):
+ """generate output and write it to files"""
+ TopBlockGenerator.write(self)
+ ParseXML.to_file(self._build_block_n_from_flow_graph_io(), self.file_path_xml)
+ ParseXML.validate_dtd(self.file_path_xml, Constants.BLOCK_DTD)
+ try:
+ os.chmod(self.file_path_xml, self._mode)
+ except:
+ pass
+
+ def _build_block_n_from_flow_graph_io(self):
+ """
+ Generate a block XML nested data from the flow graph IO
+
+ Returns:
+ a xml node tree
+ """
+ # Extract info from the flow graph
+ block_id = self._flow_graph.get_option('id')
+ parameters = self._flow_graph.get_parameters()
+
+ def var_or_value(name):
+ if name in (p.name for p in parameters):
+ return "${" + name + " }"
+ return name
+
+ # Build the nested data
+ data = collections.OrderedDict()
+ data['id'] = block_id
+ data['label'] = (
+ self._flow_graph.get_option('title') or
+ self._flow_graph.get_option('id').replace('_', ' ').title()
+ )
+ data['category'] = self._flow_graph.get_option('category')
+
+ # Parameters
+ data['parameters'] = []
+ for param_block in parameters:
+ p = collections.OrderedDict()
+ p['id'] = param_block.name
+ p['label'] = param_block.params['label'].get_value() or param_block.name
+ p['dtype'] = 'raw'
+ p['value'] = param_block.params['value'].get_value()
+ p['hide'] = param_block.params['hide'].get_value()
+ data['param'].append(p)
+
+ # Ports
+ for direction in ('inputs', 'outputs'):
+ data[direction] = []
+ for port in get_hier_block_io(self._flow_graph, direction):
+ p = collections.OrderedDict()
+ if port.domain == Constants.GR_MESSAGE_DOMAIN:
+ p['id'] = port.id
+ p['label'] = port.label
+ if port.domain != Constants.DEFAULT_DOMAIN:
+ p['domain'] = port.domain
+ p['dtype'] = port.dtype
+ if port.domain != Constants.GR_MESSAGE_DOMAIN:
+ p['vlen'] = var_or_value(port.vlen)
+ if port.optional:
+ p['optional'] = True
+ data[direction].append(p)
+
+ t = data['templates'] = collections.OrderedDict()
+
+ t['import'] = "from {0} import {0} # grc-generated hier_block".format(
+ self._flow_graph.get_option('id'))
+ # Make data
+ if parameters:
+ t['make'] = '{cls}(\n {kwargs},\n)'.format(
+ cls=block_id,
+ kwargs=',\n '.join(
+ '{key}=${key}'.format(key=param.name) for param in parameters
+ ),
+ )
+ else:
+ t['make'] = '{cls}()'.format(cls=block_id)
+ # Callback data
+ t['callback'] = [
+ 'set_{key}(${key})'.format(key=param_block.name) for param_block in parameters
+ ]
+
+
+ # Documentation
+ data['doc'] = "\n".join(field for field in (
+ self._flow_graph.get_option('author'),
+ self._flow_graph.get_option('description'),
+ self.file_path
+ ) if field)
+ data['grc_source'] = str(self._flow_graph.grc_file_path)
+
+ n = {'block': data}
+ return n
+
+
+class QtHierBlockGenerator(HierBlockGenerator):
+
+ def _build_block_n_from_flow_graph_io(self):
+ n = HierBlockGenerator._build_block_n_from_flow_graph_io(self)
+ block_n = collections.OrderedDict()
+
+ # insert flags after category
+ for key, value in six.iteritems(n['block']):
+ block_n[key] = value
+ if key == 'category':
+ block_n['flags'] = 'need_qt_gui'
+
+ if not block_n['name'].upper().startswith('QT GUI'):
+ block_n['name'] = 'QT GUI ' + block_n['name']
+
+ gui_hint_param = collections.OrderedDict()
+ gui_hint_param['name'] = 'GUI Hint'
+ gui_hint_param['key'] = 'gui_hint'
+ gui_hint_param['value'] = ''
+ gui_hint_param['type'] = 'gui_hint'
+ gui_hint_param['hide'] = 'part'
+ block_n['param'].append(gui_hint_param)
+
+ block_n['make'] += (
+ "\n<% win = 'self.' + id %>"
+ "\n${ gui_hint % win }"
+ )
+
+ return {'block': block_n}
+
+
+def get_hier_block_io(flow_graph, direction, domain=None):
+ """
+ Get a list of io ports for this flow graph.
+
+ Returns a list of dicts with: type, label, vlen, size, optional
+ """
+ pads = flow_graph.get_pad_sources() if direction == 'inputs' else flow_graph.get_pad_sinks()
+
+ ports = []
+ for pad in pads:
+ for port in (pad.sources if direction == 'inputs' else pad.sinks):
+ if domain and port.domain != domain:
+ continue
+ yield port
+
+ type_param = pad.params['type']
+ master = {
+ 'label': str(pad.params['label'].get_evaluated()),
+ 'type': str(pad.params['type'].get_evaluated()),
+ 'vlen': str(pad.params['vlen'].get_value()),
+ 'size': type_param.options.attributes[type_param.get_value()]['size'],
+ 'optional': bool(pad.params['optional'].get_evaluated()),
+ }
+
+ if domain and pad:
+ num_ports = pad.params['num_streams'].get_evaluated()
+ if num_ports <= 1:
+ yield master
+ else:
+ for i in range(num_ports):
+ clone = master.copy()
+ clone['label'] += str(i)
+ ports.append(clone)
+ else:
+ ports.append(master)
diff --git a/grc/core/generator/top_block.py b/grc/core/generator/top_block.py
new file mode 100644
index 0000000000..799ebb1076
--- /dev/null
+++ b/grc/core/generator/top_block.py
@@ -0,0 +1,284 @@
+import codecs
+import operator
+import os
+import tempfile
+import textwrap
+import time
+
+from mako.template import Template
+
+from .. import Messages, blocks
+from ..Constants import TOP_BLOCK_FILE_MODE
+from .FlowGraphProxy import FlowGraphProxy
+from ..utils import expr_utils
+
+DATA_DIR = os.path.dirname(__file__)
+FLOW_GRAPH_TEMPLATE = os.path.join(DATA_DIR, 'flow_graph.py.mako')
+flow_graph_template = Template(filename=FLOW_GRAPH_TEMPLATE)
+
+
+class TopBlockGenerator(object):
+
+ def __init__(self, flow_graph, file_path):
+ """
+ Initialize the top block generator object.
+
+ Args:
+ flow_graph: the flow graph object
+ file_path: the path to write the file to
+ """
+
+ self._flow_graph = FlowGraphProxy(flow_graph)
+ self._generate_options = self._flow_graph.get_option('generate_options')
+
+ self._mode = TOP_BLOCK_FILE_MODE
+ # Handle the case where the directory is read-only
+ # In this case, use the system's temp directory
+ if not os.access(file_path, os.W_OK):
+ file_path = tempfile.gettempdir()
+ filename = self._flow_graph.get_option('id') + '.py'
+ self.file_path = os.path.join(file_path, filename)
+ self._dirname = file_path
+
+ def _warnings(self):
+ throttling_blocks = [b for b in self._flow_graph.get_enabled_blocks()
+ if b.flags.throttle]
+ if not throttling_blocks and not self._generate_options.startswith('hb'):
+ Messages.send_warning("This flow graph may not have flow control: "
+ "no audio or RF hardware blocks found. "
+ "Add a Misc->Throttle block to your flow "
+ "graph to avoid CPU congestion.")
+ if len(throttling_blocks) > 1:
+ keys = set([b.key for b in throttling_blocks])
+ if len(keys) > 1 and 'blocks_throttle' in keys:
+ Messages.send_warning("This flow graph contains a throttle "
+ "block and another rate limiting block, "
+ "e.g. a hardware source or sink. "
+ "This is usually undesired. Consider "
+ "removing the throttle block.")
+
+ deprecated_block_keys = {b.name for b in self._flow_graph.get_enabled_blocks() if b.flags.deprecated}
+ for key in deprecated_block_keys:
+ Messages.send_warning("The block {!r} is deprecated.".format(key))
+
+ def write(self):
+ """generate output and write it to files"""
+ self._warnings()
+
+ for filename, data in self._build_python_code_from_template():
+ with codecs.open(filename, 'w', encoding='utf-8') as fp:
+ fp.write(data)
+ if filename == self.file_path:
+ try:
+ os.chmod(filename, self._mode)
+ except:
+ pass
+
+ def _build_python_code_from_template(self):
+ """
+ Convert the flow graph to python code.
+
+ Returns:
+ a string of python code
+ """
+ output = []
+
+ fg = self._flow_graph
+ title = fg.get_option('title') or fg.get_option('id').replace('_', ' ').title()
+ variables = fg.get_variables()
+ parameters = fg.get_parameters()
+ monitors = fg.get_monitors()
+
+ for block in fg.iter_enabled_blocks():
+ key = block.key
+ file_path = os.path.join(self._dirname, block.name + '.py')
+ if key == 'epy_block':
+ src = block.params['_source_code'].get_value()
+ output.append((file_path, src))
+ elif key == 'epy_module':
+ src = block.params['source_code'].get_value()
+ output.append((file_path, src))
+
+ namespace = {
+ 'flow_graph': fg,
+ 'variables': variables,
+ 'parameters': parameters,
+ 'monitors': monitors,
+ 'generate_options': self._generate_options,
+ 'generated_time': time.ctime(),
+ }
+ flow_graph_code = flow_graph_template.render(
+ title=title,
+ imports=self._imports(),
+ blocks=self._blocks(),
+ callbacks=self._callbacks(),
+ connections=self._connections(),
+ **namespace
+ )
+ # strip trailing white-space
+ flow_graph_code = "\n".join(line.rstrip() for line in flow_graph_code.split("\n"))
+ output.append((self.file_path, flow_graph_code))
+
+ return output
+
+ def _imports(self):
+ fg = self._flow_graph
+ imports = fg.imports()
+ seen = set()
+ output = []
+
+ need_path_hack = any(imp.endswith("# grc-generated hier_block") for imp in imports)
+ if need_path_hack:
+ output.insert(0, textwrap.dedent("""\
+ import os
+ import sys
+ sys.path.append(os.environ.get('GRC_HIER_PATH', os.path.expanduser('~/.grc_gnuradio')))
+ """))
+ seen.add('import os')
+ seen.add('import sys')
+
+ if fg.get_option('qt_qss_theme'):
+ imports.append('import os')
+ imports.append('import sys')
+
+ if fg.get_option('thread_safe_setters'):
+ imports.append('import threading')
+
+ def is_duplicate(l):
+ if l.startswith('import') or l.startswith('from') and l in seen:
+ return True
+ seen.add(line)
+ return False
+
+ for import_ in sorted(imports):
+ lines = import_.strip().split('\n')
+ if not lines[0]:
+ continue
+ for line in lines:
+ line = line.rstrip()
+ if not is_duplicate(line):
+ output.append(line)
+
+ return output
+
+ def _blocks(self):
+ fg = self._flow_graph
+ parameters = fg.get_parameters()
+
+ # List of blocks not including variables and imports and parameters and disabled
+ def _get_block_sort_text(block):
+ code = block.templates.render('make').replace(block.name, ' ')
+ try:
+ code += block.params['gui_hint'].get_value() # Newer gui markup w/ qtgui
+ except:
+ pass
+ return code
+
+ blocks = [
+ b for b in fg.blocks
+ if b.enabled and not (b.get_bypassed() or b.is_import or b in parameters or b.key == 'options')
+ ]
+
+ blocks = expr_utils.sort_objects(blocks, operator.attrgetter('name'), _get_block_sort_text)
+ blocks_make = []
+ for block in blocks:
+ make = block.templates.render('make')
+ if not block.is_variable:
+ make = 'self.' + block.name + ' = ' + make
+ if make:
+ blocks_make.append((block, make))
+ return blocks_make
+
+ def _callbacks(self):
+ fg = self._flow_graph
+ variables = fg.get_variables()
+ parameters = fg.get_parameters()
+
+ # List of variable names
+ var_ids = [var.name for var in parameters + variables]
+ replace_dict = dict((var_id, 'self.' + var_id) for var_id in var_ids)
+ callbacks_all = []
+ for block in fg.iter_enabled_blocks():
+ callbacks_all.extend(expr_utils.expr_replace(cb, replace_dict) for cb in block.get_callbacks())
+
+ # Map var id to callbacks
+ def uses_var_id(callback):
+ used = expr_utils.get_variable_dependencies(callback, [var_id])
+ return used and 'self.' + var_id in callback # callback might contain var_id itself
+
+ callbacks = {}
+ for var_id in var_ids:
+ callbacks[var_id] = [callback for callback in callbacks_all if uses_var_id(callback)]
+
+ return callbacks
+
+ def _connections(self):
+ fg = self._flow_graph
+ templates = {key: Template(text)
+ for key, text in fg.parent_platform.connection_templates.items()}
+
+ def make_port_sig(port):
+ if port.parent.key in ('pad_source', 'pad_sink'):
+ block = 'self'
+ key = fg.get_pad_port_global_key(port)
+ else:
+ block = 'self.' + port.parent_block.name
+ key = port.key
+
+ if not key.isdigit():
+ key.repr(key)
+
+ return '({block}, {key})'.format(block=block, key=key)
+
+ connections = fg.get_enabled_connections()
+
+ # Get the virtual blocks and resolve their connections
+ connection_factory = fg.parent_platform.Connection
+ virtual = [c for c in connections if isinstance(c.source_block, blocks.VirtualSource)]
+ for connection in virtual:
+ sink = connection.sink_port
+ for source in connection.source_port.resolve_virtual_source():
+ resolved = connection_factory(fg.orignal_flowgraph, source, sink)
+ connections.append(resolved)
+ # Remove the virtual connection
+ connections.remove(connection)
+
+ # Bypassing blocks: Need to find all the enabled connections for the block using
+ # the *connections* object rather than get_connections(). Create new connections
+ # that bypass the selected block and remove the existing ones. This allows adjacent
+ # bypassed blocks to see the newly created connections to downstream blocks,
+ # allowing them to correctly construct bypass connections.
+ bypassed_blocks = fg.get_bypassed_blocks()
+ for block in bypassed_blocks:
+ # Get the upstream connection (off of the sink ports)
+ # Use *connections* not get_connections()
+ source_connection = [c for c in connections if c.sink_port == block.sinks[0]]
+ # The source connection should never have more than one element.
+ assert (len(source_connection) == 1)
+
+ # Get the source of the connection.
+ source_port = source_connection[0].source_port
+
+ # Loop through all the downstream connections
+ for sink in (c for c in connections if c.source_port == block.sources[0]):
+ if not sink.enabled:
+ # Ignore disabled connections
+ continue
+ connection = connection_factory(fg.orignal_flowgraph, source_port, sink.sink_port)
+ connections.append(connection)
+ # Remove this sink connection
+ connections.remove(sink)
+ # Remove the source connection
+ connections.remove(source_connection[0])
+
+ # List of connections where each endpoint is enabled (sorted by domains, block names)
+ def by_domain_and_blocks(c):
+ return c.type, c.source_block.name, c.sink_block.name
+
+ rendered = []
+ for con in sorted(connections, key=by_domain_and_blocks):
+ template = templates[con.type]
+ code = template.render(make_port_sig=make_port_sig, source=con.source_port, sink=con.sink_port)
+ rendered.append(code)
+
+ return rendered
diff --git a/grc/core/io/__init__.py b/grc/core/io/__init__.py
new file mode 100644
index 0000000000..f77f1a6704
--- /dev/null
+++ b/grc/core/io/__init__.py
@@ -0,0 +1,16 @@
+# Copyright 2017 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
diff --git a/grc/core/io/yaml.py b/grc/core/io/yaml.py
new file mode 100644
index 0000000000..29b4cb81d6
--- /dev/null
+++ b/grc/core/io/yaml.py
@@ -0,0 +1,91 @@
+# Copyright 2016 Free Software Foundation, Inc.
+# This file is part of GNU Radio
+#
+# GNU Radio Companion is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by the
+# Free Software Foundation; either version 2 of the License, or (at your
+# option) any later version.
+#
+# GNU Radio Companion is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+# details.
+#
+# You should have received a copy of the GNU General Public License along with
+# this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
+
+from __future__ import absolute_import
+
+from collections import OrderedDict
+
+import six
+import yaml
+
+
+class GRCDumper(yaml.SafeDumper):
+ @classmethod
+ def add(cls, data_type):
+ def decorator(func):
+ cls.add_representer(data_type, func)
+ return func
+ return decorator
+
+ def represent_ordered_mapping(self, data):
+ value = []
+ node = yaml.MappingNode(u'tag:yaml.org,2002:map', value, flow_style=False)
+
+ if self.alias_key is not None:
+ self.represented_objects[self.alias_key] = node
+
+ for item_key, item_value in six.iteritems(data):
+ node_key = self.represent_data(item_key)
+ node_value = self.represent_data(item_value)
+ value.append((node_key, node_value))
+
+ return node
+
+ def represent_ordered_mapping_flowing(self, data):
+ node = self.represent_ordered_mapping(data)
+ node.flow_style = True
+ return node
+
+ def represent_list_flowing(self, data):
+ node = self.represent_list(data)
+ node.flow_style = True
+ return node
+
+ def represent_ml_string(self, data):
+ node = self.represent_str(data)
+ node.style = '|'
+ return node
+
+
+class OrderedDictFlowing(OrderedDict):
+ pass
+
+
+class ListFlowing(list):
+ pass
+
+
+class MultiLineString(str):
+ pass
+
+
+GRCDumper.add_representer(OrderedDict, GRCDumper.represent_ordered_mapping)
+GRCDumper.add_representer(OrderedDictFlowing, GRCDumper.represent_ordered_mapping_flowing)
+GRCDumper.add_representer(ListFlowing, GRCDumper.represent_list_flowing)
+GRCDumper.add_representer(tuple, GRCDumper.represent_list)
+GRCDumper.add_representer(MultiLineString, GRCDumper.represent_ml_string)
+GRCDumper.add_representer(yaml.nodes.ScalarNode, lambda r, n: n)
+
+
+def dump(data, stream=None, **kwargs):
+ config = dict(stream=stream, default_flow_style=False, indent=4, Dumper=GRCDumper)
+ config.update(kwargs)
+ return yaml.dump_all([data], **config)
+
+
+safe_load = yaml.safe_load
+__with_libyaml__ = yaml.__with_libyaml__
diff --git a/grc/core/platform.py b/grc/core/platform.py
new file mode 100644
index 0000000000..538bacade2
--- /dev/null
+++ b/grc/core/platform.py
@@ -0,0 +1,431 @@
+# Copyright 2008-2016 Free Software Foundation, Inc.
+# This file is part of GNU Radio
+#
+# GNU Radio Companion is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# GNU Radio Companion is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
+
+from __future__ import absolute_import, print_function
+
+from codecs import open
+from collections import namedtuple
+import glob
+import os
+import logging
+from itertools import chain
+
+import six
+from six.moves import range
+
+from . import (
+ Messages, Constants,
+ blocks, ports, errors, utils, schema_checker
+)
+
+from .Config import Config
+from .base import Element
+from .io import yaml
+from .generator import Generator
+from .FlowGraph import FlowGraph
+from .Connection import Connection
+from .Param import Param
+
+logger = logging.getLogger(__name__)
+logging.basicConfig(level=logging.INFO)
+
+
+class Platform(Element):
+
+ def __init__(self, *args, **kwargs):
+ """ Make a platform for GNU Radio """
+ Element.__init__(self, parent=None)
+
+ self.config = self.Config(*args, **kwargs)
+ self.block_docstrings = {}
+ self.block_docstrings_loaded_callback = lambda: None # dummy to be replaced by BlockTreeWindow
+
+ self._docstring_extractor = utils.extract_docs.SubprocessLoader(
+ callback_query_result=self._save_docstring_extraction_result,
+ callback_finished=lambda: self.block_docstrings_loaded_callback()
+ )
+
+ self.blocks = self.block_classes
+ self.domains = {}
+ self.connection_templates = {}
+
+ self._block_categories = {}
+ self._auto_hier_block_generate_chain = set()
+
+ if not yaml.__with_libyaml__:
+ logger.warning("Slow YAML loading (libyaml not available)")
+
+ def __str__(self):
+ return 'Platform - {}'.format(self.config.name)
+
+ @staticmethod
+ def find_file_in_paths(filename, paths, cwd):
+ """Checks the provided paths relative to cwd for a certain filename"""
+ if not os.path.isdir(cwd):
+ cwd = os.path.dirname(cwd)
+ if isinstance(paths, str):
+ paths = (p for p in paths.split(':') if p)
+
+ for path in paths:
+ path = os.path.expanduser(path)
+ if not os.path.isabs(path):
+ path = os.path.normpath(os.path.join(cwd, path))
+ file_path = os.path.join(path, filename)
+ if os.path.exists(os.path.normpath(file_path)):
+ return file_path
+
+ def load_and_generate_flow_graph(self, file_path, out_path=None, hier_only=False):
+ """Loads a flow graph from file and generates it"""
+ Messages.set_indent(len(self._auto_hier_block_generate_chain))
+ Messages.send('>>> Loading: {}\n'.format(file_path))
+ if file_path in self._auto_hier_block_generate_chain:
+ Messages.send(' >>> Warning: cyclic hier_block dependency\n')
+ return None, None
+ self._auto_hier_block_generate_chain.add(file_path)
+ try:
+ flow_graph = self.make_flow_graph()
+ flow_graph.grc_file_path = file_path
+ # Other, nested hier_blocks might be auto-loaded here
+ flow_graph.import_data(self.parse_flow_graph(file_path))
+ flow_graph.rewrite()
+ flow_graph.validate()
+ if not flow_graph.is_valid():
+ raise Exception('Flowgraph invalid')
+ if hier_only and not flow_graph.get_option('generate_options').startswith('hb'):
+ raise Exception('Not a hier block')
+ except Exception as e:
+ Messages.send('>>> Load Error: {}: {}\n'.format(file_path, str(e)))
+ return None, None
+ finally:
+ self._auto_hier_block_generate_chain.discard(file_path)
+ Messages.set_indent(len(self._auto_hier_block_generate_chain))
+
+ try:
+ generator = self.Generator(flow_graph, out_path or file_path)
+ Messages.send('>>> Generating: {}\n'.format(generator.file_path))
+ generator.write()
+ except Exception as e:
+ Messages.send('>>> Generate Error: {}: {}\n'.format(file_path, str(e)))
+ return None, None
+
+ if flow_graph.get_option('generate_options').startswith('hb'):
+ # self.load_block_xml(generator.file_path_xml)
+ # TODO: implement yml output for hier blocks
+ pass
+ return flow_graph, generator.file_path
+
+ def build_library(self, path=None):
+ """load the blocks and block tree from the search paths
+
+ path: a list of paths/files to search in or load (defaults to config)
+ """
+ self._docstring_extractor.start()
+
+ # Reset
+ self.blocks.clear()
+ self.domains.clear()
+ self.connection_templates.clear()
+ self._block_categories.clear()
+
+ # FIXME: remove this as soon as converter is stable
+ from ..converter import Converter
+ converter = Converter(self.config.block_paths, self.config.yml_block_cache)
+ converter.run()
+ logging.info('XML converter done.')
+
+ for file_path in self._iter_files_in_block_path(path):
+ try:
+ data = converter.cache[file_path]
+ except KeyError:
+ with open(file_path, encoding='utf-8') as fp:
+ data = yaml.safe_load(fp)
+
+ if file_path.endswith('.block.yml'):
+ loader = self.load_block_description
+ scheme = schema_checker.BLOCK_SCHEME
+ elif file_path.endswith('.domain.yml'):
+ loader = self.load_domain_description
+ scheme = schema_checker.DOMAIN_SCHEME
+ elif file_path.endswith('.tree.yml'):
+ loader = self.load_category_tree_description
+ scheme = None
+ else:
+ continue
+
+ try:
+ checker = schema_checker.Validator(scheme)
+ passed = checker.run(data)
+ for msg in checker.messages:
+ logger.warning('{:<40s} {}'.format(os.path.basename(file_path), msg))
+ if not passed:
+ logger.info('YAML schema check failed for: ' + file_path)
+
+ loader(data, file_path)
+ except Exception as error:
+ logger.exception('Error while loading %s', file_path)
+ logger.exception(error)
+ raise
+
+ for key, block in six.iteritems(self.blocks):
+ category = self._block_categories.get(key, block.category)
+ if not category:
+ continue
+ root = category[0]
+ if root.startswith('[') and root.endswith(']'):
+ category[0] = root[1:-1]
+ else:
+ category.insert(0, Constants.DEFAULT_BLOCK_MODULE_NAME)
+ block.category = category
+
+ self._docstring_extractor.finish()
+ # self._docstring_extractor.wait()
+ utils.hide_bokeh_gui_options_if_not_installed(self.blocks['options'])
+
+ def _iter_files_in_block_path(self, path=None, ext='yml'):
+ """Iterator for block descriptions and category trees"""
+ for entry in (path or self.config.block_paths):
+ if os.path.isfile(entry):
+ yield entry
+ elif os.path.isdir(entry):
+ pattern = os.path.join(entry, '**.' + ext)
+ yield_from = glob.iglob(pattern)
+ for file_path in yield_from:
+ yield file_path
+ else:
+ logger.debug('Ignoring invalid path entry %r', entry)
+
+ def _save_docstring_extraction_result(self, block_id, docstrings):
+ docs = {}
+ for match, docstring in six.iteritems(docstrings):
+ if not docstring or match.endswith('_sptr'):
+ continue
+ docs[match] = docstring.replace('\n\n', '\n').strip()
+ try:
+ self.blocks[block_id].documentation.update(docs)
+ except KeyError:
+ pass # in tests platform might be gone...
+
+ ##############################################
+ # Description File Loaders
+ ##############################################
+ # region loaders
+ def load_block_description(self, data, file_path):
+ log = logger.getChild('block_loader')
+
+ # don't load future block format versions
+ file_format = data['file_format']
+ if file_format < 1 or file_format > Constants.BLOCK_DESCRIPTION_FILE_FORMAT_VERSION:
+ log.error('Unknown format version %d in %s', file_format, file_path)
+ return
+
+ block_id = data.pop('id').rstrip('_')
+
+ if block_id in self.block_classes_build_in:
+ log.warning('Not overwriting build-in block %s with %s', block_id, file_path)
+ return
+ if block_id in self.blocks:
+ log.warning('Block with id "%s" overwritten by %s', block_id, file_path)
+
+ try:
+ block_cls = self.blocks[block_id] = self.new_block_class(block_id, **data)
+ except errors.BlockLoadError as error:
+ log.error('Unable to load block %s', block_id)
+ log.exception(error)
+ return
+
+ self._docstring_extractor.query(
+ block_id, block_cls.templates['imports'], block_cls.templates['make'],
+ )
+
+ def load_domain_description(self, data, file_path):
+ log = logger.getChild('domain_loader')
+ domain_id = data['id']
+ if domain_id in self.domains: # test against repeated keys
+ log.debug('Domain "{}" already exists. Ignoring: %s', file_path)
+ return
+
+ color = data.get('color', '')
+ if color.startswith('#'):
+ try:
+ tuple(int(color[o:o + 2], 16) / 255.0 for o in range(1, 6, 2))
+ except ValueError:
+ log.warning('Cannot parse color code "%s" in %s', color, file_path)
+ return
+
+ self.domains[domain_id] = self.Domain(
+ name=data.get('label', domain_id),
+ multi_in=data.get('multiple_connections_per_input', True),
+ multi_out=data.get('multiple_connections_per_output', False),
+ color=color
+ )
+ for connection in data.get('templates', []):
+ try:
+ source_id, sink_id = connection.get('type', [])
+ except ValueError:
+ log.warn('Invalid connection template.')
+ continue
+ connection_id = str(source_id), str(sink_id)
+ self.connection_templates[connection_id] = connection.get('connect', '')
+
+ def load_category_tree_description(self, data, file_path):
+ """Parse category tree file and add it to list"""
+ log = logger.getChild('tree_loader')
+ log.debug('Loading %s', file_path)
+ path = []
+
+ def load_category(name, elements):
+ if not isinstance(name, str):
+ log.debug('invalid name %r', name)
+ return
+ if isinstance(elements, list):
+ pass
+ elif isinstance(elements, str):
+ elements = [elements]
+ else:
+ log.debug('Ignoring elements of %s', name)
+ return
+ path.append(name)
+ for element in elements:
+ if isinstance(element, str):
+ block_id = element
+ self._block_categories[block_id] = list(path)
+ elif isinstance(element, dict):
+ load_category(*next(six.iteritems(element)))
+ else:
+ log.debug('Ignoring some elements of %s', name)
+ path.pop()
+
+ try:
+ module_name, categories = next(six.iteritems(data))
+ except (AttributeError, StopIteration):
+ log.warning('no valid data found')
+ else:
+ load_category(module_name, categories)
+
+ ##############################################
+ # Access
+ ##############################################
+ def parse_flow_graph(self, filename):
+ """
+ Parse a saved flow graph file.
+ Ensure that the file exists, and passes the dtd check.
+
+ Args:
+ filename: the flow graph file
+
+ Returns:
+ nested data
+ @throws exception if the validation fails
+ """
+ filename = filename or self.config.default_flow_graph
+ with open(filename, encoding='utf-8') as fp:
+ is_xml = '<flow_graph>' in fp.read(100)
+ fp.seek(0)
+ # todo: try
+ if not is_xml:
+ data = yaml.safe_load(fp)
+ validator = schema_checker.Validator(schema_checker.FLOW_GRAPH_SCHEME)
+ validator.run(data)
+ else:
+ Messages.send('>>> Converting from XML\n')
+ from ..converter.flow_graph import from_xml
+ data = from_xml(fp)
+
+ return data
+
+ def save_flow_graph(self, filename, flow_graph):
+ data = flow_graph.export_data()
+
+ try:
+ data['connections'] = [yaml.ListFlowing(i) for i in data['connections']]
+ except KeyError:
+ pass
+
+ try:
+ for d in chain([data['options']], data['blocks']):
+ d['states']['coordinate'] = yaml.ListFlowing(d['states']['coordinate'])
+ for param_id, value in list(d['parameters'].items()):
+ if value == '':
+ d['parameters'].pop(param_id)
+ except KeyError:
+ pass
+
+ out = yaml.dump(data, indent=2)
+
+ replace = [
+ ('blocks:', '\nblocks:'),
+ ('connections:', '\nconnections:'),
+ ('metadata:', '\nmetadata:'),
+ ]
+ for r in replace:
+ out = out.replace(*r)
+
+ with open(filename, 'w', encoding='utf-8') as fp:
+ fp.write(out)
+
+ def get_generate_options(self):
+ for param in self.block_classes['options'].parameters_data:
+ if param.get('id') == 'generate_options':
+ break
+ else:
+ return []
+ generate_mode_default = param.get('default')
+ return [(value, name, value == generate_mode_default)
+ for value, name in zip(param['options'], param['option_labels'])]
+
+ ##############################################
+ # Factories
+ ##############################################
+ Config = Config
+ Domain = namedtuple('Domain', 'name multi_in multi_out color')
+ Generator = Generator
+ FlowGraph = FlowGraph
+ Connection = Connection
+
+ block_classes_build_in = blocks.build_ins
+ block_classes = utils.backports.ChainMap({}, block_classes_build_in) # separates build-in from loaded blocks)
+
+ port_classes = {
+ None: ports.Port, # default
+ 'clone': ports.PortClone, # clone of ports with multiplicity > 1
+ }
+ param_classes = {
+ None: Param, # default
+ }
+
+ def make_flow_graph(self, from_filename=None):
+ fg = self.FlowGraph(parent=self)
+ if from_filename:
+ data = self.parse_flow_graph(from_filename)
+ fg.grc_file_path = from_filename
+ fg.import_data(data)
+ return fg
+
+ def new_block_class(self, block_id, **data):
+ return blocks.build(block_id, **data)
+
+ def make_block(self, parent, block_id, **kwargs):
+ cls = self.block_classes[block_id]
+ return cls(parent, **kwargs)
+
+ def make_param(self, parent, **kwargs):
+ cls = self.param_classes[kwargs.pop('cls_key', None)]
+ return cls(parent, **kwargs)
+
+ def make_port(self, parent, **kwargs):
+ cls = self.port_classes[kwargs.pop('cls_key', None)]
+ return cls(parent, **kwargs)
diff --git a/grc/core/domain.dtd b/grc/core/ports/__init__.py
index b5b0b8bf39..375b5d63e3 100644
--- a/grc/core/domain.dtd
+++ b/grc/core/ports/__init__.py
@@ -1,5 +1,5 @@
-<!--
-Copyright 2014 Free Software Foundation, Inc.
+"""
+Copyright 2008-2015 Free Software Foundation, Inc.
This file is part of GNU Radio
GNU Radio Companion is free software; you can redistribute it and/or
@@ -15,21 +15,9 @@ 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
--->
-<!ELEMENT domain (name, key, color?, multiple_sinks?, multiple_sources?, connection*)>
-<!--
- Sub level elements.
- -->
-<!ELEMENT connection (source_domain, sink_domain, make)>
-<!--
- Bottom level elements.
- Character data only.
- -->
-<!ELEMENT name (#PCDATA)>
-<!ELEMENT key (#PCDATA)>
-<!ELEMENT multiple_sinks (#PCDATA)>
-<!ELEMENT multiple_sources (#PCDATA)>
-<!ELEMENT color (#PCDATA)>
-<!ELEMENT make (#PCDATA)>
-<!ELEMENT source_domain (#PCDATA)>
-<!ELEMENT sink_domain (#PCDATA)>
+"""
+
+from __future__ import absolute_import
+
+from .port import Port
+from .clone import PortClone
diff --git a/grc/core/ports/_virtual_connections.py b/grc/core/ports/_virtual_connections.py
new file mode 100644
index 0000000000..45f4a247fd
--- /dev/null
+++ b/grc/core/ports/_virtual_connections.py
@@ -0,0 +1,126 @@
+# Copyright 2008-2016 Free Software Foundation, Inc.
+# This file is part of GNU Radio
+#
+# GNU Radio Companion is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# GNU Radio Companion is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
+
+from __future__ import absolute_import
+
+from itertools import chain
+
+from .. import blocks
+
+
+class LoopError(Exception):
+ pass
+
+
+def upstream_ports(port):
+ if port.is_sink:
+ return _sources_from_virtual_sink_port(port)
+ else:
+ return _sources_from_virtual_source_port(port)
+
+
+def _sources_from_virtual_sink_port(sink_port, _traversed=None):
+ """
+ Resolve the source port that is connected to the given virtual sink port.
+ Use the get source from virtual source to recursively resolve subsequent ports.
+ """
+ source_ports_per_virtual_connection = (
+ # there can be multiple ports per virtual connection
+ _sources_from_virtual_source_port(c.source_port, _traversed) # type: list
+ for c in sink_port.connections(enabled=True)
+ )
+ return list(chain(*source_ports_per_virtual_connection)) # concatenate generated lists of ports
+
+
+def _sources_from_virtual_source_port(source_port, _traversed=None):
+ """
+ Recursively resolve source ports over the virtual connections.
+ Keep track of traversed sources to avoid recursive loops.
+ """
+ _traversed = set(_traversed or []) # a new set!
+ if source_port in _traversed:
+ raise LoopError('Loop found when resolving port type')
+ _traversed.add(source_port)
+
+ block = source_port.parent_block
+ flow_graph = source_port.parent_flowgraph
+
+ if not isinstance(block, blocks.VirtualSource):
+ return [source_port] # nothing to resolve, we're done
+
+ stream_id = block.params['stream_id'].value
+
+ # currently the validation does not allow multiple virtual sinks and one virtual source
+ # but in the future it may...
+ connected_virtual_sink_blocks = (
+ b for b in flow_graph.iter_enabled_blocks()
+ if isinstance(b, blocks.VirtualSink) and b.params['stream_id'].value == stream_id
+ )
+ source_ports_per_virtual_connection = (
+ _sources_from_virtual_sink_port(b.sinks[0], _traversed) # type: list
+ for b in connected_virtual_sink_blocks
+ )
+ return list(chain(*source_ports_per_virtual_connection)) # concatenate generated lists of ports
+
+
+def downstream_ports(port):
+ if port.is_source:
+ return _sinks_from_virtual_source_port(port)
+ else:
+ return _sinks_from_virtual_sink_port(port)
+
+
+def _sinks_from_virtual_source_port(source_port, _traversed=None):
+ """
+ Resolve the sink port that is connected to the given virtual source port.
+ Use the get sink from virtual sink to recursively resolve subsequent ports.
+ """
+ sink_ports_per_virtual_connection = (
+ # there can be multiple ports per virtual connection
+ _sinks_from_virtual_sink_port(c.sink_port, _traversed) # type: list
+ for c in source_port.connections(enabled=True)
+ )
+ return list(chain(*sink_ports_per_virtual_connection)) # concatenate generated lists of ports
+
+
+def _sinks_from_virtual_sink_port(sink_port, _traversed=None):
+ """
+ Recursively resolve sink ports over the virtual connections.
+ Keep track of traversed sinks to avoid recursive loops.
+ """
+ _traversed = set(_traversed or []) # a new set!
+ if sink_port in _traversed:
+ raise LoopError('Loop found when resolving port type')
+ _traversed.add(sink_port)
+
+ block = sink_port.parent_block
+ flow_graph = sink_port.parent_flowgraph
+
+ if not isinstance(block, blocks.VirtualSink):
+ return [sink_port]
+
+ stream_id = block.params['stream_id'].value
+
+ connected_virtual_source_blocks = (
+ b for b in flow_graph.iter_enabled_blocks()
+ if isinstance(b, blocks.VirtualSource) and b.params['stream_id'].value == stream_id
+ )
+ sink_ports_per_virtual_connection = (
+ _sinks_from_virtual_source_port(b.sources[0], _traversed) # type: list
+ for b in connected_virtual_source_blocks
+ )
+ return list(chain(*sink_ports_per_virtual_connection)) # concatenate generated lists of ports
diff --git a/grc/core/ports/clone.py b/grc/core/ports/clone.py
new file mode 100644
index 0000000000..4e1320f81d
--- /dev/null
+++ b/grc/core/ports/clone.py
@@ -0,0 +1,38 @@
+# Copyright 2016 Free Software Foundation, Inc.
+# This file is part of GNU Radio
+#
+# GNU Radio Companion is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# GNU Radio Companion is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
+
+from .port import Port, Element
+
+
+class PortClone(Port):
+
+ def __init__(self, parent, direction, master, name, key):
+ Element.__init__(self, parent)
+ self.master_port = master
+
+ self.name = name
+ self.key = key
+ self.multiplicity = 1
+
+ def __getattr__(self, item):
+ return getattr(self.master_port, item)
+
+ def add_clone(self):
+ raise NotImplementedError()
+
+ def remove_clone(self, port):
+ raise NotImplementedError()
diff --git a/grc/core/ports/port.py b/grc/core/ports/port.py
new file mode 100644
index 0000000000..139aae0ccc
--- /dev/null
+++ b/grc/core/ports/port.py
@@ -0,0 +1,207 @@
+# Copyright 2008-2016 Free Software Foundation, Inc.
+# This file is part of GNU Radio
+#
+# GNU Radio Companion is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# GNU Radio Companion is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
+
+from __future__ import absolute_import
+
+from . import _virtual_connections
+
+from .. import Constants
+from ..base import Element
+from ..utils.descriptors import (
+ EvaluatedFlag, EvaluatedEnum, EvaluatedPInt,
+ setup_names, lazy_property
+)
+
+
+@setup_names
+class Port(Element):
+
+ is_port = True
+
+ dtype = EvaluatedEnum(list(Constants.TYPE_TO_SIZEOF.keys()), default='')
+ vlen = EvaluatedPInt()
+ multiplicity = EvaluatedPInt()
+ hidden = EvaluatedFlag()
+ optional = EvaluatedFlag()
+
+ def __init__(self, parent, direction, id, label='', domain=Constants.DEFAULT_DOMAIN, dtype='',
+ vlen='', multiplicity=1, optional=False, hide='', **_):
+ """Make a new port from nested data."""
+ Element.__init__(self, parent)
+
+ self._dir = direction
+ self.key = id
+ if not label:
+ label = id if not id.isdigit() else {'sink': 'in', 'source': 'out'}[direction] + id
+ self.name = self._base_name = label
+
+ self.domain = domain
+ self.dtype = dtype
+ self.vlen = vlen
+
+ if domain == Constants.GR_MESSAGE_DOMAIN: # ToDo: message port class
+ self.key = self.name
+ self.dtype = 'message'
+
+ self.multiplicity = multiplicity
+ self.optional = optional
+ self.hidden = hide
+ # end of args ########################################################
+ self.clones = [] # References to cloned ports (for nports > 1)
+
+ def __str__(self):
+ if self.is_source:
+ return 'Source - {}({})'.format(self.name, self.key)
+ if self.is_sink:
+ return 'Sink - {}({})'.format(self.name, self.key)
+
+ def __repr__(self):
+ return '{!r}.{}[{}]'.format(self.parent, 'sinks' if self.is_sink else 'sources', self.key)
+
+ @property
+ def item_size(self):
+ return Constants.TYPE_TO_SIZEOF[self.dtype] * self.vlen
+
+ @lazy_property
+ def is_sink(self):
+ return self._dir == 'sink'
+
+ @lazy_property
+ def is_source(self):
+ return self._dir == 'source'
+
+ @property
+ def inherit_type(self):
+ """always empty for e.g. virtual blocks, may eval to empty for 'Wildcard'"""
+ return not self.dtype
+
+ def validate(self):
+ Element.validate(self)
+ platform = self.parent_platform
+
+ num_connections = len(list(self.connections(enabled=True)))
+ need_connection = not self.optional and not self.hidden
+ if need_connection and num_connections == 0:
+ self.add_error_message('Port is not connected.')
+
+ if self.dtype not in Constants.TYPE_TO_SIZEOF.keys():
+ self.add_error_message('Type "{}" is not a possible type.'.format(self.dtype))
+
+ try:
+ domain = platform.domains[self.domain]
+ if self.is_sink and not domain.multi_in and num_connections > 1:
+ self.add_error_message('Domain "{}" can have only one upstream block'
+ ''.format(self.domain))
+ if self.is_source and not domain.multi_out and num_connections > 1:
+ self.add_error_message('Domain "{}" can have only one downstream block'
+ ''.format(self.domain))
+ except KeyError:
+ self.add_error_message('Domain key "{}" is not registered.'.format(self.domain))
+
+ def rewrite(self):
+ del self.vlen
+ del self.multiplicity
+ del self.hidden
+ del self.optional
+ del self.dtype
+
+ if self.inherit_type:
+ self.resolve_empty_type()
+
+ Element.rewrite(self)
+
+ # Update domain if was deduced from (dynamic) port type
+ if self.domain == Constants.GR_STREAM_DOMAIN and self.dtype == "message":
+ self.domain = Constants.GR_MESSAGE_DOMAIN
+ self.key = self.name
+ if self.domain == Constants.GR_MESSAGE_DOMAIN and self.dtype != "message":
+ self.domain = Constants.GR_STREAM_DOMAIN
+ self.key = '0' # Is rectified in rewrite()
+
+ def resolve_virtual_source(self):
+ """Only used by Generator after validation is passed"""
+ return _virtual_connections.upstream_ports(self)
+
+ def resolve_empty_type(self):
+ def find_port(finder):
+ try:
+ return next((p for p in finder(self) if not p.inherit_type), None)
+ except _virtual_connections.LoopError as error:
+ self.add_error_message(str(error))
+ except (StopIteration, Exception):
+ pass
+
+ try:
+ port = find_port(_virtual_connections.upstream_ports) or \
+ find_port(_virtual_connections.downstream_ports)
+ self.set_evaluated('dtype', port.dtype) # we don't want to override the template
+ self.set_evaluated('vlen', port.vlen) # we don't want to override the template
+ self.domain = port.domain
+ except AttributeError:
+ self.domain = Constants.DEFAULT_DOMAIN
+
+ def add_clone(self):
+ """
+ Create a clone of this (master) port and store a reference in self._clones.
+
+ The new port name (and key for message ports) will have index 1... appended.
+ If this is the first clone, this (master) port will get a 0 appended to its name (and key)
+
+ Returns:
+ the cloned port
+ """
+ # Add index to master port name if there are no clones yet
+ if not self.clones:
+ self.name = self._base_name + '0'
+ # Also update key for none stream ports
+ if not self.key.isdigit():
+ self.key = self.name
+
+ name = self._base_name + str(len(self.clones) + 1)
+ # Dummy value 99999 will be fixed later
+ key = '99999' if self.key.isdigit() else name
+
+ # Clone
+ port_factory = self.parent_platform.make_port
+ port = port_factory(self.parent, direction=self._dir,
+ name=name, key=key,
+ master=self, cls_key='clone')
+
+ self.clones.append(port)
+ return port
+
+ def remove_clone(self, port):
+ """
+ Remove a cloned port (from the list of clones only)
+ Remove the index 0 of the master port name (and key9 if there are no more clones left
+ """
+ self.clones.remove(port)
+ # Remove index from master port name if there are no more clones
+ if not self.clones:
+ self.name = self._base_name
+ # Also update key for none stream ports
+ if not self.key.isdigit():
+ self.key = self.name
+
+ def connections(self, enabled=None):
+ """Iterator over all connections to/from this port
+
+ enabled: None for all, True for enabled only, False for disabled only
+ """
+ for con in self.parent_flowgraph.connections:
+ if self in con and (enabled is None or enabled == con.enabled):
+ yield con
diff --git a/grc/core/schema_checker/__init__.py b/grc/core/schema_checker/__init__.py
new file mode 100644
index 0000000000..e92500ed4a
--- /dev/null
+++ b/grc/core/schema_checker/__init__.py
@@ -0,0 +1,5 @@
+from .validator import Validator
+
+from .block import BLOCK_SCHEME
+from .domain import DOMAIN_SCHEME
+from .flow_graph import FLOW_GRAPH_SCHEME
diff --git a/grc/core/schema_checker/block.py b/grc/core/schema_checker/block.py
new file mode 100644
index 0000000000..ea079b4276
--- /dev/null
+++ b/grc/core/schema_checker/block.py
@@ -0,0 +1,57 @@
+from .utils import Spec, expand, str_
+
+PARAM_SCHEME = expand(
+ base_key=str_, # todo: rename/remove
+
+ id=str_,
+ label=str_,
+ category=str_,
+
+ dtype=str_,
+ default=object,
+
+ options=list,
+ option_labels=list,
+ option_attributes=Spec(types=dict, required=False, item_scheme=(str_, list)),
+
+ hide=str_,
+)
+PORT_SCHEME = expand(
+ label=str_,
+ domain=str_,
+
+ id=str_,
+ dtype=str_,
+ vlen=(int, str_),
+
+ multiplicity=(int, str_),
+ optional=(bool, int, str_),
+ hide=(bool, str_),
+)
+TEMPLATES_SCHEME = expand(
+ imports=str_,
+ var_make=str_,
+ make=str_,
+ callbacks=list,
+)
+BLOCK_SCHEME = expand(
+ id=Spec(types=str_, required=True, item_scheme=None),
+ label=str_,
+ category=(list, str_),
+ flags=(list, str_),
+
+ parameters=Spec(types=list, required=False, item_scheme=PARAM_SCHEME),
+ inputs=Spec(types=list, required=False, item_scheme=PORT_SCHEME),
+ outputs=Spec(types=list, required=False, item_scheme=PORT_SCHEME),
+
+ asserts=(list, str_),
+ value=str_,
+
+ templates=Spec(types=dict, required=False, item_scheme=TEMPLATES_SCHEME),
+
+ documentation=str_,
+
+ file_format=Spec(types=int, required=True, item_scheme=None),
+
+ block_wrapper_path=str_, # todo: rename/remove
+)
diff --git a/grc/core/schema_checker/domain.py b/grc/core/schema_checker/domain.py
new file mode 100644
index 0000000000..86c29ed3c6
--- /dev/null
+++ b/grc/core/schema_checker/domain.py
@@ -0,0 +1,16 @@
+from .utils import Spec, expand, str_
+
+DOMAIN_CONNECTION = expand(
+ type=Spec(types=list, required=True, item_scheme=None),
+ connect=str_,
+)
+
+DOMAIN_SCHEME = expand(
+ id=Spec(types=str_, required=True, item_scheme=None),
+ label=str_,
+ color=str_,
+ multiple_connections_per_input=bool,
+ multiple_connections_per_output=bool,
+
+ templates=Spec(types=list, required=False, item_scheme=DOMAIN_CONNECTION)
+) \ No newline at end of file
diff --git a/grc/core/schema_checker/flow_graph.py b/grc/core/schema_checker/flow_graph.py
new file mode 100644
index 0000000000..746fbf4aa7
--- /dev/null
+++ b/grc/core/schema_checker/flow_graph.py
@@ -0,0 +1,23 @@
+from .utils import Spec, expand, str_
+
+OPTIONS_SCHEME = expand(
+ parameters=Spec(types=dict, required=False, item_scheme=(str_, str_)),
+ states=Spec(types=dict, required=False, item_scheme=(str_, str_)),
+)
+
+BLOCK_SCHEME = expand(
+ name=str_,
+ id=str_,
+ **OPTIONS_SCHEME
+)
+
+FLOW_GRAPH_SCHEME = expand(
+ options=Spec(types=dict, required=False, item_scheme=OPTIONS_SCHEME),
+ blocks=Spec(types=dict, required=False, item_scheme=BLOCK_SCHEME),
+ connections=list,
+
+ metadata=Spec(types=dict, required=True, item_scheme=expand(
+ file_format=Spec(types=int, required=True, item_scheme=None),
+ ))
+
+)
diff --git a/grc/core/schema_checker/utils.py b/grc/core/schema_checker/utils.py
new file mode 100644
index 0000000000..a9cf4c0175
--- /dev/null
+++ b/grc/core/schema_checker/utils.py
@@ -0,0 +1,27 @@
+import collections
+
+import six
+
+Spec = collections.namedtuple('Spec', 'types required item_scheme')
+
+
+def expand(**kwargs):
+ def expand_spec(spec):
+ if not isinstance(spec, Spec):
+ types_ = spec if isinstance(spec, tuple) else (spec,)
+ spec = Spec(types=types_, required=False, item_scheme=None)
+ elif not isinstance(spec.types, tuple):
+ spec = Spec(types=(spec.types,), required=spec.required,
+ item_scheme=spec.item_scheme)
+ return spec
+ return {key: expand_spec(value) for key, value in kwargs.items()}
+
+
+str_ = six.string_types
+
+
+class Message(collections.namedtuple('Message', 'path type message')):
+ fmt = '{path}: {type}: {message}'
+
+ def __str__(self):
+ return self.fmt.format(**self._asdict())
diff --git a/grc/core/schema_checker/validator.py b/grc/core/schema_checker/validator.py
new file mode 100644
index 0000000000..ab4d43bc67
--- /dev/null
+++ b/grc/core/schema_checker/validator.py
@@ -0,0 +1,102 @@
+# Copyright 2016 Free Software Foundation, Inc.
+# This file is part of GNU Radio
+#
+# GNU Radio Companion is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by the
+# Free Software Foundation; either version 2 of the License, or (at your
+# option) any later version.
+#
+# GNU Radio Companion is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+# details.
+#
+# You should have received a copy of the GNU General Public License along with
+# this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
+
+from __future__ import print_function
+
+import six
+
+from .utils import Message, Spec
+
+
+class Validator(object):
+
+ def __init__(self, scheme=None):
+ self._path = []
+ self.scheme = scheme
+ self.messages = []
+ self.passed = False
+
+ def run(self, data):
+ if not self.scheme:
+ return True
+ self._reset()
+ self._path.append('block')
+ self._check(data, self.scheme)
+ self._path.pop()
+ return self.passed
+
+ def _reset(self):
+ del self.messages[:]
+ del self._path[:]
+ self.passed = True
+
+ def _check(self, data, scheme):
+ if not data or not isinstance(data, dict):
+ self._error('Empty data or not a dict')
+ return
+ if isinstance(scheme, dict):
+ self._check_dict(data, scheme)
+ else:
+ self._check_var_key_dict(data, *scheme)
+
+ def _check_var_key_dict(self, data, key_type, value_scheme):
+ for key, value in six.iteritems(data):
+ if not isinstance(key, key_type):
+ self._error('Key type {!r} for {!r} not in valid types'.format(
+ type(value).__name__, key))
+ if isinstance(value_scheme, Spec):
+ self._check_dict(value, value_scheme)
+ elif not isinstance(value, value_scheme):
+ self._error('Value type {!r} for {!r} not in valid types'.format(
+ type(value).__name__, key))
+
+ def _check_dict(self, data, scheme):
+ for key, (types_, required, item_scheme) in six.iteritems(scheme):
+ try:
+ value = data[key]
+ except KeyError:
+ if required:
+ self._error('Missing required entry {!r}'.format(key))
+ continue
+
+ self._check_value(value, types_, item_scheme, label=key)
+
+ for key in set(data).difference(scheme):
+ self._warn('Ignoring extra key {!r}'.format(key))
+
+ def _check_list(self, data, scheme, label):
+ for i, item in enumerate(data):
+ self._path.append('{}[{}]'.format(label, i))
+ self._check(item, scheme)
+ self._path.pop()
+
+ def _check_value(self, value, types_, item_scheme, label):
+ if not isinstance(value, types_):
+ self._error('Value type {!r} for {!r} not in valid types'.format(
+ type(value).__name__, label))
+ if item_scheme:
+ if isinstance(value, list):
+ self._check_list(value, item_scheme, label)
+ elif isinstance(value, dict):
+ self._check(value, item_scheme)
+
+ def _error(self, msg):
+ self.messages.append(Message('.'.join(self._path), 'error', msg))
+ self.passed = False
+
+ def _warn(self, msg):
+ self.messages.append(Message('.'.join(self._path), 'warn', msg))
diff --git a/grc/core/utils/__init__.py b/grc/core/utils/__init__.py
index 2aed42d762..660eb594a5 100644
--- a/grc/core/utils/__init__.py
+++ b/grc/core/utils/__init__.py
@@ -1,4 +1,4 @@
-# Copyright 2008-2015 Free Software Foundation, Inc.
+# Copyright 2016 Free Software Foundation, Inc.
# This file is part of GNU Radio
#
# GNU Radio Companion is free software; you can redistribute it and/or
@@ -15,9 +15,7 @@
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
-import expr_utils
-import epy_block_io
-import extract_docs
+from __future__ import absolute_import
-from odict import odict
-from hide_bokeh_gui_options_if_not_installed import hide_bokeh_gui_options_if_not_installed
+from . import epy_block_io, expr_utils, extract_docs, flow_graph_complexity
+from .hide_bokeh_gui_options_if_not_installed import hide_bokeh_gui_options_if_not_installed
diff --git a/grc/core/utils/CMakeLists.txt b/grc/core/utils/backports/__init__.py
index 3ba65258a5..a24ee3ae01 100644
--- a/grc/core/utils/CMakeLists.txt
+++ b/grc/core/utils/backports/__init__.py
@@ -1,4 +1,4 @@
-# Copyright 2015 Free Software Foundation, Inc.
+# Copyright 2016 Free Software Foundation, Inc.
#
# This file is part of GNU Radio
#
@@ -17,9 +17,9 @@
# the Free Software Foundation, Inc., 51 Franklin Street,
# Boston, MA 02110-1301, USA.
-file(GLOB py_files "*.py")
+from __future__ import absolute_import
-GR_PYTHON_INSTALL(
- FILES ${py_files}
- DESTINATION ${GR_PYTHON_DIR}/gnuradio/grc/core/utils
-)
+try:
+ from collections import ChainMap
+except ImportError:
+ from .chainmap import ChainMap
diff --git a/grc/core/utils/backports/chainmap.py b/grc/core/utils/backports/chainmap.py
new file mode 100644
index 0000000000..1f4f4a96fb
--- /dev/null
+++ b/grc/core/utils/backports/chainmap.py
@@ -0,0 +1,106 @@
+# from https://hg.python.org/cpython/file/default/Lib/collections/__init__.py
+
+from collections import MutableMapping
+
+
+class ChainMap(MutableMapping):
+ """ A ChainMap groups multiple dicts (or other mappings) together
+ to create a single, updateable view.
+
+ The underlying mappings are stored in a list. That list is public and can
+ be accessed or updated using the *maps* attribute. There is no other
+ state.
+
+ Lookups search the underlying mappings successively until a key is found.
+ In contrast, writes, updates, and deletions only operate on the first
+ mapping.
+
+ """
+
+ def __init__(self, *maps):
+ """Initialize a ChainMap by setting *maps* to the given mappings.
+ If no mappings are provided, a single empty dictionary is used.
+
+ """
+ self.maps = list(maps) or [{}] # always at least one map
+
+ def __missing__(self, key):
+ raise KeyError(key)
+
+ def __getitem__(self, key):
+ for mapping in self.maps:
+ try:
+ return mapping[key] # can't use 'key in mapping' with defaultdict
+ except KeyError:
+ pass
+ return self.__missing__(key) # support subclasses that define __missing__
+
+ def get(self, key, default=None):
+ return self[key] if key in self else default
+
+ def __len__(self):
+ return len(set().union(*self.maps)) # reuses stored hash values if possible
+
+ def __iter__(self):
+ return iter(set().union(*self.maps))
+
+ def __contains__(self, key):
+ return any(key in m for m in self.maps)
+
+ def __bool__(self):
+ return any(self.maps)
+
+ def __repr__(self):
+ return '{0.__class__.__name__}({1})'.format(
+ self, ', '.join(map(repr, self.maps)))
+
+ @classmethod
+ def fromkeys(cls, iterable, *args):
+ """Create a ChainMap with a single dict created from the iterable."""
+ return cls(dict.fromkeys(iterable, *args))
+
+ def copy(self):
+ """New ChainMap or subclass with a new copy of maps[0] and refs to maps[1:]"""
+ return self.__class__(self.maps[0].copy(), *self.maps[1:])
+
+ __copy__ = copy
+
+ def new_child(self, m=None): # like Django's Context.push()
+ """New ChainMap with a new map followed by all previous maps.
+ If no map is provided, an empty dict is used.
+ """
+ if m is None:
+ m = {}
+ return self.__class__(m, *self.maps)
+
+ @property
+ def parents(self): # like Django's Context.pop()
+ """New ChainMap from maps[1:]."""
+ return self.__class__(*self.maps[1:])
+
+ def __setitem__(self, key, value):
+ self.maps[0][key] = value
+
+ def __delitem__(self, key):
+ try:
+ del self.maps[0][key]
+ except KeyError:
+ raise KeyError('Key not found in the first mapping: {!r}'.format(key))
+
+ def popitem(self):
+ """Remove and return an item pair from maps[0]. Raise KeyError is maps[0] is empty."""
+ try:
+ return self.maps[0].popitem()
+ except KeyError:
+ raise KeyError('No keys found in the first mapping.')
+
+ def pop(self, key, *args):
+ """Remove *key* from maps[0] and return its value. Raise KeyError if *key* not in maps[0]."""
+ try:
+ return self.maps[0].pop(key, *args)
+ except KeyError:
+ raise KeyError('Key not found in the first mapping: {!r}'.format(key))
+
+ def clear(self):
+ """Clear maps[0], leaving maps[1:] intact."""
+ self.maps[0].clear()
diff --git a/grc/core/utils/shlex.py b/grc/core/utils/backports/shlex.py
index 6b620fa396..6b620fa396 100644
--- a/grc/core/utils/shlex.py
+++ b/grc/core/utils/backports/shlex.py
diff --git a/grc/core/utils/complexity.py b/grc/core/utils/complexity.py
deleted file mode 100644
index baa8040db4..0000000000
--- a/grc/core/utils/complexity.py
+++ /dev/null
@@ -1,49 +0,0 @@
-
-def calculate_flowgraph_complexity(flowgraph):
- """ Determines the complexity of a flowgraph """
- dbal = 0
- for block in flowgraph.blocks:
- # Skip options block
- if block.get_key() == 'options':
- continue
-
- # Don't worry about optional sinks?
- sink_list = filter(lambda c: not c.get_optional(), block.get_sinks())
- source_list = filter(lambda c: not c.get_optional(), block.get_sources())
- sinks = float(len(sink_list))
- sources = float(len(source_list))
- base = max(min(sinks, sources), 1)
-
- # Port ratio multiplier
- if min(sinks, sources) > 0:
- multi = sinks / sources
- multi = (1 / multi) if multi > 1 else multi
- else:
- multi = 1
-
- # Connection ratio multiplier
- sink_multi = max(float(sum(map(lambda c: len(c.get_connections()), sink_list)) / max(sinks, 1.0)), 1.0)
- source_multi = max(float(sum(map(lambda c: len(c.get_connections()), source_list)) / max(sources, 1.0)), 1.0)
- dbal = dbal + (base * multi * sink_multi * source_multi)
-
- blocks = float(len(flowgraph.blocks))
- connections = float(len(flowgraph.connections))
- elements = blocks + connections
- disabled_connections = len(filter(lambda c: not c.get_enabled(), flowgraph.connections))
- variables = elements - blocks - connections
- enabled = float(len(flowgraph.get_enabled_blocks()))
-
- # Disabled multiplier
- if enabled > 0:
- disabled_multi = 1 / (max(1 - ((blocks - enabled) / max(blocks, 1)), 0.05))
- else:
- disabled_multi = 1
-
- # Connection multiplier (How many connections )
- if (connections - disabled_connections) > 0:
- conn_multi = 1 / (max(1 - (disabled_connections / max(connections, 1)), 0.05))
- else:
- conn_multi = 1
-
- final = round(max((dbal - 1) * disabled_multi * conn_multi * connections, 0.0) / 1000000, 6)
- return final
diff --git a/grc/core/utils/descriptors/__init__.py b/grc/core/utils/descriptors/__init__.py
new file mode 100644
index 0000000000..80c5259230
--- /dev/null
+++ b/grc/core/utils/descriptors/__init__.py
@@ -0,0 +1,26 @@
+# Copyright 2016 Free Software Foundation, Inc.
+# This file is part of GNU Radio
+#
+# GNU Radio Companion is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# GNU Radio Companion is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
+
+from ._lazy import lazy_property, nop_write
+
+from .evaluated import (
+ Evaluated,
+ EvaluatedEnum,
+ EvaluatedPInt,
+ EvaluatedFlag,
+ setup_names,
+)
diff --git a/grc/core/utils/descriptors/_lazy.py b/grc/core/utils/descriptors/_lazy.py
new file mode 100644
index 0000000000..a0cb126932
--- /dev/null
+++ b/grc/core/utils/descriptors/_lazy.py
@@ -0,0 +1,39 @@
+# Copyright 2016 Free Software Foundation, Inc.
+# This file is part of GNU Radio
+#
+# GNU Radio Companion is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# GNU Radio Companion is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
+
+import functools
+
+
+class lazy_property(object):
+
+ def __init__(self, func):
+ self.func = func
+ functools.update_wrapper(self, func)
+
+ def __get__(self, instance, owner):
+ if instance is None:
+ return self
+ value = self.func(instance)
+ setattr(instance, self.func.__name__, value)
+ return value
+
+
+def nop_write(prop):
+ """Make this a property with a nop setter"""
+ def nop(self, value):
+ pass
+ return prop.setter(nop)
diff --git a/grc/core/utils/descriptors/evaluated.py b/grc/core/utils/descriptors/evaluated.py
new file mode 100644
index 0000000000..313cee5b96
--- /dev/null
+++ b/grc/core/utils/descriptors/evaluated.py
@@ -0,0 +1,112 @@
+# Copyright 2016 Free Software Foundation, Inc.
+# This file is part of GNU Radio
+#
+# GNU Radio Companion is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# GNU Radio Companion is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
+
+
+class Evaluated(object):
+ def __init__(self, expected_type, default, name=None):
+ self.expected_type = expected_type
+ self.default = default
+
+ self.name = name or 'evaled_property_{}'.format(id(self))
+ self.eval_function = self.default_eval_func
+
+ @property
+ def name_raw(self):
+ return '_' + self.name
+
+ def default_eval_func(self, instance):
+ raw = getattr(instance, self.name_raw)
+ try:
+ value = instance.parent_block.evaluate(raw)
+ except Exception as error:
+ if raw:
+ instance.add_error_message("Failed to eval '{}': {}".format(raw, error))
+ return self.default
+
+ if not isinstance(value, self.expected_type):
+ instance.add_error_message("Can not cast evaluated value '{}' to type {}"
+ "".format(value, self.expected_type))
+ return self.default
+ # print(instance, self.name, raw, value)
+ return value
+
+ def __call__(self, func):
+ self.name = func.__name__
+ self.eval_function = func
+ return self
+
+ def __get__(self, instance, owner):
+ if instance is None:
+ return self
+ attribs = instance.__dict__
+ try:
+ value = attribs[self.name]
+ except KeyError:
+ value = attribs[self.name] = self.eval_function(instance)
+ return value
+
+ def __set__(self, instance, value):
+ attribs = instance.__dict__
+ value = value or self.default
+ if isinstance(value, str) and value.startswith('${') and value.endswith('}'):
+ attribs[self.name_raw] = value[2:-1].strip()
+ else:
+ attribs[self.name] = type(self.default)(value)
+
+ def __delete__(self, instance):
+ attribs = instance.__dict__
+ if self.name_raw in attribs:
+ attribs.pop(self.name, None)
+
+
+class EvaluatedEnum(Evaluated):
+ def __init__(self, allowed_values, default=None, name=None):
+ self.allowed_values = allowed_values if isinstance(allowed_values, (list, tuple)) else \
+ allowed_values.split()
+ default = default if default is not None else self.allowed_values[0]
+ super(EvaluatedEnum, self).__init__(str, default, name)
+
+ def default_eval_func(self, instance):
+ value = super(EvaluatedEnum, self).default_eval_func(instance)
+ if value not in self.allowed_values:
+ instance.add_error_message("Value '{}' not in allowed values".format(value))
+ return self.default
+ return value
+
+
+class EvaluatedPInt(Evaluated):
+ def __init__(self, name=None):
+ super(EvaluatedPInt, self).__init__(int, 1, name)
+
+ def default_eval_func(self, instance):
+ value = super(EvaluatedPInt, self).default_eval_func(instance)
+ if value < 1:
+ # todo: log
+ return self.default
+ return value
+
+
+class EvaluatedFlag(Evaluated):
+ def __init__(self, name=None):
+ super(EvaluatedFlag, self).__init__((bool, int), False, name)
+
+
+def setup_names(cls):
+ for name, attrib in cls.__dict__.items():
+ if isinstance(attrib, Evaluated):
+ attrib.name = name
+ return cls
diff --git a/grc/core/utils/epy_block_io.py b/grc/core/utils/epy_block_io.py
index a094ab7ad5..823116adb9 100644
--- a/grc/core/utils/epy_block_io.py
+++ b/grc/core/utils/epy_block_io.py
@@ -1,7 +1,12 @@
+from __future__ import absolute_import
+
import inspect
import collections
+import six
+from six.moves import zip
+
TYPE_MAP = {
'complex64': 'complex', 'complex': 'complex',
@@ -32,10 +37,10 @@ def _ports(sigs, msgs):
def _find_block_class(source_code, cls):
ns = {}
try:
- exec source_code in ns
+ exec(source_code, ns)
except Exception as e:
raise ValueError("Can't interpret source code: " + str(e))
- for var in ns.itervalues():
+ for var in six.itervalues(ns):
if inspect.isclass(var) and issubclass(var, cls):
return var
raise ValueError('No python block class found in code')
@@ -53,7 +58,7 @@ def extract(cls):
spec = inspect.getargspec(cls.__init__)
init_args = spec.args[1:]
- defaults = map(repr, spec.defaults or ())
+ defaults = [repr(arg) for arg in (spec.defaults or ())]
doc = cls.__doc__ or cls.__init__.__doc__ or ''
cls_name = cls.__name__
diff --git a/grc/core/utils/expr_utils.py b/grc/core/utils/expr_utils.py
index 2059ceff9f..427585e93c 100644
--- a/grc/core/utils/expr_utils.py
+++ b/grc/core/utils/expr_utils.py
@@ -17,18 +17,111 @@ along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
"""
+from __future__ import absolute_import, print_function
+
import string
-VAR_CHARS = string.letters + string.digits + '_'
+import six
+
+
+def expr_replace(expr, replace_dict):
+ """
+ Search for vars in the expression and add the prepend.
+
+ Args:
+ expr: an expression string
+ replace_dict: a dict of find:replace
+
+ Returns:
+ a new expression with the prepend
+ """
+ expr_splits = _expr_split(expr, var_chars=VAR_CHARS + '.')
+ for i, es in enumerate(expr_splits):
+ if es in list(replace_dict.keys()):
+ expr_splits[i] = replace_dict[es]
+ return ''.join(expr_splits)
+
+
+def get_variable_dependencies(expr, vars):
+ """
+ Return a set of variables used in this expression.
+
+ Args:
+ expr: an expression string
+ vars: a list of variable names
+
+ Returns:
+ a subset of vars used in the expression
+ """
+ expr_toks = _expr_split(expr)
+ return set(v for v in vars if v in expr_toks)
+
+
+def sort_objects(objects, get_id, get_expr):
+ """
+ Sort a list of objects according to their expressions.
+
+ Args:
+ objects: the list of objects to sort
+ get_id: the function to extract an id from the object
+ get_expr: the function to extract an expression from the object
+
+ Returns:
+ a list of sorted objects
+ """
+ id2obj = {get_id(obj): obj for obj in objects}
+ # Map obj id to expression code
+ id2expr = {get_id(obj): get_expr(obj) for obj in objects}
+ # Sort according to dependency
+ sorted_ids = _sort_variables(id2expr)
+ # Return list of sorted objects
+ return [id2obj[id] for id in sorted_ids]
+
+
+import ast
+
+
+def dependencies(expr, names=None):
+ node = ast.parse(expr, mode='eval')
+ used_ids = frozenset([n.id for n in ast.walk(node) if isinstance(n, ast.Name)])
+ return used_ids & names if names else used_ids
+
+
+def sort_objects2(objects, id_getter, expr_getter, check_circular=True):
+ known_ids = {id_getter(obj) for obj in objects}
+
+ def dependent_ids(obj):
+ deps = dependencies(expr_getter(obj))
+ return [id_ if id_ in deps else None for id_ in known_ids]
-class graph(object):
+ objects = sorted(objects, key=dependent_ids)
+
+ if check_circular: # walk var defines step by step
+ defined_ids = set() # variables defined so far
+ for obj in objects:
+ deps = dependencies(expr_getter(obj), known_ids)
+ if not defined_ids.issuperset(deps): # can't have an undefined dep
+ raise RuntimeError(obj, deps, defined_ids)
+ defined_ids.add(id_getter(obj)) # define this one
+
+ return objects
+
+
+
+
+VAR_CHARS = string.ascii_letters + string.digits + '_'
+
+
+class _graph(object):
"""
Simple graph structure held in a dictionary.
"""
- def __init__(self): self._graph = dict()
+ def __init__(self):
+ self._graph = dict()
- def __str__(self): return str(self._graph)
+ def __str__(self):
+ return str(self._graph)
def add_node(self, node_key):
if node_key in self._graph:
@@ -50,13 +143,13 @@ class graph(object):
self._graph[src_node_key].remove(dest_node_key)
def get_nodes(self):
- return self._graph.keys()
+ return list(self._graph.keys())
def get_edges(self, node_key):
return self._graph[node_key]
-def expr_split(expr, var_chars=VAR_CHARS):
+def _expr_split(expr, var_chars=VAR_CHARS):
"""
Split up an expression by non alphanumeric characters, including underscore.
Leave strings in-tact.
@@ -85,43 +178,10 @@ def expr_split(expr, var_chars=VAR_CHARS):
toks.append(char)
tok = ''
toks.append(tok)
- return filter(lambda t: t, toks)
-
-
-def expr_replace(expr, replace_dict):
- """
- Search for vars in the expression and add the prepend.
-
- Args:
- expr: an expression string
- replace_dict: a dict of find:replace
-
- Returns:
- a new expression with the prepend
- """
- expr_splits = expr_split(expr, var_chars=VAR_CHARS + '.')
- for i, es in enumerate(expr_splits):
- if es in replace_dict.keys():
- expr_splits[i] = replace_dict[es]
- return ''.join(expr_splits)
+ return [t for t in toks if t]
-def get_variable_dependencies(expr, vars):
- """
- Return a set of variables used in this expression.
-
- Args:
- expr: an expression string
- vars: a list of variable names
-
- Returns:
- a subset of vars used in the expression
- """
- expr_toks = expr_split(expr)
- return set(var for var in vars if var in expr_toks)
-
-
-def get_graph(exprs):
+def _get_graph(exprs):
"""
Get a graph representing the variable dependencies
@@ -131,19 +191,19 @@ def get_graph(exprs):
Returns:
a graph of variable deps
"""
- vars = exprs.keys()
+ vars = list(exprs.keys())
# Get dependencies for each expression, load into graph
- var_graph = graph()
+ var_graph = _graph()
for var in vars:
var_graph.add_node(var)
- for var, expr in exprs.iteritems():
+ for var, expr in six.iteritems(exprs):
for dep in get_variable_dependencies(expr, vars):
if dep != var:
var_graph.add_edge(dep, var)
return var_graph
-def sort_variables(exprs):
+def _sort_variables(exprs):
"""
Get a list of variables in order of dependencies.
@@ -154,12 +214,12 @@ def sort_variables(exprs):
a list of variable names
@throws Exception circular dependencies
"""
- var_graph = get_graph(exprs)
+ 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())
+ indep_vars = [var for var in var_graph.get_nodes() if not var_graph.get_edges(var)]
if not indep_vars:
raise Exception('circular dependency caught in sort_variables')
# Add the indep vars to the end of the list
@@ -168,24 +228,3 @@ def sort_variables(exprs):
for var in indep_vars:
var_graph.remove_node(var)
return reversed(sorted_vars)
-
-
-def sort_objects(objects, get_id, get_expr):
- """
- Sort a list of objects according to their expressions.
-
- Args:
- objects: the list of objects to sort
- get_id: the function to extract an id from the object
- get_expr: the function to extract an expression from the object
-
- Returns:
- a list of sorted objects
- """
- id2obj = dict([(get_id(obj), obj) for obj in objects])
- # Map obj id to expression code
- id2expr = dict([(get_id(obj), get_expr(obj)) for obj in objects])
- # Sort according to dependency
- sorted_ids = sort_variables(id2expr)
- # Return list of sorted objects
- return [id2obj[id] for id in sorted_ids]
diff --git a/grc/core/utils/extract_docs.py b/grc/core/utils/extract_docs.py
index a6e0bc971e..7688f98de5 100644
--- a/grc/core/utils/extract_docs.py
+++ b/grc/core/utils/extract_docs.py
@@ -17,15 +17,19 @@ along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
"""
+from __future__ import absolute_import, print_function
+
import sys
import re
import subprocess
import threading
import json
-import Queue
import random
import itertools
+import six
+from six.moves import queue, filter, range
+
###############################################################################
# The docstring extraction
@@ -94,8 +98,7 @@ def docstring_from_make(key, imports, make):
if '$' in blk_cls:
raise ValueError('Not an identifier')
ns = dict()
- for _import in imports:
- exec(_import.strip(), ns)
+ exec(imports.strip(), ns)
blk = eval(blk_cls, ns)
doc_strings = {key: blk.__doc__}
@@ -124,7 +127,7 @@ class SubprocessLoader(object):
self.callback_query_result = callback_query_result
self.callback_finished = callback_finished or (lambda: None)
- self._queue = Queue.Queue()
+ self._queue = queue.Queue()
self._thread = None
self._worker = None
self._shutdown = threading.Event()
@@ -157,14 +160,15 @@ class SubprocessLoader(object):
cmd, args = self._last_cmd
if cmd == 'query':
msg += " (crashed while loading {0!r})".format(args[0])
- print >> sys.stderr, msg
+ print(msg, file=sys.stderr)
continue # restart
else:
break # normal termination, return
finally:
- self._worker.terminate()
+ if self._worker:
+ self._worker.terminate()
else:
- print >> sys.stderr, "Warning: docstring loader crashed too often"
+ print("Warning: docstring loader crashed too often", file=sys.stderr)
self._thread = None
self._worker = None
self.callback_finished()
@@ -203,9 +207,9 @@ class SubprocessLoader(object):
key, docs = args
self.callback_query_result(key, docs)
elif cmd == 'error':
- print args
+ print(args)
else:
- print >> sys.stderr, "Unknown response:", cmd, args
+ print("Unknown response:", cmd, args, file=sys.stderr)
def query(self, key, imports=None, make=None):
""" Request docstring extraction for a certain key """
@@ -270,12 +274,12 @@ if __name__ == '__worker__':
elif __name__ == '__main__':
def callback(key, docs):
- print key
- for match, doc in docs.iteritems():
- print '-->', match
- print doc.strip()
- print
- print
+ print(key)
+ for match, doc in six.iteritems(docs):
+ print('-->', match)
+ print(str(doc).strip())
+ print()
+ print()
r = SubprocessLoader(callback)
diff --git a/grc/core/utils/flow_graph_complexity.py b/grc/core/utils/flow_graph_complexity.py
new file mode 100644
index 0000000000..e8962b0ae3
--- /dev/null
+++ b/grc/core/utils/flow_graph_complexity.py
@@ -0,0 +1,54 @@
+
+def calculate(flowgraph):
+ """ Determines the complexity of a flowgraph """
+
+ try:
+ dbal = 0.0
+ for block in flowgraph.blocks:
+ if block.key == "options":
+ continue
+
+ # Determine the base value for this block
+ sinks = sum(1.0 for port in block.sinks if not port.optional)
+ sources = sum(1.0 for port in block.sources if not port.optional)
+ base = max(min(sinks, sources), 1)
+
+ # Determine the port multiplier
+ block_connections = 0.0
+ for port in block.sources:
+ block_connections += sum(1.0 for c in port.connections())
+ source_multi = max(block_connections / max(sources, 1.0), 1.0)
+
+ # Port ratio multiplier
+ multi = 1.0
+ if min(sinks, sources) > 0:
+ multi = float(sinks / sources)
+ multi = float(1 / multi) if multi > 1 else multi
+
+ dbal += base * multi * source_multi
+
+ blocks = float(len(flowgraph.blocks) - 1)
+ connections = float(len(flowgraph.connections))
+ variables = float(len(flowgraph.get_variables()))
+
+ enabled = float(len(flowgraph.get_enabled_blocks()))
+ enabled_connections = float(len(flowgraph.get_enabled_connections()))
+ disabled_connections = connections - enabled_connections
+
+ # Disabled multiplier
+ if enabled > 0:
+ disabled_multi = 1 / (max(1 - ((blocks - enabled) / max(blocks, 1)), 0.05))
+ else:
+ disabled_multi = 1
+
+ # Connection multiplier (How many connections )
+ if (connections - disabled_connections) > 0:
+ conn_multi = 1 / (max(1 - (disabled_connections / max(connections, 1)), 0.05))
+ else:
+ conn_multi = 1
+
+ final = round(max((dbal - 1) * disabled_multi * conn_multi * connections, 0.0) / 1000000, 6)
+ return final
+
+ except Exception:
+ return "<Error>"
diff --git a/grc/core/utils/hide_bokeh_gui_options_if_not_installed.py b/grc/core/utils/hide_bokeh_gui_options_if_not_installed.py
index fc0141851a..ab4a42b2e7 100644
--- a/grc/core/utils/hide_bokeh_gui_options_if_not_installed.py
+++ b/grc/core/utils/hide_bokeh_gui_options_if_not_installed.py
@@ -16,13 +16,12 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
-def hide_bokeh_gui_options_if_not_installed(options):
+def hide_bokeh_gui_options_if_not_installed(options_blk):
try:
import bokehgui
except ImportError:
- generate_option = options.get_param('generate_options')
- list_generate_option = generate_option.get_options()
- for option in list_generate_option:
- if option.get_key() == 'bokeh_gui':
- list_generate_option.remove(option)
- return
+ for param in options_blk.parameters_data:
+ if param['id'] == 'generate_options':
+ ind = param['options'].index('bokeh_gui')
+ del param['options'][ind]
+ del param['option_labels'][ind]
diff --git a/grc/core/utils/odict.py b/grc/core/utils/odict.py
deleted file mode 100644
index 85927e869f..0000000000
--- a/grc/core/utils/odict.py
+++ /dev/null
@@ -1,115 +0,0 @@
-"""
-Copyright 2008-2015 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 UserDict import DictMixin
-
-
-class odict(DictMixin):
-
- def __init__(self, d={}):
- self._keys = list(d.keys())
- self._data = dict(d.copy())
-
- def __setitem__(self, key, value):
- if key not in self._data:
- self._keys.append(key)
- self._data[key] = value
-
- def __getitem__(self, key):
- return self._data[key]
-
- def __delitem__(self, key):
- del self._data[key]
- self._keys.remove(key)
-
- def keys(self):
- return list(self._keys)
-
- def copy(self):
- copy_dict = odict()
- copy_dict._data = self._data.copy()
- copy_dict._keys = list(self._keys)
- return copy_dict
-
- def insert_after(self, pos_key, key, val):
- """
- Insert the new key, value entry after the entry given by the position key.
- If the positional key is None, insert at the end.
-
- Args:
- pos_key: the positional key
- key: the key for the new entry
- val: the value for the new entry
- """
- index = (pos_key is None) and len(self._keys) or self._keys.index(pos_key)
- if key in self._keys:
- raise KeyError('Cannot insert, key "{}" already exists'.format(str(key)))
- self._keys.insert(index+1, key)
- self._data[key] = val
-
- def insert_before(self, pos_key, key, val):
- """
- Insert the new key, value entry before the entry given by the position key.
- If the positional key is None, insert at the begining.
-
- Args:
- pos_key: the positional key
- key: the key for the new entry
- val: the value for the new entry
- """
- index = (pos_key is not None) and self._keys.index(pos_key) or 0
- if key in self._keys:
- raise KeyError('Cannot insert, key "{}" already exists'.format(str(key)))
- self._keys.insert(index, key)
- self._data[key] = val
-
- def find(self, key):
- """
- Get the value for this key if exists.
-
- Args:
- key: the key to search for
-
- Returns:
- the value or None
- """
- if key in self:
- return self[key]
- return None
-
- def findall(self, key):
- """
- Get a list of values for this key.
-
- Args:
- key: the key to search for
-
- Returns:
- a list of values or empty list
- """
- obj = self.find(key)
- if obj is None:
- obj = list()
- if isinstance(obj, list):
- return obj
- return [obj]
-
- def clear(self):
- self._data.clear()
- del self._keys[:] \ No newline at end of file
diff --git a/grc/gui/Actions.py b/grc/gui/Actions.py
index 6eccab75fb..d214f28049 100644
--- a/grc/gui/Actions.py
+++ b/grc/gui/Actions.py
@@ -17,284 +17,326 @@ 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
+from __future__ import absolute_import
-import Preferences
+import six
+import logging
-NO_MODS_MASK = 0
+from gi.repository import Gtk, Gdk, Gio, GLib, GObject
-########################################################################
-# Actions API
-########################################################################
-_actions_keypress_dict = dict()
-_keymap = gtk.gdk.keymap_get_default()
-_used_mods_mask = NO_MODS_MASK
-
-
-def handle_key_press(event):
- """
- Call the action associated with the key press event.
- Both the key value and the mask must have a match.
-
- Args:
- event: a gtk key press event
-
- Returns:
- true if handled
- """
- _used_mods_mask = reduce(lambda x, y: x | y, [mod_mask for keyval, mod_mask in _actions_keypress_dict], NO_MODS_MASK)
- # extract the key value and the consumed modifiers
- keyval, egroup, level, consumed = _keymap.translate_keyboard_state(
- event.hardware_keycode, event.state, event.group)
- # get the modifier mask and ignore irrelevant modifiers
- mod_mask = event.state & ~consumed & _used_mods_mask
- # look up the keypress and call the action
- try:
- _actions_keypress_dict[(keyval, mod_mask)]()
- except KeyError:
- return False # not handled
- else:
- return True # handled here
-
-_all_actions_list = list()
-
-
-def get_all_actions():
- return _all_actions_list
-
-_accel_group = gtk.AccelGroup()
-
-
-def get_accel_group():
- return _accel_group
-
-
-class _ActionBase(object):
- """
- Base class for Action and ToggleAction
- Register actions and keypresses with this module.
- """
- def __init__(self, label, keypresses):
- _all_actions_list.append(self)
- for i in range(len(keypresses)/2):
- keyval, mod_mask = keypresses[i*2:(i+1)*2]
- # register this keypress
- if _actions_keypress_dict.has_key((keyval, mod_mask)):
- raise KeyError('keyval/mod_mask pair already registered "%s"' % str((keyval, mod_mask)))
- _actions_keypress_dict[(keyval, mod_mask)] = self
- # set the accelerator group, and accelerator path
- # register the key name and mod mask with the accelerator path
- if label is None:
- continue # dont register accel
- accel_path = '<main>/' + self.get_name()
- self.set_accel_group(get_accel_group())
- self.set_accel_path(accel_path)
- gtk.accel_map_add_entry(accel_path, keyval, mod_mask)
- self.args = None
+
+log = logging.getLogger(__name__)
+
+
+def filter_from_dict(vars):
+ return filter(lambda x: isinstance(x[1], Action), vars.items())
+
+
+class Namespace(object):
+
+ def __init__(self):
+ self._actions = {}
+
+ def add(self, action):
+ key = action.props.name
+ self._actions[key] = action
+
+ def connect(self, name, handler):
+ #log.debug("Connecting action <{}> to handler <{}>".format(name, handler.__name__))
+ self._actions[name].connect('activate', handler)
+
+ def register(self, name, parameter=None, handler=None, label=None, tooltip=None,
+ icon_name=None, keypresses=None, preference_name=None, default=None):
+ # Check types
+ if not isinstance(name, str):
+ raise TypeError("Cannot register fuction: 'name' must be a str")
+ if parameter and not isinstance(parameter, str):
+ raise TypeError("Cannot register fuction: 'parameter' must be a str")
+ if handler and not callable(handler):
+ raise TypeError("Cannot register fuction: 'handler' must be callable")
+
+ # Check if the name has a prefix.
+ prefix = None
+ if name.startswith("app.") or name.startswith("win."):
+ # Set a prefix for later and remove it
+ prefix = name[0:3]
+ name = name[4:]
+
+ if handler:
+ log.debug("Register action [{}, prefix={}, param={}, handler={}]".format(
+ name, prefix, parameter, handler.__name__))
+ else:
+ log.debug("Register action [{}, prefix={}, param={}, handler=None]".format(
+ name, prefix, parameter))
+
+ action = Action(name, parameter, label=label, tooltip=tooltip,
+ icon_name=icon_name, keypresses=keypresses, prefix=prefix,
+ preference_name=preference_name, default=default)
+ if handler:
+ action.connect('activate', handler)
+
+ key = name
+ if prefix:
+ key = "{}.{}".format(prefix, name)
+ if prefix == "app":
+ pass
+ #self.app.add_action(action)
+ elif prefix == "win":
+ pass
+ #self.win.add_action(action)
+
+ #log.debug("Registering action as '{}'".format(key))
+ self._actions[key] = action
+ return action
+
+
+ # If the actions namespace is called, trigger an action
+ def __call__(self, name):
+ # Try to parse the action string.
+ valid, action_name, target_value = Action.parse_detailed_name(name)
+ if not valid:
+ raise Exception("Invalid action string: '{}'".format(name))
+ if action_name not in self._actions:
+ raise Exception("Action '{}' is not registered!".format(action_name))
+
+ if target_value:
+ self._actions[action_name].activate(target_value)
+ else:
+ self._actions[action_name].activate()
+
+ def __getitem__(self, key):
+ return self._actions[key]
+
+ def __iter__(self):
+ return self._actions.itervalues()
+
+ def __repr__(self):
+ return str(self)
+
+ def get_actions(self):
+ return self._actions
def __str__(self):
- """
- The string representation should be the name of the action id.
- Try to find the action id for this action by searching this module.
- """
- for name, value in globals().iteritems():
- if value == self:
- return name
- return self.get_name()
-
- def __repr__(self): return str(self)
-
- def __call__(self, *args):
- """
- Emit the activate signal when called with ().
- """
- self.args = args
- self.emit('activate')
-
-
-class Action(gtk.Action, _ActionBase):
- """
- A custom Action class based on gtk.Action.
- Pass additional arguments such as keypresses.
- """
-
- def __init__(self, keypresses=(), name=None, label=None, tooltip=None,
- stock_id=None):
- """
- Create a new Action instance.
-
- Args:
- key_presses: a tuple of (keyval1, mod_mask1, keyval2, mod_mask2, ...)
- the: regular gtk.Action parameters (defaults to None)
- """
- if name is None:
- name = label
- gtk.Action.__init__(self, name=name, label=label, tooltip=tooltip,
- stock_id=stock_id)
- _ActionBase.__init__(self, label, keypresses)
-
-
-class ToggleAction(gtk.ToggleAction, _ActionBase):
- """
- A custom Action class based on gtk.ToggleAction.
- Pass additional arguments such as keypresses.
- """
-
- def __init__(self, keypresses=(), name=None, label=None, tooltip=None,
- stock_id=None, preference_name=None, default=True):
- """
- Create a new ToggleAction instance.
-
- Args:
- key_presses: a tuple of (keyval1, mod_mask1, keyval2, mod_mask2, ...)
- the: regular gtk.Action parameters (defaults to None)
- """
- if name is None:
- name = label
- gtk.ToggleAction.__init__(self, name=name, label=label,
- tooltip=tooltip, stock_id=stock_id)
- _ActionBase.__init__(self, label, keypresses)
+ s = "{Actions:"
+ for key in self._actions:
+ s += " {},".format(key)
+ s = s.rstrip(",") + "}"
+ return s
+
+
+class Action(Gio.SimpleAction):
+
+ # Change these to normal python properties.
+ #prefs_name
+
+ def __init__(self, name, parameter=None, label=None, tooltip=None,
+ icon_name=None, keypresses=None, prefix=None,
+ preference_name=None, default=None):
+ self.name = name
+ self.label = label
+ self.tooltip = tooltip
+ self.icon_name = icon_name
+ self.keypresses = keypresses
+ self.prefix = prefix
self.preference_name = preference_name
self.default = default
- def load_from_preferences(self):
+ # Don't worry about checking types here, since it's done in register()
+ # Save the parameter type to use for converting in __call__
+ self.type = None
+
+ variant = None
+ state = None
+ if parameter:
+ variant = GLib.VariantType.new(parameter)
+ if preference_name:
+ state = GLib.Variant.new_boolean(True)
+ Gio.SimpleAction.__init__(self, name=name, parameter_type=variant, state=state)
+
+ def enable(self):
+ self.props.enabled = True
+
+ def disable(self):
+ self.props.enabled = False
+
+ def set_enabled(self, state):
+ if not isinstance(state, bool):
+ raise TypeError("State must be True/False.")
+ self.props.enabled = state
+
+ def __str__(self):
+ return self.props.name
+
+ def __repr__(self):
+ return str(self)
+
+ def get_active(self):
+ if self.props.state:
+ return self.props.state.get_boolean()
+ return False
+
+ def set_active(self, state):
+ if not isinstance(state, bool):
+ raise TypeError("State must be True/False.")
+ self.change_state(GLib.Variant.new_boolean(state))
+
+ # Allows actions to be directly called.
+ def __call__(self, parameter=None):
+ if self.type and parameter:
+ # Try to convert it to the correct type.
+ try:
+ param = GLib.Variant(self.type, parameter)
+ self.activate(param)
+ except TypeError:
+ raise TypeError("Invalid parameter type for action '{}'. Expected: '{}'".format(self.get_name(), self.type))
+ else:
+ self.activate()
+
+ def load_from_preferences(self, *args):
+ log.debug("load_from_preferences({})".format(args))
if self.preference_name is not None:
- self.set_active(Preferences.entry(
- self.preference_name, default=bool(self.default)))
+ config = Gtk.Application.get_default().config
+ self.set_active(config.entry(self.preference_name, default=bool(self.default)))
- def save_to_preferences(self):
+ def save_to_preferences(self, *args):
+ log.debug("save_to_preferences({})".format(args))
if self.preference_name is not None:
- Preferences.entry(self.preference_name, value=self.get_active())
+ config = Gtk.Application.get_default().config
+ config.entry(self.preference_name, value=self.get_active())
+
+
+actions = Namespace()
+
+
+def get_actions():
+ return actions.get_actions()
+
+
+def connect(action, handler=None):
+ return actions.connect(action, handler=handler)
+
########################################################################
-# Actions
+# Old Actions
########################################################################
-PAGE_CHANGE = Action()
-EXTERNAL_UPDATE = Action()
-VARIABLE_EDITOR_UPDATE = Action()
-FLOW_GRAPH_NEW = Action(
+PAGE_CHANGE = actions.register("win.page_change")
+EXTERNAL_UPDATE = actions.register("app.external_update")
+VARIABLE_EDITOR_UPDATE = actions.register("app.variable_editor_update")
+FLOW_GRAPH_NEW = actions.register("app.flowgraph.new",
label='_New',
tooltip='Create a new flow graph',
- stock_id=gtk.STOCK_NEW,
- keypresses=(gtk.keysyms.n, gtk.gdk.CONTROL_MASK),
+ icon_name='document-new',
+ keypresses=["<Ctrl>n"],
+ parameter="s",
)
-FLOW_GRAPH_OPEN = Action(
+FLOW_GRAPH_OPEN = actions.register("app.flowgraph.open",
label='_Open',
tooltip='Open an existing flow graph',
- stock_id=gtk.STOCK_OPEN,
- keypresses=(gtk.keysyms.o, gtk.gdk.CONTROL_MASK),
+ icon_name='document-open',
+ keypresses=["<Ctrl>o"],
)
-FLOW_GRAPH_OPEN_RECENT = Action(
+FLOW_GRAPH_OPEN_RECENT = actions.register("app.flowgraph.open_recent",
label='Open _Recent',
tooltip='Open a recently used flow graph',
- stock_id=gtk.STOCK_OPEN,
+ icon_name='document-open-recent',
+ parameter="s",
)
-FLOW_GRAPH_SAVE = Action(
+FLOW_GRAPH_CLEAR_RECENT = actions.register("app.flowgraph.clear_recent")
+FLOW_GRAPH_SAVE = actions.register("app.flowgraph.save",
label='_Save',
tooltip='Save the current flow graph',
- stock_id=gtk.STOCK_SAVE,
- keypresses=(gtk.keysyms.s, gtk.gdk.CONTROL_MASK),
+ icon_name='document-save',
+ keypresses=["<Ctrl>s"],
)
-FLOW_GRAPH_SAVE_AS = Action(
+FLOW_GRAPH_SAVE_AS = actions.register("app.flowgraph.save_as",
label='Save _As',
tooltip='Save the current flow graph as...',
- stock_id=gtk.STOCK_SAVE_AS,
- keypresses=(gtk.keysyms.s, gtk.gdk.CONTROL_MASK | gtk.gdk.SHIFT_MASK),
+ icon_name='document-save-as',
+ keypresses=["<Ctrl><Shift>s"],
)
-FLOW_GRAPH_SAVE_A_COPY = Action(
- label='Save A Copy',
- tooltip='Save the copy of current flowgraph',
+FLOW_GRAPH_SAVE_COPY = actions.register("app.flowgraph.save_copy",
+ label='Save Copy',
+ tooltip='Save a copy of current flow graph',
)
-FLOW_GRAPH_DUPLICATE = Action(
+FLOW_GRAPH_DUPLICATE = actions.register("app.flowgraph.duplicate",
label='_Duplicate',
- tooltip='Create a duplicate of current flowgraph',
- stock_id=gtk.STOCK_COPY,
- keypresses=(gtk.keysyms.d, gtk.gdk.CONTROL_MASK | gtk.gdk.SHIFT_MASK),
+ tooltip='Create a duplicate of current flow graph',
+ #stock_id=Gtk.STOCK_COPY,
+ keypresses=["<Ctrl><Shift>d"],
)
-FLOW_GRAPH_CLOSE = Action(
+FLOW_GRAPH_CLOSE = actions.register("app.flowgraph.close",
label='_Close',
tooltip='Close the current flow graph',
- stock_id=gtk.STOCK_CLOSE,
- keypresses=(gtk.keysyms.w, gtk.gdk.CONTROL_MASK),
+ icon_name='window-close',
+ keypresses=["<Ctrl>w"],
)
-APPLICATION_INITIALIZE = Action()
-APPLICATION_QUIT = Action(
+APPLICATION_INITIALIZE = actions.register("app.initialize")
+APPLICATION_QUIT = actions.register("app.quit",
label='_Quit',
tooltip='Quit program',
- stock_id=gtk.STOCK_QUIT,
- keypresses=(gtk.keysyms.q, gtk.gdk.CONTROL_MASK),
+ icon_name='application-exit',
+ keypresses=["<Ctrl>q"],
)
-FLOW_GRAPH_UNDO = Action(
+FLOW_GRAPH_UNDO = actions.register("win.undo",
label='_Undo',
tooltip='Undo a change to the flow graph',
- stock_id=gtk.STOCK_UNDO,
- keypresses=(gtk.keysyms.z, gtk.gdk.CONTROL_MASK),
+ icon_name='edit-undo',
+ keypresses=["<Ctrl>z"],
)
-FLOW_GRAPH_REDO = Action(
+FLOW_GRAPH_REDO = actions.register("win.redo",
label='_Redo',
tooltip='Redo a change to the flow graph',
- stock_id=gtk.STOCK_REDO,
- keypresses=(gtk.keysyms.y, gtk.gdk.CONTROL_MASK),
+ icon_name='edit-redo',
+ keypresses=["<Ctrl>y"],
)
-NOTHING_SELECT = Action()
-SELECT_ALL = Action(
+NOTHING_SELECT = actions.register("win.unselect")
+SELECT_ALL = actions.register("win.select_all",
label='Select _All',
tooltip='Select all blocks and connections in the flow graph',
- stock_id=gtk.STOCK_SELECT_ALL,
- keypresses=(gtk.keysyms.a, gtk.gdk.CONTROL_MASK),
+ icon_name='edit-select-all',
+ keypresses=["<Ctrl>a"],
)
-ELEMENT_SELECT = Action()
-ELEMENT_CREATE = Action()
-ELEMENT_DELETE = Action(
+ELEMENT_SELECT = actions.register("win.select")
+ELEMENT_CREATE = actions.register("win.add")
+ELEMENT_DELETE = actions.register("win.delete",
label='_Delete',
tooltip='Delete the selected blocks',
- stock_id=gtk.STOCK_DELETE,
- keypresses=(gtk.keysyms.Delete, NO_MODS_MASK),
+ icon_name='edit-delete',
)
-BLOCK_MOVE = Action()
-BLOCK_ROTATE_CCW = Action(
+BLOCK_MOVE = actions.register("win.block_move")
+BLOCK_ROTATE_CCW = actions.register("win.block_rotate_ccw",
label='Rotate Counterclockwise',
tooltip='Rotate the selected blocks 90 degrees to the left',
- stock_id=gtk.STOCK_GO_BACK,
- keypresses=(gtk.keysyms.Left, NO_MODS_MASK),
+ icon_name='object-rotate-left',
)
-BLOCK_ROTATE_CW = Action(
+BLOCK_ROTATE_CW = actions.register("win.block_rotate",
label='Rotate Clockwise',
tooltip='Rotate the selected blocks 90 degrees to the right',
- stock_id=gtk.STOCK_GO_FORWARD,
- keypresses=(gtk.keysyms.Right, NO_MODS_MASK),
+ icon_name='object-rotate-right',
)
-BLOCK_VALIGN_TOP = Action(
+BLOCK_VALIGN_TOP = actions.register("win.block_align_top",
label='Vertical Align Top',
tooltip='Align tops of selected blocks',
- keypresses=(gtk.keysyms.t, gtk.gdk.SHIFT_MASK),
)
-BLOCK_VALIGN_MIDDLE = Action(
+BLOCK_VALIGN_MIDDLE = actions.register("win.block_align_middle",
label='Vertical Align Middle',
tooltip='Align centers of selected blocks vertically',
- keypresses=(gtk.keysyms.m, gtk.gdk.SHIFT_MASK),
)
-BLOCK_VALIGN_BOTTOM = Action(
+BLOCK_VALIGN_BOTTOM = actions.register("win.block_align_bottom",
label='Vertical Align Bottom',
tooltip='Align bottoms of selected blocks',
- keypresses=(gtk.keysyms.b, gtk.gdk.SHIFT_MASK),
)
-BLOCK_HALIGN_LEFT = Action(
+BLOCK_HALIGN_LEFT = actions.register("win.block_align_left",
label='Horizontal Align Left',
tooltip='Align left edges of blocks selected blocks',
- keypresses=(gtk.keysyms.l, gtk.gdk.SHIFT_MASK),
)
-BLOCK_HALIGN_CENTER = Action(
+BLOCK_HALIGN_CENTER = actions.register("win.block_align_center",
label='Horizontal Align Center',
tooltip='Align centers of selected blocks horizontally',
- keypresses=(gtk.keysyms.c, gtk.gdk.SHIFT_MASK),
)
-BLOCK_HALIGN_RIGHT = Action(
+BLOCK_HALIGN_RIGHT = actions.register("win.block_align_right",
label='Horizontal Align Right',
tooltip='Align right edges of selected blocks',
- keypresses=(gtk.keysyms.r, gtk.gdk.SHIFT_MASK),
)
BLOCK_ALIGNMENTS = [
BLOCK_VALIGN_TOP,
@@ -305,234 +347,222 @@ BLOCK_ALIGNMENTS = [
BLOCK_HALIGN_CENTER,
BLOCK_HALIGN_RIGHT,
]
-BLOCK_PARAM_MODIFY = Action(
+BLOCK_PARAM_MODIFY = actions.register("win.block_modify",
label='_Properties',
tooltip='Modify params for the selected block',
- stock_id=gtk.STOCK_PROPERTIES,
- keypresses=(gtk.keysyms.Return, NO_MODS_MASK),
+ icon_name='document-properties',
)
-BLOCK_ENABLE = Action(
+BLOCK_ENABLE = actions.register("win.block_enable",
label='E_nable',
tooltip='Enable the selected blocks',
- stock_id=gtk.STOCK_CONNECT,
- keypresses=(gtk.keysyms.e, NO_MODS_MASK),
+ icon_name='network-wired',
)
-BLOCK_DISABLE = Action(
+BLOCK_DISABLE = actions.register("win.block_disable",
label='D_isable',
tooltip='Disable the selected blocks',
- stock_id=gtk.STOCK_DISCONNECT,
- keypresses=(gtk.keysyms.d, NO_MODS_MASK),
+ icon_name='network-wired-disconnected',
)
-BLOCK_BYPASS = Action(
+BLOCK_BYPASS = actions.register("win.block_bypass",
label='_Bypass',
tooltip='Bypass the selected block',
- stock_id=gtk.STOCK_MEDIA_FORWARD,
- keypresses=(gtk.keysyms.b, NO_MODS_MASK),
+ icon_name='media-seek-forward',
)
-TOGGLE_SNAP_TO_GRID = ToggleAction(
+TOGGLE_SNAP_TO_GRID = actions.register("win.snap_to_grid",
label='_Snap to grid',
tooltip='Snap blocks to a grid for an easier connection alignment',
- preference_name='snap_to_grid'
+ preference_name='snap_to_grid',
)
-TOGGLE_HIDE_DISABLED_BLOCKS = ToggleAction(
+TOGGLE_HIDE_DISABLED_BLOCKS = actions.register("win.hide_disabled",
label='Hide _Disabled Blocks',
tooltip='Toggle visibility of disabled blocks and connections',
- stock_id=gtk.STOCK_MISSING_IMAGE,
- keypresses=(gtk.keysyms.d, gtk.gdk.CONTROL_MASK),
+ icon_name='image-missing',
+ keypresses=["<Ctrl>d"],
+ preference_name='hide_disabled',
)
-TOGGLE_HIDE_VARIABLES = ToggleAction(
+TOGGLE_HIDE_VARIABLES = actions.register("win.hide_variables",
label='Hide Variables',
tooltip='Hide all variable blocks',
preference_name='hide_variables',
default=False,
)
-TOGGLE_FLOW_GRAPH_VAR_EDITOR = ToggleAction(
+TOGGLE_FLOW_GRAPH_VAR_EDITOR = actions.register("win.toggle_variable_editor",
label='Show _Variable Editor',
tooltip='Show the variable editor. Modify variables and imports in this flow graph',
- stock_id=gtk.STOCK_EDIT,
+ icon_name='accessories-text-editor',
default=True,
- keypresses=(gtk.keysyms.e, gtk.gdk.CONTROL_MASK),
+ keypresses=["<Ctrl>e"],
preference_name='variable_editor_visable',
)
-TOGGLE_FLOW_GRAPH_VAR_EDITOR_SIDEBAR = ToggleAction(
+TOGGLE_FLOW_GRAPH_VAR_EDITOR_SIDEBAR = actions.register("win.toggle_variable_editor_sidebar",
label='Move the Variable Editor to the Sidebar',
tooltip='Move the variable editor to the sidebar',
default=False,
preference_name='variable_editor_sidebar',
)
-TOGGLE_AUTO_HIDE_PORT_LABELS = ToggleAction(
+TOGGLE_AUTO_HIDE_PORT_LABELS = actions.register("win.auto_hide_port_labels",
label='Auto-Hide _Port Labels',
tooltip='Automatically hide port labels',
preference_name='auto_hide_port_labels'
)
-TOGGLE_SHOW_BLOCK_COMMENTS = ToggleAction(
+TOGGLE_SHOW_BLOCK_COMMENTS = actions.register("win.show_block_comments",
label='Show Block Comments',
tooltip="Show comment beneath each block",
preference_name='show_block_comments'
)
-TOGGLE_SHOW_CODE_PREVIEW_TAB = ToggleAction(
+TOGGLE_SHOW_CODE_PREVIEW_TAB = actions.register("win.toggle_code_preview",
label='Generated Code Preview',
tooltip="Show a preview of the code generated for each Block in its "
"Properties Dialog",
preference_name='show_generated_code_tab',
default=False,
)
-TOGGLE_SHOW_FLOWGRAPH_COMPLEXITY = ToggleAction(
+TOGGLE_SHOW_FLOWGRAPH_COMPLEXITY = actions.register("win.show_flowgraph_complexity",
label='Show Flowgraph Complexity',
tooltip="How many Balints is the flowgraph...",
preference_name='show_flowgraph_complexity',
default=False,
)
-BLOCK_CREATE_HIER = Action(
+BLOCK_CREATE_HIER = actions.register("win.block_create_hier",
label='C_reate Hier',
tooltip='Create hier block from selected blocks',
- stock_id=gtk.STOCK_CONNECT,
-# keypresses=(gtk.keysyms.c, NO_MODS_MASK),
+ icon_name='document-new',
)
-BLOCK_CUT = Action(
+BLOCK_CUT = actions.register("win.block_cut",
label='Cu_t',
tooltip='Cut',
- stock_id=gtk.STOCK_CUT,
- keypresses=(gtk.keysyms.x, gtk.gdk.CONTROL_MASK),
+ icon_name='edit-cut',
+ keypresses=["<Ctrl>x"],
)
-BLOCK_COPY = Action(
+BLOCK_COPY = actions.register("win.block_copy",
label='_Copy',
tooltip='Copy',
- stock_id=gtk.STOCK_COPY,
- keypresses=(gtk.keysyms.c, gtk.gdk.CONTROL_MASK),
+ icon_name='edit-copy',
+ keypresses=["<Ctrl>c"],
)
-BLOCK_PASTE = Action(
+BLOCK_PASTE = actions.register("win.block_paste",
label='_Paste',
tooltip='Paste',
- stock_id=gtk.STOCK_PASTE,
- keypresses=(gtk.keysyms.v, gtk.gdk.CONTROL_MASK),
+ icon_name='edit-paste',
+ keypresses=["<Ctrl>v"],
)
-ERRORS_WINDOW_DISPLAY = Action(
+ERRORS_WINDOW_DISPLAY = actions.register("app.errors",
label='Flowgraph _Errors',
tooltip='View flow graph errors',
- stock_id=gtk.STOCK_DIALOG_ERROR,
+ icon_name='dialog-error',
)
-TOGGLE_CONSOLE_WINDOW = ToggleAction(
+TOGGLE_CONSOLE_WINDOW = actions.register("win.toggle_console_window",
label='Show _Console Panel',
tooltip='Toggle visibility of the console',
- keypresses=(gtk.keysyms.r, gtk.gdk.CONTROL_MASK),
+ keypresses=["<Ctrl>r"],
preference_name='console_window_visible'
)
-TOGGLE_BLOCKS_WINDOW = ToggleAction(
+# TODO: Might be able to convert this to a Gio.PropertyAction eventually.
+# actions would need to be defined in the correct class and not globally
+TOGGLE_BLOCKS_WINDOW = actions.register("win.toggle_blocks_window",
label='Show _Block Tree Panel',
tooltip='Toggle visibility of the block tree widget',
- keypresses=(gtk.keysyms.b, gtk.gdk.CONTROL_MASK),
+ keypresses=["<Ctrl>b"],
preference_name='blocks_window_visible'
)
-TOGGLE_SCROLL_LOCK = ToggleAction(
+TOGGLE_SCROLL_LOCK = actions.register("win.console.scroll_lock",
label='Console Scroll _Lock',
tooltip='Toggle scroll lock for the console window',
preference_name='scroll_lock'
)
-ABOUT_WINDOW_DISPLAY = Action(
+ABOUT_WINDOW_DISPLAY = actions.register("app.about",
label='_About',
tooltip='About this program',
- stock_id=gtk.STOCK_ABOUT,
+ icon_name='help-about',
)
-HELP_WINDOW_DISPLAY = Action(
+HELP_WINDOW_DISPLAY = actions.register("app.help",
label='_Help',
tooltip='Usage tips',
- stock_id=gtk.STOCK_HELP,
- keypresses=(gtk.keysyms.F1, NO_MODS_MASK),
+ icon_name='help-contents',
+ keypresses=["F1"],
)
-TYPES_WINDOW_DISPLAY = Action(
+TYPES_WINDOW_DISPLAY = actions.register("app.types",
label='_Types',
tooltip='Types color mapping',
- stock_id=gtk.STOCK_DIALOG_INFO,
+ icon_name='dialog-information',
)
-FLOW_GRAPH_GEN = Action(
+FLOW_GRAPH_GEN = actions.register("app.flowgraph.generate",
label='_Generate',
tooltip='Generate the flow graph',
- stock_id=gtk.STOCK_CONVERT,
- keypresses=(gtk.keysyms.F5, NO_MODS_MASK),
+ icon_name='insert-object',
+ keypresses=["F5"],
)
-FLOW_GRAPH_EXEC = Action(
+FLOW_GRAPH_EXEC = actions.register("app.flowgraph.execute",
label='_Execute',
tooltip='Execute the flow graph',
- stock_id=gtk.STOCK_MEDIA_PLAY,
- keypresses=(gtk.keysyms.F6, NO_MODS_MASK),
+ icon_name='media-playback-start',
+ keypresses=["F6"],
)
-FLOW_GRAPH_KILL = Action(
+FLOW_GRAPH_KILL = actions.register("app.flowgraph.kill",
label='_Kill',
tooltip='Kill the flow graph',
- stock_id=gtk.STOCK_STOP,
- keypresses=(gtk.keysyms.F7, NO_MODS_MASK),
+ icon_name='media-playback-stop',
+ keypresses=["F7"],
)
-FLOW_GRAPH_SCREEN_CAPTURE = Action(
+FLOW_GRAPH_SCREEN_CAPTURE = actions.register("app.flowgraph.screen_capture",
label='Screen Ca_pture',
tooltip='Create a screen capture of the flow graph',
- stock_id=gtk.STOCK_PRINT,
- keypresses=(gtk.keysyms.Print, NO_MODS_MASK),
-)
-PORT_CONTROLLER_DEC = Action(
- keypresses=(gtk.keysyms.minus, NO_MODS_MASK, gtk.keysyms.KP_Subtract, NO_MODS_MASK),
-)
-PORT_CONTROLLER_INC = Action(
- keypresses=(gtk.keysyms.plus, NO_MODS_MASK, gtk.keysyms.KP_Add, NO_MODS_MASK),
-)
-BLOCK_INC_TYPE = Action(
- keypresses=(gtk.keysyms.Down, NO_MODS_MASK),
-)
-BLOCK_DEC_TYPE = Action(
- keypresses=(gtk.keysyms.Up, NO_MODS_MASK),
-)
-RELOAD_BLOCKS = Action(
+ icon_name='printer',
+ keypresses=["<Ctrl>p"],
+)
+PORT_CONTROLLER_DEC = actions.register("win.port_controller_dec")
+PORT_CONTROLLER_INC = actions.register("win.port_controller_inc")
+BLOCK_INC_TYPE = actions.register("win.block_inc_type")
+BLOCK_DEC_TYPE = actions.register("win.block_dec_type")
+RELOAD_BLOCKS = actions.register("app.reload_blocks",
label='Reload _Blocks',
tooltip='Reload Blocks',
- stock_id=gtk.STOCK_REFRESH
+ icon_name='view-refresh'
)
-FIND_BLOCKS = Action(
+FIND_BLOCKS = actions.register("win.find_blocks",
label='_Find Blocks',
tooltip='Search for a block by name (and key)',
- stock_id=gtk.STOCK_FIND,
- keypresses=(gtk.keysyms.f, gtk.gdk.CONTROL_MASK,
- gtk.keysyms.slash, NO_MODS_MASK),
+ icon_name='edit-find',
+ keypresses=["<Ctrl>f", "slash"],
)
-CLEAR_CONSOLE = Action(
+CLEAR_CONSOLE = actions.register("win.console.clear",
label='_Clear Console',
tooltip='Clear Console',
- stock_id=gtk.STOCK_CLEAR,
+ icon_name='edit-clear',
)
-SAVE_CONSOLE = Action(
+SAVE_CONSOLE = actions.register("win.console.save",
label='_Save Console',
tooltip='Save Console',
- stock_id=gtk.STOCK_SAVE,
+ icon_name='edit-save',
)
-OPEN_HIER = Action(
+OPEN_HIER = actions.register("win.open_hier",
label='Open H_ier',
tooltip='Open the source of the selected hierarchical block',
- stock_id=gtk.STOCK_JUMP_TO,
+ icon_name='go-jump',
)
-BUSSIFY_SOURCES = Action(
+BUSSIFY_SOURCES = actions.register("win.bussify_sources",
label='Toggle So_urce Bus',
tooltip='Gang source ports into a single bus port',
- stock_id=gtk.STOCK_JUMP_TO,
+ icon_name='go-jump',
)
-BUSSIFY_SINKS = Action(
+BUSSIFY_SINKS = actions.register("win.bussify_sinks",
label='Toggle S_ink Bus',
tooltip='Gang sink ports into a single bus port',
- stock_id=gtk.STOCK_JUMP_TO,
+ icon_name='go-jump',
)
-XML_PARSER_ERRORS_DISPLAY = Action(
+XML_PARSER_ERRORS_DISPLAY = actions.register("app.xml_errors",
label='_Parser Errors',
tooltip='View errors that occurred while parsing XML files',
- stock_id=gtk.STOCK_DIALOG_ERROR,
+ icon_name='dialog-error',
)
-FLOW_GRAPH_OPEN_QSS_THEME = Action(
+FLOW_GRAPH_OPEN_QSS_THEME = actions.register("app.open_qss_theme",
label='Set Default QT GUI _Theme',
tooltip='Set a default QT Style Sheet file to use for QT GUI',
- stock_id=gtk.STOCK_OPEN,
+ icon_name='document-open',
)
-TOOLS_RUN_FDESIGN = Action(
+TOOLS_RUN_FDESIGN = actions.register("app.filter_design",
label='Filter Design Tool',
tooltip='Execute gr_filter_design',
- stock_id=gtk.STOCK_EXECUTE,
-)
-TOOLS_MORE_TO_COME = Action(
- label='More to come',
+ icon_name='media-playback-start',
)
+POST_HANDLER = actions.register("app.post_handler")
+READY = actions.register("app.ready")
diff --git a/grc/gui/ActionHandler.py b/grc/gui/Application.py
index aba9033d74..70cf9b78b2 100644
--- a/grc/gui/ActionHandler.py
+++ b/grc/gui/Application.py
@@ -18,33 +18,36 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
"""
-import gobject
-import gtk
+from __future__ import absolute_import, print_function
+
+import logging
import os
import subprocess
-from . import Dialogs, Preferences, Actions, Executor, Constants, Utils
-from .FileDialogs import (OpenFlowGraphFileDialog, SaveFlowGraphFileDialog,
- SaveConsoleFileDialog, SaveScreenShotDialog,
- OpenQSSFileDialog)
+from gi.repository import Gtk, Gio, GLib, GObject
+
+from . import Constants, Dialogs, Actions, Executor, FileDialogs, Utils, Bars
+
from .MainWindow import MainWindow
-from .ParserErrorsDialog import ParserErrorsDialog
+# from .ParserErrorsDialog import ParserErrorsDialog
from .PropsDialog import PropsDialog
-from ..core import ParseXML, Messages
+from ..core import Messages
-gobject.threads_init()
+log = logging.getLogger(__name__)
-class ActionHandler:
+
+class Application(Gtk.Application):
"""
The action handler will setup all the major window components,
and handle button presses and flow graph operations from the GUI.
"""
def __init__(self, file_paths, platform):
+ Gtk.Application.__init__(self)
"""
- ActionHandler constructor.
+ Application constructor.
Create the main window, setup the message handler, import the preferences,
and connect all of the action handlers. Finally, enter the gtk main loop and block.
@@ -54,41 +57,57 @@ class ActionHandler:
"""
self.clipboard = None
self.dialog = None
- for action in Actions.get_all_actions(): action.connect('activate', self._handle_action)
- #setup the main window
+
+ # Setup the main window
self.platform = platform
- self.main_window = MainWindow(platform, self._handle_action)
+ self.config = platform.config
+
+ log.debug("Application()")
+ # Connect all actions to _handle_action
+ for x in Actions.get_actions():
+ Actions.connect(x, handler=self._handle_action)
+ Actions.actions[x].enable()
+ if x.startswith("app."):
+ self.add_action(Actions.actions[x])
+ # Setup the shortcut keys
+ # These are the globally defined shortcuts
+ keypress = Actions.actions[x].keypresses
+ if keypress:
+ self.set_accels_for_action(x, keypress)
+
+ # Initialize
+ self.init_file_paths = [os.path.abspath(file_path) for file_path in file_paths]
+ self.init = False
+
+ def do_startup(self):
+ Gtk.Application.do_startup(self)
+ log.debug("Application.do_startup()")
+
+ # Setup the menu
+ log.debug("Creating menu")
+ '''
+ self.menu = Bars.Menu()
+ self.set_menu()
+ if self.prefers_app_menu():
+ self.set_app_menu(self.menu)
+ else:
+ self.set_menubar(self.menu)
+ '''
+
+ def do_activate(self):
+ Gtk.Application.do_activate(self)
+ log.debug("Application.do_activate()")
+
+ self.main_window = MainWindow(self, self.platform)
self.main_window.connect('delete-event', self._quit)
- self.main_window.connect('key-press-event', self._handle_key_press)
- self.get_page = self.main_window.get_page
self.get_focus_flag = self.main_window.get_focus_flag
+
#setup the messages
Messages.register_messenger(self.main_window.add_console_line)
- Messages.send_init(platform)
- #initialize
- self.init_file_paths = [os.path.abspath(file_path) for file_path in file_paths]
- self.init = False
- Actions.APPLICATION_INITIALIZE()
+ Messages.send_init(self.platform)
- def _handle_key_press(self, widget, event):
- """
- Handle key presses from the keyboard and translate key combinations into actions.
- This key press handler is called prior to the gtk key press handler.
- This handler bypasses built in accelerator key handling when in focus because
- * some keys are ignored by the accelerators like the direction keys,
- * some keys are not registered to any accelerators but are still used.
- When not in focus, gtk and the accelerators handle the the key press.
-
- Returns:
- false to let gtk handle the key action
- """
- # prevent key event stealing while the search box is active
- # .has_focus() only in newer versions 2.17+?
- # .is_focus() seems to work, but exactly the same
- if self.main_window.btwin.search_entry.flags() & gtk.HAS_FOCUS:
- return False
- if not self.get_focus_flag(): return False
- return Actions.handle_key_press(event)
+ log.debug("Calling Actions.APPLICATION_INITIALIZE")
+ Actions.APPLICATION_INITIALIZE()
def _quit(self, window, event):
"""
@@ -103,61 +122,111 @@ class ActionHandler:
return True
def _handle_action(self, action, *args):
- #print action
+ log.debug("_handle_action({0}, {1})".format(action, args))
main = self.main_window
- page = main.get_page()
- flow_graph = page.get_flow_graph() if page else None
+ page = main.current_page
+ flow_graph = page.flow_graph if page else None
def flow_graph_update(fg=flow_graph):
- main.vars.update_gui()
+ main.vars.update_gui(fg.blocks)
fg.update()
##################################################
# Initialize/Quit
##################################################
if action == Actions.APPLICATION_INITIALIZE:
- file_path_to_show = Preferences.file_open()
- for file_path in (self.init_file_paths or Preferences.get_open_files()):
+ log.debug("APPLICATION_INITIALIZE")
+ file_path_to_show = self.config.file_open()
+ for file_path in (self.init_file_paths or self.config.get_open_files()):
if os.path.exists(file_path):
main.new_page(file_path, show=file_path_to_show == file_path)
- if not self.get_page():
+ if not main.current_page:
main.new_page() # ensure that at least a blank page exists
main.btwin.search_entry.hide()
- # Disable all actions, then re-enable a few
- for action in Actions.get_all_actions():
- action.set_sensitive(False) # set all actions disabled
+ """
+ Only disable certain actions on startup. Each of these actions are
+ conditionally enabled in _handle_action, so disable them first.
+ - FLOW_GRAPH_UNDO/REDO are set in gui/StateCache.py
+ - XML_PARSER_ERRORS_DISPLAY is set in RELOAD_BLOCKS
+
+ TODO: These 4 should probably be included, but they are not currently
+ enabled anywhere else:
+ - PORT_CONTROLLER_DEC, PORT_CONTROLLER_INC
+ - BLOCK_INC_TYPE, BLOCK_DEC_TYPE
+
+ TODO: These should be handled better. They are set in
+ update_exec_stop(), but not anywhere else
+ - FLOW_GRAPH_GEN, FLOW_GRAPH_EXEC, FLOW_GRAPH_KILL
+ """
+ for action in (
+ Actions.ERRORS_WINDOW_DISPLAY,
+ Actions.ELEMENT_DELETE,
+ Actions.BLOCK_PARAM_MODIFY,
+ Actions.BLOCK_ROTATE_CCW,
+ Actions.BLOCK_ROTATE_CW,
+ Actions.BLOCK_VALIGN_TOP,
+ Actions.BLOCK_VALIGN_MIDDLE,
+ Actions.BLOCK_VALIGN_BOTTOM,
+ Actions.BLOCK_HALIGN_LEFT,
+ Actions.BLOCK_HALIGN_CENTER,
+ Actions.BLOCK_HALIGN_RIGHT,
+ Actions.BLOCK_CUT,
+ Actions.BLOCK_COPY,
+ Actions.BLOCK_PASTE,
+ Actions.BLOCK_ENABLE,
+ Actions.BLOCK_DISABLE,
+ Actions.BLOCK_BYPASS,
+ Actions.BLOCK_CREATE_HIER,
+ Actions.OPEN_HIER,
+ Actions.BUSSIFY_SOURCES,
+ Actions.BUSSIFY_SINKS,
+ Actions.FLOW_GRAPH_SAVE,
+ Actions.FLOW_GRAPH_UNDO,
+ Actions.FLOW_GRAPH_REDO,
+ Actions.XML_PARSER_ERRORS_DISPLAY
+ ):
+ action.disable()
+
+ # Load preferences
for action in (
- Actions.APPLICATION_QUIT, Actions.FLOW_GRAPH_NEW,
- Actions.FLOW_GRAPH_OPEN, Actions.FLOW_GRAPH_SAVE_AS,
- Actions.FLOW_GRAPH_DUPLICATE, Actions.FLOW_GRAPH_SAVE_A_COPY,
- Actions.FLOW_GRAPH_CLOSE, Actions.ABOUT_WINDOW_DISPLAY,
- Actions.FLOW_GRAPH_SCREEN_CAPTURE, Actions.HELP_WINDOW_DISPLAY,
- Actions.TYPES_WINDOW_DISPLAY, Actions.TOGGLE_BLOCKS_WINDOW,
- Actions.TOGGLE_CONSOLE_WINDOW, Actions.TOGGLE_HIDE_DISABLED_BLOCKS,
- Actions.TOOLS_RUN_FDESIGN, Actions.TOGGLE_SCROLL_LOCK,
- Actions.CLEAR_CONSOLE, Actions.SAVE_CONSOLE,
- Actions.TOGGLE_AUTO_HIDE_PORT_LABELS, Actions.TOGGLE_SNAP_TO_GRID,
+ Actions.TOGGLE_BLOCKS_WINDOW,
+ Actions.TOGGLE_CONSOLE_WINDOW,
+ Actions.TOGGLE_HIDE_DISABLED_BLOCKS,
+ Actions.TOGGLE_SCROLL_LOCK,
+ Actions.TOGGLE_AUTO_HIDE_PORT_LABELS,
+ Actions.TOGGLE_SNAP_TO_GRID,
Actions.TOGGLE_SHOW_BLOCK_COMMENTS,
Actions.TOGGLE_SHOW_CODE_PREVIEW_TAB,
Actions.TOGGLE_SHOW_FLOWGRAPH_COMPLEXITY,
- Actions.FLOW_GRAPH_OPEN_QSS_THEME,
Actions.TOGGLE_FLOW_GRAPH_VAR_EDITOR,
Actions.TOGGLE_FLOW_GRAPH_VAR_EDITOR_SIDEBAR,
Actions.TOGGLE_HIDE_VARIABLES,
- Actions.SELECT_ALL,
):
- action.set_sensitive(True)
+ action.set_enabled(True)
if hasattr(action, 'load_from_preferences'):
action.load_from_preferences()
- if ParseXML.xml_failures:
- Messages.send_xml_errors_if_any(ParseXML.xml_failures)
- Actions.XML_PARSER_ERRORS_DISPLAY.set_sensitive(True)
+
+ # Hide the panels *IF* it's saved in preferences
+ main.update_panel_visibility(main.BLOCKS, Actions.TOGGLE_BLOCKS_WINDOW.get_active())
+ main.update_panel_visibility(main.CONSOLE, Actions.TOGGLE_CONSOLE_WINDOW.get_active())
+ main.update_panel_visibility(main.VARIABLES, Actions.TOGGLE_FLOW_GRAPH_VAR_EDITOR.get_active())
+
+ #if ParseXML.xml_failures:
+ # Messages.send_xml_errors_if_any(ParseXML.xml_failures)
+ # Actions.XML_PARSER_ERRORS_DISPLAY.set_enabled(True)
+
+ # Force an update on the current page to match loaded preferences.
+ # In the future, change the __init__ order to load preferences first
+ page = main.current_page
+ if page:
+ page.flow_graph.update()
+
self.init = True
elif action == Actions.APPLICATION_QUIT:
if main.close_pages():
- gtk.main_quit()
+ Gtk.main_quit()
exit(0)
##################################################
# Selections
@@ -171,21 +240,16 @@ class ActionHandler:
##################################################
# Enable/Disable
##################################################
- elif action == Actions.BLOCK_ENABLE:
- if flow_graph.enable_selected(True):
+ elif action in (Actions.BLOCK_ENABLE, Actions.BLOCK_DISABLE, Actions.BLOCK_BYPASS):
+ changed = flow_graph.change_state_selected(new_state={
+ Actions.BLOCK_ENABLE: 'enabled',
+ Actions.BLOCK_DISABLE: 'disabled',
+ Actions.BLOCK_BYPASS: 'bypassed',
+ }[action])
+ if changed:
flow_graph_update()
- page.get_state_cache().save_new_state(flow_graph.export_data())
- page.set_saved(False)
- elif action == Actions.BLOCK_DISABLE:
- if flow_graph.enable_selected(False):
- flow_graph_update()
- page.get_state_cache().save_new_state(flow_graph.export_data())
- page.set_saved(False)
- elif action == Actions.BLOCK_BYPASS:
- if flow_graph.bypass_selected():
- flow_graph_update()
- page.get_state_cache().save_new_state(flow_graph.export_data())
- page.set_saved(False)
+ page.state_cache.save_new_state(flow_graph.export_data())
+ page.saved = False
##################################################
# Cut/Copy/Paste
##################################################
@@ -198,15 +262,15 @@ class ActionHandler:
if self.clipboard:
flow_graph.paste_from_clipboard(self.clipboard)
flow_graph_update()
- page.get_state_cache().save_new_state(flow_graph.export_data())
- page.set_saved(False)
+ page.state_cache.save_new_state(flow_graph.export_data())
+ page.saved = False
##################################################
# Create heir block
##################################################
elif action == Actions.BLOCK_CREATE_HIER:
# keeping track of coordinates for pasting later
- coords = flow_graph.get_selected_blocks()[0].get_coordinate()
+ coords = flow_graph.selected_blocks()[0].coordinate
x,y = coords
x_min = x
y_min = y
@@ -215,22 +279,22 @@ class ActionHandler:
params = [];
# Save the state of the leaf blocks
- for block in flow_graph.get_selected_blocks():
+ for block in flow_graph.selected_blocks():
# Check for string variables within the blocks
- for param in block.get_params():
+ for param in block.params.values():
for variable in flow_graph.get_variables():
# If a block parameter exists that is a variable, create a parameter for it
- if param.get_value() == variable.get_id():
+ if param.get_value() == variable.name:
params.append(param.get_value())
for flow_param in flow_graph.get_parameters():
# If a block parameter exists that is a parameter, create a parameter for it
- if param.get_value() == flow_param.get_id():
+ if param.get_value() == flow_param.name:
params.append(param.get_value())
# keep track of x,y mins for pasting later
- (x,y) = block.get_coordinate()
+ (x,y) = block.coordinate
if x < x_min:
x_min = x
if y < y_min:
@@ -239,15 +303,15 @@ class ActionHandler:
for connection in block.connections:
# Get id of connected blocks
- source_id = connection.get_source().get_parent().get_id()
- sink_id = connection.get_sink().get_parent().get_id()
+ source_id = connection.source_block.name
+ sink_id = connection.sink_block.name
# If connected block is not in the list of selected blocks create a pad for it
- if flow_graph.get_block(source_id) not in flow_graph.get_selected_blocks():
- pads.append({'key': connection.get_sink().get_key(), 'coord': connection.get_source().get_coordinate(), 'block_id' : block.get_id(), 'direction': 'source'})
+ if flow_graph.get_block(source_id) not in flow_graph.selected_blocks():
+ pads.append({'key': connection.sink_port.key, 'coord': connection.source_port.coordinate, 'block_id' : block.name, 'direction': 'source'})
- if flow_graph.get_block(sink_id) not in flow_graph.get_selected_blocks():
- pads.append({'key': connection.get_source().get_key(), 'coord': connection.get_sink().get_coordinate(), 'block_id' : block.get_id(), 'direction': 'sink'})
+ if flow_graph.get_block(sink_id) not in flow_graph.selected_blocks():
+ pads.append({'key': connection.source_port.key, 'coord': connection.sink_port.coordinate, 'block_id' : block.name, 'direction': 'sink'})
# Copy the selected blocks and paste them into a new page
@@ -261,10 +325,10 @@ class ActionHandler:
# Set flow graph to heir block type
top_block = flow_graph.get_block("top_block")
- top_block.get_param('generate_options').set_value('hb')
+ top_block.params['generate_options'].set_value('hb')
# this needs to be a unique name
- top_block.get_param('id').set_value('new_heir')
+ top_block.params['id'].set_value('new_heir')
# Remove the default samp_rate variable block that is created
remove_me = flow_graph.get_block("samp_rate")
@@ -276,7 +340,7 @@ class ActionHandler:
for param in params:
param_id = flow_graph.add_new_block('parameter',(x_pos,10))
param_block = flow_graph.get_block(param_id)
- param_block.get_param('id').set_value(param)
+ param_block.params['id'].set_value(param)
x_pos = x_pos + 100
for pad in pads:
@@ -288,16 +352,16 @@ class ActionHandler:
# setup the references to the sink and source
pad_block = flow_graph.get_block(pad_id)
- pad_sink = pad_block.get_sinks()[0]
+ pad_sink = pad_block.sinks[0]
source_block = flow_graph.get_block(pad['block_id'])
source = source_block.get_source(pad['key'])
# Ensure the port types match
- while pad_sink.get_type() != source.get_type():
+ while pad_sink.dtype != source.dtype:
# Special case for some blocks that have non-standard type names, e.g. uhd
- if pad_sink.get_type() == 'complex' and source.get_type() == 'fc32':
+ if pad_sink.dtype == 'complex' and source.dtype == 'fc32':
break;
pad_block.type_controller_modify(1)
@@ -309,15 +373,15 @@ class ActionHandler:
# setup the references to the sink and source
pad_block = flow_graph.get_block(pad_id)
- pad_source = pad_block.get_sources()[0]
+ pad_source = pad_block.sources[0]
sink_block = flow_graph.get_block(pad['block_id'])
sink = sink_block.get_sink(pad['key'])
# Ensure the port types match
- while sink.get_type() != pad_source.get_type():
+ while sink.dtype != pad_source.dtype:
# Special case for some blocks that have non-standard type names, e.g. uhd
- if pad_source.get_type() == 'complex' and sink.get_type() == 'fc32':
+ if pad_source.dtype == 'complex' and sink.dtype == 'fc32':
break;
pad_block.type_controller_modify(1)
@@ -332,311 +396,327 @@ class ActionHandler:
# Move/Rotate/Delete/Create
##################################################
elif action == Actions.BLOCK_MOVE:
- page.get_state_cache().save_new_state(flow_graph.export_data())
- page.set_saved(False)
+ page.state_cache.save_new_state(flow_graph.export_data())
+ page.saved = False
elif action in Actions.BLOCK_ALIGNMENTS:
if flow_graph.align_selected(action):
- page.get_state_cache().save_new_state(flow_graph.export_data())
- page.set_saved(False)
+ page.state_cache.save_new_state(flow_graph.export_data())
+ page.saved = False
elif action == Actions.BLOCK_ROTATE_CCW:
if flow_graph.rotate_selected(90):
flow_graph_update()
- page.get_state_cache().save_new_state(flow_graph.export_data())
- page.set_saved(False)
+ page.state_cache.save_new_state(flow_graph.export_data())
+ page.saved = False
elif action == Actions.BLOCK_ROTATE_CW:
if flow_graph.rotate_selected(-90):
flow_graph_update()
- page.get_state_cache().save_new_state(flow_graph.export_data())
- page.set_saved(False)
+ page.state_cache.save_new_state(flow_graph.export_data())
+ page.saved = False
elif action == Actions.ELEMENT_DELETE:
if flow_graph.remove_selected():
flow_graph_update()
- page.get_state_cache().save_new_state(flow_graph.export_data())
+ page.state_cache.save_new_state(flow_graph.export_data())
Actions.NOTHING_SELECT()
- page.set_saved(False)
+ page.saved = False
elif action == Actions.ELEMENT_CREATE:
flow_graph_update()
- page.get_state_cache().save_new_state(flow_graph.export_data())
+ page.state_cache.save_new_state(flow_graph.export_data())
Actions.NOTHING_SELECT()
- page.set_saved(False)
+ page.saved = False
elif action == Actions.BLOCK_INC_TYPE:
if flow_graph.type_controller_modify_selected(1):
flow_graph_update()
- page.get_state_cache().save_new_state(flow_graph.export_data())
- page.set_saved(False)
+ page.state_cache.save_new_state(flow_graph.export_data())
+ page.saved = False
elif action == Actions.BLOCK_DEC_TYPE:
if flow_graph.type_controller_modify_selected(-1):
flow_graph_update()
- page.get_state_cache().save_new_state(flow_graph.export_data())
- page.set_saved(False)
+ page.state_cache.save_new_state(flow_graph.export_data())
+ page.saved = False
elif action == Actions.PORT_CONTROLLER_INC:
if flow_graph.port_controller_modify_selected(1):
flow_graph_update()
- page.get_state_cache().save_new_state(flow_graph.export_data())
- page.set_saved(False)
+ page.state_cache.save_new_state(flow_graph.export_data())
+ page.saved = False
elif action == Actions.PORT_CONTROLLER_DEC:
if flow_graph.port_controller_modify_selected(-1):
flow_graph_update()
- page.get_state_cache().save_new_state(flow_graph.export_data())
- page.set_saved(False)
+ page.state_cache.save_new_state(flow_graph.export_data())
+ page.saved = False
##################################################
# Window stuff
##################################################
elif action == Actions.ABOUT_WINDOW_DISPLAY:
- platform = flow_graph.get_parent()
- Dialogs.AboutDialog(platform.config, main)
+ Dialogs.show_about(main, self.platform.config)
elif action == Actions.HELP_WINDOW_DISPLAY:
- Dialogs.HelpDialog(main)
+ Dialogs.show_help(main)
elif action == Actions.TYPES_WINDOW_DISPLAY:
- Dialogs.TypesDialog(flow_graph.get_parent(), main)
+ Dialogs.show_types(main)
elif action == Actions.ERRORS_WINDOW_DISPLAY:
- Dialogs.ErrorsDialog(flow_graph, main)
+ Dialogs.ErrorsDialog(main, flow_graph).run_and_destroy()
elif action == Actions.TOGGLE_CONSOLE_WINDOW:
+ action.set_active(not action.get_active())
main.update_panel_visibility(main.CONSOLE, action.get_active())
action.save_to_preferences()
elif action == Actions.TOGGLE_BLOCKS_WINDOW:
+ # This would be better matched to a Gio.PropertyAction, but to do
+ # this, actions would have to be defined in the window not globally
+ action.set_active(not action.get_active())
main.update_panel_visibility(main.BLOCKS, action.get_active())
action.save_to_preferences()
elif action == Actions.TOGGLE_SCROLL_LOCK:
+ action.set_active(not action.get_active())
active = action.get_active()
- main.text_display.scroll_lock = active
+ main.console.text_display.scroll_lock = active
if active:
- main.text_display.scroll_to_end()
+ main.console.text_display.scroll_to_end()
action.save_to_preferences()
elif action == Actions.CLEAR_CONSOLE:
- main.text_display.clear()
+ main.console.text_display.clear()
elif action == Actions.SAVE_CONSOLE:
- file_path = SaveConsoleFileDialog(page.get_file_path(), main).run()
+ file_path = FileDialogs.SaveConsole(main, page.file_path).run()
if file_path is not None:
- main.text_display.save(file_path)
+ main.console.text_display.save(file_path)
elif action == Actions.TOGGLE_HIDE_DISABLED_BLOCKS:
+ action.set_active(not action.get_active())
Actions.NOTHING_SELECT()
elif action == Actions.TOGGLE_AUTO_HIDE_PORT_LABELS:
+ action.set_active(not action.get_active())
action.save_to_preferences()
for page in main.get_pages():
- page.get_flow_graph().create_shapes()
+ page.flow_graph.create_shapes()
elif action in (Actions.TOGGLE_SNAP_TO_GRID,
Actions.TOGGLE_SHOW_BLOCK_COMMENTS,
Actions.TOGGLE_SHOW_CODE_PREVIEW_TAB):
+ action.set_active(not action.get_active())
action.save_to_preferences()
elif action == Actions.TOGGLE_SHOW_FLOWGRAPH_COMPLEXITY:
+ action.set_active(not action.get_active())
action.save_to_preferences()
for page in main.get_pages():
- flow_graph_update(page.get_flow_graph())
+ flow_graph_update(page.flow_graph)
elif action == Actions.TOGGLE_HIDE_VARIABLES:
- # Call the variable editor TOGGLE in case it needs to be showing
- Actions.TOGGLE_FLOW_GRAPH_VAR_EDITOR()
+ action.set_active(not action.get_active())
+ active = action.get_active()
+ # Either way, triggering this should simply trigger the variable editor
+ # to be visible.
+ varedit = Actions.TOGGLE_FLOW_GRAPH_VAR_EDITOR
+ if active:
+ log.debug("Variables are hidden. Forcing the variable panel to be visible.")
+ varedit.disable()
+ else:
+ varedit.enable()
+ # Just force it to show.
+ varedit.set_active(True)
+ main.update_panel_visibility(main.VARIABLES)
Actions.NOTHING_SELECT()
action.save_to_preferences()
+ varedit.save_to_preferences()
+ flow_graph_update()
elif action == Actions.TOGGLE_FLOW_GRAPH_VAR_EDITOR:
- # See if the variables are hidden
- if Actions.TOGGLE_HIDE_VARIABLES.get_active():
- # Force this to be shown
- main.update_panel_visibility(main.VARIABLES, True)
- action.set_active(True)
- action.set_sensitive(False)
- else:
- if action.get_sensitive():
- main.update_panel_visibility(main.VARIABLES, action.get_active())
- else: # This is occurring after variables are un-hidden
- # Leave it enabled
- action.set_sensitive(True)
- action.set_active(True)
- #Actions.TOGGLE_FLOW_GRAPH_VAR_EDITOR_SIDEBAR.set_sensitive(action.get_active())
+ # TODO: There may be issues at startup since these aren't triggered
+ # the same was as Gtk.Actions when loading preferences.
+ action.set_active(not action.get_active())
+ # Just assume this was triggered because it was enabled.
+ main.update_panel_visibility(main.VARIABLES, action.get_active())
+ action.save_to_preferences()
+
+ # Actions.TOGGLE_FLOW_GRAPH_VAR_EDITOR_SIDEBAR.set_enabled(action.get_active())
action.save_to_preferences()
elif action == Actions.TOGGLE_FLOW_GRAPH_VAR_EDITOR_SIDEBAR:
+ action.set_active(not action.get_active())
if self.init:
- md = gtk.MessageDialog(main,
- gtk.DIALOG_DESTROY_WITH_PARENT, gtk.MESSAGE_INFO,
- gtk.BUTTONS_CLOSE, "Moving the variable editor requires a restart of GRC.")
- md.run()
- md.destroy()
+ Dialogs.MessageDialogWrapper(
+ main, Gtk.MessageType.INFO, Gtk.ButtonsType.CLOSE,
+ markup="Moving the variable editor requires a restart of GRC."
+ ).run_and_destroy()
action.save_to_preferences()
##################################################
# Param Modifications
##################################################
elif action == Actions.BLOCK_PARAM_MODIFY:
- if action.args:
- selected_block = action.args[0]
- else:
- selected_block = flow_graph.get_selected_block()
+ selected_block = args[0] if args[0] else flow_graph.selected_block
if selected_block:
- self.dialog = PropsDialog(selected_block, main)
- response = gtk.RESPONSE_APPLY
- while response == gtk.RESPONSE_APPLY: # rerun the dialog if Apply was hit
+ self.dialog = PropsDialog(self.main_window, selected_block)
+ response = Gtk.ResponseType.APPLY
+ while response == Gtk.ResponseType.APPLY: # rerun the dialog if Apply was hit
response = self.dialog.run()
- if response in (gtk.RESPONSE_APPLY, gtk.RESPONSE_ACCEPT):
+ if response in (Gtk.ResponseType.APPLY, Gtk.ResponseType.ACCEPT):
flow_graph_update()
- page.get_state_cache().save_new_state(flow_graph.export_data())
- page.set_saved(False)
+ page.state_cache.save_new_state(flow_graph.export_data())
+ page.saved = False
else: # restore the current state
- n = page.get_state_cache().get_current_state()
+ n = page.state_cache.get_current_state()
flow_graph.import_data(n)
flow_graph_update()
- if response == gtk.RESPONSE_APPLY:
+ if response == Gtk.ResponseType.APPLY:
# null action, that updates the main window
Actions.ELEMENT_SELECT()
self.dialog.destroy()
self.dialog = None
elif action == Actions.EXTERNAL_UPDATE:
- page.get_state_cache().save_new_state(flow_graph.export_data())
+ page.state_cache.save_new_state(flow_graph.export_data())
flow_graph_update()
if self.dialog is not None:
self.dialog.update_gui(force=True)
- page.set_saved(False)
+ page.saved = False
elif action == Actions.VARIABLE_EDITOR_UPDATE:
- page.get_state_cache().save_new_state(flow_graph.export_data())
+ page.state_cache.save_new_state(flow_graph.export_data())
flow_graph_update()
- page.set_saved(False)
+ page.saved = False
##################################################
# View Parser Errors
##################################################
elif action == Actions.XML_PARSER_ERRORS_DISPLAY:
- ParserErrorsDialog(ParseXML.xml_failures).run()
+ # ParserErrorsDialog(ParseXML.xml_failures).run()
+ pass
##################################################
# Undo/Redo
##################################################
elif action == Actions.FLOW_GRAPH_UNDO:
- n = page.get_state_cache().get_prev_state()
+ n = page.state_cache.get_prev_state()
if n:
flow_graph.unselect()
flow_graph.import_data(n)
flow_graph_update()
- page.set_saved(False)
+ page.saved = False
elif action == Actions.FLOW_GRAPH_REDO:
- n = page.get_state_cache().get_next_state()
+ n = page.state_cache.get_next_state()
if n:
flow_graph.unselect()
flow_graph.import_data(n)
flow_graph_update()
- page.set_saved(False)
+ page.saved = False
##################################################
# New/Open/Save/Close
##################################################
elif action == Actions.FLOW_GRAPH_NEW:
main.new_page()
if args:
- flow_graph = main.get_flow_graph()
- flow_graph._options_block.get_param('generate_options').set_value(args[0])
+ flow_graph = main.current_page.flow_graph
+ flow_graph._options_block.params['generate_options'].set_value(str(args[0])[1:-1])
flow_graph_update(flow_graph)
elif action == Actions.FLOW_GRAPH_OPEN:
- file_paths = args if args else OpenFlowGraphFileDialog(main, page.get_file_path()).run()
- if file_paths: #open a new page for each file, show only the first
+ file_paths = args[0] if args[0] else FileDialogs.OpenFlowGraph(main, page.file_path).run()
+ if file_paths: # Open a new page for each file, show only the first
for i,file_path in enumerate(file_paths):
main.new_page(file_path, show=(i==0))
- Preferences.add_recent_file(file_path)
+ self.config.add_recent_file(file_path)
main.tool_bar.refresh_submenus()
- main.menu_bar.refresh_submenus()
- main.vars.update_gui()
-
+ #main.menu_bar.refresh_submenus()
elif action == Actions.FLOW_GRAPH_OPEN_QSS_THEME:
- file_paths = OpenQSSFileDialog(main, self.platform.config.install_prefix +
- '/share/gnuradio/themes/').run()
+ file_paths = FileDialogs.OpenQSS(main, self.platform.config.install_prefix +
+ '/share/gnuradio/themes/').run()
if file_paths:
- try:
- prefs = self.platform.config.prefs
- prefs.set_string("qtgui", "qss", file_paths[0])
- prefs.save()
- except Exception as e:
- Messages.send("Failed to save QSS preference: " + str(e))
+ self.platform.config.default_qss_theme = file_paths[0]
elif action == Actions.FLOW_GRAPH_CLOSE:
main.close_page()
elif action == Actions.FLOW_GRAPH_SAVE:
#read-only or undefined file path, do save-as
- if page.get_read_only() or not page.get_file_path():
+ if page.get_read_only() or not page.file_path:
Actions.FLOW_GRAPH_SAVE_AS()
#otherwise try to save
else:
try:
- ParseXML.to_file(flow_graph.export_data(), page.get_file_path())
- flow_graph.grc_file_path = page.get_file_path()
- page.set_saved(True)
+ self.platform.save_flow_graph(page.file_path, flow_graph)
+ flow_graph.grc_file_path = page.file_path
+ page.saved = True
except IOError:
- Messages.send_fail_save(page.get_file_path())
- page.set_saved(False)
+ Messages.send_fail_save(page.file_path)
+ page.saved = False
elif action == Actions.FLOW_GRAPH_SAVE_AS:
- file_path = SaveFlowGraphFileDialog(main, page.get_file_path()).run()
+ file_path = FileDialogs.SaveFlowGraph(main, page.file_path).run()
if file_path is not None:
- page.set_file_path(file_path)
+ page.file_path = os.path.abspath(file_path)
Actions.FLOW_GRAPH_SAVE()
- Preferences.add_recent_file(file_path)
+ self.config.add_recent_file(file_path)
main.tool_bar.refresh_submenus()
- main.menu_bar.refresh_submenus()
- elif action == Actions.FLOW_GRAPH_SAVE_A_COPY:
+ #TODO
+ #main.menu_bar.refresh_submenus()
+ elif action == Actions.FLOW_GRAPH_SAVE_COPY:
try:
- if not page.get_file_path():
+ if not page.file_path:
+ # Make sure the current flowgraph has been saved
Actions.FLOW_GRAPH_SAVE_AS()
else:
- dup_file_path = page.get_file_path()
+ dup_file_path = page.file_path
dup_file_name = '.'.join(dup_file_path.split('.')[:-1]) + "_copy" # Assuming .grc extension at the end of file_path
- dup_file_path_temp = dup_file_name+'.grc'
+ dup_file_path_temp = dup_file_name + Constants.FILE_EXTENSION
count = 1
while os.path.exists(dup_file_path_temp):
- dup_file_path_temp = dup_file_name+'('+str(count)+').grc'
+ dup_file_path_temp = '{}({}){}'.format(dup_file_name, count, Constants.FILE_EXTENSION)
count += 1
- dup_file_path_user = SaveFlowGraphFileDialog(main, dup_file_path_temp).run()
+ dup_file_path_user = FileDialogs.SaveFlowGraph(main, dup_file_path_temp).run()
if dup_file_path_user is not None:
- ParseXML.to_file(flow_graph.export_data(), dup_file_path_user)
+ self.platform.save_flow_graph(dup_file_path_user, flow_graph)
Messages.send('Saved Copy to: "' + dup_file_path_user + '"\n')
except IOError:
Messages.send_fail_save("Can not create a copy of the flowgraph\n")
elif action == Actions.FLOW_GRAPH_DUPLICATE:
- flow_graph = main.get_flow_graph()
+ previous = flow_graph
+ # Create a new page
main.new_page()
- curr_page = main.get_page()
- new_flow_graph = main.get_flow_graph()
- new_flow_graph.import_data(flow_graph.export_data())
+ page = main.current_page
+ new_flow_graph = page.flow_graph
+ # Import the old data and mark the current as not saved
+ new_flow_graph.import_data(previous.export_data())
flow_graph_update(new_flow_graph)
- curr_page.set_saved(False)
+ page.saved = False
elif action == Actions.FLOW_GRAPH_SCREEN_CAPTURE:
file_path, background_transparent = SaveScreenShotDialog(main, page.get_file_path()).run()
if file_path is not None:
- pixbuf = flow_graph.get_drawing_area().get_screenshot(background_transparent)
- pixbuf.save(file_path, Constants.IMAGE_FILE_EXTENSION[1:])
+ try:
+ Utils.make_screenshot(flow_graph, file_path, background_transparent)
+ except ValueError:
+ Messages.send('Failed to generate screen shot\n')
##################################################
# Gen/Exec/Stop
##################################################
elif action == Actions.FLOW_GRAPH_GEN:
- if not page.get_proc():
- if not page.get_saved() or not page.get_file_path():
- Actions.FLOW_GRAPH_SAVE() #only save if file path missing or not saved
- if page.get_saved() and page.get_file_path():
+ if not page.process:
+ if not page.saved or not page.file_path:
+ Actions.FLOW_GRAPH_SAVE() # only save if file path missing or not saved
+ if page.saved and page.file_path:
generator = page.get_generator()
try:
- Messages.send_start_gen(generator.get_file_path())
+ Messages.send_start_gen(generator.file_path)
generator.write()
except Exception as e:
Messages.send_fail_gen(e)
else:
self.generator = None
elif action == Actions.FLOW_GRAPH_EXEC:
- if not page.get_proc():
+ if not page.process:
Actions.FLOW_GRAPH_GEN()
xterm = self.platform.config.xterm_executable
- if Preferences.xterm_missing() != xterm:
+ Dialogs.show_missing_xterm(main, xterm)
+ if self.config.xterm_missing() != xterm:
if not os.path.exists(xterm):
- Dialogs.MissingXTermDialog(xterm, main)
- Preferences.xterm_missing(xterm)
- if page.get_saved() and page.get_file_path():
+ Dialogs.show_missing_xterm(main, xterm)
+ self.config.xterm_missing(xterm)
+ if page.saved and page.file_path:
Executor.ExecFlowGraphThread(
flow_graph_page=page,
xterm_executable=xterm,
callback=self.update_exec_stop
)
elif action == Actions.FLOW_GRAPH_KILL:
- if page.get_proc():
+ if page.process:
try:
page.term_proc()
except:
- print "could not terminate process: %d" % page.get_proc().pid
+ print("could not terminate process: %d" % page.get_proc().pid)
+
elif action == Actions.PAGE_CHANGE: # pass and run the global actions
pass
elif action == Actions.RELOAD_BLOCKS:
- self.platform.build_block_library()
+ self.platform.build_library()
main.btwin.repopulate()
- Actions.XML_PARSER_ERRORS_DISPLAY.set_sensitive(bool(
- ParseXML.xml_failures))
- Messages.send_xml_errors_if_any(ParseXML.xml_failures)
+
+ #todo: implement parser error dialog for YAML
+ #Actions.XML_PARSER_ERRORS_DISPLAY.set_enabled(bool(ParseXML.xml_failures))
+ #Messages.send_xml_errors_if_any(ParseXML.xml_failures)
+
# Force a redraw of the graph, by getting the current state and re-importing it
main.update_pages()
@@ -645,21 +725,20 @@ class ActionHandler:
main.btwin.search_entry.show()
main.btwin.search_entry.grab_focus()
elif action == Actions.OPEN_HIER:
- for b in flow_graph.get_selected_blocks():
- if b._grc_source:
- main.new_page(b._grc_source, show=True)
+ for b in flow_graph.selected_blocks():
+ grc_source = b.extra_data.get('grc_source', '')
+ if grc_source:
+ main.new_page(b.grc_source, show=True)
elif action == Actions.BUSSIFY_SOURCES:
- n = {'name':'bus', 'type':'bus'}
- for b in flow_graph.get_selected_blocks():
- b.bussify(n, 'source')
+ for b in flow_graph.selected_blocks():
+ b.bussify('source')
flow_graph._old_selected_port = None
flow_graph._new_selected_port = None
Actions.ELEMENT_CREATE()
elif action == Actions.BUSSIFY_SINKS:
- n = {'name':'bus', 'type':'bus'}
- for b in flow_graph.get_selected_blocks():
- b.bussify(n, 'sink')
+ for b in flow_graph.selected_blocks():
+ b.bussify('sink')
flow_graph._old_selected_port = None
flow_graph._new_selected_port = None
Actions.ELEMENT_CREATE()
@@ -669,71 +748,67 @@ class ActionHandler:
shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
else:
- print '!!! Action "%s" not handled !!!' % action
+ log.warning('!!! Action "%s" not handled !!!' % action)
##################################################
# Global Actions for all States
##################################################
- page = main.get_page() # page and flowgraph might have changed
- flow_graph = page.get_flow_graph() if page else None
+ page = main.current_page # page and flow graph might have changed
+ flow_graph = page.flow_graph if page else None
- selected_blocks = flow_graph.get_selected_blocks()
+ selected_blocks = list(flow_graph.selected_blocks())
selected_block = selected_blocks[0] if selected_blocks else None
#update general buttons
- Actions.ERRORS_WINDOW_DISPLAY.set_sensitive(not flow_graph.is_valid())
- Actions.ELEMENT_DELETE.set_sensitive(bool(flow_graph.get_selected_elements()))
- Actions.BLOCK_PARAM_MODIFY.set_sensitive(bool(selected_block))
- Actions.BLOCK_ROTATE_CCW.set_sensitive(bool(selected_blocks))
- Actions.BLOCK_ROTATE_CW.set_sensitive(bool(selected_blocks))
+ Actions.ERRORS_WINDOW_DISPLAY.set_enabled(not flow_graph.is_valid())
+ Actions.ELEMENT_DELETE.set_enabled(bool(flow_graph.selected_elements))
+ Actions.BLOCK_PARAM_MODIFY.set_enabled(bool(selected_block))
+ Actions.BLOCK_ROTATE_CCW.set_enabled(bool(selected_blocks))
+ Actions.BLOCK_ROTATE_CW.set_enabled(bool(selected_blocks))
#update alignment options
for act in Actions.BLOCK_ALIGNMENTS:
if act:
- act.set_sensitive(len(selected_blocks) > 1)
+ act.set_enabled(len(selected_blocks) > 1)
#update cut/copy/paste
- Actions.BLOCK_CUT.set_sensitive(bool(selected_blocks))
- Actions.BLOCK_COPY.set_sensitive(bool(selected_blocks))
- Actions.BLOCK_PASTE.set_sensitive(bool(self.clipboard))
+ Actions.BLOCK_CUT.set_enabled(bool(selected_blocks))
+ Actions.BLOCK_COPY.set_enabled(bool(selected_blocks))
+ Actions.BLOCK_PASTE.set_enabled(bool(self.clipboard))
#update enable/disable/bypass
- can_enable = any(block.get_state() != Constants.BLOCK_ENABLED
+ can_enable = any(block.state != 'enabled'
for block in selected_blocks)
- can_disable = any(block.get_state() != Constants.BLOCK_DISABLED
+ can_disable = any(block.state != 'disabled'
for block in selected_blocks)
- can_bypass_all = all(block.can_bypass() for block in selected_blocks) \
- and any (not block.get_bypassed() for block in selected_blocks)
- Actions.BLOCK_ENABLE.set_sensitive(can_enable)
- Actions.BLOCK_DISABLE.set_sensitive(can_disable)
- Actions.BLOCK_BYPASS.set_sensitive(can_bypass_all)
-
- Actions.BLOCK_CREATE_HIER.set_sensitive(bool(selected_blocks))
- Actions.OPEN_HIER.set_sensitive(bool(selected_blocks))
- Actions.BUSSIFY_SOURCES.set_sensitive(bool(selected_blocks))
- Actions.BUSSIFY_SINKS.set_sensitive(bool(selected_blocks))
- Actions.RELOAD_BLOCKS.set_sensitive(True)
- Actions.FIND_BLOCKS.set_sensitive(True)
- #set the exec and stop buttons
+ can_bypass_all = (
+ all(block.can_bypass() for block in selected_blocks) and
+ any(not block.get_bypassed() for block in selected_blocks)
+ )
+ Actions.BLOCK_ENABLE.set_enabled(can_enable)
+ Actions.BLOCK_DISABLE.set_enabled(can_disable)
+ Actions.BLOCK_BYPASS.set_enabled(can_bypass_all)
+
+ Actions.BLOCK_CREATE_HIER.set_enabled(bool(selected_blocks))
+ Actions.OPEN_HIER.set_enabled(bool(selected_blocks))
+ #Actions.BUSSIFY_SOURCES.set_enabled(bool(selected_blocks))
+ #Actions.BUSSIFY_SINKS.set_enabled(bool(selected_blocks))
+ Actions.RELOAD_BLOCKS.enable()
+ Actions.FIND_BLOCKS.enable()
+
self.update_exec_stop()
- #saved status
- Actions.FLOW_GRAPH_SAVE.set_sensitive(not page.get_saved())
+
+ Actions.FLOW_GRAPH_SAVE.set_enabled(not page.saved)
main.update()
- try: #set the size of the flow graph area (if changed)
- new_size = Utils.scale(
- flow_graph.get_option('window_size') or
- self.platform.config.default_canvas_size
- )
- if flow_graph.get_size() != tuple(new_size):
- flow_graph.set_size(*new_size)
- except: pass
- #draw the flow graph
+
flow_graph.update_selected()
- flow_graph.queue_draw()
- return True #action was handled
+ page.drawing_area.queue_draw()
+
+ return True # Action was handled
def update_exec_stop(self):
"""
Update the exec and stop buttons.
Lock and unlock the mutex for race conditions with exec flow graph threads.
"""
- sensitive = self.main_window.get_flow_graph().is_valid() and not self.get_page().get_proc()
- Actions.FLOW_GRAPH_GEN.set_sensitive(sensitive)
- Actions.FLOW_GRAPH_EXEC.set_sensitive(sensitive)
- Actions.FLOW_GRAPH_KILL.set_sensitive(self.get_page().get_proc() is not None)
+ page = self.main_window.current_page
+ sensitive = page.flow_graph.is_valid() and not page.process
+ Actions.FLOW_GRAPH_GEN.set_enabled(sensitive)
+ Actions.FLOW_GRAPH_EXEC.set_enabled(sensitive)
+ Actions.FLOW_GRAPH_KILL.set_enabled(page.process is not None)
diff --git a/grc/gui/Bars.py b/grc/gui/Bars.py
index d9bc2aedb7..2a8040f5d5 100644
--- a/grc/gui/Bars.py
+++ b/grc/gui/Bars.py
@@ -1,5 +1,5 @@
"""
-Copyright 2007, 2008, 2009, 2015 Free Software Foundation, Inc.
+Copyright 2007, 2008, 2009, 2015, 2016 Free Software Foundation, Inc.
This file is part of GNU Radio
GNU Radio Companion is free software; you can redistribute it and/or
@@ -17,304 +17,301 @@ 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
+from __future__ import absolute_import
+
+import logging
+
+from gi.repository import Gtk, GObject, Gio, GLib
from . import Actions
+log = logging.getLogger(__name__)
+
+
+'''
+# Menu/Toolbar Lists:
+#
+# Sub items can be 1 of 3 types
+# - List Creates a section within the current menu
+# - Tuple Creates a submenu using a string or action as the parent. The child
+# can be another menu list or an identifier used to call a helper function.
+# - Action Appends a new menu item to the current menu
+#
+
+LIST_NAME = [
+ [Action1, Action2], # New section
+ (Action3, [Action4, Action5]), # Submenu with action as parent
+ ("Label", [Action6, Action7]), # Submenu with string as parent
+ ("Label2", "helper") # Submenu with helper function. Calls 'create_helper()'
+]
+'''
+
+
# The list of actions for the toolbar.
-TOOLBAR_LIST = (
- (Actions.FLOW_GRAPH_NEW, 'flow_graph_new'),
- (Actions.FLOW_GRAPH_OPEN, 'flow_graph_recent'),
- Actions.FLOW_GRAPH_SAVE,
- Actions.FLOW_GRAPH_CLOSE,
- None,
- Actions.TOGGLE_FLOW_GRAPH_VAR_EDITOR,
- Actions.FLOW_GRAPH_SCREEN_CAPTURE,
- None,
- Actions.BLOCK_CUT,
- Actions.BLOCK_COPY,
- Actions.BLOCK_PASTE,
- Actions.ELEMENT_DELETE,
- None,
- Actions.FLOW_GRAPH_UNDO,
- Actions.FLOW_GRAPH_REDO,
- None,
- Actions.ERRORS_WINDOW_DISPLAY,
- Actions.FLOW_GRAPH_GEN,
- Actions.FLOW_GRAPH_EXEC,
- Actions.FLOW_GRAPH_KILL,
- None,
- Actions.BLOCK_ROTATE_CCW,
- Actions.BLOCK_ROTATE_CW,
- None,
- Actions.BLOCK_ENABLE,
- Actions.BLOCK_DISABLE,
- Actions.BLOCK_BYPASS,
- Actions.TOGGLE_HIDE_DISABLED_BLOCKS,
- None,
- Actions.FIND_BLOCKS,
- Actions.RELOAD_BLOCKS,
- Actions.OPEN_HIER,
-)
+TOOLBAR_LIST = [
+ [(Actions.FLOW_GRAPH_NEW, 'flow_graph_new'), Actions.FLOW_GRAPH_OPEN,
+ (Actions.FLOW_GRAPH_OPEN_RECENT, 'flow_graph_recent'), Actions.FLOW_GRAPH_SAVE, Actions.FLOW_GRAPH_CLOSE],
+ [Actions.TOGGLE_FLOW_GRAPH_VAR_EDITOR, Actions.FLOW_GRAPH_SCREEN_CAPTURE],
+ [Actions.BLOCK_CUT, Actions.BLOCK_COPY, Actions.BLOCK_PASTE, Actions.ELEMENT_DELETE],
+ [Actions.FLOW_GRAPH_UNDO, Actions.FLOW_GRAPH_REDO],
+ [Actions.ERRORS_WINDOW_DISPLAY, Actions.FLOW_GRAPH_GEN, Actions.FLOW_GRAPH_EXEC, Actions.FLOW_GRAPH_KILL],
+ [Actions.BLOCK_ROTATE_CCW, Actions.BLOCK_ROTATE_CW],
+ [Actions.BLOCK_ENABLE, Actions.BLOCK_DISABLE, Actions.BLOCK_BYPASS, Actions.TOGGLE_HIDE_DISABLED_BLOCKS],
+ [Actions.FIND_BLOCKS, Actions.RELOAD_BLOCKS, Actions.OPEN_HIER]
+]
+
# The list of actions and categories for the menu bar.
-MENU_BAR_LIST = (
- (gtk.Action('File', '_File', None, None), [
- 'flow_graph_new',
- Actions.FLOW_GRAPH_DUPLICATE,
- Actions.FLOW_GRAPH_OPEN,
- 'flow_graph_recent',
- None,
- Actions.FLOW_GRAPH_SAVE,
- Actions.FLOW_GRAPH_SAVE_AS,
- Actions.FLOW_GRAPH_SAVE_A_COPY,
- None,
- Actions.FLOW_GRAPH_SCREEN_CAPTURE,
- None,
- Actions.FLOW_GRAPH_CLOSE,
- Actions.APPLICATION_QUIT,
- ]),
- (gtk.Action('Edit', '_Edit', None, None), [
- Actions.FLOW_GRAPH_UNDO,
- Actions.FLOW_GRAPH_REDO,
- None,
- Actions.BLOCK_CUT,
- Actions.BLOCK_COPY,
- Actions.BLOCK_PASTE,
- Actions.ELEMENT_DELETE,
- Actions.SELECT_ALL,
- None,
- Actions.BLOCK_ROTATE_CCW,
- Actions.BLOCK_ROTATE_CW,
- (gtk.Action('Align', '_Align', None, None), Actions.BLOCK_ALIGNMENTS),
- None,
- Actions.BLOCK_ENABLE,
- Actions.BLOCK_DISABLE,
- Actions.BLOCK_BYPASS,
- None,
- Actions.BLOCK_PARAM_MODIFY,
- ]),
- (gtk.Action('View', '_View', None, None), [
- Actions.TOGGLE_BLOCKS_WINDOW,
- None,
- Actions.TOGGLE_CONSOLE_WINDOW,
- Actions.TOGGLE_SCROLL_LOCK,
- Actions.SAVE_CONSOLE,
- Actions.CLEAR_CONSOLE,
- None,
- Actions.TOGGLE_HIDE_VARIABLES,
- Actions.TOGGLE_FLOW_GRAPH_VAR_EDITOR,
- Actions.TOGGLE_FLOW_GRAPH_VAR_EDITOR_SIDEBAR,
- None,
- Actions.TOGGLE_HIDE_DISABLED_BLOCKS,
- Actions.TOGGLE_AUTO_HIDE_PORT_LABELS,
- Actions.TOGGLE_SNAP_TO_GRID,
- Actions.TOGGLE_SHOW_BLOCK_COMMENTS,
- None,
- Actions.TOGGLE_SHOW_CODE_PREVIEW_TAB,
- None,
- Actions.ERRORS_WINDOW_DISPLAY,
- Actions.FIND_BLOCKS,
- ]),
- (gtk.Action('Run', '_Run', None, None), [
- Actions.FLOW_GRAPH_GEN,
- Actions.FLOW_GRAPH_EXEC,
- Actions.FLOW_GRAPH_KILL,
- ]),
- (gtk.Action('Tools', '_Tools', None, None), [
- Actions.TOOLS_RUN_FDESIGN,
- Actions.FLOW_GRAPH_OPEN_QSS_THEME,
- None,
- Actions.TOGGLE_SHOW_FLOWGRAPH_COMPLEXITY,
- None,
- Actions.TOOLS_MORE_TO_COME,
- ]),
- (gtk.Action('Help', '_Help', None, None), [
- Actions.HELP_WINDOW_DISPLAY,
- Actions.TYPES_WINDOW_DISPLAY,
- Actions.XML_PARSER_ERRORS_DISPLAY,
- None,
- Actions.ABOUT_WINDOW_DISPLAY,
- ]),
-)
+MENU_BAR_LIST = [
+ ('_File', [
+ [(Actions.FLOW_GRAPH_NEW, 'flow_graph_new'), Actions.FLOW_GRAPH_DUPLICATE,
+ Actions.FLOW_GRAPH_OPEN, (Actions.FLOW_GRAPH_OPEN_RECENT, 'flow_graph_recent')],
+ [Actions.FLOW_GRAPH_SAVE, Actions.FLOW_GRAPH_SAVE_AS, Actions.FLOW_GRAPH_SAVE_COPY],
+ [Actions.FLOW_GRAPH_SCREEN_CAPTURE],
+ [Actions.FLOW_GRAPH_CLOSE, Actions.APPLICATION_QUIT]
+ ]),
+ ('_Edit', [
+ [Actions.FLOW_GRAPH_UNDO, Actions.FLOW_GRAPH_REDO],
+ [Actions.BLOCK_CUT, Actions.BLOCK_COPY, Actions.BLOCK_PASTE, Actions.ELEMENT_DELETE, Actions.SELECT_ALL],
+ [Actions.BLOCK_ROTATE_CCW, Actions.BLOCK_ROTATE_CW, ('_Align', Actions.BLOCK_ALIGNMENTS)],
+ [Actions.BLOCK_ENABLE, Actions.BLOCK_DISABLE, Actions.BLOCK_BYPASS],
+ [Actions.BLOCK_PARAM_MODIFY]
+ ]),
+ ('_View', [
+ [Actions.TOGGLE_BLOCKS_WINDOW],
+ [Actions.TOGGLE_CONSOLE_WINDOW, Actions.TOGGLE_SCROLL_LOCK, Actions.SAVE_CONSOLE, Actions.CLEAR_CONSOLE],
+ [Actions.TOGGLE_HIDE_VARIABLES, Actions.TOGGLE_FLOW_GRAPH_VAR_EDITOR, Actions.TOGGLE_FLOW_GRAPH_VAR_EDITOR_SIDEBAR],
+ [Actions.TOGGLE_HIDE_DISABLED_BLOCKS, Actions.TOGGLE_AUTO_HIDE_PORT_LABELS, Actions.TOGGLE_SNAP_TO_GRID, Actions.TOGGLE_SHOW_BLOCK_COMMENTS],
+ [Actions.TOGGLE_SHOW_CODE_PREVIEW_TAB],
+ [Actions.ERRORS_WINDOW_DISPLAY, Actions.FIND_BLOCKS],
+ ]),
+ ('_Run', [
+ Actions.FLOW_GRAPH_GEN, Actions.FLOW_GRAPH_EXEC, Actions.FLOW_GRAPH_KILL
+ ]),
+ ('_Tools', [
+ [Actions.TOOLS_RUN_FDESIGN, Actions.FLOW_GRAPH_OPEN_QSS_THEME],
+ [Actions.TOGGLE_SHOW_FLOWGRAPH_COMPLEXITY]
+ ]),
+ ('_Help', [
+ [Actions.HELP_WINDOW_DISPLAY, Actions.TYPES_WINDOW_DISPLAY, Actions.XML_PARSER_ERRORS_DISPLAY],
+ [Actions.ABOUT_WINDOW_DISPLAY]
+ ])]
+
# The list of actions for the context menu.
CONTEXT_MENU_LIST = [
- Actions.BLOCK_CUT,
- Actions.BLOCK_COPY,
- Actions.BLOCK_PASTE,
- Actions.ELEMENT_DELETE,
- None,
- Actions.BLOCK_ROTATE_CCW,
- Actions.BLOCK_ROTATE_CW,
- Actions.BLOCK_ENABLE,
- Actions.BLOCK_DISABLE,
- Actions.BLOCK_BYPASS,
- None,
- (gtk.Action('More', '_More', None, None), [
- Actions.BLOCK_CREATE_HIER,
- Actions.OPEN_HIER,
- None,
- Actions.BUSSIFY_SOURCES,
- Actions.BUSSIFY_SINKS,
- ]),
- Actions.BLOCK_PARAM_MODIFY
+ [Actions.BLOCK_CUT, Actions.BLOCK_COPY, Actions.BLOCK_PASTE, Actions.ELEMENT_DELETE],
+ [Actions.BLOCK_ROTATE_CCW, Actions.BLOCK_ROTATE_CW, Actions.BLOCK_ENABLE, Actions.BLOCK_DISABLE, Actions.BLOCK_BYPASS],
+ [("_More", [
+ [Actions.BLOCK_CREATE_HIER, Actions.OPEN_HIER],
+ [Actions.BUSSIFY_SOURCES, Actions.BUSSIFY_SINKS]]
+ )],
+ [Actions.BLOCK_PARAM_MODIFY],
]
-class SubMenuCreator(object):
+class SubMenuHelper(object):
+ ''' Generates custom submenus for the main menu or toolbar. '''
- def __init__(self, generate_modes, action_handler_callback):
- self.generate_modes = generate_modes
- self.action_handler_callback = action_handler_callback
- self.submenus = []
+ def __init__(self):
+ self.submenus = {}
- def create_submenu(self, action_tuple, item):
- func = getattr(self, '_fill_' + action_tuple[1] + "_submenu")
- self.submenus.append((action_tuple[0], func, item))
- self.refresh_submenus()
+ def build_submenu(self, name, obj, set_func):
+ # Get the correct helper function
+ create_func = getattr(self, "create_{}".format(name))
+ # Save the helper functions for rebuilding the menu later
+ self.submenus[name] = (create_func, obj, set_func)
+ # Actually build the menu
+ set_func(obj, create_func())
def refresh_submenus(self):
- for action, func, item in self.submenus:
- try:
- item.set_property("menu", func(action))
- except TypeError:
- item.set_property("submenu", func(action))
- item.set_property('sensitive', True)
-
- def callback_adaptor(self, item, action_key):
- action, key = action_key
- self.action_handler_callback(action, key)
-
- def _fill_flow_graph_new_submenu(self, action):
- """Sub menu to create flow-graph with pre-set generate mode"""
- menu = gtk.Menu()
- for key, name, default in self.generate_modes:
- if default:
- item = Actions.FLOW_GRAPH_NEW.create_menu_item()
- item.set_label(name)
- else:
- item = gtk.MenuItem(name, use_underline=False)
- item.connect('activate', self.callback_adaptor, (action, key))
- menu.append(item)
- menu.show_all()
+ for name in self.submenus:
+ create_func, obj, set_func = self.submenus[name]
+ print ("refresh", create_func, obj, set_func)
+ set_func(obj, create_func())
+
+ def create_flow_graph_new(self):
+ """ Different flowgraph types """
+ menu = Gio.Menu()
+ platform = Gtk.Application.get_default().platform
+ generate_modes = platform.get_generate_options()
+ for key, name, default in generate_modes:
+ target = "app.flowgraph.new::{}".format(key)
+ menu.append(name, target)
return menu
- def _fill_flow_graph_recent_submenu(self, action):
- """menu showing recent flow-graphs"""
- import Preferences
- menu = gtk.Menu()
- recent_files = Preferences.get_recent_files()
+ def create_flow_graph_recent(self):
+ """ Recent flow graphs """
+
+ config = Gtk.Application.get_default().config
+ recent_files = config.get_recent_files()
+ menu = Gio.Menu()
if len(recent_files) > 0:
+ files = Gio.Menu()
for i, file_name in enumerate(recent_files):
- item = gtk.MenuItem("%d. %s" % (i+1, file_name), use_underline=False)
- item.connect('activate', self.callback_adaptor,
- (action, file_name))
- menu.append(item)
- menu.show_all()
- return menu
- return None
+ target = "app.flowgraph.open_recent::{}".format(file_name)
+ files.append(file_name, target)
+ menu.append_section(None, files)
+ #clear = Gio.Menu()
+ #clear.append("Clear recent files", "app.flowgraph.clear_recent")
+ #menu.append_section(None, clear)
+ else:
+ # Show an empty menu
+ menuitem = Gio.MenuItem.new("No items found", "app.none")
+ menu.append_item(menuitem)
+ return menu
-class Toolbar(gtk.Toolbar, SubMenuCreator):
- """The gtk toolbar with actions added from the toolbar list."""
+class MenuHelper(SubMenuHelper):
+ """
+ Recursively builds a menu from a given list of actions.
- def __init__(self, generate_modes, action_handler_callback):
- """
- Parse the list of action names in the toolbar list.
- Look up the action for each name in the action list and add it to the
- toolbar.
- """
- gtk.Toolbar.__init__(self)
- self.set_style(gtk.TOOLBAR_ICONS)
- SubMenuCreator.__init__(self, generate_modes, action_handler_callback)
-
- for action in TOOLBAR_LIST:
- if isinstance(action, tuple) and isinstance(action[1], str):
- # create a button with a sub-menu
- action[0].set_tool_item_type(gtk.MenuToolButton)
- item = action[0].create_tool_item()
- self.create_submenu(action, item)
- self.refresh_submenus()
-
- elif action is None:
- item = gtk.SeparatorToolItem()
-
- else:
- action.set_tool_item_type(gtk.ToolButton)
- item = action.create_tool_item()
- # this reset of the tooltip property is required
- # (after creating the tool item) for the tooltip to show
- action.set_property('tooltip', action.get_property('tooltip'))
- self.add(item)
-
-
-class MenuHelperMixin(object):
- """Mixin class to help build menus from the above action lists"""
-
- def _fill_menu(self, actions, menu=None):
- """Create a menu from list of actions"""
- menu = menu or gtk.Menu()
+ Args:
+ - actions: List of actions to build the menu
+ - menu: Current menu being built
+
+ Notes:
+ - Tuple: Create a new submenu from the parent (1st) and child (2nd) elements
+ - Action: Append to current menu
+ - List: Start a new section
+ """
+
+ def __init__(self):
+ SubMenuHelper.__init__(self)
+
+ def build_menu(self, actions, menu):
for item in actions:
if isinstance(item, tuple):
- menu_item = self._make_sub_menu(*item)
- elif isinstance(item, str):
- menu_item = getattr(self, 'create_' + item)()
- elif item is None:
- menu_item = gtk.SeparatorMenuItem()
- else:
- menu_item = item.create_menu_item()
- menu.append(menu_item)
- menu.show_all()
- return menu
+ # Create a new submenu
+ parent, child = (item[0], item[1])
+
+ # Create the parent
+ label, target = (parent, None)
+ if isinstance(parent, Actions.Action):
+ label = parent.label
+ target = "{}.{}".format(parent.prefix, parent.name)
+ menuitem = Gio.MenuItem.new(label, None)
+ if hasattr(parent, "icon_name"):
+ menuitem.set_icon(Gio.Icon.new_for_string(parent.icon_name))
+
+ # Create the new submenu
+ if isinstance(child, list):
+ submenu = Gio.Menu()
+ self.build_menu(child, submenu)
+ menuitem.set_submenu(submenu)
+ elif isinstance(child, str):
+ # Child is the name of the submenu to create
+ def set_func(obj, menu):
+ obj.set_submenu(menu)
+ self.build_submenu(child, menuitem, set_func)
+ menu.append_item(menuitem)
+
+ elif isinstance(item, list):
+ # Create a new section
+ section = Gio.Menu()
+ self.build_menu(item, section)
+ menu.append_section(None, section)
+
+ elif isinstance(item, Actions.Action):
+ # Append a new menuitem
+ target = "{}.{}".format(item.prefix, item.name)
+ menuitem = Gio.MenuItem.new(item.label, target)
+ if item.icon_name:
+ menuitem.set_icon(Gio.Icon.new_for_string(item.icon_name))
+ menu.append_item(menuitem)
+
+
+class ToolbarHelper(SubMenuHelper):
+ """
+ Builds a toolbar from a given list of actions.
+
+ Args:
+ - actions: List of actions to build the menu
+ - item: Current menu being built
+
+ Notes:
+ - Tuple: Create a new submenu from the parent (1st) and child (2nd) elements
+ - Action: Append to current menu
+ - List: Start a new section
+ """
- def _make_sub_menu(self, main, actions):
- """Create a submenu from a main action and a list of actions"""
- main = main.create_menu_item()
- main.set_submenu(self._fill_menu(actions))
- return main
+ def __init__(self):
+ SubMenuHelper.__init__(self)
+ def build_toolbar(self, actions, current):
+ for item in actions:
+ if isinstance(item, list):
+ # Toolbar's don't have sections like menus, so call this function
+ # recursively with the "section" and just append a separator.
+ self.build_toolbar(item, self)
+ current.insert(Gtk.SeparatorToolItem.new(), -1)
+
+ elif isinstance(item, tuple):
+ parent, child = (item[0], item[1])
+ # Create an item with a submenu
+ # Generate the submenu and add to the item.
+ # Add the item to the toolbar
+ button = Gtk.MenuToolButton.new()
+ # The tuple should be made up of an Action and something.
+ button.set_label(parent.label)
+ button.set_tooltip_text(parent.tooltip)
+ button.set_icon_name(parent.icon_name)
+
+ target = "{}.{}".format(parent.prefix, parent.name)
+ button.set_action_name(target)
+
+ def set_func(obj, menu):
+ obj.set_menu(Gtk.Menu.new_from_model(menu))
+
+ self.build_submenu(child, button, set_func)
+ current.insert(button, -1)
+
+ elif isinstance(item, Actions.Action):
+ button = Gtk.ToolButton.new()
+ button.set_label(item.label)
+ button.set_tooltip_text(item.tooltip)
+ button.set_icon_name(item.icon_name)
+ target = "{}.{}".format(item.prefix, item.name)
+ button.set_action_name(target)
+ current.insert(button, -1)
+
+
+class Menu(Gio.Menu, MenuHelper):
+ """ Main Menu """
-class MenuBar(gtk.MenuBar, MenuHelperMixin, SubMenuCreator):
- """The gtk menu bar with actions added from the menu bar list."""
+ def __init__(self):
+ GObject.GObject.__init__(self)
+ MenuHelper.__init__(self)
- def __init__(self, generate_modes, action_handler_callback):
- """
- Parse the list of submenus from the menubar list.
- For each submenu, get a list of action names.
- Look up the action for each name in the action list and add it to the
- submenu. Add the submenu to the menu bar.
- """
- gtk.MenuBar.__init__(self)
- SubMenuCreator.__init__(self, generate_modes, action_handler_callback)
- for main_action, actions in MENU_BAR_LIST:
- self.append(self._make_sub_menu(main_action, actions))
+ log.debug("Building the main menu")
+ self.build_menu(MENU_BAR_LIST, self)
- def create_flow_graph_new(self):
- main = gtk.ImageMenuItem(gtk.STOCK_NEW)
- main.set_label(Actions.FLOW_GRAPH_NEW.get_label())
- func = self._fill_flow_graph_new_submenu
- self.submenus.append((Actions.FLOW_GRAPH_NEW, func, main))
- self.refresh_submenus()
- return main
- def create_flow_graph_recent(self):
- main = gtk.ImageMenuItem(gtk.STOCK_OPEN)
- main.set_label(Actions.FLOW_GRAPH_OPEN_RECENT.get_label())
- func = self._fill_flow_graph_recent_submenu
- self.submenus.append((Actions.FLOW_GRAPH_OPEN, func, main))
- self.refresh_submenus()
- if main.get_submenu() is None:
- main.set_property('sensitive', False)
- return main
+class ContextMenu(Gio.Menu, MenuHelper):
+ """ Context menu for the drawing area """
+
+ def __init__(self):
+ GObject.GObject.__init__(self)
+ log.debug("Building the context menu")
+ self.build_menu(CONTEXT_MENU_LIST, self)
-class ContextMenu(gtk.Menu, MenuHelperMixin):
- """The gtk menu with actions added from the context menu list."""
+
+class Toolbar(Gtk.Toolbar, ToolbarHelper):
+ """ The gtk toolbar with actions added from the toolbar list. """
def __init__(self):
- gtk.Menu.__init__(self)
- self._fill_menu(CONTEXT_MENU_LIST, self)
+ """
+ Parse the list of action names in the toolbar list.
+ Look up the action for each name in the action list and add it to the
+ toolbar.
+ """
+ GObject.GObject.__init__(self)
+ ToolbarHelper.__init__(self)
+
+ self.set_style(Gtk.ToolbarStyle.ICONS)
+ #self.get_style_context().add_class(Gtk.STYLE_CLASS_PRIMARY_TOOLBAR)
+
+ #SubMenuCreator.__init__(self)
+ self.build_toolbar(TOOLBAR_LIST, self)
diff --git a/grc/gui/Block.py b/grc/gui/Block.py
deleted file mode 100644
index b90ea485ee..0000000000
--- a/grc/gui/Block.py
+++ /dev/null
@@ -1,350 +0,0 @@
-"""
-Copyright 2007, 2008, 2009 Free Software Foundation, Inc.
-This file is part of GNU Radio
-
-GNU Radio Companion is free software; you can redistribute it and/or
-modify it under the terms of the GNU General Public License
-as published by the Free Software Foundation; either version 2
-of the License, or (at your option) any later version.
-
-GNU Radio Companion is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-GNU General Public License for more details.
-
-You should have received a copy of the GNU General Public License
-along with this program; if not, write to the Free Software
-Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
-"""
-
-import pygtk
-pygtk.require('2.0')
-import gtk
-import pango
-
-from . import Actions, Colors, Utils, Constants
-
-from . Element import Element
-from ..core.Param import num_to_str
-from ..core.utils import odict
-from ..core.utils.complexity import calculate_flowgraph_complexity
-from ..core.Block import Block as _Block
-
-BLOCK_MARKUP_TMPL="""\
-#set $foreground = $block.is_valid() and 'black' or 'red'
-<span foreground="$foreground" font_desc="$font"><b>$encode($block.get_name())</b></span>"""
-
-# Includes the additional complexity markup if enabled
-COMMENT_COMPLEXITY_MARKUP_TMPL="""\
-#set $foreground = $block.get_enabled() and '#444' or '#888'
-#if $complexity
-<span foreground="#444" size="medium" font_desc="$font"><b>$encode($complexity)</b></span>#slurp
-#end if
-#if $complexity and $comment
-<span></span>
-#end if
-#if $comment
-<span foreground="$foreground" font_desc="$font">$encode($comment)</span>#slurp
-#end if
-"""
-
-
-class Block(Element, _Block):
- """The graphical signal block."""
-
- def __init__(self, flow_graph, n):
- """
- Block contructor.
- Add graphics related params to the block.
- """
- _Block.__init__(self, flow_graph, n)
-
- self.W = 0
- self.H = 0
- #add the position param
- self.get_params().append(self.get_parent().get_parent().Param(
- block=self,
- n=odict({
- 'name': 'GUI Coordinate',
- 'key': '_coordinate',
- 'type': 'raw',
- 'value': '(0, 0)',
- 'hide': 'all',
- })
- ))
- self.get_params().append(self.get_parent().get_parent().Param(
- block=self,
- n=odict({
- 'name': 'GUI Rotation',
- 'key': '_rotation',
- 'type': 'raw',
- 'value': '0',
- 'hide': 'all',
- })
- ))
- Element.__init__(self)
- self._comment_pixmap = None
- self.has_busses = [False, False] # source, sink
-
- def get_coordinate(self):
- """
- Get the coordinate from the position param.
-
- Returns:
- the coordinate tuple (x, y) or (0, 0) if failure
- """
- proximity = Constants.BORDER_PROXIMITY_SENSITIVITY
- try: #should evaluate to tuple
- x, y = Utils.scale(eval(self.get_param('_coordinate').get_value()))
- fgW, fgH = self.get_parent().get_size()
- if x <= 0:
- x = 0
- elif x >= fgW - proximity:
- x = fgW - proximity
- if y <= 0:
- y = 0
- elif y >= fgH - proximity:
- y = fgH - proximity
- return (x, y)
- except:
- self.set_coordinate((0, 0))
- return (0, 0)
-
- def set_coordinate(self, coor):
- """
- Set the coordinate into the position param.
-
- Args:
- coor: the coordinate tuple (x, y)
- """
- if Actions.TOGGLE_SNAP_TO_GRID.get_active():
- offset_x, offset_y = (0, self.H/2) if self.is_horizontal() else (self.H/2, 0)
- coor = (
- Utils.align_to_grid(coor[0] + offset_x) - offset_x,
- Utils.align_to_grid(coor[1] + offset_y) - offset_y
- )
- self.get_param('_coordinate').set_value(str(Utils.scale(coor, reverse=True)))
-
- def bound_move_delta(self, delta_coor):
- """
- Limit potential moves from exceeding the bounds of the canvas
-
- Args:
- delta_coor: requested delta coordinate (dX, dY) to move
-
- Returns:
- The delta coordinate possible to move while keeping the block on the canvas
- or the input (dX, dY) on failure
- """
- dX, dY = delta_coor
-
- try:
- fgW, fgH = self.get_parent().get_size()
- x, y = Utils.scale(eval(self.get_param('_coordinate').get_value()))
- if self.is_horizontal():
- sW, sH = self.W, self.H
- else:
- sW, sH = self.H, self.W
-
- if x + dX < 0:
- dX = -x
- elif dX + x + sW >= fgW:
- dX = fgW - x - sW
- if y + dY < 0:
- dY = -y
- elif dY + y + sH >= fgH:
- dY = fgH - y - sH
- except:
- pass
-
- return ( dX, dY )
-
- def get_rotation(self):
- """
- Get the rotation from the position param.
-
- Returns:
- the rotation in degrees or 0 if failure
- """
- try: #should evaluate to dict
- rotation = eval(self.get_param('_rotation').get_value())
- return int(rotation)
- except:
- self.set_rotation(Constants.POSSIBLE_ROTATIONS[0])
- return Constants.POSSIBLE_ROTATIONS[0]
-
- def set_rotation(self, rot):
- """
- Set the rotation into the position param.
-
- Args:
- rot: the rotation in degrees
- """
- self.get_param('_rotation').set_value(str(rot))
-
- def create_shapes(self):
- """Update the block, parameters, and ports when a change occurs."""
- Element.create_shapes(self)
- if self.is_horizontal(): self.add_area((0, 0), (self.W, self.H))
- elif self.is_vertical(): self.add_area((0, 0), (self.H, self.W))
-
- def create_labels(self):
- """Create the labels for the signal block."""
- Element.create_labels(self)
- self._bg_color = self.is_dummy_block and Colors.MISSING_BLOCK_BACKGROUND_COLOR or \
- self.get_bypassed() and Colors.BLOCK_BYPASSED_COLOR or \
- self.get_enabled() and Colors.BLOCK_ENABLED_COLOR or Colors.BLOCK_DISABLED_COLOR
-
- layouts = list()
- #create the main layout
- layout = gtk.DrawingArea().create_pango_layout('')
- layouts.append(layout)
- layout.set_markup(Utils.parse_template(BLOCK_MARKUP_TMPL, block=self, font=Constants.BLOCK_FONT))
- self.label_width, self.label_height = layout.get_pixel_size()
- #display the params
- if self.is_dummy_block:
- markups = [
- '<span foreground="black" font_desc="{font}"><b>key: </b>{key}</span>'
- ''.format(font=Constants.PARAM_FONT, key=self._key)
- ]
- else:
- markups = [param.get_markup() for param in self.get_params() if param.get_hide() not in ('all', 'part')]
- if markups:
- layout = gtk.DrawingArea().create_pango_layout('')
- layout.set_spacing(Constants.LABEL_SEPARATION * pango.SCALE)
- layout.set_markup('\n'.join(markups))
- layouts.append(layout)
- w, h = layout.get_pixel_size()
- self.label_width = max(w, self.label_width)
- self.label_height += h + Constants.LABEL_SEPARATION
- width = self.label_width
- height = self.label_height
- #setup the pixmap
- pixmap = self.get_parent().new_pixmap(width, height)
- gc = pixmap.new_gc()
- gc.set_foreground(self._bg_color)
- pixmap.draw_rectangle(gc, True, 0, 0, width, height)
- #draw the layouts
- h_off = 0
- for i,layout in enumerate(layouts):
- w,h = layout.get_pixel_size()
- if i == 0: w_off = (width-w)/2
- else: w_off = 0
- pixmap.draw_layout(gc, w_off, h_off, layout)
- h_off = h + h_off + Constants.LABEL_SEPARATION
- #create vertical and horizontal pixmaps
- self.horizontal_label = pixmap
- if self.is_vertical():
- self.vertical_label = self.get_parent().new_pixmap(height, width)
- Utils.rotate_pixmap(gc, self.horizontal_label, self.vertical_label)
- #calculate width and height needed
- W = self.label_width + 2 * Constants.BLOCK_LABEL_PADDING
-
- def get_min_height_for_ports():
- visible_ports = filter(lambda p: not p.get_hide(), ports)
- min_height = 2*Constants.PORT_BORDER_SEPARATION + len(visible_ports) * Constants.PORT_SEPARATION
- if visible_ports:
- min_height -= ports[0].H
- return min_height
- H = max(
- [ # labels
- self.label_height + 2 * Constants.BLOCK_LABEL_PADDING
- ] +
- [ # ports
- get_min_height_for_ports() for ports in (self.get_sources_gui(), self.get_sinks_gui())
- ] +
- [ # bus ports only
- 2 * Constants.PORT_BORDER_SEPARATION +
- sum([port.H + Constants.PORT_SPACING for port in ports if port.get_type() == 'bus']) - Constants.PORT_SPACING
- for ports in (self.get_sources_gui(), self.get_sinks_gui())
- ]
- )
- self.W, self.H = Utils.align_to_grid((W, H))
- self.has_busses = [
- any(port.get_type() == 'bus' for port in ports)
- for ports in (self.get_sources_gui(), self.get_sinks_gui())
- ]
- self.create_comment_label()
-
- def create_comment_label(self):
- comment = self.get_comment() # Returns None if there are no comments
- complexity = None
-
- # Show the flowgraph complexity on the top block if enabled
- if Actions.TOGGLE_SHOW_FLOWGRAPH_COMPLEXITY.get_active() and self.get_key() == "options":
- complexity = calculate_flowgraph_complexity(self.get_parent())
- complexity = "Complexity: {}bal".format(num_to_str(complexity))
-
- layout = gtk.DrawingArea().create_pango_layout('')
- layout.set_markup(Utils.parse_template(COMMENT_COMPLEXITY_MARKUP_TMPL,
- block=self,
- comment=comment,
- complexity=complexity,
- font=Constants.BLOCK_FONT))
-
- # Setup the pixel map. Make sure that layout not empty
- width, height = layout.get_pixel_size()
- if width and height:
- padding = Constants.BLOCK_LABEL_PADDING
- pixmap = self.get_parent().new_pixmap(width + 2 * padding,
- height + 2 * padding)
- gc = pixmap.new_gc()
- gc.set_foreground(Colors.COMMENT_BACKGROUND_COLOR)
- pixmap.draw_rectangle(
- gc, True, 0, 0, width + 2 * padding, height + 2 * padding)
- pixmap.draw_layout(gc, padding, padding, layout)
- self._comment_pixmap = pixmap
- else:
- self._comment_pixmap = None
-
- def draw(self, gc, window):
- """
- Draw the signal block with label and inputs/outputs.
-
- Args:
- gc: the graphics context
- window: the gtk window to draw on
- """
- # draw ports
- for port in self.get_ports_gui():
- port.draw(gc, window)
- # draw main block
- x, y = self.get_coordinate()
- Element.draw(
- self, gc, window, bg_color=self._bg_color,
- border_color=self.is_highlighted() and Colors.HIGHLIGHT_COLOR or
- self.is_dummy_block and Colors.MISSING_BLOCK_BORDER_COLOR or Colors.BORDER_COLOR,
- )
- #draw label image
- if self.is_horizontal():
- window.draw_drawable(gc, self.horizontal_label, 0, 0, x+Constants.BLOCK_LABEL_PADDING, y+(self.H-self.label_height)/2, -1, -1)
- elif self.is_vertical():
- window.draw_drawable(gc, self.vertical_label, 0, 0, x+(self.H-self.label_height)/2, y+Constants.BLOCK_LABEL_PADDING, -1, -1)
-
- def what_is_selected(self, coor, coor_m=None):
- """
- Get the element that is selected.
-
- Args:
- coor: the (x,y) tuple
- coor_m: the (x_m, y_m) tuple
-
- Returns:
- this block, a port, or None
- """
- for port in self.get_ports_gui():
- port_selected = port.what_is_selected(coor, coor_m)
- if port_selected: return port_selected
- return Element.what_is_selected(self, coor, coor_m)
-
- def draw_comment(self, gc, window):
- if not self._comment_pixmap:
- return
- x, y = self.get_coordinate()
-
- if self.is_horizontal():
- y += self.H + Constants.BLOCK_LABEL_PADDING
- else:
- x += self.H + Constants.BLOCK_LABEL_PADDING
-
- window.draw_drawable(gc, self._comment_pixmap, 0, 0, x, y, -1, -1)
diff --git a/grc/gui/BlockTreeWindow.py b/grc/gui/BlockTreeWindow.py
index 900cbd3151..3b9b8642f9 100644
--- a/grc/gui/BlockTreeWindow.py
+++ b/grc/gui/BlockTreeWindow.py
@@ -1,5 +1,5 @@
"""
-Copyright 2007, 2008, 2009 Free Software Foundation, Inc.
+Copyright 2007, 2008, 2009, 2016 Free Software Foundation, Inc.
This file is part of GNU Radio
GNU Radio Companion is free software; you can redistribute it and/or
@@ -17,71 +17,56 @@ along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
"""
-import pygtk
-pygtk.require('2.0')
-import gtk
-import gobject
-
-from . import Actions, Utils
-from . import Constants
-
-
-NAME_INDEX = 0
-KEY_INDEX = 1
-DOC_INDEX = 2
-
-DOC_MARKUP_TMPL = """\
-#set $docs = []
-#if $doc.get('')
- #set $docs += $doc.pop('').splitlines() + ['']
-#end if
-#for b, d in $doc.iteritems()
- #set $docs += ['--- {0} ---'.format(b)] + d.splitlines() + ['']
-#end for
-#set $len_out = 0
-#for $n, $line in $enumerate($docs[:-1])
-#if $n
-
-#end if
-$encode($line)#slurp
-#set $len_out += $len($line)
-#if $n > 10 or $len_out > 500
-
-...#slurp
-#break
-#end if
-#end for
-#if $len_out == 0
-undocumented#slurp
-#end if"""
-
-CAT_MARKUP_TMPL = """
-#set $name = $cat[-1]
-#if len($cat) > 1
-Category: $cat[-1]
-##
-#elif $name == 'Core'
-Module: Core
-
-This subtree is meant for blocks included with GNU Radio (in-tree).
-##
-#elif $name == $default_module
-This subtree holds all blocks (from OOT modules) that specify no module name. \
-The module name is the root category enclosed in square brackets.
-
-Please consider contacting OOT module maintainer for any block in here \
-and kindly ask to update their GRC Block Descriptions or Block Tree to include a module name.
-#else
-Module: $name
-##
-#end if
-""".strip()
-
-
-class BlockTreeWindow(gtk.VBox):
+from __future__ import absolute_import
+import six
+
+from gi.repository import Gtk, Gdk, GObject
+
+from . import Actions, Utils, Constants
+
+
+NAME_INDEX, KEY_INDEX, DOC_INDEX = range(3)
+
+
+def _format_doc(doc):
+ docs = []
+ if doc.get(''):
+ docs += doc.pop('').splitlines() + ['']
+ for block_name, docstring in six.iteritems(doc):
+ docs.append('--- {0} ---'.format(block_name))
+ docs += docstring.splitlines()
+ docs.append('')
+ out = ''
+ for n, line in enumerate(docs[:-1]):
+ if n:
+ out += '\n'
+ out += Utils.encode(line)
+ if n > 10 or len(out) > 500:
+ out += '\n...'
+ break
+ return out or 'undocumented'
+
+
+def _format_cat_tooltip(category):
+ tooltip = '{}: {}'.format('Category' if len(category) > 1 else 'Module', category[-1])
+
+ if category == ('Core',):
+ tooltip += '\n\nThis subtree is meant for blocks included with GNU Radio (in-tree).'
+
+ elif category == (Constants.DEFAULT_BLOCK_MODULE_NAME,):
+ tooltip += '\n\n' + Constants.DEFAULT_BLOCK_MODULE_TOOLTIP
+
+ return tooltip
+
+
+class BlockTreeWindow(Gtk.VBox):
"""The block selection panel."""
- def __init__(self, platform, get_flow_graph):
+ __gsignals__ = {
+ 'create_new_block': (GObject.SignalFlags.RUN_FIRST, None, (str,))
+ }
+
+ def __init__(self, platform):
"""
BlockTreeWindow constructor.
Create a tree view of the possible blocks in the platform.
@@ -90,58 +75,52 @@ class BlockTreeWindow(gtk.VBox):
Args:
platform: the particular platform will all block prototypes
- get_flow_graph: get the selected flow graph
"""
- gtk.VBox.__init__(self)
+ Gtk.VBox.__init__(self)
self.platform = platform
- self.get_flow_graph = get_flow_graph
# search entry
- self.search_entry = gtk.Entry()
+ self.search_entry = Gtk.Entry()
try:
- self.search_entry.set_icon_from_stock(gtk.ENTRY_ICON_PRIMARY, gtk.STOCK_FIND)
- self.search_entry.set_icon_activatable(gtk.ENTRY_ICON_PRIMARY, False)
- self.search_entry.set_icon_from_stock(gtk.ENTRY_ICON_SECONDARY, gtk.STOCK_CLOSE)
+ self.search_entry.set_icon_from_icon_name(Gtk.EntryIconPosition.PRIMARY, 'edit-find')
+ self.search_entry.set_icon_activatable(Gtk.EntryIconPosition.PRIMARY, False)
+ self.search_entry.set_icon_from_icon_name(Gtk.EntryIconPosition.SECONDARY, 'window-close')
self.search_entry.connect('icon-release', self._handle_icon_event)
except AttributeError:
pass # no icon for old pygtk
self.search_entry.connect('changed', self._update_search_tree)
self.search_entry.connect('key-press-event', self._handle_search_key_press)
- self.pack_start(self.search_entry, False)
+ self.pack_start(self.search_entry, False, False, 0)
# make the tree model for holding blocks and a temporary one for search results
- self.treestore = gtk.TreeStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
- self.treestore_search = gtk.TreeStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
+ self.treestore = Gtk.TreeStore(GObject.TYPE_STRING, GObject.TYPE_STRING, GObject.TYPE_STRING)
+ self.treestore_search = Gtk.TreeStore(GObject.TYPE_STRING, GObject.TYPE_STRING, GObject.TYPE_STRING)
- self.treeview = gtk.TreeView(self.treestore)
+ self.treeview = Gtk.TreeView(model=self.treestore)
self.treeview.set_enable_search(False) # disable pop up search box
self.treeview.set_search_column(-1) # really disable search
self.treeview.set_headers_visible(False)
- self.treeview.add_events(gtk.gdk.BUTTON_PRESS_MASK)
+ self.treeview.add_events(Gdk.EventMask.BUTTON_PRESS_MASK)
self.treeview.connect('button-press-event', self._handle_mouse_button_press)
self.treeview.connect('key-press-event', self._handle_search_key_press)
- self.treeview.get_selection().set_mode('single')
- renderer = gtk.CellRendererText()
- column = gtk.TreeViewColumn('Blocks', renderer, text=NAME_INDEX)
+ self.treeview.get_selection().set_mode(Gtk.SelectionMode.SINGLE)
+ renderer = Gtk.CellRendererText()
+ column = Gtk.TreeViewColumn('Blocks', renderer, text=NAME_INDEX)
self.treeview.append_column(column)
- # try to enable the tooltips (available in pygtk 2.12 and above)
- try:
- self.treeview.set_tooltip_column(DOC_INDEX)
- except:
- pass
+ self.treeview.set_tooltip_column(DOC_INDEX)
# setup sort order
column.set_sort_column_id(0)
- self.treestore.set_sort_column_id(0, gtk.SORT_ASCENDING)
+ self.treestore.set_sort_column_id(0, Gtk.SortType.ASCENDING)
# setup drag and drop
- self.treeview.enable_model_drag_source(gtk.gdk.BUTTON1_MASK, Constants.DND_TARGETS, gtk.gdk.ACTION_COPY)
+ self.treeview.enable_model_drag_source(Gdk.ModifierType.BUTTON1_MASK, Constants.DND_TARGETS, Gdk.DragAction.COPY)
self.treeview.connect('drag-data-get', self._handle_drag_get_data)
# make the scrolled window to hold the tree view
- scrolled_window = gtk.ScrolledWindow()
- scrolled_window.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
- scrolled_window.add_with_viewport(self.treeview)
+ scrolled_window = Gtk.ScrolledWindow()
+ scrolled_window.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
+ scrolled_window.add(self.treeview)
scrolled_window.set_size_request(Constants.DEFAULT_BLOCKS_WINDOW_WIDTH, -1)
- self.pack_start(scrolled_window)
+ self.pack_start(scrolled_window, True, True, 0)
# map categories to iters, automatic mapping for root
self._categories = {tuple(): None}
self._categories_search = {tuple(): None}
@@ -154,7 +133,7 @@ class BlockTreeWindow(gtk.VBox):
def repopulate(self):
self.clear()
- for block in self.platform.blocks.itervalues():
+ for block in six.itervalues(self.platform.blocks):
if block.category:
self.add_block(block)
self.expand_module_in_tree()
@@ -188,15 +167,13 @@ class BlockTreeWindow(gtk.VBox):
iter_ = treestore.insert_before(categories[parent_category[:-1]], None)
treestore.set_value(iter_, NAME_INDEX, parent_cat_name)
treestore.set_value(iter_, KEY_INDEX, '')
- treestore.set_value(iter_, DOC_INDEX, Utils.parse_template(
- CAT_MARKUP_TMPL, cat=parent_category, default_module=Constants.DEFAULT_BLOCK_MODULE_NAME))
+ treestore.set_value(iter_, DOC_INDEX, _format_cat_tooltip(parent_cat_name))
categories[parent_category] = iter_
-
# add block
iter_ = treestore.insert_before(categories[category], None)
- treestore.set_value(iter_, NAME_INDEX, block.get_name())
- treestore.set_value(iter_, KEY_INDEX, block.get_key())
- treestore.set_value(iter_, DOC_INDEX, Utils.parse_template(DOC_MARKUP_TMPL, doc=block.get_doc()))
+ treestore.set_value(iter_, KEY_INDEX, block.key)
+ treestore.set_value(iter_, NAME_INDEX, block.label)
+ treestore.set_value(iter_, DOC_INDEX, _format_doc(block.documentation))
def update_docs(self):
"""Update the documentation column of every block"""
@@ -206,8 +183,7 @@ class BlockTreeWindow(gtk.VBox):
if not key:
return # category node, no doc string
block = self.platform.blocks[key]
- doc = Utils.parse_template(DOC_MARKUP_TMPL, doc=block.get_doc())
- model.set_value(iter_, DOC_INDEX, doc)
+ model.set_value(iter_, DOC_INDEX, _format_doc(block.documentation))
self.treestore.foreach(update_doc)
self.treestore_search.foreach(update_doc)
@@ -226,16 +202,6 @@ class BlockTreeWindow(gtk.VBox):
treestore, iter = selection.get_selected()
return iter and treestore.get_value(iter, KEY_INDEX) or ''
- def _add_selected_block(self):
- """
- Add the selected block with the given key to the flow graph.
- """
- key = self._get_selected_block_key()
- if key:
- self.get_flow_graph().add_new_block(key)
- return True
- return False
-
def _expand_category(self):
treestore, iter = self.treeview.get_selection().get_selected()
if iter and treestore.iter_has_child(iter):
@@ -246,9 +212,9 @@ class BlockTreeWindow(gtk.VBox):
## Event Handlers
############################################################
def _handle_icon_event(self, widget, icon, event):
- if icon == gtk.ENTRY_ICON_PRIMARY:
+ if icon == Gtk.EntryIconPosition.PRIMARY:
pass
- elif icon == gtk.ENTRY_ICON_SECONDARY:
+ elif icon == Gtk.EntryIconPosition.SECONDARY:
widget.set_text('')
self.search_entry.hide()
@@ -258,8 +224,8 @@ class BlockTreeWindow(gtk.VBox):
self.treeview.set_model(self.treestore)
self.expand_module_in_tree()
else:
- matching_blocks = filter(lambda b: key in b.get_key().lower() or key in b.get_name().lower(),
- self.platform.blocks.values())
+ matching_blocks = [b for b in list(self.platform.blocks.values())
+ if key in b.key.lower() or key in b.label.lower()]
self.treestore_search.clear()
self._categories_search = {tuple(): None}
@@ -270,7 +236,7 @@ class BlockTreeWindow(gtk.VBox):
def _handle_search_key_press(self, widget, event):
"""Handle Return and Escape key events in search entry and treeview"""
- if event.keyval == gtk.keysyms.Return:
+ if event.keyval == Gdk.KEY_Return:
# add block on enter
if widget == self.search_entry:
@@ -280,24 +246,28 @@ class BlockTreeWindow(gtk.VBox):
selected = self.treestore_search.iter_children(selected)
if selected is not None:
key = self.treestore_search.get_value(selected, KEY_INDEX)
- if key: self.get_flow_graph().add_new_block(key)
+ if key: self.emit('create_new_block', key)
elif widget == self.treeview:
- self._add_selected_block() or self._expand_category()
+ key = self._get_selected_block_key()
+ if key:
+ self.emit('create_new_block', key)
+ else:
+ self._expand_category()
else:
return False # propagate event
- elif event.keyval == gtk.keysyms.Escape:
+ elif event.keyval == Gdk.KEY_Escape:
# reset the search
self.search_entry.set_text('')
self.search_entry.hide()
- elif (event.state & gtk.gdk.CONTROL_MASK and event.keyval == gtk.keysyms.f) \
- or event.keyval == gtk.keysyms.slash:
+ elif (event.get_state() & Gdk.ModifierType.CONTROL_MASK and event.keyval == Gdk.KEY_f) \
+ or event.keyval == Gdk.KEY_slash:
# propagation doesn't work although treeview search is disabled =(
# manually trigger action...
Actions.FIND_BLOCKS.activate()
- elif event.state & gtk.gdk.CONTROL_MASK and event.keyval == gtk.keysyms.b:
+ elif event.get_state() & Gdk.ModifierType.CONTROL_MASK and event.keyval == Gdk.KEY_b:
# ugly...
Actions.TOGGLE_BLOCKS_WINDOW.activate()
@@ -313,12 +283,15 @@ class BlockTreeWindow(gtk.VBox):
Only call set when the key is valid to ignore DND from categories.
"""
key = self._get_selected_block_key()
- if key: selection_data.set(selection_data.target, 8, key)
+ if key:
+ selection_data.set_text(key, len(key))
def _handle_mouse_button_press(self, widget, event):
"""
Handle the mouse button press.
If a left double click is detected, call add selected block.
"""
- if event.button == 1 and event.type == gtk.gdk._2BUTTON_PRESS:
- self._add_selected_block()
+ if event.button == 1 and event.type == Gdk.EventType._2BUTTON_PRESS:
+ key = self._get_selected_block_key()
+ if key:
+ self.emit('create_new_block', key)
diff --git a/grc/gui/CMakeLists.txt b/grc/gui/CMakeLists.txt
deleted file mode 100644
index dc661c44ed..0000000000
--- a/grc/gui/CMakeLists.txt
+++ /dev/null
@@ -1,32 +0,0 @@
-# Copyright 2011 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.
-
-file(GLOB py_files "*.py")
-
-GR_PYTHON_INSTALL(
- FILES ${py_files}
- DESTINATION ${GR_PYTHON_DIR}/gnuradio/grc/gui
-)
-
-install(
- FILES
- ${CMAKE_CURRENT_SOURCE_DIR}/icon.png
- DESTINATION ${GR_PYTHON_DIR}/gnuradio/grc/gui
- COMPONENT "grc"
-)
diff --git a/grc/gui/Colors.py b/grc/gui/Colors.py
deleted file mode 100644
index 023a4e7038..0000000000
--- a/grc/gui/Colors.py
+++ /dev/null
@@ -1,49 +0,0 @@
-"""
-Copyright 2008,2013 Free Software Foundation, Inc.
-This file is part of GNU Radio
-
-GNU Radio Companion is free software; you can redistribute it and/or
-modify it under the terms of the GNU General Public License
-as published by the Free Software Foundation; either version 2
-of the License, or (at your option) any later version.
-
-GNU Radio Companion is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-GNU General Public License for more details.
-
-You should have received a copy of the GNU General Public License
-along with this program; if not, write to the Free Software
-Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
-"""
-try:
- 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)
-
- HIGHLIGHT_COLOR = get_color('#00FFFF')
- BORDER_COLOR = get_color('#444444')
- # missing blocks stuff
- MISSING_BLOCK_BACKGROUND_COLOR = get_color('#FFF2F2')
- MISSING_BLOCK_BORDER_COLOR = get_color('red')
- #param entry boxes
- ENTRYENUM_CUSTOM_COLOR = get_color('#EEEEEE')
- #flow graph color constants
- FLOWGRAPH_BACKGROUND_COLOR = get_color('#FFFFFF')
- COMMENT_BACKGROUND_COLOR = get_color('#F3F3F3')
- FLOWGRAPH_EDGE_COLOR = COMMENT_BACKGROUND_COLOR
- #block color constants
- BLOCK_ENABLED_COLOR = get_color('#F1ECFF')
- BLOCK_DISABLED_COLOR = get_color('#CCCCCC')
- BLOCK_BYPASSED_COLOR = get_color('#F4FF81')
- #connection color constants
- CONNECTION_ENABLED_COLOR = get_color('black')
- CONNECTION_DISABLED_COLOR = get_color('#BBBBBB')
- CONNECTION_ERROR_COLOR = get_color('red')
-except:
- print 'Unable to import Colors'
-
-DEFAULT_DOMAIN_COLOR_CODE = '#777777'
diff --git a/grc/gui/Config.py b/grc/gui/Config.py
index 9b0c5d4afe..6135296660 100644
--- a/grc/gui/Config.py
+++ b/grc/gui/Config.py
@@ -17,13 +17,26 @@ along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
"""
+from __future__ import absolute_import, print_function
+
import sys
import os
-from ..core.Config import Config as _Config
+
+from ..core.Config import Config as CoreConfig
from . import Constants
+from six.moves import configparser
-class Config(_Config):
+HEADER = """\
+# This contains only GUI settings for GRC and is not meant for users to edit.
+#
+# GRC settings not accessible through the GUI are in gnuradio.conf under
+# section [grc].
+
+"""
+
+
+class Config(CoreConfig):
name = 'GNU Radio Companion'
@@ -31,44 +44,163 @@ class Config(_Config):
'GRC_PREFS_PATH', os.path.expanduser('~/.gnuradio/grc.conf'))
def __init__(self, install_prefix, *args, **kwargs):
- _Config.__init__(self, *args, **kwargs)
+ CoreConfig.__init__(self, *args, **kwargs)
self.install_prefix = install_prefix
Constants.update_font_size(self.font_size)
+ self.parser = configparser.ConfigParser()
+ for section in ['main', 'files_open', 'files_recent']:
+ try:
+ self.parser.add_section(section)
+ except Exception as e:
+ print(e)
+ try:
+ self.parser.read(self.gui_prefs_file)
+ except Exception as err:
+ print(err, file=sys.stderr)
+
+ def save(self):
+ try:
+ with open(self.gui_prefs_file, 'w') as fp:
+ fp.write(HEADER)
+ self.parser.write(fp)
+ except Exception as err:
+ print(err, file=sys.stderr)
+
+ def entry(self, key, value=None, default=None):
+ if value is not None:
+ self.parser.set('main', key, str(value))
+ result = value
+ else:
+ _type = type(default) if default is not None else str
+ getter = {
+ bool: self.parser.getboolean,
+ int: self.parser.getint,
+ }.get(_type, self.parser.get)
+ try:
+ result = getter('main', key)
+ except (AttributeError, configparser.Error):
+ result = _type() if default is None else default
+ return result
+
@property
def editor(self):
- return self.prefs.get_string('grc', 'editor', '')
+ return self._gr_prefs.get_string('grc', 'editor', '')
@editor.setter
def editor(self, value):
- self.prefs.get_string('grc', 'editor', value)
- self.prefs.save()
+ self._gr_prefs.get_string('grc', 'editor', value)
+ self._gr_prefs.save()
@property
def xterm_executable(self):
- return self.prefs.get_string('grc', 'xterm_executable', 'xterm')
+ return self._gr_prefs.get_string('grc', 'xterm_executable', 'xterm')
@property
def default_canvas_size(self):
try: # ugly, but matches current code style
- raw = self.prefs.get_string('grc', 'canvas_default_size', '1280, 1024')
+ raw = self._gr_prefs.get_string('grc', 'canvas_default_size', '1280, 1024')
value = tuple(int(x.strip('() ')) for x in raw.split(','))
if len(value) != 2 or not all(300 < x < 4096 for x in value):
raise Exception()
return value
except:
- print >> sys.stderr, "Error: invalid 'canvas_default_size' setting."
+ print("Error: invalid 'canvas_default_size' setting.", file=sys.stderr)
return Constants.DEFAULT_CANVAS_SIZE_DEFAULT
@property
def font_size(self):
try: # ugly, but matches current code style
- font_size = self.prefs.get_long('grc', 'canvas_font_size',
- Constants.DEFAULT_FONT_SIZE)
+ font_size = self._gr_prefs.get_long('grc', 'canvas_font_size',
+ Constants.DEFAULT_FONT_SIZE)
if font_size <= 0:
raise Exception()
except:
font_size = Constants.DEFAULT_FONT_SIZE
- print >> sys.stderr, "Error: invalid 'canvas_font_size' setting."
+ print("Error: invalid 'canvas_font_size' setting.", file=sys.stderr)
return font_size
+
+ @property
+ def default_qss_theme(self):
+ return self._gr_prefs.get_string('qtgui', 'qss', '')
+
+ @default_qss_theme.setter
+ def default_qss_theme(self, value):
+ self._gr_prefs.set_string("qtgui", "qss", value)
+ self._gr_prefs.save()
+
+ ##### Originally from Preferences.py #####
+ def main_window_size(self, size=None):
+ if size is None:
+ size = [None, None]
+ w = self.entry('main_window_width', size[0], default=1)
+ h = self.entry('main_window_height', size[1], default=1)
+ return w, h
+
+ def file_open(self, filename=None):
+ return self.entry('file_open', filename, default='')
+
+ def set_file_list(self, key, files):
+ self.parser.remove_section(key) # clear section
+ self.parser.add_section(key)
+ for i, filename in enumerate(files):
+ self.parser.set(key, '%s_%d' % (key, i), filename)
+
+ def get_file_list(self, key):
+ try:
+ files = [value for name, value in self.parser.items(key)
+ if name.startswith('%s_' % key)]
+ except (AttributeError, configparser.Error):
+ files = []
+ return files
+
+ def get_open_files(self):
+ return self.get_file_list('files_open')
+
+ def set_open_files(self, files):
+ return self.set_file_list('files_open', files)
+
+ def get_recent_files(self):
+ """ Gets recent files, removes any that do not exist and re-saves it """
+ files = list(filter(os.path.exists, self.get_file_list('files_recent')))
+ self.set_recent_files(files)
+ return files
+
+ def set_recent_files(self, files):
+ return self.set_file_list('files_recent', files)
+
+ def add_recent_file(self, file_name):
+ # double check file_name
+ if os.path.exists(file_name):
+ recent_files = self.get_recent_files()
+ if file_name in recent_files:
+ recent_files.remove(file_name) # Attempt removal
+ recent_files.insert(0, file_name) # Insert at start
+ self.set_recent_files(recent_files[:10]) # Keep up to 10 files
+
+ def console_window_position(self, pos=None):
+ return self.entry('console_window_position', pos, default=-1) or 1
+
+ def blocks_window_position(self, pos=None):
+ return self.entry('blocks_window_position', pos, default=-1) or 1
+
+ def variable_editor_position(self, pos=None, sidebar=False):
+ # Figure out default
+ if sidebar:
+ w, h = self.main_window_size()
+ return self.entry('variable_editor_sidebar_position', pos, default=int(h*0.7))
+ else:
+ return self.entry('variable_editor_position', pos, default=int(self.blocks_window_position()*0.5))
+
+ def variable_editor_sidebar(self, pos=None):
+ return self.entry('variable_editor_sidebar', pos, default=False)
+
+ def variable_editor_confirm_delete(self, pos=None):
+ return self.entry('variable_editor_confirm_delete', pos, default=True)
+
+ def xterm_missing(self, cmd=None):
+ return self.entry('xterm_missing', cmd, default='INVALID_XTERM_SETTING')
+
+ def screen_shot_background_transparent(self, transparent=None):
+ return self.entry('screen_shot_background_transparent', transparent, default=False)
diff --git a/grc/gui/Connection.py b/grc/gui/Connection.py
deleted file mode 100644
index 50361c19d0..0000000000
--- a/grc/gui/Connection.py
+++ /dev/null
@@ -1,181 +0,0 @@
-"""
-Copyright 2007, 2008, 2009 Free Software Foundation, Inc.
-This file is part of GNU Radio
-
-GNU Radio Companion is free software; you can redistribute it and/or
-modify it under the terms of the GNU General Public License
-as published by the Free Software Foundation; either version 2
-of the License, or (at your option) any later version.
-
-GNU Radio Companion is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-GNU General Public License for more details.
-
-You should have received a copy of the GNU General Public License
-along with this program; if not, write to the Free Software
-Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
-"""
-
-import gtk
-
-import Colors
-import Utils
-from Constants import CONNECTOR_ARROW_BASE, CONNECTOR_ARROW_HEIGHT
-from Element import Element
-
-from ..core.Constants import GR_MESSAGE_DOMAIN
-from ..core.Connection import Connection as _Connection
-
-
-class Connection(Element, _Connection):
- """
- A graphical connection for ports.
- The connection has 2 parts, the arrow and the wire.
- The coloring of the arrow and wire exposes the status of 3 states:
- enabled/disabled, valid/invalid, highlighted/non-highlighted.
- The wire coloring exposes the enabled and highlighted states.
- The arrow coloring exposes the enabled and valid states.
- """
-
- def __init__(self, **kwargs):
- Element.__init__(self)
- _Connection.__init__(self, **kwargs)
- # can't use Colors.CONNECTION_ENABLED_COLOR here, might not be defined (grcc)
- self._bg_color = self._arrow_color = self._color = None
-
- def get_coordinate(self):
- """
- Get the 0,0 coordinate.
- Coordinates are irrelevant in connection.
-
- Returns:
- 0, 0
- """
- return 0, 0
-
- def get_rotation(self):
- """
- Get the 0 degree rotation.
- Rotations are irrelevant in connection.
-
- Returns:
- 0
- """
- return 0
-
- def create_shapes(self):
- """Precalculate relative coordinates."""
- Element.create_shapes(self)
- self._sink_rot = None
- self._source_rot = None
- self._sink_coor = None
- self._source_coor = None
- #get the source coordinate
- try:
- connector_length = self.get_source().get_connector_length()
- except:
- return
- 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()),
- ]
- source_domain = self.get_source().get_domain()
- sink_domain = self.get_sink().get_domain()
- self.line_attributes[0] = 2 if source_domain != sink_domain else 0
- self.line_attributes[1] = gtk.gdk.LINE_DOUBLE_DASH \
- if not source_domain == sink_domain == GR_MESSAGE_DOMAIN \
- else gtk.gdk.LINE_ON_OFF_DASH
- get_domain_color = lambda d: Colors.get_color((
- self.get_parent().get_parent().domains.get(d, {})
- ).get('color') or Colors.DEFAULT_DOMAIN_COLOR_CODE)
- self._color = get_domain_color(source_domain)
- self._bg_color = get_domain_color(sink_domain)
- self._arrow_color = self._bg_color if self.is_valid() else Colors.CONNECTION_ERROR_COLOR
- self._update_after_move()
-
- def _update_after_move(self):
- """Calculate coordinates."""
- self.clear() #FIXME do i want this here?
- #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, gc, window):
- """
- Draw the connection.
-
- Args:
- gc: the graphics context
- 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.create_shapes()
- elif self._sink_coor != sink.get_coordinate() or self._source_coor != source.get_coordinate():
- try:
- self._update_after_move()
- except:
- return
- #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
- mod_color = lambda color: (
- Colors.HIGHLIGHT_COLOR if self.is_highlighted() else
- Colors.CONNECTION_DISABLED_COLOR if not self.get_enabled() else
- color
- )
- Element.draw(self, gc, window, mod_color(self._color), mod_color(self._bg_color))
- # draw arrow on sink port
- try:
- gc.set_foreground(mod_color(self._arrow_color))
- gc.set_line_attributes(0, gtk.gdk.LINE_SOLID, gtk.gdk.CAP_BUTT, gtk.gdk.JOIN_MITER)
- window.draw_polygon(gc, True, self._arrow)
- except:
- pass
diff --git a/grc/gui/Console.py b/grc/gui/Console.py
new file mode 100644
index 0000000000..0ae862493d
--- /dev/null
+++ b/grc/gui/Console.py
@@ -0,0 +1,57 @@
+"""
+Copyright 2008, 2009, 2011 Free Software Foundation, Inc.
+This file is part of GNU Radio
+
+GNU Radio Companion is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation; either version 2
+of the License, or (at your option) any later version.
+
+GNU Radio Companion is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
+"""
+
+from __future__ import absolute_import
+
+import os
+import logging
+
+import gi
+gi.require_version('Gtk', '3.0')
+from gi.repository import Gtk, Gdk, GObject
+
+from .Constants import DEFAULT_CONSOLE_WINDOW_WIDTH
+from .Dialogs import TextDisplay, MessageDialogWrapper
+
+from ..core import Messages
+
+
+log = logging.getLogger(__name__)
+
+
+class Console(Gtk.ScrolledWindow):
+ def __init__(self):
+ Gtk.ScrolledWindow.__init__(self)
+ log.debug("console()")
+ self.app = Gtk.Application.get_default()
+
+ self.text_display = TextDisplay()
+
+ self.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
+ self.add(self.text_display)
+ self.set_size_request(-1, DEFAULT_CONSOLE_WINDOW_WIDTH)
+
+ def add_line(self, line):
+ """
+ Place line at the end of the text buffer, then scroll its window all the way down.
+
+ Args:
+ line: the new text
+ """
+ self.text_display.insert(line)
diff --git a/grc/gui/Constants.py b/grc/gui/Constants.py
index f77221e52d..a3d08cbe38 100644
--- a/grc/gui/Constants.py
+++ b/grc/gui/Constants.py
@@ -17,17 +17,16 @@ along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
"""
-import gtk
+from __future__ import absolute_import
+
+from gi.repository import Gtk, Gdk
from ..core.Constants import *
# default path for the open/save dialogs
DEFAULT_FILE_PATH = os.getcwd() if os.name != 'nt' else os.path.expanduser("~/Documents")
-
-# file extensions
-IMAGE_FILE_EXTENSION = '.png'
-TEXT_FILE_EXTENSION = '.txt'
+FILE_EXTENSION = '.grc'
# name for new/unsaved flow graphs
NEW_FLOGRAPH_TITLE = 'untitled'
@@ -36,7 +35,7 @@ NEW_FLOGRAPH_TITLE = 'untitled'
MIN_WINDOW_WIDTH = 600
MIN_WINDOW_HEIGHT = 400
# dialog constraints
-MIN_DIALOG_WIDTH = 500
+MIN_DIALOG_WIDTH = 600
MIN_DIALOG_HEIGHT = 500
# default sizes
DEFAULT_BLOCKS_WINDOW_WIDTH = 100
@@ -53,7 +52,7 @@ PARAM_FONT = "Sans 7.5"
STATE_CACHE_SIZE = 42
# Shared targets for drag and drop of blocks
-DND_TARGETS = [('STRING', gtk.TARGET_SAME_APP, 0)]
+DND_TARGETS = [('STRING', Gtk.TargetFlags.SAME_APP, 0)]
# label constraint dimensions
LABEL_SEPARATION = 3
@@ -70,6 +69,7 @@ PORT_SEPARATION = 32
PORT_MIN_WIDTH = 20
PORT_LABEL_HIDDEN_WIDTH = 10
+PORT_EXTRA_BUS_HEIGHT = 40
# minimal length of connector
CONNECTOR_EXTENSION_MINIMAL = 11
@@ -78,17 +78,14 @@ CONNECTOR_EXTENSION_MINIMAL = 11
CONNECTOR_EXTENSION_INCREMENT = 11
# connection arrow dimensions
-CONNECTOR_ARROW_BASE = 13
-CONNECTOR_ARROW_HEIGHT = 17
+CONNECTOR_ARROW_BASE = 10
+CONNECTOR_ARROW_HEIGHT = 13
# possible rotations in degrees
POSSIBLE_ROTATIONS = (0, 90, 180, 270)
-# How close can the mouse get to the window border before mouse events are ignored.
-BORDER_PROXIMITY_SENSITIVITY = 50
-
# How close the mouse can get to the edge of the visible window before scrolling is invoked.
-SCROLL_PROXIMITY_SENSITIVITY = 30
+SCROLL_PROXIMITY_SENSITIVITY = 50
# When the window has to be scrolled, move it this distance in the required direction.
SCROLL_DISTANCE = 15
@@ -96,8 +93,18 @@ SCROLL_DISTANCE = 15
# How close the mouse click can be to a line and register a connection select.
LINE_SELECT_SENSITIVITY = 5
-_SCREEN_RESOLUTION = gtk.gdk.screen_get_default().get_resolution()
-DPI_SCALING = _SCREEN_RESOLUTION / 96.0 if _SCREEN_RESOLUTION > 0 else 1.0
+DEFAULT_BLOCK_MODULE_TOOLTIP = """\
+This subtree holds all blocks (from OOT modules) that specify no module name. \
+The module name is the root category enclosed in square brackets.
+
+Please consider contacting OOT module maintainer for any block in here \
+and kindly ask to update their GRC Block Descriptions or Block Tree to include a module name."""
+
+
+# _SCREEN = Gdk.Screen.get_default()
+# _SCREEN_RESOLUTION = _SCREEN.get_resolution() if _SCREEN else -1
+# DPI_SCALING = _SCREEN_RESOLUTION / 96.0 if _SCREEN_RESOLUTION > 0 else 1.0
+DPI_SCALING = 1.0 # todo: figure out the GTK3 way (maybe cairo does this for us
def update_font_size(font_size):
diff --git a/grc/gui/Dialogs.py b/grc/gui/Dialogs.py
index e23f42759a..f58ea78ca2 100644
--- a/grc/gui/Dialogs.py
+++ b/grc/gui/Dialogs.py
@@ -1,33 +1,36 @@
-"""
-Copyright 2008, 2009 Free Software Foundation, Inc.
-This file is part of GNU Radio
-
-GNU Radio Companion is free software; you can redistribute it and/or
-modify it under the terms of the GNU General Public License
-as published by the Free Software Foundation; either version 2
-of the License, or (at your option) any later version.
-
-GNU Radio Companion is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-GNU General Public License for more details.
-
-You should have received a copy of the GNU General Public License
-along with this program; if not, write to the Free Software
-Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
-"""
-
-import gtk
+# Copyright 2008, 2009, 2016 Free Software Foundation, Inc.
+# This file is part of GNU Radio
+#
+# GNU Radio Companion is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# GNU Radio Companion is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
+
+from __future__ import absolute_import
import sys
+import textwrap
from distutils.spawn import find_executable
-from . import Utils, Actions
+from gi.repository import Gtk
+
+from . import Utils, Actions, Constants
from ..core import Messages
-class SimpleTextDisplay(gtk.TextView):
- """A non editable gtk text view."""
+class SimpleTextDisplay(Gtk.TextView):
+ """
+ A non user-editable gtk text view.
+ """
def __init__(self, text=''):
"""
@@ -36,16 +39,18 @@ class SimpleTextDisplay(gtk.TextView):
Args:
text: the text to display (string)
"""
- text_buffer = gtk.TextBuffer()
- text_buffer.set_text(text)
- self.set_text = text_buffer.set_text
- gtk.TextView.__init__(self, text_buffer)
+ Gtk.TextView.__init__(self)
+ self.set_text = self.get_buffer().set_text
+ self.set_text(text)
self.set_editable(False)
self.set_cursor_visible(False)
- self.set_wrap_mode(gtk.WRAP_WORD_CHAR)
+ self.set_wrap_mode(Gtk.WrapMode.WORD_CHAR)
class TextDisplay(SimpleTextDisplay):
+ """
+ A non user-editable scrollable text view with popup menu.
+ """
def __init__(self, text=''):
"""
@@ -59,230 +64,314 @@ class TextDisplay(SimpleTextDisplay):
self.connect("populate-popup", self.populate_popup)
def insert(self, line):
- # make backspaces work
+ """
+ Append text after handling backspaces and auto-scroll.
+
+ Args:
+ line: the text to append (string)
+ """
line = self._consume_backspaces(line)
- # add the remaining text to buffer
self.get_buffer().insert(self.get_buffer().get_end_iter(), line)
- # Automatically scroll on insert
self.scroll_to_end()
def _consume_backspaces(self, line):
- """removes text from the buffer if line starts with \b*"""
- if not line: return
+ """
+ Removes text from the buffer if line starts with '\b'
+
+ Args:
+ line: a string which may contain backspaces
+
+ Returns:
+ The string that remains from 'line' with leading '\b's removed.
+ """
+ if not line:
+ return
+
# for each \b delete one char from the buffer
back_count = 0
start_iter = self.get_buffer().get_end_iter()
while len(line) > back_count and line[back_count] == '\b':
# stop at the beginning of a line
- if not start_iter.starts_line(): start_iter.backward_char()
+ if not start_iter.starts_line():
+ start_iter.backward_char()
back_count += 1
- # remove chars
+ # remove chars from buffer
self.get_buffer().delete(start_iter, self.get_buffer().get_end_iter())
- # return remaining text
return line[back_count:]
def scroll_to_end(self):
+ """ Update view's scroll position. """
if self.scroll_lock:
- buffer = self.get_buffer()
- buffer.move_mark(buffer.get_insert(), buffer.get_end_iter())
- self.scroll_to_mark(buffer.get_insert(), 0.0)
+ buf = self.get_buffer()
+ mark = buf.get_insert()
+ buf.move_mark(mark, buf.get_end_iter())
+ self.scroll_mark_onscreen(mark)
def clear(self):
- buffer = self.get_buffer()
- buffer.delete(buffer.get_start_iter(), buffer.get_end_iter())
+ """ Clear all text from buffer. """
+ buf = self.get_buffer()
+ buf.delete(buf.get_start_iter(), buf.get_end_iter())
def save(self, file_path):
- console_file = open(file_path, 'w')
- buffer = self.get_buffer()
- console_file.write(buffer.get_text(buffer.get_start_iter(), buffer.get_end_iter(), True))
- console_file.close()
+ """
+ Save context of buffer to the given file.
- # Callback functions to handle the scrolling lock and clear context menus options
- # Action functions are set by the ActionHandler's init function
+ Args:
+ file_path: location to save buffer contents
+ """
+ with open(file_path, 'w') as logfile:
+ buf = self.get_buffer()
+ logfile.write(buf.get_text(buf.get_start_iter(),
+ buf.get_end_iter(), True))
+
+ # Action functions are set by the Application's init function
def clear_cb(self, menu_item, web_view):
+ """ Callback function to clear the text buffer """
Actions.CLEAR_CONSOLE()
def scroll_back_cb(self, menu_item, web_view):
+ """ Callback function to toggle scroll lock """
Actions.TOGGLE_SCROLL_LOCK()
def save_cb(self, menu_item, web_view):
+ """ Callback function to save the buffer """
Actions.SAVE_CONSOLE()
def populate_popup(self, view, menu):
"""Create a popup menu for the scroll lock and clear functions"""
- menu.append(gtk.SeparatorMenuItem())
+ menu.append(Gtk.SeparatorMenuItem())
- lock = gtk.CheckMenuItem("Scroll Lock")
+ lock = Gtk.CheckMenuItem("Scroll Lock")
menu.append(lock)
lock.set_active(self.scroll_lock)
lock.connect('activate', self.scroll_back_cb, view)
- save = gtk.ImageMenuItem(gtk.STOCK_SAVE)
+ save = Gtk.ImageMenuItem(Gtk.STOCK_SAVE)
menu.append(save)
save.connect('activate', self.save_cb, view)
- clear = gtk.ImageMenuItem(gtk.STOCK_CLEAR)
+ clear = Gtk.ImageMenuItem(Gtk.STOCK_CLEAR)
menu.append(clear)
clear.connect('activate', self.clear_cb, view)
menu.show_all()
return False
-def MessageDialogHelper(type, buttons, parent, title=None, markup=None, default_response=None, extra_buttons=None):
- """
- Create a modal message dialog and run it.
-
- Required args:
- type: the type of message: gtk.MESSAGE_INFO, gtk.MESSAGE_WARNING, gtk.MESSAGE_QUESTION or gtk.MESSAGE_ERROR
- buttons: the predefined set of buttons to use:
- gtk.BUTTONS_NONE, gtk.BUTTONS_OK, gtk.BUTTONS_CLOSE, gtk.BUTTONS_CANCEL, gtk.BUTTONS_YES_NO,
- gtk.BUTTONS_OK_CANCEL
- parent: gtk parent window (which will be blocked explicitly by this modal dialog)
-
- Optional args:
- title: the title of the window (string)
- markup: the message text with pango markup
- default_response: if set, determines which button is highlighted by default
- extra_buttons: a tuple containing pairs of values; each value is the button's text and the button's return value
-
- Returns:
- the gtk response from run()
- """
- message_dialog = gtk.MessageDialog(parent, gtk.DIALOG_MODAL, type, buttons)
- if title: message_dialog.set_title(title)
- if markup: message_dialog.set_markup(markup)
- if extra_buttons: message_dialog.add_buttons(*extra_buttons)
- if default_response: message_dialog.set_default_response(default_response)
- response = message_dialog.run()
- message_dialog.destroy()
- return response
-
-
-ERRORS_MARKUP_TMPL="""\
-#for $i, $err_msg in enumerate($errors)
-<b>Error $i:</b>
-$encode($err_msg.replace('\t', ' '))
-
-#end for"""
-
-
-def ErrorsDialog(flowgraph, parent):
- MessageDialogHelper(
- type=gtk.MESSAGE_ERROR,
- buttons=gtk.BUTTONS_CLOSE,
- parent=parent,
- title='Flow Graph Errors',
- markup=Utils.parse_template(ERRORS_MARKUP_TMPL, errors=flowgraph.get_error_messages()),
- )
+class MessageDialogWrapper(Gtk.MessageDialog):
+ """ Run a message dialog. """
+ def __init__(self, parent, message_type, buttons, title=None, markup=None,
+ default_response=None, extra_buttons=None):
+ """
+ Create a modal message dialog.
-class AboutDialog(gtk.AboutDialog):
- """A cute little about dialog."""
-
- def __init__(self, config, parent):
- """AboutDialog constructor."""
- gtk.AboutDialog.__init__(self)
- self.set_transient_for(parent)
- self.set_name(config.name)
- self.set_version(config.version)
- self.set_license(config.license)
- self.set_copyright(config.license.splitlines()[0])
- self.set_website(config.website)
- self.run()
- self.destroy()
-
-
-def HelpDialog(parent):
- MessageDialogHelper(
- type=gtk.MESSAGE_INFO,
- buttons=gtk.BUTTONS_CLOSE,
- parent=parent,
- title='Help',
- markup="""\
-<b>Usage Tips</b>
-
-<u>Add block</u>: drag and drop or double click a block in the block selection window.
-<u>Rotate block</u>: Select a block, press left/right on the keyboard.
-<u>Change type</u>: Select a block, press up/down on the keyboard.
-<u>Edit parameters</u>: double click on a block in the flow graph.
-<u>Make connection</u>: click on the source port of one block, then click on the sink port of another block.
-<u>Remove connection</u>: select the connection and press delete, or drag the connection.
-
-* See the menu for other keyboard shortcuts."""
- )
+ Args:
+ message_type: the type of message may be one of:
+ Gtk.MessageType.INFO
+ Gtk.MessageType.WARNING
+ Gtk.MessageType.QUESTION or Gtk.MessageType.ERROR
+ buttons: the predefined set of buttons to use:
+ Gtk.ButtonsType.NONE
+ Gtk.ButtonsType.OK
+ Gtk.ButtonsType.CLOSE
+ Gtk.ButtonsType.CANCEL
+ Gtk.ButtonsType.YES_NO
+ Gtk.ButtonsType.OK_CANCEL
+ title: the title of the window (string)
+ markup: the message text with pango markup
+ default_response: if set, determines which button is highlighted by default
+ extra_buttons: a tuple containing pairs of values:
+ each value is the button's text and the button's return value
-COLORS_DIALOG_MARKUP_TMPL = """\
-<b>Color Mapping</b>
-
-#if $colors
- #set $max_len = max([len(color[0]) for color in $colors]) + 10
- #for $title, $color_spec in $colors
-<span background="$color_spec"><tt>$($encode($title).center($max_len))</tt></span>
- #end for
-#end if
-"""
-
-
-def TypesDialog(platform, parent):
- MessageDialogHelper(
- type=gtk.MESSAGE_INFO,
- buttons=gtk.BUTTONS_CLOSE,
- parent=parent,
- title='Types',
- markup=Utils.parse_template(COLORS_DIALOG_MARKUP_TMPL,
- colors=platform.get_colors())
+ """
+ Gtk.MessageDialog.__init__(
+ self, transient_for=parent, modal=True, destroy_with_parent=True,
+ message_type=message_type, buttons=buttons
+ )
+ if title:
+ self.set_title(title)
+ if markup:
+ self.set_markup(markup)
+ if extra_buttons:
+ self.add_buttons(*extra_buttons)
+ if default_response:
+ self.set_default_response(default_response)
+
+ def run_and_destroy(self):
+ response = self.run()
+ self.hide()
+ return response
+
+
+class ErrorsDialog(Gtk.Dialog):
+ """ Display flowgraph errors. """
+
+ def __init__(self, parent, flowgraph):
+ """Create a listview of errors"""
+ Gtk.Dialog.__init__(
+ self,
+ title='Errors and Warnings',
+ transient_for=parent,
+ modal=True,
+ destroy_with_parent=True,
+ )
+ self.add_buttons(Gtk.STOCK_OK, Gtk.ResponseType.ACCEPT)
+ self.set_size_request(750, Constants.MIN_DIALOG_HEIGHT)
+ self.set_border_width(10)
+
+ self.store = Gtk.ListStore(str, str, str)
+ self.update(flowgraph)
+
+ self.treeview = Gtk.TreeView(model=self.store)
+ for i, column_title in enumerate(["Block", "Aspect", "Message"]):
+ renderer = Gtk.CellRendererText()
+ column = Gtk.TreeViewColumn(column_title, renderer, text=i)
+ column.set_sort_column_id(i) # liststore id matches treeview id
+ column.set_resizable(True)
+ self.treeview.append_column(column)
+
+ self.scrollable = Gtk.ScrolledWindow()
+ self.scrollable.set_vexpand(True)
+ self.scrollable.add(self.treeview)
+
+ self.vbox.pack_start(self.scrollable, True, True, 0)
+ self.show_all()
+
+ def update(self, flowgraph):
+ self.store.clear()
+ for element, message in flowgraph.iter_error_messages():
+ if element.is_block:
+ src, aspect = element.name, ''
+ elif element.is_connection:
+ src = element.source_block.name
+ aspect = "Connection to '{}'".format(element.sink_block.name)
+ elif element.is_port:
+ src = element.parent_block.name
+ aspect = "{} '{}'".format('Sink' if element.is_sink else 'Source', element.name)
+ elif element.is_param:
+ src = element.parent_block.name
+ aspect = "Param '{}'".format(element.name)
+ else:
+ src = aspect = ''
+ self.store.append([src, aspect, message])
+
+ def run_and_destroy(self):
+ response = self.run()
+ self.hide()
+ return response
+
+
+def show_about(parent, config):
+ ad = Gtk.AboutDialog(transient_for=parent)
+ ad.set_program_name(config.name)
+ ad.set_name('')
+ ad.set_license(config.license)
+
+ py_version = sys.version.split()[0]
+ ad.set_version("{} (Python {})".format(config.version, py_version))
+
+ try:
+ ad.set_logo(Gtk.IconTheme().load_icon('gnuradio-grc', 64, 0))
+ except:
+ pass
+
+ #ad.set_comments("")
+ ad.set_copyright(config.license.splitlines()[0])
+ ad.set_website(config.website)
+
+ ad.connect("response", lambda action, param: action.hide())
+ ad.show()
+
+
+def show_help(parent):
+ """ Display basic usage tips. """
+ markup = textwrap.dedent("""\
+ <b>Usage Tips</b>
+ \n\
+ <u>Add block</u>: drag and drop or double click a block in the block selection window.
+ <u>Rotate block</u>: Select a block, press left/right on the keyboard.
+ <u>Change type</u>: Select a block, press up/down on the keyboard.
+ <u>Edit parameters</u>: double click on a block in the flow graph.
+ <u>Make connection</u>: click on the source port of one block, then click on the sink port of another block.
+ <u>Remove connection</u>: select the connection and press delete, or drag the connection.
+ \n\
+ * See the menu for other keyboard shortcuts.\
+ """)
+
+ MessageDialogWrapper(
+ parent, Gtk.MessageType.INFO, Gtk.ButtonsType.CLOSE, title='Help', markup=markup
+ ).run_and_destroy()
+
+
+def show_types(parent):
+ """ Display information about standard data types. """
+ colors = [(name, color) for name, key, sizeof, color in Constants.CORE_TYPES]
+ max_len = 10 + max(len(name) for name, code in colors)
+
+ message = '\n'.join(
+ '<span background="{color}"><tt>{name}</tt></span>'
+ ''.format(color=color, name=Utils.encode(name).center(max_len))
+ for name, color in colors
)
+ MessageDialogWrapper(
+ parent, Gtk.MessageType.INFO, Gtk.ButtonsType.CLOSE, title='Types - Color Mapping', markup=message
+ ).run_and_destroy()
-def MissingXTermDialog(xterm, parent):
- MessageDialogHelper(
- type=gtk.MESSAGE_WARNING,
- buttons=gtk.BUTTONS_OK,
- parent=parent,
- title='Warning: missing xterm executable',
- markup=("The xterm executable {0!r} is missing.\n\n"
- "You can change this setting in your gnuradio.conf, in "
- "section [grc], 'xterm_executable'.\n"
- "\n"
- "(This message is shown only once)").format(xterm)
- )
+
+def show_missing_xterm(parent, xterm):
+ markup = textwrap.dedent("""\
+ The xterm executable {0!r} is missing.
+ You can change this setting in your gnurado.conf, in section [grc], 'xterm_executable'.
+ \n\
+ (This message is shown only once)\
+ """).format(xterm)
+
+ MessageDialogWrapper(
+ parent, message_type=Gtk.MessageType.WARNING, buttons=Gtk.ButtonsType.OK,
+ title='Warning: missing xterm executable', markup=markup
+ ).run_and_destroy()
-def ChooseEditorDialog(config, parent):
- # Give the option to either choose an editor or use the default
- # Always return true/false so the caller knows it was successful
+def choose_editor(parent, config):
+ """
+ Give the option to either choose an editor or use the default.
+ """
+ if config.editor and find_executable(config.editor):
+ return config.editor
+
buttons = (
- 'Choose Editor', gtk.RESPONSE_YES,
- 'Use Default', gtk.RESPONSE_NO,
- gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL
- )
- response = MessageDialogHelper(
- gtk.MESSAGE_QUESTION, gtk.BUTTONS_NONE, parent,
- 'Choose Editor', 'Would you like to choose the editor to use?', gtk.RESPONSE_YES, buttons
+ 'Choose Editor', Gtk.ResponseType.YES,
+ 'Use Default', Gtk.ResponseType.NO,
+ Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL
)
+ response = MessageDialogWrapper(
+ parent, message_type=Gtk.MessageType.QUESTION, buttons=Gtk.ButtonsType.NONE,
+ title='Choose Editor', markup='Would you like to choose the editor to use?',
+ default_response=Gtk.ResponseType.YES, extra_buttons=buttons
+ ).run_and_destroy()
- # Handle the inital default/choose/cancel response
+ # Handle the initial default/choose/cancel response
# User wants to choose the editor to use
- if response == gtk.RESPONSE_YES:
- file_dialog = gtk.FileChooserDialog(
- 'Select an Editor...', parent,
- gtk.FILE_CHOOSER_ACTION_OPEN,
- ('gtk-cancel', gtk.RESPONSE_CANCEL, 'gtk-open', gtk.RESPONSE_OK)
+ editor = ''
+ if response == Gtk.ResponseType.YES:
+ file_dialog = Gtk.FileChooserDialog(
+ 'Select an Editor...', None,
+ Gtk.FileChooserAction.OPEN,
+ ('gtk-cancel', Gtk.ResponseType.CANCEL, 'gtk-open', Gtk.ResponseType.OK),
+ transient_for=parent
)
file_dialog.set_select_multiple(False)
file_dialog.set_local_only(True)
file_dialog.set_current_folder('/usr/bin')
try:
- if file_dialog.run() == gtk.RESPONSE_OK:
- config.editor = file_path = file_dialog.get_filename()
- file_dialog.destroy()
- return file_path
+ if file_dialog.run() == Gtk.ResponseType.OK:
+ editor = file_dialog.get_filename()
finally:
- file_dialog.destroy()
+ file_dialog.hide()
# Go with the default editor
- elif response == gtk.RESPONSE_NO:
- # Determine the platform
+ elif response == Gtk.ResponseType.NO:
try:
process = None
if sys.platform.startswith('linux'):
@@ -292,13 +381,10 @@ def ChooseEditorDialog(config, parent):
if process is None:
raise ValueError("Can't find default editor executable")
# Save
- config.editor = process
- return process
+ editor = config.editor = process
except Exception:
Messages.send('>>> Unable to load the default editor. Please choose an editor.\n')
- # Just reset of the constant and force the user to select an editor the next time
- config.editor = ''
- return
- Messages.send('>>> No editor selected.\n')
- return
+ if editor == '':
+ Messages.send('>>> No editor selected.\n')
+ return editor
diff --git a/grc/gui/DrawingArea.py b/grc/gui/DrawingArea.py
index 6a1df27a8c..648f1df849 100644
--- a/grc/gui/DrawingArea.py
+++ b/grc/gui/DrawingArea.py
@@ -17,15 +17,16 @@ 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
+from __future__ import absolute_import
-from Constants import MIN_WINDOW_WIDTH, MIN_WINDOW_HEIGHT, DND_TARGETS
-import Colors
+from gi.repository import Gtk, Gdk
+from .canvas.colors import FLOWGRAPH_BACKGROUND_COLOR
+from . import Constants
+from . import Actions
-class DrawingArea(gtk.DrawingArea):
+
+class DrawingArea(Gtk.DrawingArea):
"""
DrawingArea is the gtk pixel map that graphical elements may draw themselves on.
The drawing area also responds to mouse and key events.
@@ -39,137 +40,225 @@ class DrawingArea(gtk.DrawingArea):
Args:
main_window: the main_window containing all flow graphs
"""
+ Gtk.DrawingArea.__init__(self)
+
+ self._flow_graph = flow_graph
+ self.set_property('can_focus', True)
+
+ self.zoom_factor = 1.0
+ self._update_after_zoom = False
self.ctrl_mask = False
self.mod1_mask = False
- self._flow_graph = flow_graph
- gtk.DrawingArea.__init__(self)
- self.set_size_request(MIN_WINDOW_WIDTH, MIN_WINDOW_HEIGHT)
+ self.button_state = [False] * 10
+
+ # self.set_size_request(MIN_WINDOW_WIDTH, MIN_WINDOW_HEIGHT)
self.connect('realize', self._handle_window_realize)
- self.connect('configure-event', self._handle_window_configure)
- self.connect('expose-event', self._handle_window_expose)
+ self.connect('draw', self.draw)
self.connect('motion-notify-event', self._handle_mouse_motion)
self.connect('button-press-event', self._handle_mouse_button_press)
self.connect('button-release-event', self._handle_mouse_button_release)
self.connect('scroll-event', self._handle_mouse_scroll)
self.add_events(
- gtk.gdk.BUTTON_PRESS_MASK | \
- gtk.gdk.POINTER_MOTION_MASK | \
- gtk.gdk.BUTTON_RELEASE_MASK | \
- gtk.gdk.LEAVE_NOTIFY_MASK | \
- gtk.gdk.ENTER_NOTIFY_MASK | \
- gtk.gdk.FOCUS_CHANGE_MASK
+ Gdk.EventMask.BUTTON_PRESS_MASK |
+ Gdk.EventMask.POINTER_MOTION_MASK |
+ Gdk.EventMask.BUTTON_RELEASE_MASK |
+ Gdk.EventMask.SCROLL_MASK |
+ Gdk.EventMask.LEAVE_NOTIFY_MASK |
+ Gdk.EventMask.ENTER_NOTIFY_MASK
+ # Gdk.EventMask.FOCUS_CHANGE_MASK
)
- #setup drag and drop
- self.drag_dest_set(gtk.DEST_DEFAULT_ALL, DND_TARGETS, gtk.gdk.ACTION_COPY)
+
+ # This may not be the correct place to be handling the user events
+ # Should this be in the page instead?
+ # Or should more of the page functionality move here?
+ self.connect('key_press_event', self._handle_key_press)
+
+ # setup drag and drop
+ self.drag_dest_set(Gtk.DestDefaults.ALL, [], Gdk.DragAction.COPY)
self.connect('drag-data-received', self._handle_drag_data_received)
- #setup the focus flag
+ self.drag_dest_set_target_list(None)
+ self.drag_dest_add_text_targets()
+
+ # setup the focus flag
self._focus_flag = False
self.get_focus_flag = lambda: self._focus_flag
- def _handle_notify_event(widget, event, focus_flag): self._focus_flag = focus_flag
+
+ def _handle_notify_event(widget, event, focus_flag):
+ self._focus_flag = focus_flag
+
self.connect('leave-notify-event', _handle_notify_event, False)
self.connect('enter-notify-event', _handle_notify_event, True)
- self.set_flags(gtk.CAN_FOCUS) # self.set_can_focus(True)
- self.connect('focus-out-event', self._handle_focus_lost_event)
-
- def new_pixmap(self, width, height):
- return gtk.gdk.Pixmap(self.window, width, height, -1)
+ # todo: fix
+# self.set_flags(Gtk.CAN_FOCUS) # self.set_can_focus(True)
+# self.connect('focus-out-event', self._handle_focus_lost_event)
- def get_screenshot(self, transparent_bg=False):
- pixmap = self._pixmap
- W, H = pixmap.get_size()
- pixbuf = gtk.gdk.Pixbuf(gtk.gdk.COLORSPACE_RGB, 0, 8, W, H)
- pixbuf.fill(0xFF + Colors.FLOWGRAPH_BACKGROUND_COLOR.pixel << 8)
- pixbuf.get_from_drawable(pixmap, pixmap.get_colormap(), 0, 0, 0, 0, W-1, H-1)
- if transparent_bg:
- bgc = Colors.FLOWGRAPH_BACKGROUND_COLOR
- pixbuf = pixbuf.add_alpha(True, bgc.red, bgc.green, bgc.blue)
- return pixbuf
+ # Setup a map of the accelerator keys to the action to trigger
+ self.accels = {
+ Gtk.accelerator_parse('d'): Actions.BLOCK_DISABLE,
+ Gtk.accelerator_parse('e'): Actions.BLOCK_ENABLE,
+ Gtk.accelerator_parse('b'): Actions.BLOCK_BYPASS,
+ Gtk.accelerator_parse('c'): Actions.BLOCK_CREATE_HIER,
+ Gtk.accelerator_parse('Up'): Actions.BLOCK_DEC_TYPE,
+ Gtk.accelerator_parse('Down'): Actions.BLOCK_INC_TYPE,
+ Gtk.accelerator_parse('Left'): Actions.BLOCK_ROTATE_CCW,
+ Gtk.accelerator_parse('Right'): Actions.BLOCK_ROTATE_CW,
+ Gtk.accelerator_parse('minus'): Actions.PORT_CONTROLLER_DEC,
+ Gtk.accelerator_parse('plus'): Actions.PORT_CONTROLLER_INC,
+ Gtk.accelerator_parse('Add'): Actions.PORT_CONTROLLER_INC,
+ Gtk.accelerator_parse('Subtract'): Actions.PORT_CONTROLLER_DEC,
+ Gtk.accelerator_parse('Return'): Actions.BLOCK_PARAM_MODIFY,
+ Gtk.accelerator_parse('<Shift>t'): Actions.BLOCK_VALIGN_TOP,
+ Gtk.accelerator_parse('<Shift>m'): Actions.BLOCK_VALIGN_MIDDLE,
+ Gtk.accelerator_parse('<Shift>b'): Actions.BLOCK_VALIGN_BOTTOM,
+ Gtk.accelerator_parse('<Shift>l'): Actions.BLOCK_HALIGN_LEFT,
+ Gtk.accelerator_parse('<Shift>c'): Actions.BLOCK_HALIGN_CENTER,
+ Gtk.accelerator_parse('<Shift>r'): Actions.BLOCK_HALIGN_RIGHT,
+ }
##########################################################################
- ## Handlers
+ # Handlers
##########################################################################
def _handle_drag_data_received(self, widget, drag_context, x, y, selection_data, info, time):
"""
Handle a drag and drop by adding a block at the given coordinate.
"""
- self._flow_graph.add_new_block(selection_data.data, (x, y))
+ coords = x / self.zoom_factor, y / self.zoom_factor
+ self._flow_graph.add_new_block(selection_data.get_text(), coords)
def _handle_mouse_scroll(self, widget, event):
- if event.state & gtk.gdk.SHIFT_MASK:
- if event.direction == gtk.gdk.SCROLL_UP:
- event.direction = gtk.gdk.SCROLL_LEFT
- else:
- event.direction = gtk.gdk.SCROLL_RIGHT
+ if event.get_state() & Gdk.ModifierType.CONTROL_MASK:
+ change = 1.2 if event.direction == Gdk.ScrollDirection.UP else 1/1.2
+ zoom_factor = min(max(self.zoom_factor * change, 0.1), 5.0)
+
+ if zoom_factor != self.zoom_factor:
+ self.zoom_factor = zoom_factor
+ self._update_after_zoom = True
+ self.queue_draw()
+ return True
+
+ return False
def _handle_mouse_button_press(self, widget, event):
"""
Forward button click information to the flow graph.
"""
self.grab_focus()
- self.ctrl_mask = event.state & gtk.gdk.CONTROL_MASK
- self.mod1_mask = event.state & gtk.gdk.MOD1_MASK
- if event.button == 1: self._flow_graph.handle_mouse_selector_press(
- double_click=(event.type == gtk.gdk._2BUTTON_PRESS),
- coordinate=(event.x, event.y),
- )
- if event.button == 3: self._flow_graph.handle_mouse_context_press(
- coordinate=(event.x, event.y),
- event=event,
- )
+ self.ctrl_mask = event.get_state() & Gdk.ModifierType.CONTROL_MASK
+ self.mod1_mask = event.get_state() & Gdk.ModifierType.MOD1_MASK
+ self.button_state[event.button] = True
+
+ if event.button == 1:
+ double_click = (event.type == Gdk.EventType._2BUTTON_PRESS)
+ self.button_state[1] = not double_click
+ self._flow_graph.handle_mouse_selector_press(
+ double_click=double_click,
+ coordinate=self._translate_event_coords(event),
+ )
+ elif event.button == 3:
+ self._flow_graph.handle_mouse_context_press(
+ coordinate=self._translate_event_coords(event),
+ event=event,
+ )
def _handle_mouse_button_release(self, widget, event):
"""
Forward button release information to the flow graph.
"""
- self.ctrl_mask = event.state & gtk.gdk.CONTROL_MASK
- self.mod1_mask = event.state & gtk.gdk.MOD1_MASK
- if event.button == 1: self._flow_graph.handle_mouse_selector_release(
- coordinate=(event.x, event.y),
- )
+ self.ctrl_mask = event.get_state() & Gdk.ModifierType.CONTROL_MASK
+ self.mod1_mask = event.get_state() & Gdk.ModifierType.MOD1_MASK
+ self.button_state[event.button] = False
+ if event.button == 1:
+ self._flow_graph.handle_mouse_selector_release(
+ coordinate=self._translate_event_coords(event),
+ )
def _handle_mouse_motion(self, widget, event):
"""
Forward mouse motion information to the flow graph.
"""
- self.ctrl_mask = event.state & gtk.gdk.CONTROL_MASK
- self.mod1_mask = event.state & gtk.gdk.MOD1_MASK
+ self.ctrl_mask = event.get_state() & Gdk.ModifierType.CONTROL_MASK
+ self.mod1_mask = event.get_state() & Gdk.ModifierType.MOD1_MASK
+
+ if self.button_state[1]:
+ self._auto_scroll(event)
+
self._flow_graph.handle_mouse_motion(
- coordinate=(event.x, event.y),
+ coordinate=self._translate_event_coords(event),
)
+ def _handle_key_press(self, widget, event):
+ """
+ Handle specific keypresses when the drawing area has focus that
+ triggers actions by the user.
+ """
+ key = event.keyval
+ mod = event.state
+
+ try:
+ action = self.accels[(key, mod)]
+ action()
+ return True
+ except KeyError:
+ return False
+
+ def _update_size(self):
+ w, h = self._flow_graph.get_extents()[2:]
+ self.set_size_request(w * self.zoom_factor + 100, h * self.zoom_factor + 100)
+
+ def _auto_scroll(self, event):
+ x, y = event.x, event.y
+ scrollbox = self.get_parent().get_parent()
+
+ self._update_size()
+
+ def scroll(pos, adj):
+ """scroll if we moved near the border"""
+ adj_val = adj.get_value()
+ adj_len = adj.get_page_size()
+ if pos - adj_val > adj_len - Constants.SCROLL_PROXIMITY_SENSITIVITY:
+ adj.set_value(adj_val + Constants.SCROLL_DISTANCE)
+ adj.emit('changed')
+ elif pos - adj_val < Constants.SCROLL_PROXIMITY_SENSITIVITY:
+ adj.set_value(adj_val - Constants.SCROLL_DISTANCE)
+ adj.emit('changed')
+
+ scroll(x, scrollbox.get_hadjustment())
+ scroll(y, scrollbox.get_vadjustment())
+
def _handle_window_realize(self, widget):
"""
Called when the window is realized.
Update the flowgraph, which calls new pixmap.
"""
self._flow_graph.update()
+ self._update_size()
- def _handle_window_configure(self, widget, event):
- """
- Called when the window is resized.
- Create a new pixmap for background buffer.
- """
- self._pixmap = self.new_pixmap(*self.get_size_request())
+ def draw(self, widget, cr):
+ width = widget.get_allocated_width()
+ height = widget.get_allocated_height()
- def _handle_window_expose(self, widget, event):
- """
- Called when window is exposed, or queue_draw is called.
- Double buffering: draw to pixmap, then draw pixmap to window.
- """
- gc = self.window.new_gc()
- self._flow_graph.draw(gc, self._pixmap)
- self.window.draw_drawable(gc, self._pixmap, 0, 0, 0, 0, -1, -1)
- # draw a light grey line on the bottom and right end of the canvas.
- # this is useful when the theme uses the same panel bg color as the canvas
- W, H = self._pixmap.get_size()
- gc.set_foreground(Colors.FLOWGRAPH_EDGE_COLOR)
- self.window.draw_line(gc, 0, H-1, W, H-1)
- self.window.draw_line(gc, W-1, 0, W-1, H)
+ cr.set_source_rgba(*FLOWGRAPH_BACKGROUND_COLOR)
+ cr.rectangle(0, 0, width, height)
+ cr.fill()
+
+ cr.scale(self.zoom_factor, self.zoom_factor)
+ cr.set_line_width(2.0 / self.zoom_factor)
+
+ if self._update_after_zoom:
+ self._flow_graph.create_labels(cr)
+ self._flow_graph.create_shapes()
+ self._update_size()
+ self._update_after_zoom = False
+
+ self._flow_graph.draw(cr)
+
+ def _translate_event_coords(self, event):
+ return event.x / self.zoom_factor, event.y / self.zoom_factor
def _handle_focus_lost_event(self, widget, event):
# don't clear selection while context menu is active
- if not self._flow_graph.get_context_menu().flags() & gtk.VISIBLE:
+ if not self._flow_graph.context_menu.get_take_focus():
self._flow_graph.unselect()
self._flow_graph.update_selected()
self._flow_graph.queue_draw()
diff --git a/grc/gui/Element.py b/grc/gui/Element.py
deleted file mode 100644
index 9385424772..0000000000
--- a/grc/gui/Element.py
+++ /dev/null
@@ -1,278 +0,0 @@
-"""
-Copyright 2007, 2008, 2009 Free Software Foundation, Inc.
-This file is part of GNU Radio
-
-GNU Radio Companion is free software; you can redistribute it and/or
-modify it under the terms of the GNU General Public License
-as published by the Free Software Foundation; either version 2
-of the License, or (at your option) any later version.
-
-GNU Radio Companion is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-GNU General Public License for more details.
-
-You should have received a copy of the GNU General Public License
-along with this program; if not, write to the Free Software
-Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
-"""
-
-from Constants import LINE_SELECT_SENSITIVITY
-from Constants import POSSIBLE_ROTATIONS
-
-import gtk
-
-
-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):
- """
- 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)
- self.line_attributes = [
- 0, gtk.gdk.LINE_SOLID, gtk.gdk.CAP_BUTT, gtk.gdk.JOIN_MITER
- ]
-
- def is_horizontal(self, rotation=None):
- """
- Is this element horizontal?
- If rotation is None, use this element's rotation.
-
- Args:
- rotation: the optional rotation
-
- Returns:
- true if rotation is horizontal
- """
- rotation = rotation or self.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.
-
- Args:
- rotation: the optional rotation
-
- Returns:
- true if rotation is vertical
- """
- rotation = rotation or self.get_rotation()
- return rotation in (90, 270)
-
- def create_labels(self):
- """
- Create labels (if applicable) and call on all children.
- Call this base method before creating labels in the element.
- """
- for child in self.get_children():child.create_labels()
-
- def create_shapes(self):
- """
- Create shapes (if applicable) and call on all children.
- Call this base method before creating shapes in the element.
- """
- self.clear()
- for child in self.get_children(): child.create_shapes()
-
- def draw(self, gc, window, border_color, bg_color):
- """
- Draw in the given window.
-
- Args:
- gc: the graphics context
- window: the gtk window to draw on
- border_color: the color for lines and rectangle borders
- bg_color: the color for the inside of the rectangle
- """
- X, Y = self.get_coordinate()
- gc.set_line_attributes(*self.line_attributes)
- for (rX, rY), (W, H) in self._areas_list:
- aX = X + rX
- aY = Y + rY
- gc.set_foreground(bg_color)
- window.draw_rectangle(gc, True, aX, aY, W, H)
- gc.set_foreground(border_color)
- window.draw_rectangle(gc, False, aX, aY, W, H)
- for (x1, y1), (x2, y2) in self._lines_list:
- gc.set_foreground(border_color)
- gc.set_background(bg_color)
- window.draw_line(gc, X+x1, Y+y1, X+x2, Y+y2)
-
- def rotate(self, rotation):
- """
- Rotate all of the areas by 90 degrees.
-
- Args:
- rotation: multiple of 90 degrees
- """
- self.set_rotation((self.get_rotation() + rotation)%360)
-
- def clear(self):
- """Empty the lines and areas."""
- self._areas_list = list()
- self._lines_list = list()
-
- def set_coordinate(self, coor):
- """
- Set the reference coordinate.
-
- Args:
- coor: the coordinate tuple (x,y)
- """
- self.coor = coor
-
- # def get_parent(self):
- # """
- # Get the parent of this element.
- #
- # Returns:
- # the parent
- # """
- # return self.parent
-
- def set_highlighted(self, highlighted):
- """
- Set the highlight status.
-
- Args:
- highlighted: true to enable highlighting
- """
- self.highlighted = highlighted
-
- def is_highlighted(self):
- """
- Get the highlight status.
-
- Returns:
- true if highlighted
- """
- return self.highlighted
-
- def get_coordinate(self):
- """Get the coordinate.
-
- Returns:
- 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.
-
- Args:
- 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):
- """
- 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.
-
- Args:
- rel_coor: (x,y) offset from this element's coordinate
- area: (width,height) tuple
- """
- self._areas_list.append((rel_coor, area))
-
- def add_line(self, rel_coor1, rel_coor2):
- """
- 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.
-
- Args:
- rel_coor1: relative (x1,y1) tuple
- rel_coor2: relative (x2,y2) tuple
- """
- self._lines_list.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?
-
- Args:
- coor: the selection coordinate, tuple x, y
- coor_m: an additional selection coordinate.
-
- Returns:
- self if one of the areas/lines encompasses coor, else None.
- """
- #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_list:
- 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_list:
- 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_list:
- 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_list:
- if x1 == x2: x1, x2 = x1-LINE_SELECT_SENSITIVITY, x2+LINE_SELECT_SENSITIVITY
- if y1 == y2: y1, y2 = y1-LINE_SELECT_SENSITIVITY, y2+LINE_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.
-
- Returns:
- the rotation
- """
- return self.rotation
-
- def set_rotation(self, rotation):
- """
- Set the rotation in degrees.
-
- Args:
- 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 mouse_over(self):
- pass
-
- def mouse_out(self):
- pass
diff --git a/grc/gui/Executor.py b/grc/gui/Executor.py
index f5a75ab55b..552a7554dc 100644
--- a/grc/gui/Executor.py
+++ b/grc/gui/Executor.py
@@ -15,14 +15,16 @@
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
+from __future__ import absolute_import
+
+import os
+import shlex
import subprocess
import threading
from distutils.spawn import find_executable
-import gobject
-import os
+from gi.repository import GLib
-from ..core.utils import shlex
from ..core import Messages
@@ -32,20 +34,16 @@ class ExecFlowGraphThread(threading.Thread):
def __init__(self, flow_graph_page, xterm_executable, callback):
"""
ExecFlowGraphThread constructor.
-
- Args:
- action_handler: an instance of an ActionHandler
"""
threading.Thread.__init__(self)
self.page = flow_graph_page # store page and dont use main window calls in run
- self.flow_graph = self.page.get_flow_graph()
+ self.flow_graph = self.page.flow_graph
self.xterm_executable = xterm_executable
self.update_callback = callback
try:
- self.process = self._popen()
- self.page.set_proc(self.process)
+ self.process = self.page.process = self._popen()
self.update_callback()
self.start()
except Exception as e:
@@ -79,18 +77,18 @@ class ExecFlowGraphThread(threading.Thread):
def run(self):
"""
Wait on the executing process by reading from its stdout.
- Use gobject.idle_add when calling functions that modify gtk objects.
+ Use GObject.idle_add when calling functions that modify gtk objects.
"""
# handle completion
r = "\n"
while r:
- gobject.idle_add(Messages.send_verbose_exec, r)
+ GLib.idle_add(Messages.send_verbose_exec, r)
r = os.read(self.process.stdout.fileno(), 1024)
self.process.poll()
- gobject.idle_add(self.done)
+ GLib.idle_add(self.done)
def done(self):
"""Perform end of execution tasks."""
Messages.send_end_exec(self.process.returncode)
- self.page.set_proc(None)
+ self.page.process = None
self.update_callback()
diff --git a/grc/gui/FileDialogs.py b/grc/gui/FileDialogs.py
index c4daae3ad6..dbcecf91ab 100644
--- a/grc/gui/FileDialogs.py
+++ b/grc/gui/FileDialogs.py
@@ -17,140 +17,99 @@ 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
-from Dialogs import MessageDialogHelper
-from Constants import \
- DEFAULT_FILE_PATH, IMAGE_FILE_EXTENSION, TEXT_FILE_EXTENSION, \
- NEW_FLOGRAPH_TITLE
-import Preferences
-from os import path
-import Utils
-
-##################################################
-# Constants
-##################################################
-OPEN_FLOW_GRAPH = 'open flow graph'
-SAVE_FLOW_GRAPH = 'save flow graph'
-SAVE_CONSOLE = 'save console'
-SAVE_IMAGE = 'save image'
-OPEN_QSS_THEME = 'open qss theme'
-
-FILE_OVERWRITE_MARKUP_TMPL="""\
-File <b>$encode($filename)</b> Exists!\nWould you like to overwrite the existing file?"""
-
-FILE_DNE_MARKUP_TMPL="""\
-File <b>$encode($filename)</b> Does not Exist!"""
-
-
-
-# File Filters
-def get_flow_graph_files_filter():
- filter = gtk.FileFilter()
- filter.set_name('Flow Graph Files')
- filter.add_pattern('*'+Preferences.file_extension())
- return filter
-
-
-def get_text_files_filter():
- filter = gtk.FileFilter()
- filter.set_name('Text Files')
- filter.add_pattern('*'+TEXT_FILE_EXTENSION)
- return filter
-
-
-def get_image_files_filter():
- filter = gtk.FileFilter()
- filter.set_name('Image Files')
- filter.add_pattern('*'+IMAGE_FILE_EXTENSION)
- return filter
-
+from __future__ import absolute_import
-def get_all_files_filter():
- filter = gtk.FileFilter()
- filter.set_name('All Files')
- filter.add_pattern('*')
- return filter
+from os import path
+from gi.repository import Gtk
-def get_qss_themes_filter():
- filter = gtk.FileFilter()
- filter.set_name('QSS Themes')
- filter.add_pattern('*.qss')
- return filter
+from . import Constants, Utils, Dialogs
-# File Dialogs
-class FileDialogHelper(gtk.FileChooserDialog):
+class FileDialogHelper(Gtk.FileChooserDialog, object):
"""
A wrapper class for the gtk file chooser dialog.
Implement a file chooser dialog with only necessary parameters.
"""
+ title = ''
+ action = Gtk.FileChooserAction.OPEN
+ filter_label = ''
+ filter_ext = ''
- def __init__(self, action, title, parent):
+ def __init__(self, parent, current_file_path):
"""
FileDialogHelper contructor.
Create a save or open dialog with cancel and ok buttons.
Use standard settings: no multiple selection, local files only, and the * filter.
Args:
- action: gtk.FILE_CHOOSER_ACTION_OPEN or gtk.FILE_CHOOSER_ACTION_SAVE
+ action: Gtk.FileChooserAction.OPEN or Gtk.FileChooserAction.SAVE
title: the title of the dialog (string)
"""
- ok_stock = {gtk.FILE_CHOOSER_ACTION_OPEN : 'gtk-open', gtk.FILE_CHOOSER_ACTION_SAVE : 'gtk-save'}[action]
- gtk.FileChooserDialog.__init__(self, title, parent, action, ('gtk-cancel', gtk.RESPONSE_CANCEL, ok_stock, gtk.RESPONSE_OK))
+ ok_stock = {
+ Gtk.FileChooserAction.OPEN: 'gtk-open',
+ Gtk.FileChooserAction.SAVE: 'gtk-save'
+ }[self.action]
+
+ Gtk.FileChooserDialog.__init__(self, title=self.title, action=self.action,
+ transient_for=parent)
+ self.add_buttons('gtk-cancel', Gtk.ResponseType.CANCEL, ok_stock, Gtk.ResponseType.OK)
self.set_select_multiple(False)
self.set_local_only(True)
- self.add_filter(get_all_files_filter())
+
+ self.parent = parent
+ self.current_file_path = current_file_path or path.join(
+ Constants.DEFAULT_FILE_PATH, Constants.NEW_FLOGRAPH_TITLE + Constants.FILE_EXTENSION)
+
+ self.set_current_folder(path.dirname(current_file_path)) # current directory
+ self.setup_filters()
+
+ def setup_filters(self, filters=None):
+ set_default = True
+ filters = filters or ([(self.filter_label, self.filter_ext)] if self.filter_label else [])
+ filters.append(('All Files', ''))
+ for label, ext in filters:
+ if not label:
+ continue
+ f = Gtk.FileFilter()
+ f.set_name(label)
+ f.add_pattern('*' + ext)
+ self.add_filter(f)
+ if not set_default:
+ self.set_filter(f)
+ set_default = True
+
+ def run(self):
+ """Get the filename and destroy the dialog."""
+ response = Gtk.FileChooserDialog.run(self)
+ filename = self.get_filename() if response == Gtk.ResponseType.OK else None
+ self.destroy()
+ return filename
-class FileDialog(FileDialogHelper):
+class SaveFileDialog(FileDialogHelper):
"""A dialog box to save or open flow graph files. This is a base class, do not use."""
+ action = Gtk.FileChooserAction.SAVE
- def __init__(self, parent, current_file_path=''):
- """
- FileDialog constructor.
+ def __init__(self, parent, current_file_path):
+ super(SaveFileDialog, self).__init__(parent, current_file_path)
+ self.set_current_name(path.splitext(path.basename(self.current_file_path))[0] + self.filter_ext)
+ self.set_create_folders(True)
+ self.set_do_overwrite_confirmation(True)
- Args:
- current_file_path: the current directory or path to the open flow graph
- """
- if not current_file_path: current_file_path = path.join(DEFAULT_FILE_PATH, NEW_FLOGRAPH_TITLE + Preferences.file_extension())
- if self.type == OPEN_FLOW_GRAPH:
- FileDialogHelper.__init__(self, gtk.FILE_CHOOSER_ACTION_OPEN, 'Open a Flow Graph from a File...', parent)
- self.add_and_set_filter(get_flow_graph_files_filter())
- self.set_select_multiple(True)
- elif self.type == SAVE_FLOW_GRAPH:
- FileDialogHelper.__init__(self, gtk.FILE_CHOOSER_ACTION_SAVE, 'Save a Flow Graph to a File...', parent)
- self.add_and_set_filter(get_flow_graph_files_filter())
- self.set_current_name(path.basename(current_file_path))
- elif self.type == SAVE_CONSOLE:
- FileDialogHelper.__init__(self, gtk.FILE_CHOOSER_ACTION_SAVE, 'Save Console to a File...', parent)
- self.add_and_set_filter(get_text_files_filter())
- file_path = path.splitext(path.basename(current_file_path))[0]
- self.set_current_name(file_path) #show the current filename
- elif self.type == SAVE_IMAGE:
- FileDialogHelper.__init__(self, gtk.FILE_CHOOSER_ACTION_SAVE, 'Save a Flow Graph Screen Shot...', parent)
- self.add_and_set_filter(get_image_files_filter())
- current_file_path = current_file_path + IMAGE_FILE_EXTENSION
- self.set_current_name(path.basename(current_file_path)) #show the current filename
- elif self.type == OPEN_QSS_THEME:
- FileDialogHelper.__init__(self, gtk.FILE_CHOOSER_ACTION_OPEN, 'Open a QSS theme...', parent)
- self.add_and_set_filter(get_qss_themes_filter())
- self.set_select_multiple(False)
- self.set_current_folder(path.dirname(current_file_path)) #current directory
-
- def add_and_set_filter(self, filter):
- """
- Add the gtk file filter to the list of filters and set it as the default file filter.
- Args:
- filter: a gtk file filter.
- """
- self.add_filter(filter)
- self.set_filter(filter)
+class OpenFileDialog(FileDialogHelper):
+ """A dialog box to save or open flow graph files. This is a base class, do not use."""
+ action = Gtk.FileChooserAction.OPEN
- def get_rectified_filename(self):
+ def show_missing_message(self, filename):
+ Dialogs.MessageDialogWrapper(
+ self.parent,
+ Gtk.MessageType.WARNING, Gtk.ButtonsType.CLOSE, 'Cannot Open!',
+ 'File <b>{filename}</b> Does not Exist!'.format(filename=Utils.encode(filename)),
+ ).run_and_destroy()
+
+ def get_filename(self):
"""
Run the dialog and get the filename.
If this is a save dialog and the file name is missing the extension, append the file extension.
@@ -160,82 +119,82 @@ class FileDialog(FileDialogHelper):
Returns:
the complete file path
"""
- if gtk.FileChooserDialog.run(self) != gtk.RESPONSE_OK: return None #response was cancel
- #############################################
- # Handle Save Dialogs
- #############################################
- if self.type in (SAVE_FLOW_GRAPH, SAVE_CONSOLE, SAVE_IMAGE):
- filename = self.get_filename()
- extension = {
- SAVE_FLOW_GRAPH: Preferences.file_extension(),
- SAVE_CONSOLE: TEXT_FILE_EXTENSION,
- SAVE_IMAGE: IMAGE_FILE_EXTENSION,
- }[self.type]
- #append the missing file extension if the filter matches
- if path.splitext(filename)[1].lower() != extension: filename += extension
- self.set_current_name(path.basename(filename)) #show the filename with extension
- if path.exists(filename): #ask the user to confirm overwrite
- if MessageDialogHelper(
- gtk.MESSAGE_QUESTION, gtk.BUTTONS_YES_NO, 'Confirm Overwrite!',
- Utils.parse_template(FILE_OVERWRITE_MARKUP_TMPL, filename=filename),
- ) == gtk.RESPONSE_NO: return self.get_rectified_filename()
- return filename
- #############################################
- # Handle Open Dialogs
- #############################################
- elif self.type in (OPEN_FLOW_GRAPH, OPEN_QSS_THEME):
- filenames = self.get_filenames()
- for filename in filenames:
- if not path.exists(filename): #show a warning and re-run
- MessageDialogHelper(
- gtk.MESSAGE_WARNING, gtk.BUTTONS_CLOSE, 'Cannot Open!',
- Utils.parse_template(FILE_DNE_MARKUP_TMPL, filename=filename),
- )
- return self.get_rectified_filename()
- return filenames
+ filenames = Gtk.FileChooserDialog.get_filenames(self)
+ for filename in filenames:
+ if not path.exists(filename):
+ self.show_missing_message(filename)
+ return None # rerun
+ return filenames
- def run(self):
- """
- Get the filename and destroy the dialog.
-
- Returns:
- the filename or None if a close/cancel occurred.
- """
- filename = self.get_rectified_filename()
- self.destroy()
- return filename
+class OpenFlowGraph(OpenFileDialog):
+ title = 'Open a Flow Graph from a File...'
+ filter_label = 'Flow Graph Files'
+ filter_ext = Constants.FILE_EXTENSION
-class OpenFlowGraphFileDialog(FileDialog):
- type = OPEN_FLOW_GRAPH
+ def __init__(self, parent, current_file_path=''):
+ super(OpenFlowGraph, self).__init__(parent, current_file_path)
+ self.set_select_multiple(True)
-class SaveFlowGraphFileDialog(FileDialog):
- type = SAVE_FLOW_GRAPH
+class OpenQSS(OpenFileDialog):
+ title = 'Open a QSS theme...'
+ filter_label = 'QSS Themes'
+ filter_ext = '.qss'
-class OpenQSSFileDialog(FileDialog):
- type = OPEN_QSS_THEME
+class SaveFlowGraph(SaveFileDialog):
+ title = 'Save a Flow Graph to a File...'
+ filter_label = 'Flow Graph Files'
+ filter_ext = Constants.FILE_EXTENSION
-class SaveConsoleFileDialog(FileDialog):
- type = SAVE_CONSOLE
+class SaveConsole(SaveFileDialog):
+ title = 'Save Console to a File...'
+ filter_label = 'Test Files'
+ filter_ext = '.txt'
-class SaveImageFileDialog(FileDialog):
- type = SAVE_IMAGE
+class SaveScreenShot(SaveFileDialog):
+ title = 'Save a Flow Graph Screen Shot...'
+ filters = [('PDF Files', '.pdf'), ('PNG Files', '.png'), ('SVG Files', '.svg')]
+ filter_ext = '.pdf' # the default
+ def __init__(self, parent, current_file_path=''):
+ super(SaveScreenShot, self).__init__(parent, current_file_path)
-class SaveScreenShotDialog(SaveImageFileDialog):
+ self.config = Gtk.Application.get_default().config
- def __init__(self, parent, current_file_path=''):
- SaveImageFileDialog.__init__(self, parent, current_file_path)
- self._button = button = gtk.CheckButton('_Background transparent')
- self._button.set_active(Preferences.screen_shot_background_transparent())
+ self._button = button = Gtk.CheckButton(label='Background transparent')
+ self._button.set_active(self.config.screen_shot_background_transparent())
self.set_extra_widget(button)
+ def setup_filters(self, filters=None):
+ super(SaveScreenShot, self).setup_filters(self.filters)
+
+ def show_missing_message(self, filename):
+ Dialogs.MessageDialogWrapper(
+ self.parent,
+ Gtk.MessageType.ERROR, Gtk.ButtonsType.CLOSE, 'Can not Save!',
+ 'File Extention of <b>{filename}</b> not supported!'.format(filename=Utils.encode(filename)),
+ ).run_and_destroy()
+
def run(self):
- filename = SaveImageFileDialog.run(self)
+ valid_exts = {ext for label, ext in self.filters}
+ filename = None
+ while True:
+ response = Gtk.FileChooserDialog.run(self)
+ if response != Gtk.ResponseType.OK:
+ filename = None
+ break
+
+ filename = self.get_filename()
+ if path.splitext(filename)[1] in valid_exts:
+ break
+
+ self.show_missing_message(filename)
+
bg_transparent = self._button.get_active()
- Preferences.screen_shot_background_transparent(bg_transparent)
+ self.config.screen_shot_background_transparent(bg_transparent)
+ self.destroy()
return filename, bg_transparent
diff --git a/grc/gui/MainWindow.py b/grc/gui/MainWindow.py
index 205881f2e5..e737610a79 100644
--- a/grc/gui/MainWindow.py
+++ b/grc/gui/MainWindow.py
@@ -17,52 +17,32 @@ along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
"""
+from __future__ import absolute_import
+
import os
+import logging
-import gtk
+from gi.repository import Gtk, Gdk, GObject
-from . import Bars, Actions, Preferences, Utils
+from . import Bars, Actions, Utils
from .BlockTreeWindow import BlockTreeWindow
+from .Console import Console
from .VariableEditor import VariableEditor
from .Constants import \
NEW_FLOGRAPH_TITLE, DEFAULT_CONSOLE_WINDOW_WIDTH
-from .Dialogs import TextDisplay, MessageDialogHelper
-from .NotebookPage import NotebookPage
+from .Dialogs import TextDisplay, MessageDialogWrapper
+from .Notebook import Notebook, Page
from ..core import Messages
-MAIN_WINDOW_TITLE_TMPL = """\
-#if not $saved
-*#slurp
-#end if
-#if $basename
-$basename#slurp
-#else
-$new_flowgraph_title#slurp
-#end if
-#if $read_only
- (read only)#slurp
-#end if
-#if $dirname
- - $dirname#slurp
-#end if
- - $platform_name#slurp
-"""
-PAGE_TITLE_MARKUP_TMPL = """\
-#set $foreground = $saved and 'black' or 'red'
-<span foreground="$foreground">$encode($title or $new_flowgraph_title)</span>#slurp
-#if $read_only
- (ro)#slurp
-#end if
-"""
+log = logging.getLogger(__name__)
############################################################
# Main window
############################################################
-
-class MainWindow(gtk.Window):
+class MainWindow(Gtk.ApplicationWindow):
"""The topmost window with menus, the tool bar, and other major windows."""
# Constants the action handler can use to indicate which panel visibility to change.
@@ -70,105 +50,114 @@ class MainWindow(gtk.Window):
CONSOLE = 1
VARIABLES = 2
- def __init__(self, platform, action_handler_callback):
+ def __init__(self, app, platform):
"""
MainWindow constructor
Setup the menu, toolbar, flow graph editor notebook, block selection window...
"""
- self._platform = platform
+ Gtk.ApplicationWindow.__init__(self, title="GNU Radio Companion", application=app)
+ log.debug("__init__()")
- gen_opts = platform.blocks['options'].get_param('generate_options')
- generate_mode_default = gen_opts.get_value()
- generate_modes = [
- (o.get_key(), o.get_name(), o.get_key() == generate_mode_default)
- for o in gen_opts.get_options()]
+ self._platform = platform
+ self.app = app
+ self.config = platform.config
- # Load preferences
- Preferences.load(platform)
+ # Add all "win" actions to the local
+ for x in Actions.get_actions():
+ if x.startswith("win."):
+ self.add_action(Actions.actions[x])
# Setup window
- gtk.Window.__init__(self, gtk.WINDOW_TOPLEVEL)
- vbox = gtk.VBox()
+ vbox = Gtk.VBox()
self.add(vbox)
- icon_theme = gtk.icon_theme_get_default()
+ icon_theme = Gtk.IconTheme.get_default()
icon = icon_theme.lookup_icon("gnuradio-grc", 48, 0)
if not icon:
# Set window icon
self.set_icon_from_file(os.path.dirname(os.path.abspath(__file__)) + "/icon.png")
# Create the menu bar and toolbar
- self.add_accel_group(Actions.get_accel_group())
- self.menu_bar = Bars.MenuBar(generate_modes, action_handler_callback)
- vbox.pack_start(self.menu_bar, False)
- self.tool_bar = Bars.Toolbar(generate_modes, action_handler_callback)
- vbox.pack_start(self.tool_bar, False)
+ generate_modes = platform.get_generate_options()
+
+ # This needs to be replaced
+ # Have an option for either the application menu or this menu
+ self.menu_bar = Gtk.MenuBar.new_from_model(Bars.Menu())
+ vbox.pack_start(self.menu_bar, False, False, 0)
+
+ self.tool_bar = Bars.Toolbar()
+ self.tool_bar.set_hexpand(True)
+ # Show the toolbar
+ self.tool_bar.show()
+ vbox.pack_start(self.tool_bar, False, False, 0)
# Main parent container for the different panels
- self.container = gtk.HPaned()
- vbox.pack_start(self.container)
+ self.main = Gtk.HPaned() #(orientation=Gtk.Orientation.HORIZONTAL)
+ vbox.pack_start(self.main, True, True, 0)
# Create the notebook
- self.notebook = gtk.Notebook()
+ self.notebook = Notebook()
self.page_to_be_closed = None
- self.current_page = None
- self.notebook.set_show_border(False)
- self.notebook.set_scrollable(True) # scroll arrows for page tabs
- self.notebook.connect('switch-page', self._handle_page_change)
+
+ self.current_page = None # type: Page
# Create the console window
- self.text_display = TextDisplay()
- self.console_window = gtk.ScrolledWindow()
- self.console_window.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
- self.console_window.add(self.text_display)
- self.console_window.set_size_request(-1, DEFAULT_CONSOLE_WINDOW_WIDTH)
+ self.console = Console()
# Create the block tree and variable panels
- self.btwin = BlockTreeWindow(platform, self.get_flow_graph)
- self.vars = VariableEditor(platform, self.get_flow_graph)
+ self.btwin = BlockTreeWindow(platform)
+ self.btwin.connect('create_new_block', self._add_block_to_current_flow_graph)
+ self.vars = VariableEditor()
+ self.vars.connect('create_new_block', self._add_block_to_current_flow_graph)
+ self.vars.connect('remove_block', self._remove_block_from_current_flow_graph)
# Figure out which place to put the variable editor
- self.left = gtk.VPaned()
- self.right = gtk.VPaned()
- self.left_subpanel = gtk.HPaned()
+ self.left = Gtk.VPaned() #orientation=Gtk.Orientation.VERTICAL)
+ self.right = Gtk.VPaned() #orientation=Gtk.Orientation.VERTICAL)
+ self.left_subpanel = Gtk.HPaned() #orientation=Gtk.Orientation.HORIZONTAL)
- self.variable_panel_sidebar = Preferences.variable_editor_sidebar()
+ self.variable_panel_sidebar = self.config.variable_editor_sidebar()
if self.variable_panel_sidebar:
self.left.pack1(self.notebook)
- self.left.pack2(self.console_window, False)
+ self.left.pack2(self.console, False)
self.right.pack1(self.btwin)
self.right.pack2(self.vars, False)
else:
# Put the variable editor in a panel with the console
self.left.pack1(self.notebook)
- self.left_subpanel.pack1(self.console_window, shrink=False)
+ self.left_subpanel.pack1(self.console, shrink=False)
self.left_subpanel.pack2(self.vars, resize=False, shrink=True)
self.left.pack2(self.left_subpanel, False)
# Create the right panel
self.right.pack1(self.btwin)
- self.container.pack1(self.left)
- self.container.pack2(self.right, False)
+ self.main.pack1(self.left)
+ self.main.pack2(self.right, False)
- # load preferences and show the main window
- self.resize(*Preferences.main_window_size())
- self.container.set_position(Preferences.blocks_window_position())
- self.left.set_position(Preferences.console_window_position())
+ # Load preferences and show the main window
+ self.resize(*self.config.main_window_size())
+ self.main.set_position(self.config.blocks_window_position())
+ self.left.set_position(self.config.console_window_position())
if self.variable_panel_sidebar:
- self.right.set_position(Preferences.variable_editor_position(sidebar=True))
+ self.right.set_position(self.config.variable_editor_position(sidebar=True))
else:
- self.left_subpanel.set_position(Preferences.variable_editor_position())
+ self.left_subpanel.set_position(self.config.variable_editor_position())
self.show_all()
- self.console_window.hide()
- self.vars.hide()
- self.btwin.hide()
+ log.debug("Main window ready")
############################################################
# Event Handlers
############################################################
+ def _add_block_to_current_flow_graph(self, widget, key):
+ self.current_flow_graph.add_new_block(key)
+
+ def _remove_block_from_current_flow_graph(self, widget, key):
+ block = self.current_flow_graph.get_block(key)
+ self.current_flow_graph.remove_element(block)
+
def _quit(self, window, event):
"""
Handle the delete event from the main window.
@@ -181,20 +170,6 @@ class MainWindow(gtk.Window):
Actions.APPLICATION_QUIT()
return True
- def _handle_page_change(self, notebook, page, page_num):
- """
- Handle a page change. When the user clicks on a new tab,
- reload the flow graph to update the vars window and
- call handle states (select nothing) to update the buttons.
-
- Args:
- notebook: the notebook
- page: new page
- page_num: new page number
- """
- self.current_page = self.notebook.get_nth_page(page_num)
- Actions.PAGE_CHANGE()
-
def update_panel_visibility(self, panel, visibility=True):
"""
Handles changing visibility of panels.
@@ -204,19 +179,19 @@ class MainWindow(gtk.Window):
if panel == self.BLOCKS:
if visibility:
- self.btwin.show()
+ self.btwin.show()
else:
- self.btwin.hide()
+ self.btwin.hide()
elif panel == self.CONSOLE:
if visibility:
- self.console_window.show()
+ self.console.show()
else:
- self.console_window.hide()
+ self.console.hide()
elif panel == self.VARIABLES:
if visibility:
- self.vars.show()
+ self.vars.show()
else:
- self.vars.hide()
+ self.vars.hide()
else:
return
@@ -231,7 +206,7 @@ class MainWindow(gtk.Window):
self.right.hide()
else:
self.right.show()
- if not (self.vars.get_property('visible')) and not (self.console_window.get_property('visible')):
+ if not (self.vars.get_property('visible')) and not (self.console.get_property('visible')):
self.left_subpanel.hide()
else:
self.left_subpanel.show()
@@ -240,6 +215,14 @@ class MainWindow(gtk.Window):
# Console Window
############################################################
+ @property
+ def current_page(self):
+ return self.notebook.current_page
+
+ @current_page.setter
+ def current_page(self, page):
+ self.notebook.current_page = page
+
def add_console_line(self, line):
"""
Place line at the end of the text buffer, then scroll its window all the way down.
@@ -247,7 +230,7 @@ class MainWindow(gtk.Window):
Args:
line: the new text
"""
- self.text_display.insert(line)
+ self.console.add_line(line)
############################################################
# Pages: create and close
@@ -269,26 +252,24 @@ class MainWindow(gtk.Window):
return
try: #try to load from file
if file_path: Messages.send_start_load(file_path)
- flow_graph = self._platform.get_new_flow_graph()
+ flow_graph = self._platform.make_flow_graph()
flow_graph.grc_file_path = file_path
#print flow_graph
- page = NotebookPage(
+ page = Page(
self,
flow_graph=flow_graph,
file_path=file_path,
)
if file_path: Messages.send_end_load()
- except Exception, e: #return on failure
+ except Exception as e: #return on failure
Messages.send_fail_load(e)
if isinstance(e, KeyError) and str(e) == "'options'":
# This error is unrecoverable, so crash gracefully
exit(-1)
return
#add this page to the notebook
- self.notebook.append_page(page, page.get_tab())
- try: self.notebook.set_tab_reorderable(page, True)
- except: pass #gtk too old
- self.notebook.set_tab_label_packing(page, False, False, gtk.PACK_START)
+ self.notebook.append_page(page, page.tab)
+ self.notebook.set_tab_reorderable(page, True)
#only show if blank or manual
if not file_path or show: self._set_page(page)
@@ -299,26 +280,26 @@ class MainWindow(gtk.Window):
Returns:
true if all closed
"""
- open_files = filter(lambda file: file, self._get_files()) #filter blank files
- open_file = self.get_page().get_file_path()
+ open_files = [file for file in self._get_files() if file] #filter blank files
+ open_file = self.current_page.file_path
#close each page
- for page in sorted(self.get_pages(), key=lambda p: p.get_saved()):
+ for page in sorted(self.get_pages(), key=lambda p: p.saved):
self.page_to_be_closed = page
closed = self.close_page(False)
if not closed:
break
if self.notebook.get_n_pages(): return False
#save state before closing
- Preferences.set_open_files(open_files)
- Preferences.file_open(open_file)
- Preferences.main_window_size(self.get_size())
- Preferences.console_window_position(self.left.get_position())
- Preferences.blocks_window_position(self.container.get_position())
+ self.config.set_open_files(open_files)
+ self.config.file_open(open_file)
+ self.config.main_window_size(self.get_size())
+ self.config.console_window_position(self.left.get_position())
+ self.config.blocks_window_position(self.main.get_position())
if self.variable_panel_sidebar:
- Preferences.variable_editor_position(self.right.get_position(), sidebar=True)
+ self.config.variable_editor_position(self.right.get_position(), sidebar=True)
else:
- Preferences.variable_editor_position(self.left_subpanel.get_position())
- Preferences.save()
+ self.config.variable_editor_position(self.left_subpanel.get_position())
+ self.config.save()
return True
def close_page(self, ensure=True):
@@ -330,23 +311,24 @@ class MainWindow(gtk.Window):
Args:
ensure: boolean
"""
- if not self.page_to_be_closed: self.page_to_be_closed = self.get_page()
+ if not self.page_to_be_closed: self.page_to_be_closed = self.current_page
#show the page if it has an executing flow graph or is unsaved
- if self.page_to_be_closed.get_proc() or not self.page_to_be_closed.get_saved():
+ if self.page_to_be_closed.process or not self.page_to_be_closed.saved:
self._set_page(self.page_to_be_closed)
#unsaved? ask the user
- if not self.page_to_be_closed.get_saved():
+ if not self.page_to_be_closed.saved:
response = self._save_changes() # return value is either OK, CLOSE, or CANCEL
- if response == gtk.RESPONSE_OK:
+ if response == Gtk.ResponseType.OK:
Actions.FLOW_GRAPH_SAVE() #try to save
- if not self.page_to_be_closed.get_saved(): #still unsaved?
+ if not self.page_to_be_closed.saved: #still unsaved?
self.page_to_be_closed = None #set the page to be closed back to None
return False
- elif response == gtk.RESPONSE_CANCEL:
+ elif response == Gtk.ResponseType.CANCEL:
self.page_to_be_closed = None
return False
#stop the flow graph if executing
- if self.page_to_be_closed.get_proc(): Actions.FLOW_GRAPH_KILL()
+ if self.page_to_be_closed.process:
+ Actions.FLOW_GRAPH_KILL()
#remove the page
self.notebook.remove_page(self.notebook.page_num(self.page_to_be_closed))
if ensure and self.notebook.get_n_pages() == 0: self.new_page() #no pages, make a new one
@@ -362,69 +344,49 @@ class MainWindow(gtk.Window):
Set the title of the main window.
Set the titles on the page tabs.
Show/hide the console window.
-
- Args:
- title: the window title
"""
- gtk.Window.set_title(self, Utils.parse_template(MAIN_WINDOW_TITLE_TMPL,
- basename=os.path.basename(self.get_page().get_file_path()),
- dirname=os.path.dirname(self.get_page().get_file_path()),
- new_flowgraph_title=NEW_FLOGRAPH_TITLE,
- read_only=self.get_page().get_read_only(),
- saved=self.get_page().get_saved(),
- platform_name=self._platform.config.name,
- )
- )
- #set tab titles
- for page in self.get_pages(): page.set_markup(
- Utils.parse_template(PAGE_TITLE_MARKUP_TMPL,
- #get filename and strip out file extension
- title=os.path.splitext(os.path.basename(page.get_file_path()))[0],
- read_only=page.get_read_only(), saved=page.get_saved(),
- new_flowgraph_title=NEW_FLOGRAPH_TITLE,
- )
- )
- #show/hide notebook tabs
+ page = self.current_page
+
+ basename = os.path.basename(page.file_path)
+ dirname = os.path.dirname(page.file_path)
+ Gtk.Window.set_title(self, ''.join((
+ '*' if not page.saved else '', basename if basename else NEW_FLOGRAPH_TITLE,
+ '(read only)' if page.get_read_only() else '', ' - ',
+ dirname if dirname else self._platform.config.name,
+ )))
+ # set tab titles
+ for page in self.get_pages():
+ file_name = os.path.splitext(os.path.basename(page.file_path))[0]
+ page.set_markup('<span foreground="{foreground}">{title}{ro}</span>'.format(
+ foreground='black' if page.saved else 'red', ro=' (ro)' if page.get_read_only() else '',
+ title=Utils.encode(file_name or NEW_FLOGRAPH_TITLE),
+ ))
+ # show/hide notebook tabs
self.notebook.set_show_tabs(len(self.get_pages()) > 1)
- # Need to update the variable window when changing
- self.vars.update_gui()
+ # Need to update the variable window when changing
+ self.vars.update_gui(self.current_flow_graph.blocks)
def update_pages(self):
"""
Forces a reload of all the pages in this notebook.
"""
for page in self.get_pages():
- success = page.get_flow_graph().reload()
+ success = page.flow_graph.reload()
if success: # Only set saved if errors occurred during import
- page.set_saved(False)
-
- def get_page(self):
- """
- Get the selected page.
+ page.saved = False
- Returns:
- the selected page
- """
- return self.current_page
-
- def get_flow_graph(self):
- """
- Get the selected flow graph.
-
- Returns:
- the selected flow graph
- """
- return self.get_page().get_flow_graph()
+ @property
+ def current_flow_graph(self):
+ return self.current_page.flow_graph
def get_focus_flag(self):
"""
Get the focus flag from the current page.
-
Returns:
the focus flag
"""
- return self.get_page().get_drawing_area().get_focus_flag()
+ return self.current_page.drawing_area.get_focus_flag()
############################################################
# Helpers
@@ -448,14 +410,14 @@ class MainWindow(gtk.Window):
the response_id (see buttons variable below)
"""
buttons = (
- 'Close without saving', gtk.RESPONSE_CLOSE,
- gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL,
- gtk.STOCK_SAVE, gtk.RESPONSE_OK
- )
- return MessageDialogHelper(
- gtk.MESSAGE_QUESTION, gtk.BUTTONS_NONE, self, 'Unsaved Changes!',
- 'Would you like to save changes before closing?', gtk.RESPONSE_OK, buttons
+ 'Close without saving', Gtk.ResponseType.CLOSE,
+ Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
+ Gtk.STOCK_SAVE, Gtk.ResponseType.OK
)
+ return MessageDialogWrapper(
+ self, Gtk.MessageType.QUESTION, Gtk.ButtonsType.NONE, 'Unsaved Changes!',
+ 'Would you like to save changes before closing?', Gtk.ResponseType.OK, buttons
+ ).run_and_destroy()
def _get_files(self):
"""
@@ -464,7 +426,7 @@ class MainWindow(gtk.Window):
Returns:
list of file paths
"""
- return map(lambda page: page.get_file_path(), self.get_pages())
+ return [page.file_path for page in self.get_pages()]
def get_pages(self):
"""
@@ -473,4 +435,5 @@ class MainWindow(gtk.Window):
Returns:
list of pages
"""
- return [self.notebook.get_nth_page(page_num) for page_num in range(self.notebook.get_n_pages())]
+ return [self.notebook.get_nth_page(page_num)
+ for page_num in range(self.notebook.get_n_pages())]
diff --git a/grc/gui/Notebook.py b/grc/gui/Notebook.py
new file mode 100644
index 0000000000..9f63190b31
--- /dev/null
+++ b/grc/gui/Notebook.py
@@ -0,0 +1,187 @@
+"""
+Copyright 2008, 2009, 2011 Free Software Foundation, Inc.
+This file is part of GNU Radio
+
+GNU Radio Companion is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation; either version 2
+of the License, or (at your option) any later version.
+
+GNU Radio Companion is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
+"""
+
+from __future__ import absolute_import
+import os
+import logging
+
+from gi.repository import Gtk, Gdk, GObject
+
+from . import Actions
+from .StateCache import StateCache
+from .Constants import MIN_WINDOW_WIDTH, MIN_WINDOW_HEIGHT
+from .DrawingArea import DrawingArea
+
+
+log = logging.getLogger(__name__)
+
+
+class Notebook(Gtk.Notebook):
+ def __init__(self):
+ Gtk.Notebook.__init__(self)
+ log.debug("notebook()")
+ self.app = Gtk.Application.get_default()
+ self.current_page = None
+
+ self.set_show_border(False)
+ self.set_scrollable(True)
+ self.connect('switch-page', self._handle_page_change)
+
+ self.add_events(Gdk.EventMask.SCROLL_MASK)
+ self.connect('scroll-event', self._handle_scroll)
+ self._ignore_consecutive_scrolls = 0
+
+ def _handle_page_change(self, notebook, page, page_num):
+ """
+ Handle a page change. When the user clicks on a new tab,
+ reload the flow graph to update the vars window and
+ call handle states (select nothing) to update the buttons.
+
+ Args:
+ notebook: the notebook
+ page: new page
+ page_num: new page number
+ """
+ self.current_page = self.get_nth_page(page_num)
+ Actions.PAGE_CHANGE()
+
+ def _handle_scroll(self, widget, event):
+ # Not sure how to handle this at the moment.
+ natural = True
+ # Slow it down
+ if self._ignore_consecutive_scrolls == 0:
+ if event.direction in (Gdk.ScrollDirection.UP, Gdk.ScrollDirection.LEFT):
+ if natural:
+ self.prev_page()
+ else:
+ self.next_page()
+ elif event.direction in (Gdk.ScrollDirection.DOWN, Gdk.ScrollDirection.RIGHT):
+ if natural:
+ self.next_page()
+ else:
+ self.prev_page()
+ self._ignore_consecutive_scrolls = 3
+ else:
+ self._ignore_consecutive_scrolls -= 1
+ return False
+
+
+class Page(Gtk.HBox):
+ """A page in the notebook."""
+
+ def __init__(self, main_window, flow_graph, file_path=''):
+ """
+ Page constructor.
+
+ Args:
+ main_window: main window
+ file_path: path to a flow graph file
+ """
+ Gtk.HBox.__init__(self)
+
+ self.main_window = main_window
+ self.flow_graph = flow_graph
+ self.file_path = file_path
+
+ self.process = None
+ self.saved = True
+
+ # import the file
+ initial_state = flow_graph.parent_platform.parse_flow_graph(file_path)
+ flow_graph.import_data(initial_state)
+ self.state_cache = StateCache(initial_state)
+
+ # tab box to hold label and close button
+ self.label = Gtk.Label()
+ image = Gtk.Image.new_from_icon_name('window-close', Gtk.IconSize.MENU)
+ image_box = Gtk.HBox(homogeneous=False, spacing=0)
+ image_box.pack_start(image, True, False, 0)
+ button = Gtk.Button()
+ button.connect("clicked", self._handle_button)
+ button.set_relief(Gtk.ReliefStyle.NONE)
+ button.add(image_box)
+
+ tab = self.tab = Gtk.HBox(homogeneous=False, spacing=0)
+ tab.pack_start(self.label, False, False, 0)
+ tab.pack_start(button, False, False, 0)
+ tab.show_all()
+
+ # setup scroll window and drawing area
+ self.drawing_area = DrawingArea(flow_graph)
+ flow_graph.drawing_area = self.drawing_area
+
+ self.scrolled_window = Gtk.ScrolledWindow()
+ self.scrolled_window.set_size_request(MIN_WINDOW_WIDTH, MIN_WINDOW_HEIGHT)
+ self.scrolled_window.set_policy(Gtk.PolicyType.ALWAYS, Gtk.PolicyType.ALWAYS)
+ self.scrolled_window.connect('key-press-event', self._handle_scroll_window_key_press)
+
+ self.scrolled_window.add(self.drawing_area)
+ self.pack_start(self.scrolled_window, True, True, 0)
+ self.show_all()
+
+ def _handle_scroll_window_key_press(self, widget, event):
+ is_ctrl_pg = (
+ event.state & Gdk.ModifierType.CONTROL_MASK and
+ event.keyval in (Gdk.KEY_Page_Up, Gdk.KEY_Page_Down)
+ )
+ if is_ctrl_pg:
+ return self.get_parent().event(event)
+
+ def get_generator(self):
+ """
+ Get the generator object for this flow graph.
+
+ Returns:
+ generator
+ """
+ platform = self.flow_graph.parent_platform
+ return platform.Generator(self.flow_graph, os.path.dirname(self.file_path))
+
+ def _handle_button(self, button):
+ """
+ The button was clicked.
+ Make the current page selected, then close.
+
+ Args:
+ the: button
+ """
+ self.main_window.page_to_be_closed = self
+ Actions.FLOW_GRAPH_CLOSE()
+
+ def set_markup(self, markup):
+ """
+ Set the markup in this label.
+
+ Args:
+ markup: the new markup text
+ """
+ self.label.set_markup(markup)
+
+ def get_read_only(self):
+ """
+ Get the read-only state of the file.
+ Always false for empty path.
+
+ Returns:
+ true for read-only
+ """
+ if not self.file_path:
+ return False
+ return (os.path.exists(self.file_path) and
+ not os.access(self.file_path, os.W_OK))
diff --git a/grc/gui/NotebookPage.py b/grc/gui/NotebookPage.py
deleted file mode 100644
index 79ad8bf207..0000000000
--- a/grc/gui/NotebookPage.py
+++ /dev/null
@@ -1,244 +0,0 @@
-"""
-Copyright 2008, 2009, 2011 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
-import gobject
-import Actions
-from StateCache import StateCache
-from Constants import MIN_WINDOW_WIDTH, MIN_WINDOW_HEIGHT
-from DrawingArea import DrawingArea
-import os
-
-
-class NotebookPage(gtk.HBox):
- """A page in the notebook."""
-
- def __init__(self, main_window, flow_graph, file_path=''):
- """
- Page constructor.
-
- Args:
- main_window: main window
- file_path: path to a flow graph file
- """
- self._flow_graph = flow_graph
- self.process = None
- #import the file
- self.main_window = main_window
- self.file_path = file_path
- initial_state = flow_graph.get_parent().parse_flow_graph(file_path)
- self.state_cache = StateCache(initial_state)
- self.saved = True
- #import the data to the flow graph
- self.get_flow_graph().import_data(initial_state)
- #initialize page gui
- gtk.HBox.__init__(self, False, 0)
- self.show()
- #tab box to hold label and close button
- self.tab = gtk.HBox(False, 0)
- #setup tab label
- self.label = gtk.Label()
- self.tab.pack_start(self.label, False)
- #setup button image
- image = gtk.Image()
- image.set_from_stock('gtk-close', gtk.ICON_SIZE_MENU)
- #setup image box
- image_box = gtk.HBox(False, 0)
- image_box.pack_start(image, True, False, 0)
- #setup the button
- button = gtk.Button()
- button.connect("clicked", self._handle_button)
- button.set_relief(gtk.RELIEF_NONE)
- button.add(image_box)
- #button size
- w, h = gtk.icon_size_lookup_for_settings(button.get_settings(), gtk.ICON_SIZE_MENU)
- button.set_size_request(w+6, h+6)
- self.tab.pack_start(button, False)
- self.tab.show_all()
- #setup scroll window and drawing area
- self.scrolled_window = gtk.ScrolledWindow()
- self.scrolled_window.set_size_request(MIN_WINDOW_WIDTH, MIN_WINDOW_HEIGHT)
- self.scrolled_window.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
- self.scrolled_window.connect('key-press-event', self._handle_scroll_window_key_press)
- self.drawing_area = DrawingArea(self.get_flow_graph())
- self.scrolled_window.add_with_viewport(self.get_drawing_area())
- self.pack_start(self.scrolled_window)
- #inject drawing area into flow graph
- self.get_flow_graph().drawing_area = self.get_drawing_area()
- self.show_all()
-
- def get_drawing_area(self): return self.drawing_area
-
- def _handle_scroll_window_key_press(self, widget, event):
- """forward Ctrl-PgUp/Down to NotebookPage (switch fg instead of horiz. scroll"""
- is_ctrl_pg = (
- event.state & gtk.gdk.CONTROL_MASK and
- event.keyval in (gtk.keysyms.Page_Up, gtk.keysyms.Page_Down)
- )
- if is_ctrl_pg:
- return self.get_parent().event(event)
-
- def get_generator(self):
- """
- Get the generator object for this flow graph.
-
- Returns:
- generator
- """
- platform = self.get_flow_graph().get_parent()
- return platform.Generator(self.get_flow_graph(), self.get_file_path())
-
- def _handle_button(self, button):
- """
- The button was clicked.
- Make the current page selected, then close.
-
- Args:
- the: button
- """
- self.main_window.page_to_be_closed = self
- Actions.FLOW_GRAPH_CLOSE()
-
- def set_markup(self, markup):
- """
- Set the markup in this label.
-
- Args:
- markup: the new markup text
- """
- self.label.set_markup(markup)
-
- def get_tab(self):
- """
- Get the gtk widget for this page's tab.
-
- Returns:
- gtk widget
- """
- return self.tab
-
- def get_proc(self):
- """
- Get the subprocess for the flow graph.
-
- Returns:
- the subprocess object
- """
- return self.process
-
- def set_proc(self, process):
- """
- Set the subprocess object.
-
- Args:
- process: the new subprocess
- """
- self.process = process
-
- def term_proc(self):
- """
- Terminate the subprocess object
-
- Add a callback to kill the process
- after 2 seconds if not already terminated
- """
- def kill(process):
- """
- Kill process if not already terminated
-
- Called by gobject.timeout_add
-
- Returns:
- False to stop timeout_add periodic calls
- """
- is_terminated = process.poll()
- if is_terminated is None:
- process.kill()
- return False
-
- self.get_proc().terminate()
- gobject.timeout_add(2000, kill, self.get_proc())
-
- def get_flow_graph(self):
- """
- Get the flow graph.
-
- Returns:
- the flow graph
- """
- return self._flow_graph
-
- def get_read_only(self):
- """
- Get the read-only state of the file.
- Always false for empty path.
-
- Returns:
- true for read-only
- """
- if not self.get_file_path(): return False
- return os.path.exists(self.get_file_path()) and \
- not os.access(self.get_file_path(), os.W_OK)
-
- def get_file_path(self):
- """
- Get the file path for the flow graph.
-
- Returns:
- the file path or ''
- """
- return self.file_path
-
- def set_file_path(self, file_path=''):
- """
- Set the file path, '' for no file path.
-
- Args:
- file_path: file path string
- """
- self.file_path = os.path.abspath(file_path) if file_path else ''
-
- def get_saved(self):
- """
- Get the saved status for the flow graph.
-
- Returns:
- true if saved
- """
- return self.saved
-
- def set_saved(self, saved=True):
- """
- Set the saved status.
-
- Args:
- saved: boolean status
- """
- self.saved = saved
-
- def get_state_cache(self):
- """
- Get the state cache for the flow graph.
-
- Returns:
- the state cache
- """
- return self.state_cache
diff --git a/grc/gui/Param.py b/grc/gui/Param.py
deleted file mode 100644
index 7f90a7bea1..0000000000
--- a/grc/gui/Param.py
+++ /dev/null
@@ -1,440 +0,0 @@
-"""
-Copyright 2007-2011 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 pygtk
-pygtk.require('2.0')
-import gtk
-
-from . import Colors, Constants
-from .Element import Element
-from . import Utils
-
-from ..core.Param import Param as _Param
-
-
-class InputParam(gtk.HBox):
- """The base class for an input parameter inside the input parameters dialog."""
- expand = False
-
- def __init__(self, param, parent, changed_callback=None, editing_callback=None):
- gtk.HBox.__init__(self)
- self.param = param
- self._parent = parent
- self._changed_callback = changed_callback
- self._editing_callback = editing_callback
- self.label = gtk.Label() #no label, markup is added by set_markup
- self.label.set_size_request(Utils.scale_scalar(150), -1)
- self.pack_start(self.label, False)
- self.set_markup = lambda m: self.label.set_markup(m)
- self.tp = None
- self._have_pending_changes = False
- #connect events
- self.connect('show', self._update_gui)
-
- def set_color(self, color):
- pass
-
- def set_tooltip_text(self, text):
- pass
-
- def get_text(self):
- raise NotImplementedError()
-
- def _update_gui(self, *args):
- """
- Set the markup, color, tooltip, show/hide.
- """
- #set the markup
- has_cb = \
- hasattr(self.param.get_parent(), 'get_callbacks') and \
- filter(lambda c: self.param.get_key() in c, self.param.get_parent()._callbacks)
- self.set_markup(Utils.parse_template(PARAM_LABEL_MARKUP_TMPL,
- param=self.param, has_cb=has_cb,
- modified=self._have_pending_changes))
- #set the color
- self.set_color(self.param.get_color())
- #set the tooltip
- self.set_tooltip_text(
- Utils.parse_template(TIP_MARKUP_TMPL, param=self.param).strip(),
- )
- #show/hide
- if self.param.get_hide() == 'all': self.hide_all()
- else: self.show_all()
-
- def _mark_changed(self, *args):
- """
- Mark this param as modified on change, but validate only on focus-lost
- """
- self._have_pending_changes = True
- self._update_gui()
- if self._editing_callback:
- self._editing_callback(self, None)
-
- def _apply_change(self, *args):
- """
- Handle a gui change by setting the new param value,
- calling the callback (if applicable), and updating.
- """
- #set the new value
- self.param.set_value(self.get_text())
- #call the callback
- if self._changed_callback:
- self._changed_callback(self, None)
- else:
- self.param.validate()
- #gui update
- self._have_pending_changes = False
- self._update_gui()
-
- def _handle_key_press(self, widget, event):
- if event.keyval == gtk.keysyms.Return and event.state & gtk.gdk.CONTROL_MASK:
- self._apply_change(widget, event)
- return True
- return False
-
- def apply_pending_changes(self):
- if self._have_pending_changes:
- self._apply_change()
-
-
-class EntryParam(InputParam):
- """Provide an entry box for strings and numbers."""
-
- def __init__(self, *args, **kwargs):
- InputParam.__init__(self, *args, **kwargs)
- self._input = gtk.Entry()
- self._input.set_text(self.param.get_value())
- self._input.connect('changed', self._mark_changed)
- self._input.connect('focus-out-event', self._apply_change)
- self._input.connect('key-press-event', self._handle_key_press)
- self.pack_start(self._input, True)
-
- def get_text(self):
- return self._input.get_text()
-
- def set_color(self, color):
- need_status_color = self.label not in self.get_children()
- text_color = (
- None if not need_status_color else
- gtk.gdk.color_parse('blue') if self._have_pending_changes else
- gtk.gdk.color_parse('red') if not self.param.is_valid() else
- None)
- base_color = (
- Colors.BLOCK_DISABLED_COLOR
- if need_status_color and not self.param.get_parent().get_enabled()
- else gtk.gdk.color_parse(color)
- )
- self._input.modify_base(gtk.STATE_NORMAL, base_color)
- if text_color:
- self._input.modify_text(gtk.STATE_NORMAL, text_color)
-
- def set_tooltip_text(self, text):
- try:
- self._input.set_tooltip_text(text)
- except AttributeError:
- pass # no tooltips for old GTK
-
-
-class MultiLineEntryParam(InputParam):
- """Provide an multi-line box for strings."""
- expand = True
-
- def __init__(self, *args, **kwargs):
- InputParam.__init__(self, *args, **kwargs)
- self._buffer = gtk.TextBuffer()
- self._buffer.set_text(self.param.get_value())
- self._buffer.connect('changed', self._mark_changed)
-
- self._view = gtk.TextView(self._buffer)
- self._view.connect('focus-out-event', self._apply_change)
- self._view.connect('key-press-event', self._handle_key_press)
-
- self._sw = gtk.ScrolledWindow()
- self._sw.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
- self._sw.add_with_viewport(self._view)
-
- self.pack_start(self._sw, True)
-
- def get_text(self):
- buf = self._buffer
- return buf.get_text(buf.get_start_iter(),
- buf.get_end_iter()).strip()
-
- def set_color(self, color):
- self._view.modify_base(gtk.STATE_NORMAL, gtk.gdk.color_parse(color))
-
- def set_tooltip_text(self, text):
- try:
- self._view.set_tooltip_text(text)
- except AttributeError:
- pass # no tooltips for old GTK
-
-
-# try:
-# import gtksourceview
-# lang_manager = gtksourceview.SourceLanguagesManager()
-# py_lang = lang_manager.get_language_from_mime_type('text/x-python')
-#
-# class PythonEditorParam(InputParam):
-# expand = True
-#
-# def __init__(self, *args, **kwargs):
-# InputParam.__init__(self, *args, **kwargs)
-#
-# buf = self._buffer = gtksourceview.SourceBuffer()
-# buf.set_language(py_lang)
-# buf.set_highlight(True)
-# buf.set_text(self.param.get_value())
-# buf.connect('changed', self._mark_changed)
-#
-# view = self._view = gtksourceview.SourceView(self._buffer)
-# view.connect('focus-out-event', self._apply_change)
-# view.connect('key-press-event', self._handle_key_press)
-# view.set_tabs_width(4)
-# view.set_insert_spaces_instead_of_tabs(True)
-# view.set_auto_indent(True)
-# view.set_border_width(2)
-#
-# scroll = gtk.ScrolledWindow()
-# scroll.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
-# scroll.add_with_viewport(view)
-# self.pack_start(scroll, True)
-#
-# def get_text(self):
-# buf = self._buffer
-# return buf.get_text(buf.get_start_iter(),
-# buf.get_end_iter()).strip()
-#
-# except ImportError:
-# print "Package 'gtksourceview' not found. No Syntax highlighting."
-# PythonEditorParam = MultiLineEntryParam
-
-class PythonEditorParam(InputParam):
-
- def __init__(self, *args, **kwargs):
- InputParam.__init__(self, *args, **kwargs)
- button = self._button = gtk.Button('Open in Editor')
- button.connect('clicked', self.open_editor)
- self.pack_start(button, True)
-
- def open_editor(self, widget=None):
- flowgraph = self.param.get_parent().get_parent()
- flowgraph.install_external_editor(self.param, self._parent)
-
- def get_text(self):
- pass # we never update the value from here
-
- def set_color(self, color):
- pass
-
- def _apply_change(self, *args):
- pass
-
-
-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)
- self._input = gtk.combo_box_new_text()
- for option in self.param.get_options(): self._input.append_text(option.get_name())
- self._input.set_active(self.param.get_option_keys().index(self.param.get_value()))
- self._input.connect('changed', self._editing_callback)
- self._input.connect('changed', self._apply_change)
- self.pack_start(self._input, False)
-
- def get_text(self):
- return self.param.get_option_keys()[self._input.get_active()]
-
- def set_tooltip_text(self, text):
- try:
- self._input.set_tooltip_text(text)
- except AttributeError:
- pass # no tooltips for old GTK
-
-
-class EnumEntryParam(InputParam):
- """Provide an entry box and drop down menu for Raw Enum types."""
-
- def __init__(self, *args, **kwargs):
- InputParam.__init__(self, *args, **kwargs)
- self._input = gtk.combo_box_entry_new_text()
- for option in self.param.get_options(): self._input.append_text(option.get_name())
- try: self._input.set_active(self.param.get_option_keys().index(self.param.get_value()))
- except:
- self._input.set_active(-1)
- self._input.get_child().set_text(self.param.get_value())
- self._input.connect('changed', self._apply_change)
- self._input.get_child().connect('changed', self._mark_changed)
- self._input.get_child().connect('focus-out-event', self._apply_change)
- self._input.get_child().connect('key-press-event', self._handle_key_press)
- self.pack_start(self._input, False)
-
- def get_text(self):
- if self._input.get_active() == -1: return self._input.get_child().get_text()
- return self.param.get_option_keys()[self._input.get_active()]
-
- def set_tooltip_text(self, text):
- try:
- if self._input.get_active() == -1: #custom entry
- self._input.get_child().set_tooltip_text(text)
- else:
- self._input.set_tooltip_text(text)
- except AttributeError:
- pass # no tooltips for old GTK
-
- def set_color(self, color):
- if self._input.get_active() == -1: #custom entry, use color
- self._input.get_child().modify_base(gtk.STATE_NORMAL, gtk.gdk.color_parse(color))
- else: #from enum, make pale background
- self._input.get_child().modify_base(gtk.STATE_NORMAL, Colors.ENTRYENUM_CUSTOM_COLOR)
-
-
-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.
- """
- #get the paths
- file_path = self.param.is_valid() and self.param.get_evaluated() or ''
- (dirname, basename) = os.path.isfile(file_path) and os.path.split(file_path) or (file_path, '')
- # check for qss theme default directory
- if self.param.get_key() == 'qt_qss_theme':
- dirname = os.path.dirname(dirname) # trim filename
- if not os.path.exists(dirname):
- platform = self.param.get_parent().get_parent().get_parent()
- dirname = os.path.join(platform.config.install_prefix,
- '/share/gnuradio/themes')
- if not os.path.exists(dirname):
- dirname = os.getcwd() # fix bad paths
-
- #build the dialog
- if self.param.get_type() == 'file_open':
- file_dialog = gtk.FileChooserDialog('Open a Data File...', self._parent,
- 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...', self._parent,
- 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(basename) #show the current filename
- else:
- raise ValueError("Can't open file chooser dialog for type " + repr(self.param.get_type()))
- file_dialog.set_current_folder(dirname) #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._input.set_text(file_path)
- self._editing_callback()
- self._apply_change()
- file_dialog.destroy() #destroy the dialog
-
-
-PARAM_MARKUP_TMPL="""\
-#set $foreground = $param.is_valid() and 'black' or 'red'
-<span foreground="$foreground" font_desc="$font"><b>$encode($param.get_name()): </b>$encode(repr($param).replace('\\n',' '))</span>"""
-
-PARAM_LABEL_MARKUP_TMPL="""\
-#set $foreground = $modified and 'foreground="blue"' or not $param.is_valid() and 'foreground="red"' or ''
-#set $underline = $has_cb and 'low' or 'none'
-<span underline="$underline" $foreground font_desc="Sans 9">$encode($param.get_name())</span>"""
-
-TIP_MARKUP_TMPL="""\
-########################################
-#def truncate(string)
- #set $max_len = 100
- #set $string = str($string)
- #if len($string) > $max_len
-$('%s...%s'%($string[:$max_len/2], $string[-$max_len/2:]))#slurp
- #else
-$string#slurp
- #end if
-#end def
-########################################
-Key: $param.get_key()
-Type: $param.get_type()
-#if $param.is_valid()
-Value: $truncate($param.get_evaluated())
-#elif len($param.get_error_messages()) == 1
-Error: $(param.get_error_messages()[0])
-#else
-Error:
- #for $error_msg in $param.get_error_messages()
- * $error_msg
- #end for
-#end if"""
-
-
-class Param(Element, _Param):
- """The graphical parameter."""
-
- def __init__(self, **kwargs):
- Element.__init__(self)
- _Param.__init__(self, **kwargs)
-
- def get_input(self, *args, **kwargs):
- """
- Get the graphical gtk class to represent this parameter.
- An enum requires and combo parameter.
- A non-enum with options gets a combined entry/combo parameter.
- All others get a standard entry parameter.
-
- Returns:
- gtk input class
- """
- if self.get_type() in ('file_open', 'file_save'):
- input_widget = FileParam(self, *args, **kwargs)
-
- elif self.is_enum():
- input_widget = EnumParam(self, *args, **kwargs)
-
- elif self.get_options():
- input_widget = EnumEntryParam(self, *args, **kwargs)
-
- elif self.get_type() == '_multiline':
- input_widget = MultiLineEntryParam(self, *args, **kwargs)
-
- elif self.get_type() == '_multiline_python_external':
- input_widget = PythonEditorParam(self, *args, **kwargs)
-
- else:
- input_widget = EntryParam(self, *args, **kwargs)
-
- return input_widget
-
- def get_markup(self):
- """
- Get the markup for this param.
-
- Returns:
- a pango markup string
- """
- return Utils.parse_template(PARAM_MARKUP_TMPL,
- param=self, font=Constants.PARAM_FONT)
diff --git a/grc/gui/ParamWidgets.py b/grc/gui/ParamWidgets.py
new file mode 100644
index 0000000000..747c3ffec5
--- /dev/null
+++ b/grc/gui/ParamWidgets.py
@@ -0,0 +1,330 @@
+# Copyright 2007-2016 Free Software Foundation, Inc.
+# This file is part of GNU Radio
+#
+# GNU Radio Companion is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# GNU Radio Companion is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
+
+from __future__ import absolute_import
+import os
+
+from gi.repository import Gtk, Gdk
+
+from . import Utils
+
+
+style_provider = Gtk.CssProvider()
+
+style_provider.load_from_data(b"""
+ #dtype_complex { background-color: #3399FF; }
+ #dtype_real { background-color: #FF8C69; }
+ #dtype_float { background-color: #FF8C69; }
+ #dtype_int { background-color: #00FF99; }
+
+ #dtype_complex_vector { background-color: #3399AA; }
+ #dtype_real_vector { background-color: #CC8C69; }
+ #dtype_float_vector { background-color: #CC8C69; }
+ #dtype_int_vector { background-color: #00CC99; }
+
+ #dtype_bool { background-color: #00FF99; }
+ #dtype_hex { background-color: #00FF99; }
+ #dtype_string { background-color: #CC66CC; }
+ #dtype_id { background-color: #DDDDDD; }
+ #dtype_stream_id { background-color: #DDDDDD; }
+ #dtype_raw { background-color: #FFFFFF; }
+
+ #enum_custom { background-color: #EEEEEE; }
+""")
+
+Gtk.StyleContext.add_provider_for_screen(
+ Gdk.Screen.get_default(),
+ style_provider,
+ Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION
+)
+
+
+class InputParam(Gtk.HBox):
+ """The base class for an input parameter inside the input parameters dialog."""
+ expand = False
+
+ def __init__(self, param, changed_callback=None, editing_callback=None, transient_for=None):
+ Gtk.HBox.__init__(self)
+
+ self.param = param
+ self._changed_callback = changed_callback
+ self._editing_callback = editing_callback
+ self._transient_for = transient_for
+
+ self.label = Gtk.Label()
+ self.label.set_size_request(Utils.scale_scalar(150), -1)
+ self.label.show()
+ self.pack_start(self.label, False, False, 0)
+
+ self.tp = None
+ self._have_pending_changes = False
+
+ self.connect('show', self._update_gui)
+
+ def set_color(self, css_name):
+ pass
+
+ def set_tooltip_text(self, text):
+ pass
+
+ def get_text(self):
+ raise NotImplementedError()
+
+ def _update_gui(self, *args):
+ """
+ Set the markup, color, tooltip, show/hide.
+ """
+ self.label.set_markup(self.param.format_label_markup(self._have_pending_changes))
+ self.set_color('dtype_' + self.param.dtype)
+
+ self.set_tooltip_text(self.param.format_tooltip_text())
+
+ if self.param.hide == 'all':
+ self.hide()
+ else:
+ self.show_all()
+
+ def _mark_changed(self, *args):
+ """
+ Mark this param as modified on change, but validate only on focus-lost
+ """
+ self._have_pending_changes = True
+ self._update_gui()
+ if self._editing_callback:
+ self._editing_callback(self, None)
+
+ def _apply_change(self, *args):
+ """
+ Handle a gui change by setting the new param value,
+ calling the callback (if applicable), and updating.
+ """
+ #set the new value
+ self.param.set_value(self.get_text())
+ #call the callback
+ if self._changed_callback:
+ self._changed_callback(self, None)
+ else:
+ self.param.validate()
+ #gui update
+ self._have_pending_changes = False
+ self._update_gui()
+
+ def _handle_key_press(self, widget, event):
+ if event.keyval == Gdk.KEY_Return and event.get_state() & Gdk.ModifierType.CONTROL_MASK:
+ self._apply_change(widget, event)
+ return True
+ return False
+
+ def apply_pending_changes(self):
+ if self._have_pending_changes:
+ self._apply_change()
+
+
+class EntryParam(InputParam):
+ """Provide an entry box for strings and numbers."""
+
+ def __init__(self, *args, **kwargs):
+ InputParam.__init__(self, *args, **kwargs)
+ self._input = Gtk.Entry()
+ self._input.set_text(self.param.get_value())
+ self._input.connect('changed', self._mark_changed)
+ self._input.connect('focus-out-event', self._apply_change)
+ self._input.connect('key-press-event', self._handle_key_press)
+ self.pack_start(self._input, True, True, 0)
+
+ def get_text(self):
+ return self._input.get_text()
+
+ def set_color(self, css_name):
+ self._input.set_name(css_name)
+
+ def set_tooltip_text(self, text):
+ self._input.set_tooltip_text(text)
+
+
+class MultiLineEntryParam(InputParam):
+ """Provide an multi-line box for strings."""
+ expand = True
+
+ def __init__(self, *args, **kwargs):
+ InputParam.__init__(self, *args, **kwargs)
+ self._buffer = Gtk.TextBuffer()
+ self._buffer.set_text(self.param.get_value())
+ self._buffer.connect('changed', self._mark_changed)
+
+ self._view = Gtk.TextView()
+ self._view.set_buffer(self._buffer)
+ self._view.connect('focus-out-event', self._apply_change)
+ self._view.connect('key-press-event', self._handle_key_press)
+
+ self._sw = Gtk.ScrolledWindow()
+ self._sw.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
+ self._sw.set_shadow_type(type=Gtk.ShadowType.IN)
+ self._sw.add(self._view)
+
+ self.pack_start(self._sw, True, True, True)
+
+ def get_text(self):
+ buf = self._buffer
+ text = buf.get_text(buf.get_start_iter(), buf.get_end_iter(),
+ include_hidden_chars=False)
+ return text.strip()
+
+ def set_color(self, css_name):
+ self._view.set_name(css_name)
+
+ def set_tooltip_text(self, text):
+ self._view.set_tooltip_text(text)
+
+
+class PythonEditorParam(InputParam):
+
+ def __init__(self, *args, **kwargs):
+ InputParam.__init__(self, *args, **kwargs)
+ button = self._button = Gtk.Button(label='Open in Editor')
+ button.connect('clicked', self.open_editor)
+ self.pack_start(button, True, True, True)
+
+ def open_editor(self, widget=None):
+ self.param.parent_flowgraph.install_external_editor(self.param, parent=self._transient_for)
+
+ def get_text(self):
+ pass # we never update the value from here
+
+ def _apply_change(self, *args):
+ pass
+
+
+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)
+ self._input = Gtk.ComboBoxText()
+ for option_name in self.param.options.values():
+ self._input.append_text(option_name)
+
+ self.param_values = list(self.param.options)
+ self._input.set_active(self.param_values.index(self.param.get_value()))
+ self._input.connect('changed', self._editing_callback)
+ self._input.connect('changed', self._apply_change)
+ self.pack_start(self._input, False, False, 0)
+
+ def get_text(self):
+ return self.param_values[self._input.get_active()]
+
+ def set_tooltip_text(self, text):
+ self._input.set_tooltip_text(text)
+
+
+class EnumEntryParam(InputParam):
+ """Provide an entry box and drop down menu for Raw Enum types."""
+
+ def __init__(self, *args, **kwargs):
+ InputParam.__init__(self, *args, **kwargs)
+ self._input = Gtk.ComboBoxText.new_with_entry()
+ for option_name in self.param.options.values():
+ self._input.append_text(option_name)
+
+ self.param_values = list(self.param.options)
+ value = self.param.get_value()
+ try:
+ self._input.set_active(self.param_values.index(value))
+ except ValueError:
+ self._input.set_active(-1)
+ self._input.get_child().set_text(value)
+
+ self._input.connect('changed', self._apply_change)
+ self._input.get_child().connect('changed', self._mark_changed)
+ self._input.get_child().connect('focus-out-event', self._apply_change)
+ self._input.get_child().connect('key-press-event', self._handle_key_press)
+ self.pack_start(self._input, False, False, 0)
+
+ @property
+ def has_custom_value(self):
+ return self._input.get_active() == -1
+
+ def get_text(self):
+ if self.has_custom_value:
+ return self._input.get_child().get_text()
+ else:
+ return self.param_values[self._input.get_active()]
+
+ def set_tooltip_text(self, text):
+ if self.has_custom_value: # custom entry
+ self._input.get_child().set_tooltip_text(text)
+ else:
+ self._input.set_tooltip_text(text)
+
+ def set_color(self, css_name):
+ self._input.get_child().set_name(
+ css_name if not self.has_custom_value else 'enum_custom'
+ )
+
+
+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)
+ self._open_button = Gtk.Button(label='...')
+ self._open_button.connect('clicked', self._handle_clicked)
+ self.pack_start(self._open_button, False, False, 0)
+
+ 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.
+ """
+ # get the paths
+ file_path = self.param.is_valid() and self.param.get_evaluated() or ''
+ (dirname, basename) = os.path.isfile(file_path) and os.path.split(file_path) or (file_path, '')
+ # check for qss theme default directory
+ if self.param.key == 'qt_qss_theme':
+ dirname = os.path.dirname(dirname) # trim filename
+ if not os.path.exists(dirname):
+ config = self.param.parent_platform.config
+ dirname = os.path.join(config.install_prefix, '/share/gnuradio/themes')
+ if not os.path.exists(dirname):
+ dirname = os.getcwd() # fix bad paths
+
+ # build the dialog
+ if self.param.dtype == 'file_open':
+ file_dialog = Gtk.FileChooserDialog(
+ 'Open a Data File...', None, Gtk.FileChooserAction.OPEN,
+ ('gtk-cancel', Gtk.ResponseType.CANCEL, 'gtk-open', Gtk.ResponseType.OK),
+ transient_for=self._transient_for,
+ )
+ elif self.param.dtype == 'file_save':
+ file_dialog = Gtk.FileChooserDialog(
+ 'Save a Data File...', None, Gtk.FileChooserAction.SAVE,
+ ('gtk-cancel', Gtk.ResponseType.CANCEL, 'gtk-save', Gtk.ResponseType.OK),
+ transient_for=self._transient_for,
+ )
+ file_dialog.set_do_overwrite_confirmation(True)
+ file_dialog.set_current_name(basename) # show the current filename
+ else:
+ raise ValueError("Can't open file chooser dialog for type " + repr(self.param.dtype))
+ file_dialog.set_current_folder(dirname) # current directory
+ file_dialog.set_select_multiple(False)
+ file_dialog.set_local_only(True)
+ if Gtk.ResponseType.OK == file_dialog.run(): # run the dialog
+ file_path = file_dialog.get_filename() # get the file path
+ self._input.set_text(file_path)
+ self._editing_callback()
+ self._apply_change()
+ file_dialog.destroy() # destroy the dialog
diff --git a/grc/gui/ParserErrorsDialog.py b/grc/gui/ParserErrorsDialog.py
index 68ee459414..050b9a4f98 100644
--- a/grc/gui/ParserErrorsDialog.py
+++ b/grc/gui/ParserErrorsDialog.py
@@ -17,14 +17,16 @@ 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
+from __future__ import absolute_import
-from Constants import MIN_DIALOG_WIDTH, MIN_DIALOG_HEIGHT
+import six
+from gi.repository import Gtk, GObject
-class ParserErrorsDialog(gtk.Dialog):
+from .Constants import MIN_DIALOG_WIDTH, MIN_DIALOG_HEIGHT
+
+
+class ParserErrorsDialog(Gtk.Dialog):
"""
A dialog for viewing parser errors
"""
@@ -36,32 +38,32 @@ class ParserErrorsDialog(gtk.Dialog):
Args:
block: a block instance
"""
- gtk.Dialog.__init__(self, title='Parser Errors', buttons=(gtk.STOCK_CLOSE, gtk.RESPONSE_ACCEPT))
+ GObject.GObject.__init__(self, title='Parser Errors', buttons=(Gtk.STOCK_CLOSE, Gtk.ResponseType.ACCEPT))
self._error_logs = None
- self.tree_store = gtk.TreeStore(str)
+ self.tree_store = Gtk.TreeStore(str)
self.update_tree_store(error_logs)
- column = gtk.TreeViewColumn('XML Parser Errors by Filename')
- renderer = gtk.CellRendererText()
+ column = Gtk.TreeViewColumn('XML Parser Errors by Filename')
+ renderer = Gtk.CellRendererText()
column.pack_start(renderer, True)
column.add_attribute(renderer, 'text', 0)
column.set_sort_column_id(0)
- self.tree_view = tree_view = gtk.TreeView(self.tree_store)
+ self.tree_view = tree_view = Gtk.TreeView(self.tree_store)
tree_view.set_enable_search(False) # disable pop up search box
tree_view.set_search_column(-1) # really disable search
tree_view.set_reorderable(False)
tree_view.set_headers_visible(False)
- tree_view.get_selection().set_mode(gtk.SELECTION_NONE)
+ tree_view.get_selection().set_mode(Gtk.SelectionMode.NONE)
tree_view.append_column(column)
for row in self.tree_store:
tree_view.expand_row(row.path, False)
- scrolled_window = gtk.ScrolledWindow()
- scrolled_window.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
- scrolled_window.add_with_viewport(tree_view)
+ scrolled_window = Gtk.ScrolledWindow()
+ scrolled_window.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
+ scrolled_window.add(tree_view)
self.vbox.pack_start(scrolled_window, True)
self.set_size_request(2*MIN_DIALOG_WIDTH, MIN_DIALOG_HEIGHT)
@@ -71,7 +73,7 @@ class ParserErrorsDialog(gtk.Dialog):
"""set up data model"""
self.tree_store.clear()
self._error_logs = error_logs
- for filename, errors in error_logs.iteritems():
+ for filename, errors in six.iteritems(error_logs):
parent = self.tree_store.append(None, [str(filename)])
try:
with open(filename, 'r') as fp:
@@ -95,6 +97,6 @@ class ParserErrorsDialog(gtk.Dialog):
Returns:
true if the response was accept
"""
- response = gtk.Dialog.run(self)
+ response = Gtk.Dialog.run(self)
self.destroy()
- return response == gtk.RESPONSE_ACCEPT
+ return response == Gtk.ResponseType.ACCEPT
diff --git a/grc/gui/Platform.py b/grc/gui/Platform.py
index 500df1cce4..2a38bc619e 100644
--- a/grc/gui/Platform.py
+++ b/grc/gui/Platform.py
@@ -17,25 +17,21 @@ 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
+from __future__ import absolute_import, print_function
-from ..core.Platform import Platform as _Platform
+import sys
+import os
-from .Config import Config as _Config
-from .Block import Block as _Block
-from .Connection import Connection as _Connection
-from .Element import Element
-from .FlowGraph import FlowGraph as _FlowGraph
-from .Param import Param as _Param
-from .Port import Port as _Port
+from .Config import Config
+from . import canvas
+from ..core.platform import Platform as CorePlatform
+from ..core.utils.backports import ChainMap
-class Platform(Element, _Platform):
+class Platform(CorePlatform):
def __init__(self, *args, **kwargs):
- Element.__init__(self)
- _Platform.__init__(self, *args, **kwargs)
+ CorePlatform.__init__(self, *args, **kwargs)
# Ensure conf directories
gui_prefs_file = self.config.gui_prefs_file
@@ -58,14 +54,24 @@ class Platform(Element, _Platform):
import shutil
shutil.move(old_gui_prefs_file, gui_prefs_file)
except Exception as e:
- print >> sys.stderr, e
+ print(e, file=sys.stderr)
##############################################
- # Constructors
+ # Factories
##############################################
- FlowGraph = _FlowGraph
- Connection = _Connection
- Block = _Block
- Port = _Port
- Param = _Param
- Config = _Config
+ Config = Config
+ FlowGraph = canvas.FlowGraph
+ Connection = canvas.Connection
+
+ def new_block_class(self, block_id, **data):
+ cls = CorePlatform.new_block_class(self, block_id, **data)
+ return canvas.Block.make_cls_with_base(cls)
+
+ block_classes_build_in = {key: canvas.Block.make_cls_with_base(cls)
+ for key, cls in CorePlatform.block_classes_build_in.items()}
+ block_classes = ChainMap({}, block_classes_build_in)
+
+ port_classes = {key: canvas.Port.make_cls_with_base(cls)
+ for key, cls in CorePlatform.port_classes.items()}
+ param_classes = {key: canvas.Param.make_cls_with_base(cls)
+ for key, cls in CorePlatform.param_classes.items()}
diff --git a/grc/gui/Port.py b/grc/gui/Port.py
deleted file mode 100644
index 6314b7ede8..0000000000
--- a/grc/gui/Port.py
+++ /dev/null
@@ -1,277 +0,0 @@
-"""
-Copyright 2007, 2008, 2009 Free Software Foundation, Inc.
-This file is part of GNU Radio
-
-GNU Radio Companion is free software; you can redistribute it and/or
-modify it under the terms of the GNU General Public License
-as published by the Free Software Foundation; either version 2
-of the License, or (at your option) any later version.
-
-GNU Radio Companion is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-GNU General Public License for more details.
-
-You should have received a copy of the GNU General Public License
-along with this program; if not, write to the Free Software
-Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
-"""
-
-import pygtk
-pygtk.require('2.0')
-import gtk
-
-from . import Actions, Colors, Utils
-from .Constants import (
- PORT_SEPARATION, PORT_SPACING, CONNECTOR_EXTENSION_MINIMAL,
- CONNECTOR_EXTENSION_INCREMENT, PORT_LABEL_PADDING, PORT_MIN_WIDTH, PORT_LABEL_HIDDEN_WIDTH, PORT_FONT
-)
-from .Element import Element
-from ..core.Constants import DEFAULT_DOMAIN, GR_MESSAGE_DOMAIN
-
-from ..core.Port import Port as _Port
-
-PORT_MARKUP_TMPL="""\
-<span foreground="black" font_desc="$font">$encode($port.get_name())</span>"""
-
-
-class Port(_Port, Element):
- """The graphical port."""
-
- def __init__(self, block, n, dir):
- """
- Port contructor.
- Create list of connector coordinates.
- """
- _Port.__init__(self, block, n, dir)
- Element.__init__(self)
- self.W = self.H = self.w = self.h = 0
- self._connector_coordinate = (0, 0)
- self._connector_length = 0
- self._hovering = True
- self._force_label_unhidden = False
-
- def create_shapes(self):
- """Create new areas and labels for the port."""
- Element.create_shapes(self)
- if self.get_hide():
- return # this port is hidden, no need to create shapes
- if self.get_domain() == GR_MESSAGE_DOMAIN:
- pass
- elif self.get_domain() != DEFAULT_DOMAIN:
- self.line_attributes[0] = 2
- #get current rotation
- rotation = self.get_rotation()
- #get all sibling ports
- ports = self.get_parent().get_sources_gui() \
- if self.is_source else self.get_parent().get_sinks_gui()
- ports = filter(lambda p: not p.get_hide(), ports)
- #get the max width
- self.W = max([port.W for port in ports] + [PORT_MIN_WIDTH])
- W = self.W if not self._label_hidden() else PORT_LABEL_HIDDEN_WIDTH
- #get a numeric index for this port relative to its sibling ports
- try:
- index = ports.index(self)
- except:
- if hasattr(self, '_connector_length'):
- del self._connector_length
- return
- length = len(filter(lambda p: not p.get_hide(), ports))
- #reverse the order of ports for these rotations
- if rotation in (180, 270):
- index = length-index-1
-
- port_separation = PORT_SEPARATION \
- if not self.get_parent().has_busses[self.is_source] \
- else max([port.H for port in ports]) + PORT_SPACING
-
- offset = (self.get_parent().H - (length-1)*port_separation - self.H)/2
- #create areas and connector coordinates
- if (self.is_sink and rotation == 0) or (self.is_source and rotation == 180):
- x = -W
- y = port_separation*index+offset
- self.add_area((x, y), (W, self.H))
- self._connector_coordinate = (x-1, y+self.H/2)
- elif (self.is_source and rotation == 0) or (self.is_sink and rotation == 180):
- x = self.get_parent().W
- y = port_separation*index+offset
- self.add_area((x, y), (W, self.H))
- self._connector_coordinate = (x+1+W, y+self.H/2)
- elif (self.is_source and rotation == 90) or (self.is_sink and rotation == 270):
- y = -W
- x = port_separation*index+offset
- self.add_area((x, y), (self.H, W))
- self._connector_coordinate = (x+self.H/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*index+offset
- self.add_area((x, y), (self.H, W))
- self._connector_coordinate = (x+self.H/2, y+1+W)
- #the connector length
- self._connector_length = CONNECTOR_EXTENSION_MINIMAL + CONNECTOR_EXTENSION_INCREMENT*index
-
- def create_labels(self):
- """Create the labels for the socket."""
- Element.create_labels(self)
- self._bg_color = Colors.get_color(self.get_color())
- # create the layout
- layout = gtk.DrawingArea().create_pango_layout('')
- layout.set_markup(Utils.parse_template(PORT_MARKUP_TMPL, port=self, font=PORT_FONT))
- self.w, self.h = layout.get_pixel_size()
- self.W = 2 * PORT_LABEL_PADDING + self.w
- self.H = 2 * PORT_LABEL_PADDING + self.h * (
- 3 if self.get_type() == 'bus' else 1)
- self.H += self.H % 2
- # create the pixmap
- pixmap = self.get_parent().get_parent().new_pixmap(self.w, self.h)
- gc = pixmap.new_gc()
- gc.set_foreground(self._bg_color)
- pixmap.draw_rectangle(gc, True, 0, 0, self.w, self.h)
- pixmap.draw_layout(gc, 0, 0, layout)
- # create vertical and horizontal pixmaps
- self.horizontal_label = pixmap
- if self.is_vertical():
- self.vertical_label = self.get_parent().get_parent().new_pixmap(self.h, self.w)
- Utils.rotate_pixmap(gc, self.horizontal_label, self.vertical_label)
-
- def draw(self, gc, window):
- """
- Draw the socket with a label.
-
- Args:
- gc: the graphics context
- window: the gtk window to draw on
- """
- Element.draw(
- self, gc, window, bg_color=self._bg_color,
- border_color=self.is_highlighted() and Colors.HIGHLIGHT_COLOR or
- self.get_parent().is_dummy_block and Colors.MISSING_BLOCK_BORDER_COLOR or
- Colors.BORDER_COLOR,
- )
- if not self._areas_list or self._label_hidden():
- return # this port is either hidden (no areas) or folded (no label)
- X, Y = self.get_coordinate()
- (x, y), (w, h) = self._areas_list[0] # use the first area's sizes to place the labels
- if self.is_horizontal():
- window.draw_drawable(gc, self.horizontal_label, 0, 0, x+X+(self.W-self.w)/2, y+Y+(self.H-self.h)/2, -1, -1)
- elif self.is_vertical():
- window.draw_drawable(gc, self.vertical_label, 0, 0, x+X+(self.H-self.h)/2, y+Y+(self.W-self.w)/2, -1, -1)
-
- def get_connector_coordinate(self):
- """
- Get the coordinate where connections may attach to.
-
- Returns:
- 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.
-
- Returns:
- 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.
-
- Returns:
- the length in pixels
- """
- return self._connector_length
-
- def get_rotation(self):
- """
- Get the parent's rotation rather than self.
-
- Returns:
- the parent's rotation
- """
- return self.get_parent().get_rotation()
-
- def move(self, delta_coor):
- """
- Move the parent rather than self.
-
- Args:
- delta_corr: the (delta_x, delta_y) tuple
- """
- self.get_parent().move(delta_coor)
-
- def rotate(self, direction):
- """
- Rotate the parent rather than self.
-
- Args:
- direction: degrees to rotate
- """
- self.get_parent().rotate(direction)
-
- def get_coordinate(self):
- """
- Get the parent's coordinate rather than self.
-
- Returns:
- the parents coordinate
- """
- return self.get_parent().get_coordinate()
-
- def set_highlighted(self, highlight):
- """
- Set the parent highlight rather than self.
-
- Args:
- highlight: true to enable highlighting
- """
- self.get_parent().set_highlighted(highlight)
-
- def is_highlighted(self):
- """
- Get the parent's is highlight rather than self.
-
- Returns:
- the parent's highlighting status
- """
- return self.get_parent().is_highlighted()
-
- def _label_hidden(self):
- """
- Figure out if the label should be hidden
-
- Returns:
- true if the label should not be shown
- """
- return self._hovering and not self._force_label_unhidden and Actions.TOGGLE_AUTO_HIDE_PORT_LABELS.get_active()
-
- def force_label_unhidden(self, enable=True):
- """
- Disable showing the label on mouse-over for this port
-
- Args:
- enable: true to override the mouse-over behaviour
- """
- self._force_label_unhidden = enable
-
- def mouse_over(self):
- """
- Called from flow graph on mouse-over
- """
- self._hovering = False
- return Actions.TOGGLE_AUTO_HIDE_PORT_LABELS.get_active() # only redraw if necessary
-
- def mouse_out(self):
- """
- Called from flow graph on mouse-out
- """
- self._hovering = True
- return Actions.TOGGLE_AUTO_HIDE_PORT_LABELS.get_active() # only redraw if necessary
diff --git a/grc/gui/Preferences.py b/grc/gui/Preferences.py
deleted file mode 100644
index d377018eb4..0000000000
--- a/grc/gui/Preferences.py
+++ /dev/null
@@ -1,173 +0,0 @@
-"""
-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 ConfigParser
-
-
-HEADER = """\
-# This contains only GUI settings for GRC and is not meant for users to edit.
-#
-# GRC settings not accessible through the GUI are in gnuradio.conf under
-# section [grc].
-
-"""
-
-_platform = None
-_config_parser = ConfigParser.SafeConfigParser()
-
-
-def file_extension():
- return '.grc'
-
-
-def load(platform):
- global _platform
- _platform = platform
- # create sections
- for section in ['main', 'files_open', 'files_recent']:
- try:
- _config_parser.add_section(section)
- except Exception, e:
- print e
- try:
- _config_parser.read(_platform.get_prefs_file())
- except Exception as err:
- print >> sys.stderr, err
-
-
-def save():
- try:
- with open(_platform.get_prefs_file(), 'w') as fp:
- fp.write(HEADER)
- _config_parser.write(fp)
- except Exception as err:
- print >> sys.stderr, err
-
-
-def entry(key, value=None, default=None):
- if value is not None:
- _config_parser.set('main', key, str(value))
- result = value
- else:
- _type = type(default) if default is not None else str
- getter = {
- bool: _config_parser.getboolean,
- int: _config_parser.getint,
- }.get(_type, _config_parser.get)
- try:
- result = getter('main', key)
- except (AttributeError, ConfigParser.Error):
- result = _type() if default is None else default
- return result
-
-
-###########################################################################
-# Special methods for specific program functionalities
-###########################################################################
-
-def main_window_size(size=None):
- if size is None:
- size = [None, None]
- w = entry('main_window_width', size[0], default=1)
- h = entry('main_window_height', size[1], default=1)
- return w, h
-
-
-def file_open(filename=None):
- return entry('file_open', filename, default='')
-
-
-def set_file_list(key, files):
- _config_parser.remove_section(key) # clear section
- _config_parser.add_section(key)
- for i, filename in enumerate(files):
- _config_parser.set(key, '%s_%d' % (key, i), filename)
-
-
-def get_file_list(key):
- try:
- files = [value for name, value in _config_parser.items(key)
- if name.startswith('%s_' % key)]
- except (AttributeError, ConfigParser.Error):
- files = []
- return files
-
-
-def get_open_files():
- return get_file_list('files_open')
-
-
-def set_open_files(files):
- return set_file_list('files_open', files)
-
-
-def get_recent_files():
- """ Gets recent files, removes any that do not exist and re-saves it """
- files = filter(os.path.exists, get_file_list('files_recent'))
- set_recent_files(files)
- return files
-
-
-def set_recent_files(files):
- return set_file_list('files_recent', files)
-
-
-def add_recent_file(file_name):
- # double check file_name
- if os.path.exists(file_name):
- recent_files = get_recent_files()
- if file_name in recent_files:
- recent_files.remove(file_name) # Attempt removal
- recent_files.insert(0, file_name) # Insert at start
- set_recent_files(recent_files[:10]) # Keep up to 10 files
-
-
-def console_window_position(pos=None):
- return entry('console_window_position', pos, default=-1) or 1
-
-
-def blocks_window_position(pos=None):
- return entry('blocks_window_position', pos, default=-1) or 1
-
-
-def variable_editor_position(pos=None, sidebar=False):
- # Figure out default
- if sidebar:
- w, h = main_window_size()
- return entry('variable_editor_sidebar_position', pos, default=int(h*0.7))
- else:
- return entry('variable_editor_position', pos, default=int(blocks_window_position()*0.5))
-
-
-def variable_editor_sidebar(pos=None):
- return entry('variable_editor_sidebar', pos, default=False)
-
-
-def variable_editor_confirm_delete(pos=None):
- return entry('variable_editor_confirm_delete', pos, default=True)
-
-
-def xterm_missing(cmd=None):
- return entry('xterm_missing', cmd, default='INVALID_XTERM_SETTING')
-
-
-def screen_shot_background_transparent(transparent=None):
- return entry('screen_shot_background_transparent', transparent, default=False)
diff --git a/grc/gui/PropsDialog.py b/grc/gui/PropsDialog.py
index bb0aa8ad05..ac4506a3d8 100644
--- a/grc/gui/PropsDialog.py
+++ b/grc/gui/PropsDialog.py
@@ -17,118 +17,91 @@ 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
+from __future__ import absolute_import
+from gi.repository import Gtk, Gdk, GObject, Pango
-import Actions
-from Dialogs import SimpleTextDisplay
-from Constants import MIN_DIALOG_WIDTH, MIN_DIALOG_HEIGHT, FONT_SIZE
-import Utils
-import pango
+from . import Actions, Utils, Constants
+from .Dialogs import SimpleTextDisplay
+import six
-TAB_LABEL_MARKUP_TMPL="""\
-#set $foreground = not $valid and 'foreground="red"' or ''
-<span $foreground>$encode($tab)</span>"""
-
-def get_title_label(title):
- """
- Get a title label for the params window.
- The title will be bold, underlined, and left justified.
-
- Args:
- title: the text of the title
-
- Returns:
- a gtk object
- """
- label = gtk.Label()
- label.set_markup('\n<b><span underline="low">%s</span>:</b>\n'%title)
- hbox = gtk.HBox()
- hbox.pack_start(label, False, False, padding=11)
- return hbox
-
-
-class PropsDialog(gtk.Dialog):
+class PropsDialog(Gtk.Dialog):
"""
A dialog to set block parameters, view errors, and view documentation.
"""
- def __init__(self, block, parent):
+ def __init__(self, parent, block):
"""
Properties dialog constructor.
- Args:
+ Args:%
block: a block instance
"""
- self._hash = 0
- gtk.Dialog.__init__(
+ Gtk.Dialog.__init__(
self,
- title='Properties: %s' % block.get_name(),
- parent=parent,
- buttons=(gtk.STOCK_OK, gtk.RESPONSE_ACCEPT,
- gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT,
- gtk.STOCK_APPLY, gtk.RESPONSE_APPLY)
+ title='Properties: ' + block.label,
+ transient_for=parent,
+ modal=True,
+ destroy_with_parent=True,
+ )
+ self.add_buttons(
+ Gtk.STOCK_OK, Gtk.ResponseType.ACCEPT,
+ Gtk.STOCK_CANCEL, Gtk.ResponseType.REJECT,
+ Gtk.STOCK_APPLY, Gtk.ResponseType.APPLY,
)
- self.set_response_sensitive(gtk.RESPONSE_APPLY, False)
+ self.set_response_sensitive(Gtk.ResponseType.APPLY, False)
self.set_size_request(*Utils.scale(
- (MIN_DIALOG_WIDTH, MIN_DIALOG_HEIGHT)
+ (Constants.MIN_DIALOG_WIDTH, Constants.MIN_DIALOG_HEIGHT)
))
+
self._block = block
- self._parent = parent
+ self._hash = 0
- vpaned = gtk.VPaned()
- self.vbox.pack_start(vpaned)
+ vpaned = Gtk.VPaned()
+ self.vbox.pack_start(vpaned, True, True, 0)
# Notebook to hold param boxes
- notebook = gtk.Notebook()
+ notebook = self.notebook = Gtk.Notebook()
notebook.set_show_border(False)
notebook.set_scrollable(True) # scroll arrows for page tabs
- notebook.set_tab_pos(gtk.POS_TOP)
+ notebook.set_tab_pos(Gtk.PositionType.TOP)
vpaned.pack1(notebook, True)
# Params boxes for block parameters
- self._params_boxes = list()
- for tab in block.get_param_tab_labels():
- label = gtk.Label()
- vbox = gtk.VBox()
- scroll_box = gtk.ScrolledWindow()
- scroll_box.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
- scroll_box.add_with_viewport(vbox)
- notebook.append_page(scroll_box, label)
- self._params_boxes.append((tab, label, vbox))
+ self._params_boxes = []
+ self._build_param_tab_boxes()
# Docs for the block
self._docs_text_display = doc_view = SimpleTextDisplay()
- doc_view.get_buffer().create_tag('b', weight=pango.WEIGHT_BOLD)
- self._docs_box = gtk.ScrolledWindow()
- self._docs_box.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
- self._docs_box.add_with_viewport(self._docs_text_display)
- notebook.append_page(self._docs_box, gtk.Label("Documentation"))
+ doc_view.get_buffer().create_tag('b', weight=Pango.Weight.BOLD)
+ self._docs_box = Gtk.ScrolledWindow()
+ self._docs_box.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
+ self._docs_box.add(self._docs_text_display)
+ notebook.append_page(self._docs_box, Gtk.Label(label="Documentation"))
# Generated code for the block
if Actions.TOGGLE_SHOW_CODE_PREVIEW_TAB.get_active():
self._code_text_display = code_view = SimpleTextDisplay()
- code_view.set_wrap_mode(gtk.WRAP_NONE)
- code_view.get_buffer().create_tag('b', weight=pango.WEIGHT_BOLD)
- code_view.modify_font(pango.FontDescription(
- 'monospace %d' % FONT_SIZE))
- code_box = gtk.ScrolledWindow()
- code_box.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
- code_box.add_with_viewport(self._code_text_display)
- notebook.append_page(code_box, gtk.Label("Generated Code"))
+ code_view.set_wrap_mode(Gtk.WrapMode.NONE)
+ code_view.get_buffer().create_tag('b', weight=Pango.Weight.BOLD)
+ code_view.set_monospace(True)
+ # todo: set font size in non-deprecated way
+ # code_view.override_font(Pango.FontDescription('monospace %d' % Constants.FONT_SIZE))
+ code_box = Gtk.ScrolledWindow()
+ code_box.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
+ code_box.add(self._code_text_display)
+ notebook.append_page(code_box, Gtk.Label(label="Generated Code"))
else:
self._code_text_display = None
# Error Messages for the block
self._error_messages_text_display = SimpleTextDisplay()
- self._error_box = gtk.ScrolledWindow()
- self._error_box.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
- self._error_box.add_with_viewport(self._error_messages_text_display)
+ self._error_box = Gtk.ScrolledWindow()
+ self._error_box.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
+ self._error_box.add(self._error_messages_text_display)
vpaned.pack2(self._error_box)
- vpaned.set_position(int(0.65 * MIN_DIALOG_HEIGHT))
+ vpaned.set_position(int(0.65 * Constants.MIN_DIALOG_HEIGHT))
# Connect events
self.connect('key-press-event', self._handle_key_press)
@@ -136,6 +109,27 @@ class PropsDialog(gtk.Dialog):
self.connect('response', self._handle_response)
self.show_all() # show all (performs initial gui update)
+ def _build_param_tab_boxes(self):
+ categories = (p.category for p in self._block.params.values())
+
+ def unique_categories():
+ seen = {Constants.DEFAULT_PARAM_TAB}
+ yield Constants.DEFAULT_PARAM_TAB
+ for cat in categories:
+ if cat in seen:
+ continue
+ yield cat
+ seen.add(cat)
+
+ for category in unique_categories():
+ label = Gtk.Label()
+ vbox = Gtk.VBox()
+ scroll_box = Gtk.ScrolledWindow()
+ scroll_box.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
+ scroll_box.add(vbox)
+ self.notebook.append_page(scroll_box, label)
+ self._params_boxes.append((category, label, vbox))
+
def _params_changed(self):
"""
Have the params in this dialog changed?
@@ -148,25 +142,23 @@ class PropsDialog(gtk.Dialog):
true if changed
"""
old_hash = self._hash
- # create a tuple of things from each param that affects the params box
- self._hash = hash(tuple([(
- hash(param), param.get_name(), param.get_type(),
- param.get_hide() == 'all',
- ) for param in self._block.get_params()]))
- return self._hash != old_hash
+ new_hash = self._hash = hash(tuple(
+ (hash(param), param.name, param.dtype, param.hide == 'all',)
+ for param in self._block.params.values()
+ ))
+ return new_hash != old_hash
def _handle_changed(self, *args):
"""
A change occurred within a param:
Rewrite/validate the block and update the gui.
"""
- # update for the block
self._block.rewrite()
self._block.validate()
self.update_gui()
def _activate_apply(self, *args):
- self.set_response_sensitive(gtk.RESPONSE_APPLY, True)
+ self.set_response_sensitive(Gtk.ResponseType.APPLY, True)
def update_gui(self, widget=None, force=False):
"""
@@ -177,45 +169,49 @@ class PropsDialog(gtk.Dialog):
Update the documentation block.
Hide the box if there are no docs.
"""
- # update the params box
if force or self._params_changed():
# hide params box before changing
- for tab, label, vbox in self._params_boxes:
- vbox.hide_all()
+ for category, label, vbox in self._params_boxes:
+ vbox.hide()
# empty the params box
for child in vbox.get_children():
vbox.remove(child)
- child.destroy()
+ # child.destroy() # disabled because it throws errors...
# repopulate the params box
box_all_valid = True
- for param in filter(lambda p: p.get_tab_label() == tab, self._block.get_params()):
- if param.get_hide() == 'all':
+ for param in self._block.params.values():
+ # todo: why do we even rebuild instead of really hiding params?
+ if param.category != category or param.hide == 'all':
continue
box_all_valid = box_all_valid and param.is_valid()
- input_widget = param.get_input(self, self._handle_changed, self._activate_apply)
- vbox.pack_start(input_widget, input_widget.expand)
- label.set_markup(Utils.parse_template(TAB_LABEL_MARKUP_TMPL, valid=box_all_valid, tab=tab))
- # show params box with new params
- vbox.show_all()
- # update the errors box
+
+ input_widget = param.get_input(self._handle_changed, self._activate_apply,
+ transient_for=self.get_transient_for())
+ input_widget.show_all()
+ vbox.pack_start(input_widget, input_widget.expand, True, 1)
+
+ label.set_markup('<span {color}>{name}</span>'.format(
+ color='foreground="red"' if not box_all_valid else '', name=Utils.encode(category)
+ ))
+ vbox.show() # show params box with new params
+
if self._block.is_valid():
self._error_box.hide()
else:
self._error_box.show()
messages = '\n\n'.join(self._block.get_error_messages())
self._error_messages_text_display.set_text(messages)
- # update the docs box
+
self._update_docs_page()
- # update the generated code
self._update_generated_code_page()
def _update_docs_page(self):
"""Show documentation from XML and try to display best matching docstring"""
- buffer = self._docs_text_display.get_buffer()
- buffer.delete(buffer.get_start_iter(), buffer.get_end_iter())
- pos = buffer.get_end_iter()
+ buf = self._docs_text_display.get_buffer()
+ buf.delete(buf.get_start_iter(), buf.get_end_iter())
+ pos = buf.get_end_iter()
- docstrings = self._block.get_doc()
+ docstrings = self._block.documentation
if not docstrings:
return
@@ -223,77 +219,71 @@ class PropsDialog(gtk.Dialog):
from_xml = docstrings.pop('', '')
for line in from_xml.splitlines():
if line.lstrip() == line and line.endswith(':'):
- buffer.insert_with_tags_by_name(pos, line + '\n', 'b')
+ buf.insert_with_tags_by_name(pos, line + '\n', 'b')
else:
- buffer.insert(pos, line + '\n')
+ buf.insert(pos, line + '\n')
if from_xml:
- buffer.insert(pos, '\n')
+ buf.insert(pos, '\n')
# if given the current parameters an exact match can be made
- block_constructor = self._block.get_make().rsplit('.', 2)[-1]
+ block_constructor = self._block.templates.render('make').rsplit('.', 2)[-1]
block_class = block_constructor.partition('(')[0].strip()
if block_class in docstrings:
docstrings = {block_class: docstrings[block_class]}
# show docstring(s) extracted from python sources
- for cls_name, docstring in docstrings.iteritems():
- buffer.insert_with_tags_by_name(pos, cls_name + '\n', 'b')
- buffer.insert(pos, docstring + '\n\n')
+ for cls_name, docstring in six.iteritems(docstrings):
+ buf.insert_with_tags_by_name(pos, cls_name + '\n', 'b')
+ buf.insert(pos, docstring + '\n\n')
pos.backward_chars(2)
- buffer.delete(pos, buffer.get_end_iter())
+ buf.delete(pos, buf.get_end_iter())
def _update_generated_code_page(self):
if not self._code_text_display:
return # user disabled code preview
- buffer = self._code_text_display.get_buffer()
+ buf = self._code_text_display.get_buffer()
block = self._block
- key = block.get_key()
+ key = block.key
if key == 'epy_block':
- src = block.get_param('_source_code').get_value()
+ src = block.params['_source_code'].get_value()
elif key == 'epy_module':
- src = block.get_param('source_code').get_value()
+ src = block.params['source_code'].get_value()
else:
src = ''
def insert(header, text):
if not text:
return
- buffer.insert_with_tags_by_name(buffer.get_end_iter(), header, 'b')
- buffer.insert(buffer.get_end_iter(), text)
-
- buffer.delete(buffer.get_start_iter(), buffer.get_end_iter())
- insert('# Imports\n', '\n'.join(block.get_imports()))
- if key.startswith('variable'):
- insert('\n\n# Variables\n', block.get_var_make())
- insert('\n\n# Blocks\n', block.get_make())
+ buf.insert_with_tags_by_name(buf.get_end_iter(), header, 'b')
+ buf.insert(buf.get_end_iter(), text)
+
+ buf.delete(buf.get_start_iter(), buf.get_end_iter())
+ insert('# Imports\n', block.templates.render('imports').strip('\n'))
+ if block.is_variable:
+ insert('\n\n# Variables\n', block.templates.render('var_make'))
+ insert('\n\n# Blocks\n', block.templates.render('make'))
if src:
- insert('\n\n# External Code ({}.py)\n'.format(block.get_id()), src)
+ insert('\n\n# External Code ({}.py)\n'.format(block.name), src)
def _handle_key_press(self, widget, event):
- """
- Handle key presses from the keyboard.
- Call the ok response when enter is pressed.
-
- Returns:
- false to forward the keypress
- """
- if (event.keyval == gtk.keysyms.Return and
- event.state & gtk.gdk.CONTROL_MASK == 0 and
- not isinstance(widget.get_focus(), gtk.TextView)
- ):
- self.response(gtk.RESPONSE_ACCEPT)
+ close_dialog = (
+ event.keyval == Gdk.KEY_Return and
+ event.get_state() & Gdk.ModifierType.CONTROL_MASK == 0 and
+ not isinstance(widget.get_focus(), Gtk.TextView)
+ )
+ if close_dialog:
+ self.response(Gtk.ResponseType.ACCEPT)
return True # handled here
+
return False # forward the keypress
def _handle_response(self, widget, response):
- if response in (gtk.RESPONSE_APPLY, gtk.RESPONSE_ACCEPT):
+ if response in (Gtk.ResponseType.APPLY, Gtk.ResponseType.ACCEPT):
for tab, label, vbox in self._params_boxes:
for child in vbox.get_children():
child.apply_pending_changes()
- self.set_response_sensitive(gtk.RESPONSE_APPLY, False)
+ self.set_response_sensitive(Gtk.ResponseType.APPLY, False)
return True
return False
-
-
diff --git a/grc/gui/StateCache.py b/grc/gui/StateCache.py
index 3cdb5f30ce..ef260d6091 100644
--- a/grc/gui/StateCache.py
+++ b/grc/gui/StateCache.py
@@ -17,8 +17,9 @@ along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
"""
-import Actions
-from Constants import STATE_CACHE_SIZE
+from __future__ import absolute_import
+from . import Actions
+from .Constants import STATE_CACHE_SIZE
class StateCache(object):
"""
@@ -98,5 +99,5 @@ class StateCache(object):
"""
Update the undo and redo actions based on the number of next and prev states.
"""
- Actions.FLOW_GRAPH_REDO.set_sensitive(self.num_next_states != 0)
- Actions.FLOW_GRAPH_UNDO.set_sensitive(self.num_prev_states != 0)
+ Actions.FLOW_GRAPH_REDO.set_enabled(self.num_next_states != 0)
+ Actions.FLOW_GRAPH_UNDO.set_enabled(self.num_prev_states != 0)
diff --git a/grc/gui/Utils.py b/grc/gui/Utils.py
index 3ab8d2009e..f47c2e6b97 100644
--- a/grc/gui/Utils.py
+++ b/grc/gui/Utils.py
@@ -17,37 +17,14 @@ along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
"""
-import pygtk
-pygtk.require('2.0')
-import gtk
-import gobject
+from __future__ import absolute_import
-from Cheetah.Template import Template
+from gi.repository import GLib
+import cairo
+import six
-from Constants import POSSIBLE_ROTATIONS, CANVAS_GRID_SIZE, DPI_SCALING
-
-
-def rotate_pixmap(gc, src_pixmap, dst_pixmap, angle=gtk.gdk.PIXBUF_ROTATE_COUNTERCLOCKWISE):
- """
- Load the destination pixmap with a rotated version of the source pixmap.
- The source pixmap will be loaded into a pixbuf, rotated, and drawn to the destination pixmap.
- The pixbuf is a client-side drawable, where a pixmap is a server-side drawable.
-
- Args:
- gc: the graphics context
- src_pixmap: the source pixmap
- dst_pixmap: the destination pixmap
- angle: the angle to rotate by
- """
- width, height = src_pixmap.get_size()
- pixbuf = gtk.gdk.Pixbuf(
- colorspace=gtk.gdk.COLORSPACE_RGB,
- has_alpha=False, bits_per_sample=8,
- width=width, height=height,
- )
- pixbuf.get_from_drawable(src_pixmap, src_pixmap.get_colormap(), 0, 0, 0, 0, -1, -1)
- pixbuf = pixbuf.rotate_simple(angle)
- dst_pixmap.draw_pixbuf(gc, pixbuf, 0, 0, 0, 0)
+from .canvas.colors import FLOWGRAPH_BACKGROUND_COLOR
+from . import Constants
def get_rotated_coordinate(coor, rotation):
@@ -62,8 +39,8 @@ def get_rotated_coordinate(coor, rotation):
the rotated coordinates
"""
# handles negative angles
- rotation = (rotation + 360)%360
- if rotation not in POSSIBLE_ROTATIONS:
+ rotation = (rotation + 360) % 360
+ if rotation not in Constants.POSSIBLE_ROTATIONS:
raise ValueError('unusable rotation angle "%s"'%str(rotation))
# determine the number of degrees to rotate
cos_r, sin_r = {
@@ -73,7 +50,7 @@ def get_rotated_coordinate(coor, rotation):
return x * cos_r + y * sin_r, -x * sin_r + y * cos_r
-def get_angle_from_coordinates((x1, y1), (x2, y2)):
+def get_angle_from_coordinates(p1, p2):
"""
Given two points, calculate the vector direction from point1 to point2, directions are multiples of 90 degrees.
@@ -84,59 +61,103 @@ def get_angle_from_coordinates((x1, y1), (x2, y2)):
Returns:
the direction in degrees
"""
+ (x1, y1) = p1
+ (x2, y2) = p2
if y1 == y2: # 0 or 180
return 0 if x2 > x1 else 180
else: # 90 or 270
return 270 if y2 > y1 else 90
+def align_to_grid(coor, mode=round):
+ def align(value):
+ return int(mode(value / (1.0 * Constants.CANVAS_GRID_SIZE)) * Constants.CANVAS_GRID_SIZE)
+ try:
+ return [align(c) for c in coor]
+ except TypeError:
+ x = coor
+ return align(coor)
+
+
+def num_to_str(num):
+ """ Display logic for numbers """
+ def eng_notation(value, fmt='g'):
+ """Convert a number to a string in engineering notation. E.g., 5e-9 -> 5n"""
+ template = '{:' + fmt + '}{}'
+ magnitude = abs(value)
+ for exp, symbol in zip(range(9, -15-1, -3), 'GMk munpf'):
+ factor = 10 ** exp
+ if magnitude >= factor:
+ return template.format(value / factor, symbol.strip())
+ return template.format(value, '')
+
+ if isinstance(num, Constants.COMPLEX_TYPES):
+ num = complex(num) # Cast to python complex
+ if num == 0:
+ return '0'
+ output = eng_notation(num.real) if num.real else ''
+ output += eng_notation(num.imag, '+g' if output else 'g') + 'j' if num.imag else ''
+ return output
+ else:
+ return str(num)
+
+
def encode(value):
"""Make sure that we pass only valid utf-8 strings into markup_escape_text.
Older versions of glib seg fault if the last byte starts a multi-byte
character.
"""
+ if six.PY2:
+ valid_utf8 = value.decode('utf-8', errors='replace').encode('utf-8')
+ else:
+ valid_utf8 = value
+ return GLib.markup_escape_text(valid_utf8)
- valid_utf8 = value.decode('utf-8', errors='replace').encode('utf-8')
- return gobject.markup_escape_text(valid_utf8)
+def make_screenshot(flow_graph, file_path, transparent_bg=False):
+ if not file_path:
+ return
-class TemplateParser(object):
- def __init__(self):
- self.cache = {}
+ x_min, y_min, x_max, y_max = flow_graph.get_extents()
+ padding = Constants.CANVAS_GRID_SIZE
+ width = x_max - x_min + 2 * padding
+ height = y_max - y_min + 2 * padding
- def __call__(self, tmpl_str, **kwargs):
- """
- Parse the template string with the given args.
- Pass in the xml encode method for pango escape chars.
+ if file_path.endswith('.png'):
+ psurf = cairo.ImageSurface(cairo.FORMAT_ARGB32, width, height)
+ elif file_path.endswith('.pdf'):
+ psurf = cairo.PDFSurface(file_path, width, height)
+ elif file_path.endswith('.svg'):
+ psurf = cairo.SVGSurface(file_path, width, height)
+ else:
+ raise ValueError('Unknown file format')
- Args:
- tmpl_str: the template as a string
+ cr = cairo.Context(psurf)
- Returns:
- a string of the parsed template
- """
- kwargs['encode'] = encode
- template = self.cache.setdefault(tmpl_str, Template.compile(tmpl_str))
- return str(template(namespaces=kwargs))
+ if not transparent_bg:
+ cr.set_source_rgba(*FLOWGRAPH_BACKGROUND_COLOR)
+ cr.rectangle(0, 0, width, height)
+ cr.fill()
-parse_template = TemplateParser()
+ cr.translate(padding - x_min, padding - y_min)
+ flow_graph.create_labels(cr)
+ flow_graph.create_shapes()
+ flow_graph.draw(cr)
-def align_to_grid(coor, mode=round):
- def align(value):
- return int(mode(value / (1.0 * CANVAS_GRID_SIZE)) * CANVAS_GRID_SIZE)
- try:
- return map(align, coor)
- except TypeError:
- x = coor
- return align(coor)
+ if file_path.endswith('.png'):
+ psurf.write_to_png(file_path)
+ if file_path.endswith('.pdf') or file_path.endswith('.svg'):
+ cr.show_page()
+ psurf.finish()
def scale(coor, reverse=False):
- factor = DPI_SCALING if not reverse else 1 / DPI_SCALING
+ factor = Constants.DPI_SCALING if not reverse else 1 / Constants.DPI_SCALING
return tuple(int(x * factor) for x in coor)
+
def scale_scalar(coor, reverse=False):
- factor = DPI_SCALING if not reverse else 1 / DPI_SCALING
+ factor = Constants.DPI_SCALING if not reverse else 1 / Constants.DPI_SCALING
return int(coor * factor)
diff --git a/grc/gui/VariableEditor.py b/grc/gui/VariableEditor.py
index 45f0bb75fc..c179c8bc84 100644
--- a/grc/gui/VariableEditor.py
+++ b/grc/gui/VariableEditor.py
@@ -1,5 +1,5 @@
"""
-Copyright 2015 Free Software Foundation, Inc.
+Copyright 2015, 2016 Free Software Foundation, Inc.
This file is part of GNU Radio
GNU Radio Companion is free software; you can redistribute it and/or
@@ -17,50 +17,45 @@ along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
"""
-from operator import attrgetter
+from __future__ import absolute_import
-import pygtk
-pygtk.require('2.0')
-import gtk
-import gobject
+from gi.repository import Gtk, Gdk, GObject
-from . import Actions
-from . import Preferences
-from . import Utils
-from .Constants import DEFAULT_BLOCKS_WINDOW_WIDTH
+from . import Actions, Constants, Utils
BLOCK_INDEX = 0
ID_INDEX = 1
-class VariableEditorContextMenu(gtk.Menu):
+class VariableEditorContextMenu(Gtk.Menu):
""" A simple context menu for our variable editor """
+
def __init__(self, var_edit):
- gtk.Menu.__init__(self)
+ Gtk.Menu.__init__(self)
- self.imports = gtk.MenuItem("Add _Import")
+ self.imports = Gtk.MenuItem(label="Add _Import")
self.imports.connect('activate', var_edit.handle_action, var_edit.ADD_IMPORT)
self.add(self.imports)
- self.variables = gtk.MenuItem("Add _Variable")
+ self.variables = Gtk.MenuItem(label="Add _Variable")
self.variables.connect('activate', var_edit.handle_action, var_edit.ADD_VARIABLE)
self.add(self.variables)
- self.add(gtk.SeparatorMenuItem())
+ self.add(Gtk.SeparatorMenuItem())
- self.enable = gtk.MenuItem("_Enable")
+ self.enable = Gtk.MenuItem(label="_Enable")
self.enable.connect('activate', var_edit.handle_action, var_edit.ENABLE_BLOCK)
- self.disable = gtk.MenuItem("_Disable")
+ self.disable = Gtk.MenuItem(label="_Disable")
self.disable.connect('activate', var_edit.handle_action, var_edit.DISABLE_BLOCK)
self.add(self.enable)
self.add(self.disable)
- self.add(gtk.SeparatorMenuItem())
+ self.add(Gtk.SeparatorMenuItem())
- self.delete = gtk.MenuItem("_Delete")
+ self.delete = Gtk.MenuItem(label="_Delete")
self.delete.connect('activate', var_edit.handle_action, var_edit.DELETE_BLOCK)
self.add(self.delete)
- self.add(gtk.SeparatorMenuItem())
+ self.add(Gtk.SeparatorMenuItem())
- self.properties = gtk.MenuItem("_Properties...")
+ self.properties = Gtk.MenuItem(label="_Properties...")
self.properties.connect('activate', var_edit.handle_action, var_edit.OPEN_PROPERTIES)
self.add(self.properties)
self.show_all()
@@ -72,7 +67,7 @@ class VariableEditorContextMenu(gtk.Menu):
self.disable.set_sensitive(selected and enabled)
-class VariableEditor(gtk.VBox):
+class VariableEditor(Gtk.VBox):
# Actions that are handled by the editor
ADD_IMPORT = 0
@@ -83,23 +78,30 @@ class VariableEditor(gtk.VBox):
ENABLE_BLOCK = 5
DISABLE_BLOCK = 6
- def __init__(self, platform, get_flow_graph):
- gtk.VBox.__init__(self)
- self.platform = platform
- self.get_flow_graph = get_flow_graph
+ __gsignals__ = {
+ 'create_new_block': (GObject.SignalFlags.RUN_FIRST, None, (str,)),
+ 'remove_block': (GObject.SignalFlags.RUN_FIRST, None, (str,))
+ }
+
+ def __init__(self):
+ Gtk.VBox.__init__(self)
+ config = Gtk.Application.get_default().config
+
self._block = None
self._mouse_button_pressed = False
+ self._imports = []
+ self._variables = []
# Only use the model to store the block reference and name.
# Generate everything else dynamically
- self.treestore = gtk.TreeStore(gobject.TYPE_PYOBJECT, # Block reference
- gobject.TYPE_STRING) # Category and block name
- self.treeview = gtk.TreeView(self.treestore)
+ self.treestore = Gtk.TreeStore(GObject.TYPE_PYOBJECT, # Block reference
+ GObject.TYPE_STRING) # Category and block name
+ self.treeview = Gtk.TreeView(model=self.treestore)
self.treeview.set_enable_search(False)
self.treeview.set_search_column(-1)
#self.treeview.set_enable_search(True)
#self.treeview.set_search_column(ID_INDEX)
- self.treeview.get_selection().set_mode('single')
+ self.treeview.get_selection().set_mode(Gtk.SelectionMode.SINGLE)
self.treeview.set_headers_visible(True)
self.treeview.connect('button-press-event', self._handle_mouse_button_press)
self.treeview.connect('button-release-event', self._handle_mouse_button_release)
@@ -107,67 +109,63 @@ class VariableEditor(gtk.VBox):
self.treeview.connect('key-press-event', self._handle_key_button_press)
# Block Name or Category
- self.id_cell = gtk.CellRendererText()
+ self.id_cell = Gtk.CellRendererText()
self.id_cell.connect('edited', self._handle_name_edited_cb)
- id_column = gtk.TreeViewColumn("Id", self.id_cell, text=ID_INDEX)
+ id_column = Gtk.TreeViewColumn("Id", self.id_cell, text=ID_INDEX)
id_column.set_name("id")
id_column.set_resizable(True)
id_column.set_max_width(Utils.scale_scalar(300))
id_column.set_min_width(Utils.scale_scalar(80))
id_column.set_fixed_width(Utils.scale_scalar(100))
- id_column.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
+ id_column.set_sizing(Gtk.TreeViewColumnSizing.FIXED)
id_column.set_cell_data_func(self.id_cell, self.set_properties)
self.id_column = id_column
self.treeview.append_column(id_column)
- self.treestore.set_sort_column_id(ID_INDEX, gtk.SORT_ASCENDING)
+ self.treestore.set_sort_column_id(ID_INDEX, Gtk.SortType.ASCENDING)
# For forcing resize
self._col_width = 0
# Block Value
- self.value_cell = gtk.CellRendererText()
+ self.value_cell = Gtk.CellRendererText()
self.value_cell.connect('edited', self._handle_value_edited_cb)
- value_column = gtk.TreeViewColumn("Value", self.value_cell)
+ value_column = Gtk.TreeViewColumn("Value", self.value_cell)
value_column.set_name("value")
value_column.set_resizable(False)
value_column.set_expand(True)
value_column.set_min_width(Utils.scale_scalar(100))
- value_column.set_sizing(gtk.TREE_VIEW_COLUMN_AUTOSIZE)
+ value_column.set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE)
value_column.set_cell_data_func(self.value_cell, self.set_value)
self.value_column = value_column
self.treeview.append_column(value_column)
# Block Actions (Add, Remove)
- self.action_cell = gtk.CellRendererPixbuf()
+ self.action_cell = Gtk.CellRendererPixbuf()
value_column.pack_start(self.action_cell, False)
value_column.set_cell_data_func(self.action_cell, self.set_icon)
# Make the scrolled window to hold the tree view
- scrolled_window = gtk.ScrolledWindow()
- scrolled_window.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
- scrolled_window.add_with_viewport(self.treeview)
- scrolled_window.set_size_request(DEFAULT_BLOCKS_WINDOW_WIDTH, -1)
- self.pack_start(scrolled_window)
+ scrolled_window = Gtk.ScrolledWindow()
+ scrolled_window.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
+ scrolled_window.add(self.treeview)
+ scrolled_window.set_size_request(Constants.DEFAULT_BLOCKS_WINDOW_WIDTH, -1)
+ self.pack_start(scrolled_window, True, True, 0)
# Context menus
self._context_menu = VariableEditorContextMenu(self)
- self._confirm_delete = Preferences.variable_editor_confirm_delete()
+ self._confirm_delete = config.variable_editor_confirm_delete()
# Sets cell contents
- def set_icon(self, col, cell, model, iter):
+ def set_icon(self, col, cell, model, iter, data):
block = model.get_value(iter, BLOCK_INDEX)
- if block:
- pb = self.treeview.render_icon(gtk.STOCK_CLOSE, gtk.ICON_SIZE_MENU, None)
- else:
- pb = self.treeview.render_icon(gtk.STOCK_ADD, gtk.ICON_SIZE_MENU, None)
- cell.set_property('pixbuf', pb)
+ cell.set_property('icon-name', 'window-close' if block else 'list-add')
- def set_value(self, col, cell, model, iter):
+ def set_value(self, col, cell, model, iter, data):
sp = cell.set_property
block = model.get_value(iter, BLOCK_INDEX)
# Set the default properties for this column first.
# Some set in set_properties() may be overridden (editable for advanced variable blocks)
- self.set_properties(col, cell, model, iter)
+ self.set_properties(col, cell, model, iter, data)
# Set defaults
value = None
@@ -175,14 +173,14 @@ class VariableEditor(gtk.VBox):
# Block specific values
if block:
- if block.get_key() == 'import':
- value = block.get_param('import').get_value()
- elif block.get_key() != "variable":
+ if block.key == 'import':
+ value = block.params['import'].get_value()
+ elif block.key != "variable":
value = "<Open Properties>"
sp('editable', False)
sp('foreground', '#0D47A1')
else:
- value = block.get_param('value').get_value()
+ value = block.params['value'].get_value()
# Check if there are errors in the blocks.
# Show the block error as a tooltip
@@ -193,13 +191,13 @@ class VariableEditor(gtk.VBox):
self.set_tooltip_text(error_message[-1])
else:
# Evaluate and show the value (if it is a variable)
- if block.get_key() == "variable":
- evaluated = str(block.get_param('value').evaluate())
+ if block.key == "variable":
+ evaluated = str(block.params['value'].evaluate())
self.set_tooltip_text(evaluated)
# Always set the text value.
sp('text', value)
- def set_properties(self, col, cell, model, iter):
+ def set_properties(self, col, cell, model, iter, data):
sp = cell.set_property
block = model.get_value(iter, BLOCK_INDEX)
# Set defaults
@@ -209,7 +207,7 @@ class VariableEditor(gtk.VBox):
# Block specific changes
if block:
- if not block.get_enabled():
+ if not block.enabled:
# Disabled block. But, this should still be editable
sp('editable', True)
sp('foreground', 'gray')
@@ -218,39 +216,32 @@ class VariableEditor(gtk.VBox):
if block.get_error_messages():
sp('foreground', 'red')
- def update_gui(self):
- if not self.get_flow_graph():
- return
- self._update_blocks()
+ def update_gui(self, blocks):
+ self._imports = [block for block in blocks if block.is_import]
+ self._variables = [block for block in blocks if block.is_variable]
self._rebuild()
self.treeview.expand_all()
- def _update_blocks(self):
- self._imports = filter(attrgetter('is_import'),
- self.get_flow_graph().blocks)
- self._variables = filter(attrgetter('is_variable'),
- self.get_flow_graph().blocks)
-
def _rebuild(self, *args):
self.treestore.clear()
imports = self.treestore.append(None, [None, 'Imports'])
variables = self.treestore.append(None, [None, 'Variables'])
for block in self._imports:
- self.treestore.append(imports, [block, block.get_param('id').get_value()])
- for block in sorted(self._variables, key=lambda v: v.get_id()):
- self.treestore.append(variables, [block, block.get_param('id').get_value()])
+ self.treestore.append(imports, [block, block.params['id'].get_value()])
+ for block in sorted(self._variables, key=lambda v: v.name):
+ self.treestore.append(variables, [block, block.params['id'].get_value()])
def _handle_name_edited_cb(self, cell, path, new_text):
block = self.treestore[path][BLOCK_INDEX]
- block.get_param('id').set_value(new_text)
+ block.params['id'].set_value(new_text)
Actions.VARIABLE_EDITOR_UPDATE()
def _handle_value_edited_cb(self, cell, path, new_text):
block = self.treestore[path][BLOCK_INDEX]
if block.is_import:
- block.get_param('import').set_value(new_text)
+ block.params['import'].set_value(new_text)
else:
- block.get_param('value').set_value(new_text)
+ block.params['value'].set_value(new_text)
Actions.VARIABLE_EDITOR_UPDATE()
def handle_action(self, item, key, event=None):
@@ -259,29 +250,31 @@ class VariableEditor(gtk.VBox):
key presses or mouse clicks. Also triggers an update of the flow graph and editor.
"""
if key == self.ADD_IMPORT:
- self.get_flow_graph().add_new_block('import')
+ self.emit('create_new_block', 'import')
elif key == self.ADD_VARIABLE:
- self.get_flow_graph().add_new_block('variable')
+ self.emit('create_new_block', 'variable')
elif key == self.OPEN_PROPERTIES:
- Actions.BLOCK_PARAM_MODIFY(self._block)
+ # TODO: This probably isn't working because the action doesn't expect a parameter
+ #Actions.BLOCK_PARAM_MODIFY()
+ pass
elif key == self.DELETE_BLOCK:
- self.get_flow_graph().remove_element(self._block)
+ self.emit('remove_block', self._block.name)
elif key == self.DELETE_CONFIRM:
if self._confirm_delete:
# Create a context menu to confirm the delete operation
- confirmation_menu = gtk.Menu()
- block_id = self._block.get_param('id').get_value().replace("_", "__")
- confirm = gtk.MenuItem("Delete {}".format(block_id))
+ confirmation_menu = Gtk.Menu()
+ block_id = self._block.params['id'].get_value().replace("_", "__")
+ confirm = Gtk.MenuItem(label="Delete {}".format(block_id))
confirm.connect('activate', self.handle_action, self.DELETE_BLOCK)
confirmation_menu.add(confirm)
confirmation_menu.show_all()
- confirmation_menu.popup(None, None, None, event.button, event.time)
+ confirmation_menu.popup(None, None, None, None, event.button, event.time)
else:
self.handle_action(None, self.DELETE_BLOCK, None)
elif key == self.ENABLE_BLOCK:
- self._block.set_enabled(True)
+ self._block.state = 'enabled'
elif key == self.DISABLE_BLOCK:
- self._block.set_enabled(False)
+ self._block.state = 'disabled'
Actions.VARIABLE_EDITOR_UPDATE()
def _handle_mouse_button_press(self, widget, event):
@@ -303,12 +296,12 @@ class VariableEditor(gtk.VBox):
if event.button == 1 and col.get_name() == "value":
# Make sure this has a block (not the import/variable rows)
- if self._block and event.type == gtk.gdk._2BUTTON_PRESS:
+ if self._block and event.type == Gdk.EventType._2BUTTON_PRESS:
# Open the advanced dialog if it is a gui variable
- if self._block.get_key() not in ("variable", "import"):
+ if self._block.key not in ("variable", "import"):
self.handle_action(None, self.OPEN_PROPERTIES, event=event)
return True
- if event.type == gtk.gdk.BUTTON_PRESS:
+ if event.type == Gdk.EventType.BUTTON_PRESS:
# User is adding/removing blocks
# Make sure this is the action cell (Add/Remove Icons)
if path[2] > col.cell_get_position(self.action_cell)[0]:
@@ -321,15 +314,15 @@ class VariableEditor(gtk.VBox):
else:
self.handle_action(None, self.DELETE_CONFIRM, event=event)
return True
- elif event.button == 3 and event.type == gtk.gdk.BUTTON_PRESS:
+ elif event.button == 3 and event.type == Gdk.EventType.BUTTON_PRESS:
if self._block:
- self._context_menu.update_sensitive(True, enabled=self._block.get_enabled())
+ self._context_menu.update_sensitive(True, enabled=self._block.enabled)
else:
self._context_menu.update_sensitive(False)
- self._context_menu.popup(None, None, None, event.button, event.time)
+ self._context_menu.popup(None, None, None, None, event.button, event.time)
# Null handler. Stops the treeview from handling double click events.
- if event.type == gtk.gdk._2BUTTON_PRESS:
+ if event.type == Gdk.EventType._2BUTTON_PRESS:
return True
return False
@@ -346,10 +339,10 @@ class VariableEditor(gtk.VBox):
def _handle_key_button_press(self, widget, event):
model, path = self.treeview.get_selection().get_selected_rows()
if path and self._block:
- if self._block.get_enabled() and event.string == "d":
+ if self._block.enabled and event.string == "d":
self.handle_action(None, self.DISABLE_BLOCK, None)
return True
- elif not self._block.get_enabled() and event.string == "e":
+ elif not self._block.enabled and event.string == "e":
self.handle_action(None, self.ENABLE_BLOCK, None)
return True
return False
diff --git a/grc/gui/canvas/__init__.py b/grc/gui/canvas/__init__.py
new file mode 100644
index 0000000000..f90d10c4e6
--- /dev/null
+++ b/grc/gui/canvas/__init__.py
@@ -0,0 +1,22 @@
+# Copyright 2016 Free Software Foundation, Inc.
+# This file is part of GNU Radio
+#
+# GNU Radio Companion is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# GNU Radio Companion is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
+
+from .block import Block
+from .connection import Connection
+from .flowgraph import FlowGraph
+from .param import Param
+from .port import Port
diff --git a/grc/gui/canvas/block.py b/grc/gui/canvas/block.py
new file mode 100644
index 0000000000..33edf988c2
--- /dev/null
+++ b/grc/gui/canvas/block.py
@@ -0,0 +1,400 @@
+"""
+Copyright 2007, 2008, 2009 Free Software Foundation, Inc.
+This file is part of GNU Radio
+
+GNU Radio Companion is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation; either version 2
+of the License, or (at your option) any later version.
+
+GNU Radio Companion is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
+"""
+
+from __future__ import absolute_import, division
+
+import math
+
+import six
+from gi.repository import Gtk, Pango, PangoCairo
+
+from . import colors
+from .drawable import Drawable
+from .. import Actions, Utils, Constants
+from ..Constants import (
+ BLOCK_LABEL_PADDING, PORT_SPACING, PORT_SEPARATION, LABEL_SEPARATION,
+ PORT_BORDER_SEPARATION, BLOCK_FONT, PARAM_FONT
+)
+from ...core import utils
+from ...core.blocks import Block as CoreBlock
+
+
+class Block(CoreBlock, Drawable):
+ """The graphical signal block."""
+
+ def __init__(self, parent, **n):
+ """
+ Block constructor.
+ Add graphics related params to the block.
+ """
+ super(self.__class__, self).__init__(parent, **n)
+
+ self.states.update(coordinate=(0, 0), rotation=0)
+ self.width = self.height = 0
+ Drawable.__init__(self) # needs the states and initial sizes
+
+ self._surface_layouts = [
+ None, # title
+ None, # params
+ ]
+ self._surface_layouts_offsets = 0, 0
+ self._comment_layout = None
+
+ self._area = []
+ self._border_color = self._bg_color = colors.BLOCK_ENABLED_COLOR
+ self._font_color = list(colors.FONT_COLOR)
+
+ @property
+ def coordinate(self):
+ """
+ Get the coordinate from the position param.
+
+ Returns:
+ the coordinate tuple (x, y) or (0, 0) if failure
+ """
+ return Utils.scale(self.states['coordinate'])
+
+ @coordinate.setter
+ def coordinate(self, coor):
+ """
+ Set the coordinate into the position param.
+
+ Args:
+ coor: the coordinate tuple (x, y)
+ """
+ coor = Utils.scale(coor, reverse=True)
+ if Actions.TOGGLE_SNAP_TO_GRID.get_active():
+ offset_x, offset_y = (0, self.height / 2) if self.is_horizontal() else (self.height / 2, 0)
+ coor = (
+ Utils.align_to_grid(coor[0] + offset_x) - offset_x,
+ Utils.align_to_grid(coor[1] + offset_y) - offset_y
+ )
+ self.states['coordinate'] = coor
+
+ @property
+ def rotation(self):
+ """
+ Get the rotation from the position param.
+
+ Returns:
+ the rotation in degrees or 0 if failure
+ """
+ return self.states['rotation']
+
+ @rotation.setter
+ def rotation(self, rot):
+ """
+ Set the rotation into the position param.
+
+ Args:
+ rot: the rotation in degrees
+ """
+ self.states['rotation'] = rot
+
+ def _update_colors(self):
+ self._bg_color = (
+ colors.MISSING_BLOCK_BACKGROUND_COLOR if self.is_dummy_block else
+ colors.BLOCK_BYPASSED_COLOR if self.state == 'bypassed' else
+ colors.BLOCK_ENABLED_COLOR if self.state == 'enabled' else
+ colors.BLOCK_DISABLED_COLOR
+ )
+ self._font_color[-1] = 1.0 if self.state == 'enabled' else 0.4
+ self._border_color = (
+ colors.MISSING_BLOCK_BORDER_COLOR if self.is_dummy_block else
+ colors.BORDER_COLOR_DISABLED if not self.state == 'enabled' else colors.BORDER_COLOR
+ )
+
+ def create_shapes(self):
+ """Update the block, parameters, and ports when a change occurs."""
+ if self.is_horizontal():
+ self._area = (0, 0, self.width, self.height)
+ elif self.is_vertical():
+ self._area = (0, 0, self.height, self.width)
+ self.bounds_from_area(self._area)
+
+ # bussified = self.current_bus_structure['source'], self.current_bus_structure['sink']
+ bussified = False, False
+ for ports, has_busses in zip((self.active_sources, self.active_sinks), bussified):
+ if not ports:
+ continue
+ port_separation = PORT_SEPARATION if not has_busses else ports[0].height + PORT_SPACING
+ offset = (self.height - (len(ports) - 1) * port_separation - ports[0].height) / 2
+ for port in ports:
+ port.create_shapes()
+
+ port.coordinate = {
+ 0: (+self.width, offset),
+ 90: (offset, -port.width),
+ 180: (-port.width, offset),
+ 270: (offset, +self.width),
+ }[port.connector_direction]
+
+ offset += PORT_SEPARATION if not has_busses else port.height + PORT_SPACING
+
+ def create_labels(self, cr=None):
+ """Create the labels for the signal block."""
+
+ # (Re-)creating layouts here, because layout.context_changed() doesn't seems to work (after zoom)
+ title_layout, params_layout = self._surface_layouts = [
+ Gtk.DrawingArea().create_pango_layout(''), # title
+ Gtk.DrawingArea().create_pango_layout(''), # params
+ ]
+
+ if cr: # to fix up extents after zooming
+ PangoCairo.update_layout(cr, title_layout)
+ PangoCairo.update_layout(cr, params_layout)
+
+ title_layout.set_markup(
+ '<span {foreground} font_desc="{font}"><b>{label}</b></span>'.format(
+ foreground='foreground="red"' if not self.is_valid() else '', font=BLOCK_FONT,
+ label=Utils.encode(self.label)
+ )
+ )
+ title_width, title_height = title_layout.get_size()
+
+ # update the params layout
+ if not self.is_dummy_block:
+ markups = [param.format_block_surface_markup()
+ for param in self.params.values() if param.hide not in ('all', 'part')]
+ else:
+ markups = ['<span font_desc="{font}"><b>key: </b>{key}</span>'.format(font=PARAM_FONT, key=self.key)]
+
+ params_layout.set_spacing(LABEL_SEPARATION * Pango.SCALE)
+ params_layout.set_markup('\n'.join(markups))
+ params_width, params_height = params_layout.get_size() if markups else (0, 0)
+
+ label_width = max(title_width, params_width) / Pango.SCALE
+ label_height = title_height / Pango.SCALE
+ if markups:
+ label_height += LABEL_SEPARATION + params_height / Pango.SCALE
+
+ # calculate width and height needed
+ width = label_width + 2 * BLOCK_LABEL_PADDING
+ height = label_height + 2 * BLOCK_LABEL_PADDING
+
+ self._update_colors()
+ self.create_port_labels()
+
+ def get_min_height_for_ports(ports):
+ min_height = 2 * PORT_BORDER_SEPARATION + len(ports) * PORT_SEPARATION
+ if ports:
+ min_height -= ports[-1].height
+ return min_height
+
+ height = max(height,
+ get_min_height_for_ports(self.active_sinks),
+ get_min_height_for_ports(self.active_sources))
+
+ # def get_min_height_for_bus_ports(ports):
+ # return 2 * PORT_BORDER_SEPARATION + sum(
+ # port.height + PORT_SPACING for port in ports if port.dtype == 'bus'
+ # ) - PORT_SPACING
+ #
+ # if self.current_bus_structure['sink']:
+ # height = max(height, get_min_height_for_bus_ports(self.active_sinks))
+ # if self.current_bus_structure['source']:
+ # height = max(height, get_min_height_for_bus_ports(self.active_sources))
+
+ self.width, self.height = width, height = Utils.align_to_grid((width, height))
+
+ self._surface_layouts_offsets = [
+ (0, (height - label_height) / 2.0),
+ (0, (height - label_height) / 2.0 + LABEL_SEPARATION + title_height / Pango.SCALE)
+ ]
+
+ title_layout.set_width(width * Pango.SCALE)
+ title_layout.set_alignment(Pango.Alignment.CENTER)
+ params_layout.set_indent((width - label_width) / 2.0 * Pango.SCALE)
+
+ self.create_comment_layout()
+
+ def create_port_labels(self):
+ for ports in (self.active_sinks, self.active_sources):
+ max_width = 0
+ for port in ports:
+ port.create_labels()
+ max_width = max(max_width, port.width_with_label)
+ for port in ports:
+ port.width = max_width
+
+ def create_comment_layout(self):
+ markups = []
+
+ # Show the flow graph complexity on the top block if enabled
+ if Actions.TOGGLE_SHOW_FLOWGRAPH_COMPLEXITY.get_active() and self.key == "options":
+ complexity = utils.flow_graph_complexity.calculate(self.parent)
+ markups.append(
+ '<span foreground="#444" size="medium" font_desc="{font}">'
+ '<b>Complexity: {num}bal</b></span>'.format(num=Utils.num_to_str(complexity), font=BLOCK_FONT)
+ )
+ comment = self.comment # Returns None if there are no comments
+ if comment:
+ if markups:
+ markups.append('<span></span>')
+
+ markups.append('<span foreground="{foreground}" font_desc="{font}">{comment}</span>'.format(
+ foreground='#444' if self.enabled else '#888', font=BLOCK_FONT, comment=Utils.encode(comment)
+ ))
+ if markups:
+ layout = self._comment_layout = Gtk.DrawingArea().create_pango_layout('')
+ layout.set_markup(''.join(markups))
+ else:
+ self._comment_layout = None
+
+ def draw(self, cr):
+ """
+ Draw the signal block with label and inputs/outputs.
+ """
+ border_color = colors.HIGHLIGHT_COLOR if self.highlighted else self._border_color
+ cr.translate(*self.coordinate)
+
+ for port in self.active_ports(): # ports first
+ cr.save()
+ port.draw(cr)
+ cr.restore()
+
+ cr.rectangle(*self._area)
+ cr.set_source_rgba(*self._bg_color)
+ cr.fill_preserve()
+ cr.set_source_rgba(*border_color)
+ cr.stroke()
+
+ # title and params label
+ if self.is_vertical():
+ cr.rotate(-math.pi / 2)
+ cr.translate(-self.width, 0)
+ cr.set_source_rgba(*self._font_color)
+ for layout, offset in zip(self._surface_layouts, self._surface_layouts_offsets):
+ cr.save()
+ cr.translate(*offset)
+ PangoCairo.update_layout(cr, layout)
+ PangoCairo.show_layout(cr, layout)
+ cr.restore()
+
+ def what_is_selected(self, coor, coor_m=None):
+ """
+ Get the element that is selected.
+
+ Args:
+ coor: the (x,y) tuple
+ coor_m: the (x_m, y_m) tuple
+
+ Returns:
+ this block, a port, or None
+ """
+ for port in self.active_ports():
+ port_selected = port.what_is_selected(
+ coor=[a - b for a, b in zip(coor, self.coordinate)],
+ coor_m=[a - b for a, b in zip(coor, self.coordinate)] if coor_m is not None else None
+ )
+ if port_selected:
+ return port_selected
+ return Drawable.what_is_selected(self, coor, coor_m)
+
+ def draw_comment(self, cr):
+ if not self._comment_layout:
+ return
+ x, y = self.coordinate
+
+ if self.is_horizontal():
+ y += self.height + BLOCK_LABEL_PADDING
+ else:
+ x += self.height + BLOCK_LABEL_PADDING
+
+ cr.save()
+ cr.translate(x, y)
+ PangoCairo.update_layout(cr, self._comment_layout)
+ PangoCairo.show_layout(cr, self._comment_layout)
+ cr.restore()
+
+ def get_extents(self):
+ extent = Drawable.get_extents(self)
+ x, y = self.coordinate
+ for port in self.active_ports():
+ extent = (min_or_max(xy, offset + p_xy) for offset, min_or_max, xy, p_xy in zip(
+ (x, y, x, y), (min, min, max, max), extent, port.get_extents()
+ ))
+ return tuple(extent)
+
+ ##############################################
+ # Controller Modify
+ ##############################################
+ def type_controller_modify(self, direction):
+ """
+ Change the type controller.
+
+ Args:
+ direction: +1 or -1
+
+ Returns:
+ true for change
+ """
+ type_templates = ' '.join(p._type for p in self.params.values())
+ type_templates += ' '.join(p.get_raw('dtype') for p in (self.sinks + self.sources))
+ type_param = None
+ for key, param in six.iteritems(self.params):
+ if not param.is_enum():
+ continue
+ # Priority to the type controller
+ if param.key in type_templates:
+ type_param = param
+ break
+ # Use param if type param is unset
+ if not type_param:
+ type_param = param
+ if not type_param:
+ return False
+
+ # Try to increment the enum by direction
+ try:
+ values = list(type_param.options)
+ old_index = values.index(type_param.get_value())
+ new_index = (old_index + direction + len(values)) % len(values)
+ type_param.set_value(values[new_index])
+ return True
+ except:
+ return False
+
+ def port_controller_modify(self, direction):
+ """
+ Change the port controller.
+
+ Args:
+ direction: +1 or -1
+
+ Returns:
+ true for change
+ """
+ changed = False
+ # Concat the nports string from the private nports settings of all ports
+ nports_str = ' '.join(str(port.get_raw('multiplicity')) for port in self.ports())
+ # Modify all params whose keys appear in the nports string
+ for key, param in six.iteritems(self.params):
+ if param.is_enum() or param.key not in nports_str:
+ continue
+ # Try to increment the port controller by direction
+ try:
+ value = param.get_evaluated() + direction
+ if value > 0:
+ param.set_value(value)
+ changed = True
+ except:
+ pass
+ return changed
+
diff --git a/grc/gui/canvas/colors.py b/grc/gui/canvas/colors.py
new file mode 100644
index 0000000000..77d3203e78
--- /dev/null
+++ b/grc/gui/canvas/colors.py
@@ -0,0 +1,78 @@
+"""
+Copyright 2008,2013 Free Software Foundation, Inc.
+This file is part of GNU Radio
+
+GNU Radio Companion is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation; either version 2
+of the License, or (at your option) any later version.
+
+GNU Radio Companion is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
+"""
+
+from __future__ import absolute_import
+
+from gi.repository import Gtk, Gdk, cairo
+# import pycairo
+
+from .. import Constants
+
+
+def get_color(color_code):
+ color = Gdk.RGBA()
+ color.parse(color_code)
+ return color.red, color.green, color.blue, color.alpha
+ # chars_per_color = 2 if len(color_code) > 4 else 1
+ # offsets = range(1, 3 * chars_per_color + 1, chars_per_color)
+ # return tuple(int(color_code[o:o + 2], 16) / 255.0 for o in offsets)
+
+#################################################################################
+# fg colors
+#################################################################################
+
+HIGHLIGHT_COLOR = get_color('#00FFFF')
+BORDER_COLOR = get_color('#616161')
+BORDER_COLOR_DISABLED = get_color('#888888')
+FONT_COLOR = get_color('#000000')
+
+# Missing blocks stuff
+MISSING_BLOCK_BACKGROUND_COLOR = get_color('#FFF2F2')
+MISSING_BLOCK_BORDER_COLOR = get_color('#FF0000')
+
+# Flow graph color constants
+FLOWGRAPH_BACKGROUND_COLOR = get_color('#FFFFFF')
+COMMENT_BACKGROUND_COLOR = get_color('#F3F3F3')
+FLOWGRAPH_EDGE_COLOR = COMMENT_BACKGROUND_COLOR
+
+# Block color constants
+BLOCK_ENABLED_COLOR = get_color('#F1ECFF')
+BLOCK_DISABLED_COLOR = get_color('#CCCCCC')
+BLOCK_BYPASSED_COLOR = get_color('#F4FF81')
+
+# Connection color constants
+CONNECTION_ENABLED_COLOR = get_color('#000000')
+CONNECTION_DISABLED_COLOR = get_color('#BBBBBB')
+CONNECTION_ERROR_COLOR = get_color('#FF0000')
+
+DEFAULT_DOMAIN_COLOR = get_color('#777777')
+
+
+#################################################################################
+# port colors
+#################################################################################
+
+PORT_TYPE_TO_COLOR = {key: get_color(color) for name, key, sizeof, color in Constants.CORE_TYPES}
+PORT_TYPE_TO_COLOR.update((key, get_color(color)) for key, (_, color) in Constants.ALIAS_TYPES.items())
+
+
+#################################################################################
+# param box colors
+#################################################################################
+
diff --git a/grc/gui/canvas/connection.py b/grc/gui/canvas/connection.py
new file mode 100644
index 0000000000..56dab45570
--- /dev/null
+++ b/grc/gui/canvas/connection.py
@@ -0,0 +1,253 @@
+"""
+Copyright 2007, 2008, 2009 Free Software Foundation, Inc.
+This file is part of GNU Radio
+
+GNU Radio Companion is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation; either version 2
+of the License, or (at your option) any later version.
+
+GNU Radio Companion is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
+"""
+
+from __future__ import absolute_import, division
+
+from argparse import Namespace
+from math import pi
+
+from . import colors
+from .drawable import Drawable
+from .. import Utils
+from ..Constants import (
+ CONNECTOR_ARROW_BASE,
+ CONNECTOR_ARROW_HEIGHT,
+ GR_MESSAGE_DOMAIN,
+ LINE_SELECT_SENSITIVITY,
+)
+from ...core.Connection import Connection as CoreConnection
+from ...core.utils.descriptors import nop_write
+
+
+class Connection(CoreConnection, Drawable):
+ """
+ A graphical connection for ports.
+ The connection has 2 parts, the arrow and the wire.
+ The coloring of the arrow and wire exposes the status of 3 states:
+ enabled/disabled, valid/invalid, highlighted/non-highlighted.
+ The wire coloring exposes the enabled and highlighted states.
+ The arrow coloring exposes the enabled and valid states.
+ """
+
+ def __init__(self, *args, **kwargs):
+ super(self.__class__, self).__init__(*args, **kwargs)
+ Drawable.__init__(self)
+
+ self._line = []
+ self._line_width_factor = 1.0
+ self._color1 = self._color2 = None
+
+ self._current_port_rotations = self._current_coordinates = None
+
+ self._rel_points = None # connection coordinates relative to sink/source
+ self._arrow_rotation = 0.0 # rotation of the arrow in radians
+ self._current_cr = None # for what_is_selected() of curved line
+ self._line_path = None
+
+ @nop_write
+ @property
+ def coordinate(self):
+ return self.source_port.connector_coordinate_absolute
+
+ @nop_write
+ @property
+ def rotation(self):
+ """
+ Get the 0 degree rotation.
+ Rotations are irrelevant in connection.
+
+ Returns:
+ 0
+ """
+ return 0
+
+ def create_shapes(self):
+ """Pre-calculate relative coordinates."""
+ source = self.source_port
+ sink = self.sink_port
+ rotate = Utils.get_rotated_coordinate
+
+ # first two components relative to source connector, rest relative to sink connector
+ self._rel_points = [
+ rotate((15, 0), source.rotation), # line from 0,0 to here, bezier curve start
+ rotate((50, 0), source.rotation), # bezier curve control point 1
+ rotate((-50, 0), sink.rotation), # bezier curve control point 2
+ rotate((-15, 0), sink.rotation), # bezier curve end
+ rotate((-CONNECTOR_ARROW_HEIGHT, 0), sink.rotation), # line to arrow head
+ ]
+ self._current_coordinates = None # triggers _make_path()
+
+ def get_domain_color(domain_id):
+ domain = self.parent_platform.domains.get(domain_id, None)
+ return colors.get_color(domain.color) if domain else colors.DEFAULT_DOMAIN_COLOR
+
+ if source.domain == GR_MESSAGE_DOMAIN:
+ self._line_width_factor = 1.0
+ self._color1 = None
+ self._color2 = colors.CONNECTION_ENABLED_COLOR
+ else:
+ if source.domain != sink.domain:
+ self._line_width_factor = 2.0
+ self._color1 = get_domain_color(source.domain)
+ self._color2 = get_domain_color(sink.domain)
+
+ self._arrow_rotation = -sink.rotation / 180 * pi
+
+ if not self._bounding_points:
+ self._make_path() # no cr set --> only sets bounding_points for extent
+
+ def _make_path(self, cr=None):
+ x_pos, y_pos = self.coordinate # is source connector coordinate
+ # x_start, y_start = self.source_port.get_connector_coordinate()
+ x_end, y_end = self.sink_port.connector_coordinate_absolute
+
+ # sink connector relative to sink connector
+ x_e, y_e = x_end - x_pos, y_end - y_pos
+
+ # make rel_point all relative to source connector
+ p0 = 0, 0 # x_start - x_pos, y_start - y_pos
+ p1, p2, (dx_e1, dy_e1), (dx_e2, dy_e2), (dx_e3, dy_e3) = self._rel_points
+ p3 = x_e + dx_e1, y_e + dy_e1
+ p4 = x_e + dx_e2, y_e + dy_e2
+ p5 = x_e + dx_e3, y_e + dy_e3
+ self._bounding_points = p0, p1, p4, p5 # ignores curved part =(
+
+ if cr:
+ cr.move_to(*p0)
+ cr.line_to(*p1)
+ cr.curve_to(*(p2 + p3 + p4))
+ cr.line_to(*p5)
+ self._line_path = cr.copy_path()
+
+ def draw(self, cr):
+ """
+ Draw the connection.
+ """
+ self._current_cr = cr
+ sink = self.sink_port
+ source = self.source_port
+
+ # check for changes
+ port_rotations = (source.rotation, sink.rotation)
+ if self._current_port_rotations != port_rotations:
+ self.create_shapes() # triggers _make_path() call below
+ self._current_port_rotations = port_rotations
+
+ new_coordinates = (source.parent_block.coordinate, sink.parent_block.coordinate)
+ if self._current_coordinates != new_coordinates:
+ self._make_path(cr)
+ self._current_coordinates = new_coordinates
+
+ color1, color2 = (
+ None if color is None else
+ colors.HIGHLIGHT_COLOR if self.highlighted else
+ colors.CONNECTION_DISABLED_COLOR if not self.enabled else
+ colors.CONNECTION_ERROR_COLOR if not self.is_valid() else
+ color
+ for color in (self._color1, self._color2)
+ )
+
+ cr.translate(*self.coordinate)
+ cr.set_line_width(self._line_width_factor * cr.get_line_width())
+ cr.new_path()
+ cr.append_path(self._line_path)
+
+ arrow_pos = cr.get_current_point()
+
+ if color1: # not a message connection
+ cr.set_source_rgba(*color1)
+ cr.stroke_preserve()
+
+ if color1 != color2:
+ cr.save()
+ cr.set_dash([5.0, 5.0], 5.0 if color1 else 0.0)
+ cr.set_source_rgba(*color2)
+ cr.stroke()
+ cr.restore()
+ else:
+ cr.new_path()
+
+ cr.move_to(*arrow_pos)
+ cr.set_source_rgba(*color2)
+ cr.rotate(self._arrow_rotation)
+ cr.rel_move_to(CONNECTOR_ARROW_HEIGHT, 0)
+ cr.rel_line_to(-CONNECTOR_ARROW_HEIGHT, -CONNECTOR_ARROW_BASE/2)
+ cr.rel_line_to(0, CONNECTOR_ARROW_BASE)
+ cr.close_path()
+ cr.fill()
+
+ def what_is_selected(self, coor, coor_m=None):
+ """
+ Returns:
+ self if one of the areas/lines encompasses coor, else None.
+ """
+ if coor_m:
+ return Drawable.what_is_selected(self, coor, coor_m)
+
+ x, y = [a - b for a, b in zip(coor, self.coordinate)]
+
+ cr = self._current_cr
+
+ if cr is None:
+ return
+ cr.save()
+ cr.new_path()
+ cr.append_path(self._line_path)
+ cr.set_line_width(cr.get_line_width() * LINE_SELECT_SENSITIVITY)
+ hit = cr.in_stroke(x, y)
+ cr.restore()
+
+ if hit:
+ return self
+
+
+class DummyCoreConnection(object):
+ def __init__(self, source_port, **kwargs):
+ self.parent_platform = source_port.parent_platform
+ self.source_port = source_port
+ self.sink_port = self._dummy_port = Namespace(
+ domain=source_port.domain,
+ rotation=0,
+ coordinate=(0, 0),
+ connector_coordinate_absolute=(0, 0),
+ connector_direction=0,
+ parent_block=Namespace(coordinate=(0, 0)),
+ )
+
+ self.enabled = True
+ self.highlighted = False,
+ self.is_valid = lambda: True
+ self.update(**kwargs)
+
+ def update(self, coordinate=None, rotation=None, sink_port=None):
+ dp = self._dummy_port
+ self.sink_port = sink_port if sink_port else dp
+ if coordinate:
+ dp.coordinate = coordinate
+ dp.connector_coordinate_absolute = coordinate
+ dp.parent_block.coordinate = coordinate
+ if rotation is not None:
+ dp.rotation = rotation
+ dp.connector_direction = (180 + rotation) % 360
+
+ @property
+ def has_real_sink(self):
+ return self.sink_port is not self._dummy_port
+
+DummyConnection = Connection.make_cls_with_base(DummyCoreConnection)
diff --git a/grc/gui/canvas/drawable.py b/grc/gui/canvas/drawable.py
new file mode 100644
index 0000000000..d755d4418d
--- /dev/null
+++ b/grc/gui/canvas/drawable.py
@@ -0,0 +1,183 @@
+"""
+Copyright 2007, 2008, 2009, 2016 Free Software Foundation, Inc.
+This file is part of GNU Radio
+
+GNU Radio Companion is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation; either version 2
+of the License, or (at your option) any later version.
+
+GNU Radio Companion is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
+"""
+
+from __future__ import absolute_import
+from ..Constants import LINE_SELECT_SENSITIVITY
+
+from six.moves import zip
+
+
+class Drawable(object):
+ """
+ GraphicalElement is the base class for all graphical elements.
+ It contains an X,Y coordinate, a list of rectangular areas that the element occupies,
+ and methods to detect selection of those areas.
+ """
+
+ @classmethod
+ def make_cls_with_base(cls, super_cls):
+ name = super_cls.__name__
+ bases = (super_cls,) + cls.__bases__[1:]
+ namespace = cls.__dict__.copy()
+ return type(name, bases, namespace)
+
+ def __init__(self):
+ """
+ Make a new list of rectangular areas and lines, and set the coordinate and the rotation.
+ """
+ self.coordinate = (0, 0)
+ self.rotation = 0
+ self.highlighted = False
+
+ self._bounding_rects = []
+ self._bounding_points = []
+
+ def is_horizontal(self, rotation=None):
+ """
+ Is this element horizontal?
+ If rotation is None, use this element's rotation.
+
+ Args:
+ rotation: the optional rotation
+
+ Returns:
+ true if rotation is horizontal
+ """
+ rotation = rotation or self.rotation
+ return rotation in (0, 180)
+
+ def is_vertical(self, rotation=None):
+ """
+ Is this element vertical?
+ If rotation is None, use this element's rotation.
+
+ Args:
+ rotation: the optional rotation
+
+ Returns:
+ true if rotation is vertical
+ """
+ rotation = rotation or self.rotation
+ return rotation in (90, 270)
+
+ def rotate(self, rotation):
+ """
+ Rotate all of the areas by 90 degrees.
+
+ Args:
+ rotation: multiple of 90 degrees
+ """
+ self.rotation = (self.rotation + rotation) % 360
+
+ def move(self, delta_coor):
+ """
+ Move the element by adding the delta_coor to the current coordinate.
+
+ Args:
+ delta_coor: (delta_x,delta_y) tuple
+ """
+ x, y = self.coordinate
+ dx, dy = delta_coor
+ self.coordinate = (x + dx, y + dy)
+
+ def create_labels(self, cr=None):
+ """
+ Create labels (if applicable) and call on all children.
+ Call this base method before creating labels in the element.
+ """
+
+ def create_shapes(self):
+ """
+ Create shapes (if applicable) and call on all children.
+ Call this base method before creating shapes in the element.
+ """
+
+ def draw(self, cr):
+ raise NotImplementedError()
+
+ def bounds_from_area(self, area):
+ x1, y1, w, h = area
+ x2 = x1 + w
+ y2 = y1 + h
+ self._bounding_rects = [(x1, y1, x2, y2)]
+ self._bounding_points = [(x1, y1), (x2, y1), (x1, y2), (x2, y2)]
+
+ def bounds_from_line(self, line):
+ self._bounding_rects = rects = []
+ self._bounding_points = list(line)
+ last_point = line[0]
+ for x2, y2 in line[1:]:
+ (x1, y1), last_point = last_point, (x2, y2)
+ if x1 == x2:
+ x1, x2 = x1 - LINE_SELECT_SENSITIVITY, x2 + LINE_SELECT_SENSITIVITY
+ if y2 < y1:
+ y1, y2 = y2, y1
+ elif y1 == y2:
+ y1, y2 = y1 - LINE_SELECT_SENSITIVITY, y2 + LINE_SELECT_SENSITIVITY
+ if x2 < x1:
+ x1, x2 = x2, x1
+
+ rects.append((x1, y1, x2, y2))
+
+ def what_is_selected(self, coor, coor_m=None):
+ """
+ One coordinate specified:
+ Is this element selected at given coordinate?
+ ie: is the coordinate encompassed by one of the areas or lines?
+ Both coordinates specified:
+ Is this element within the rectangular region defined by both coordinates?
+ ie: do any area corners or line endpoints fall within the region?
+
+ Args:
+ coor: the selection coordinate, tuple x, y
+ coor_m: an additional selection coordinate.
+
+ Returns:
+ self if one of the areas/lines encompasses coor, else None.
+ """
+ x, y = [a - b for a, b in zip(coor, self.coordinate)]
+
+ if not coor_m:
+ for x1, y1, x2, y2 in self._bounding_rects:
+ if x1 <= x <= x2 and y1 <= y <= y2:
+ return self
+ else:
+ x_m, y_m = [a - b for a, b in zip(coor_m, self.coordinate)]
+ if y_m < y:
+ y, y_m = y_m, y
+ if x_m < x:
+ x, x_m = x_m, x
+
+ for x1, y1 in self._bounding_points:
+ if x <= x1 <= x_m and y <= y1 <= y_m:
+ return self
+
+ def get_extents(self):
+ x_min, y_min = x_max, y_max = self.coordinate
+ x_min += min(x for x, y in self._bounding_points)
+ y_min += min(y for x, y in self._bounding_points)
+ x_max += max(x for x, y in self._bounding_points)
+ y_max += max(y for x, y in self._bounding_points)
+ return x_min, y_min, x_max, y_max
+
+ def mouse_over(self):
+ pass
+
+ def mouse_out(self):
+ pass
diff --git a/grc/gui/FlowGraph.py b/grc/gui/canvas/flowgraph.py
index c3ce351578..af97ed3325 100644
--- a/grc/gui/FlowGraph.py
+++ b/grc/gui/canvas/flowgraph.py
@@ -1,5 +1,5 @@
"""
-Copyright 2007-2011 Free Software Foundation, Inc.
+Copyright 2007-2011, 2016q Free Software Foundation, Inc.
This file is part of GNU Radio
GNU Radio Companion is free software; you can redistribute it and/or
@@ -17,53 +17,58 @@ along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
"""
+from __future__ import absolute_import
+
+import ast
import functools
import random
from distutils.spawn import find_executable
-from itertools import chain, count
-from operator import methodcaller
-
-import gobject
+from itertools import count
-from . import Actions, Colors, Constants, Utils, Bars, Dialogs
-from .Constants import SCROLL_PROXIMITY_SENSITIVITY, SCROLL_DISTANCE
-from .Element import Element
-from .external_editor import ExternalEditor
+import six
+from gi.repository import GLib
+from six.moves import filter
-from ..core.FlowGraph import FlowGraph as _Flowgraph
-from ..core import Messages
+from . import colors
+from .drawable import Drawable
+from .connection import DummyConnection
+from .. import Actions, Constants, Utils, Bars, Dialogs
+from ..external_editor import ExternalEditor
+from ...core import Messages
+from ...core.FlowGraph import FlowGraph as CoreFlowgraph
-class FlowGraph(Element, _Flowgraph):
+class FlowGraph(CoreFlowgraph, Drawable):
"""
FlowGraph is the data structure to store graphical signal blocks,
graphical inputs and outputs,
and the connections between inputs and outputs.
"""
- def __init__(self, **kwargs):
+ def __init__(self, parent, **kwargs):
"""
FlowGraph constructor.
Create a list for signal blocks and connections. Connect mouse handlers.
"""
- Element.__init__(self)
- _Flowgraph.__init__(self, **kwargs)
- #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
+ super(self.__class__, self).__init__(parent, **kwargs)
+ Drawable.__init__(self)
+ self.drawing_area = None
+ # important vars dealing with mouse event tracking
self.element_moved = False
self.mouse_pressed = False
- self._selected_elements = []
self.press_coor = (0, 0)
- #selected ports
+ # selected
+ self.selected_elements = set()
self._old_selected_port = None
self._new_selected_port = None
# current mouse hover element
self.element_under_mouse = None
- #context menu
+ # context menu
self._context_menu = Bars.ContextMenu()
self.get_context_menu = lambda: self._context_menu
+ self._new_connection = None
+ self._elements_to_draw = []
self._external_updaters = {}
def _get_unique_id(self, base_id=''):
@@ -76,21 +81,22 @@ class FlowGraph(Element, _Flowgraph):
Returns:
a unique id
"""
+ block_ids = set(b.name for b in self.blocks)
for index in count():
block_id = '{}_{}'.format(base_id, index)
- if block_id not in (b.get_id() for b in self.blocks):
+ if block_id not in block_ids:
break
return block_id
def install_external_editor(self, param, parent=None):
- target = (param.get_parent().get_id(), param.get_key())
+ target = (param.parent_block.name, param.key)
if target in self._external_updaters:
editor = self._external_updaters[target]
else:
- config = self.get_parent().config
+ config = self.parent_platform.config
editor = (find_executable(config.editor) or
- Dialogs.ChooseEditorDialog(config, parent))
+ Dialogs.choose_editor(parent, config)) # todo: pass in parent
if not editor:
return
updater = functools.partial(
@@ -98,7 +104,7 @@ class FlowGraph(Element, _Flowgraph):
editor = self._external_updaters[target] = ExternalEditor(
editor=editor,
name=target[0], value=param.get_value(),
- callback=functools.partial(gobject.idle_add, updater)
+ callback=functools.partial(GLib.idle_add, updater)
)
editor.start()
try:
@@ -107,12 +113,12 @@ class FlowGraph(Element, _Flowgraph):
# Problem launching the editor. Need to select a new editor.
Messages.send('>>> Error opening an external editor. Please select a different editor.\n')
# Reset the editor to force the user to select a new one.
- self.get_parent().config.editor = ''
+ self.parent_platform.config.editor = ''
def handle_external_editor_change(self, new_value, target):
try:
block_id, param_key = target
- self.get_block(block_id).get_param(param_key).set_value(new_value)
+ self.get_block(block_id).params[param_key].set_value(new_value)
except (IndexError, ValueError): # block no longer exists
self._external_updaters[target].stop()
@@ -120,19 +126,6 @@ class FlowGraph(Element, _Flowgraph):
return
Actions.EXTERNAL_UPDATE()
-
- ###########################################################################
- # Access Drawing Area
- ###########################################################################
- def get_drawing_area(self): return self.drawing_area
- def queue_draw(self): self.get_drawing_area().queue_draw()
- 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_scroll_pane(self): return self.drawing_area.get_parent()
- def get_ctrl_mask(self): return self.drawing_area.ctrl_mask
- def get_mod1_mask(self): return self.drawing_area.mod1_mask
- def new_pixmap(self, *args): return self.get_drawing_area().new_pixmap(*args)
-
def add_new_block(self, key, coor=None):
"""
Add a block of the given key to this flow graph.
@@ -142,24 +135,65 @@ class FlowGraph(Element, _Flowgraph):
coor: an optional coordinate or None for random
"""
id = self._get_unique_id(key)
- #calculate the position coordinate
- W, H = self.get_size()
- h_adj = self.get_scroll_pane().get_hadjustment()
- v_adj = self.get_scroll_pane().get_vadjustment()
+ scroll_pane = self.drawing_area.get_parent().get_parent()
+ # calculate the position coordinate
+ h_adj = scroll_pane.get_hadjustment()
+ v_adj = scroll_pane.get_vadjustment()
if coor is None: coor = (
- int(random.uniform(.25, .75) * min(h_adj.page_size, W) +
- h_adj.get_value()),
- int(random.uniform(.25, .75) * min(v_adj.page_size, H) +
- v_adj.get_value()),
+ int(random.uniform(.25, .75)*h_adj.get_page_size() + h_adj.get_value()),
+ int(random.uniform(.25, .75)*v_adj.get_page_size() + v_adj.get_value()),
)
- #get the new block
+ # get the new block
block = self.new_block(key)
- block.set_coordinate(coor)
- block.set_rotation(0)
- block.get_param('id').set_value(id)
+ block.coordinate = coor
+ block.params['id'].set_value(id)
Actions.ELEMENT_CREATE()
return id
+ def make_connection(self):
+ """this selection and the last were ports, try to connect them"""
+ if self._new_connection and self._new_connection.has_real_sink:
+ self._old_selected_port = self._new_connection.source_port
+ self._new_selected_port = self._new_connection.sink_port
+ if self._old_selected_port and self._new_selected_port:
+ try:
+ self.connect(self._old_selected_port, self._new_selected_port)
+ Actions.ELEMENT_CREATE()
+ except Exception as e:
+ Messages.send_fail_connection(e)
+ self._old_selected_port = None
+ self._new_selected_port = None
+ return True
+ return False
+
+ def update(self):
+ """
+ Call the top level rewrite and validate.
+ Call the top level create labels and shapes.
+ """
+ self.rewrite()
+ self.validate()
+ self.update_elements_to_draw()
+ self.create_labels()
+ self.create_shapes()
+
+ def reload(self):
+ """
+ Reload flow-graph (with updated blocks)
+
+ Args:
+ page: the page to reload (None means current)
+ Returns:
+ False if some error occurred during import
+ """
+ success = False
+ data = self.export_data()
+ if data:
+ self.unselect()
+ success = self.import_data(data)
+ self.update()
+ return success
+
###########################################################################
# Copy Paste
###########################################################################
@@ -171,19 +205,20 @@ class FlowGraph(Element, _Flowgraph):
the clipboard
"""
#get selected blocks
- blocks = self.get_selected_blocks()
- if not blocks: return None
+ blocks = list(self.selected_blocks())
+ if not blocks:
+ return None
#calc x and y min
- x_min, y_min = blocks[0].get_coordinate()
+ x_min, y_min = blocks[0].coordinate
for block in blocks:
- x, y = block.get_coordinate()
+ x, y = block.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,
+ connections = list(filter(
+ lambda c: c.source_block in blocks and c.sink_block in blocks,
self.connections,
- )
+ ))
clipboard = (
(x_min, y_min),
[block.export_data() for block in blocks],
@@ -198,52 +233,58 @@ class FlowGraph(Element, _Flowgraph):
Args:
clipboard: the nested data of blocks, connections
"""
+ # todo: rewrite this...
selected = set()
(x_min, y_min), blocks_n, connections_n = clipboard
old_id2block = dict()
- #recalc the position
- 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
+ # recalc the position
+ scroll_pane = self.drawing_area.get_parent().get_parent()
+ h_adj = scroll_pane.get_hadjustment()
+ v_adj = scroll_pane.get_vadjustment()
+ x_off = h_adj.get_value() - x_min + h_adj.get_page_size() / 4
+ y_off = v_adj.get_value() - y_min + v_adj.get_page_size() / 4
if len(self.get_elements()) <= 1:
x_off, y_off = 0, 0
#create blocks
for block_n in blocks_n:
- block_key = block_n.find('key')
- if block_key == 'options': continue
+ block_key = block_n.get('key')
+ if block_key == 'options':
+ continue
block = self.new_block(block_key)
if not block:
continue # unknown block was pasted (e.g. dummy block)
selected.add(block)
#set params
- params = dict((n.find('key'), n.find('value'))
- for n in block_n.findall('param'))
+ param_data = {n['key']: n['value'] for n in block_n.get('param', [])}
+ for key in block.states:
+ try:
+ block.states[key] = ast.literal_eval(param_data.pop(key))
+ except (KeyError, SyntaxError, ValueError):
+ pass
if block_key == 'epy_block':
- block.get_param('_io_cache').set_value(params.pop('_io_cache'))
- block.get_param('_source_code').set_value(params.pop('_source_code'))
+ block.params['_io_cache'].set_value(param_data.pop('_io_cache'))
+ block.params['_source_code'].set_value(param_data.pop('_source_code'))
block.rewrite() # this creates the other params
- for param_key, param_value in params.iteritems():
+ for param_key, param_value in six.iteritems(param_data):
#setup id parameter
if param_key == 'id':
old_id2block[param_value] = block
#if the block id is not unique, get a new block id
- if param_value in (blk.get_id() for blk in self.blocks):
+ if param_value in (blk.name for blk in self.blocks):
param_value = self._get_unique_id(param_value)
#set value to key
- block.get_param(param_key).set_value(param_value)
+ block.params[param_key].set_value(param_value)
#move block to offset coordinate
block.move((x_off, y_off))
#update before creating connections
self.update()
#create connections
for connection_n in connections_n:
- source = old_id2block[connection_n.find('source_block_id')].get_source(connection_n.find('source_key'))
- sink = old_id2block[connection_n.find('sink_block_id')].get_sink(connection_n.find('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)
+ source = old_id2block[connection_n.get('source_block_id')].get_source(connection_n.get('source_key'))
+ sink = old_id2block[connection_n.get('sink_block_id')].get_sink(connection_n.get('sink_key'))
+ connection = self.connect(source, sink)
+ selected.add(connection)
+ self.selected_elements = selected
###########################################################################
# Modify Selected
@@ -258,7 +299,7 @@ class FlowGraph(Element, _Flowgraph):
Returns:
true for change
"""
- return any([sb.type_controller_modify(direction) for sb in self.get_selected_blocks()])
+ return any(sb.type_controller_modify(direction) for sb in self.selected_blocks())
def port_controller_modify_selected(self, direction):
"""
@@ -270,35 +311,22 @@ class FlowGraph(Element, _Flowgraph):
Returns:
true for changed
"""
- return any([sb.port_controller_modify(direction) for sb in self.get_selected_blocks()])
+ return any(sb.port_controller_modify(direction) for sb in self.selected_blocks())
- def enable_selected(self, enable):
+ def change_state_selected(self, new_state):
"""
Enable/disable the selected blocks.
Args:
- enable: true to enable
+ new_state: a block state
Returns:
true if changed
"""
changed = False
- for selected_block in self.get_selected_blocks():
- if selected_block.set_enabled(enable): changed = True
- return changed
-
- def bypass_selected(self):
- """
- Bypass the selected blocks.
-
- Args:
- None
- Returns:
- true if changed
- """
- changed = False
- for selected_block in self.get_selected_blocks():
- if selected_block.set_bypassed(): changed = True
+ for block in self.selected_blocks():
+ changed |= block.state != new_state
+ block.state = new_state
return changed
def move_selected(self, delta_coordinate):
@@ -308,10 +336,7 @@ class FlowGraph(Element, _Flowgraph):
Args:
delta_coordinate: the change in coordinates
"""
- for selected_block in self.get_selected_blocks():
- delta_coordinate = selected_block.bound_move_delta(delta_coordinate)
-
- for selected_block in self.get_selected_blocks():
+ for selected_block in self.selected_blocks():
selected_block.move(delta_coordinate)
self.element_moved = True
@@ -325,34 +350,34 @@ class FlowGraph(Element, _Flowgraph):
Returns:
True if changed, otherwise False
"""
- blocks = self.get_selected_blocks()
+ blocks = list(self.selected_blocks())
if calling_action is None or not blocks:
return False
# compute common boundary of selected objects
- min_x, min_y = max_x, max_y = blocks[0].get_coordinate()
+ min_x, min_y = max_x, max_y = blocks[0].coordinate
for selected_block in blocks:
- x, y = selected_block.get_coordinate()
+ x, y = selected_block.coordinate
min_x, min_y = min(min_x, x), min(min_y, y)
- x += selected_block.W
- y += selected_block.H
+ x += selected_block.width
+ y += selected_block.height
max_x, max_y = max(max_x, x), max(max_y, y)
ctr_x, ctr_y = (max_x + min_x)/2, (max_y + min_y)/2
# align the blocks as requested
transform = {
- Actions.BLOCK_VALIGN_TOP: lambda x, y, w, h: (x, min_y),
- Actions.BLOCK_VALIGN_MIDDLE: lambda x, y, w, h: (x, ctr_y - h/2),
- Actions.BLOCK_VALIGN_BOTTOM: lambda x, y, w, h: (x, max_y - h),
- Actions.BLOCK_HALIGN_LEFT: lambda x, y, w, h: (min_x, y),
- Actions.BLOCK_HALIGN_CENTER: lambda x, y, w, h: (ctr_x-w/2, y),
- Actions.BLOCK_HALIGN_RIGHT: lambda x, y, w, h: (max_x - w, y),
+ Actions.BLOCK_VALIGN_TOP: lambda x, y, w, h: (x, min_y),
+ Actions.BLOCK_VALIGN_MIDDLE: lambda x, y, w, h: (x, ctr_y - h/2),
+ Actions.BLOCK_VALIGN_BOTTOM: lambda x, y, w, h: (x, max_y - h),
+ Actions.BLOCK_HALIGN_LEFT: lambda x, y, w, h: (min_x, y),
+ Actions.BLOCK_HALIGN_CENTER: lambda x, y, w, h: (ctr_x-w/2, y),
+ Actions.BLOCK_HALIGN_RIGHT: lambda x, y, w, h: (max_x - w, y),
}.get(calling_action, lambda *args: args)
for selected_block in blocks:
- x, y = selected_block.get_coordinate()
- w, h = selected_block.W, selected_block.H
- selected_block.set_coordinate(transform(x, y, w, h))
+ x, y = selected_block.coordinate
+ w, h = selected_block.width, selected_block.height
+ selected_block.coordinate = transform(x, y, w, h)
return True
@@ -366,25 +391,24 @@ class FlowGraph(Element, _Flowgraph):
Returns:
true if changed, otherwise false.
"""
- if not self.get_selected_blocks():
+ if not any(self.selected_blocks()):
return False
#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():
+ min_x, min_y = max_x, max_y = self.selected_block.coordinate
+ # rotate each selected block, and find min/max coordinate
+ for selected_block in self.selected_blocks():
selected_block.rotate(rotation)
#update the min/max coordinate
- x, y = selected_block.get_coordinate()
+ x, y = selected_block.coordinate
min_x, min_y = min(min_x, x), min(min_y, y)
max_x, max_y = max(max_x, x), max(max_y, y)
#calculate center point of slected blocks
ctr_x, ctr_y = (max_x + min_x)/2, (max_y + min_y)/2
#rotate the blocks around the center point
- for selected_block in self.get_selected_blocks():
- x, y = selected_block.get_coordinate()
+ for selected_block in self.selected_blocks():
+ x, y = selected_block.coordinate
x, y = Utils.get_rotated_coordinate((x - ctr_x, y - ctr_y), rotation)
- selected_block.set_coordinate((x + ctr_x, y + ctr_y))
+ selected_block.coordinate = (x + ctr_x, y + ctr_y)
return True
def remove_selected(self):
@@ -395,116 +419,149 @@ class FlowGraph(Element, _Flowgraph):
true if changed.
"""
changed = False
- for selected_element in self.get_selected_elements():
+ for selected_element in self.selected_elements:
self.remove_element(selected_element)
changed = True
return changed
- def draw(self, gc, window):
- """
- 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.
- """
-
- W, H = self.get_size()
- hide_disabled_blocks = Actions.TOGGLE_HIDE_DISABLED_BLOCKS.get_active()
- hide_variables = Actions.TOGGLE_HIDE_VARIABLES.get_active()
-
- #draw the background
- gc.set_foreground(Colors.FLOWGRAPH_BACKGROUND_COLOR)
- window.draw_rectangle(gc, True, 0, 0, W, H)
-
- # draw comments first
- if Actions.TOGGLE_SHOW_BLOCK_COMMENTS.get_active():
- for block in self.blocks:
- if hide_variables and (block.is_variable or block.is_import):
- continue # skip hidden disabled blocks and connections
- if block.get_enabled():
- block.draw_comment(gc, window)
- #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
- gc.set_foreground(Colors.HIGHLIGHT_COLOR)
- window.draw_rectangle(gc, True, x, y, w, h)
- gc.set_foreground(Colors.BORDER_COLOR)
- window.draw_rectangle(gc, False, x, y, w, h)
- #draw blocks on top of connections
- blocks = sorted(self.blocks, key=methodcaller('get_enabled'))
- for element in chain(self.connections, blocks):
- if hide_disabled_blocks and not element.get_enabled():
- continue # skip hidden disabled blocks and connections
- if hide_variables and (element.is_variable or element.is_import):
- continue # skip hidden disabled blocks and connections
- element.draw(gc, window)
- #draw selected blocks on top of selected connections
- for selected_element in self.get_selected_connections() + self.get_selected_blocks():
- selected_element.draw(gc, window)
-
def update_selected(self):
"""
Remove deleted elements from the selected elements list.
Update highlighting so only the selected are highlighted.
"""
- selected_elements = self.get_selected_elements()
+ selected_elements = self.selected_elements
elements = self.get_elements()
- #remove deleted elements
- for selected in selected_elements:
- if selected in elements: continue
+ # remove deleted elements
+ for selected in list(selected_elements):
+ if selected in elements:
+ continue
selected_elements.remove(selected)
- if self._old_selected_port and self._old_selected_port.get_parent() not in elements:
+ if self._old_selected_port and self._old_selected_port.parent not in elements:
self._old_selected_port = None
- if self._new_selected_port and self._new_selected_port.get_parent() not in elements:
+ if self._new_selected_port and self._new_selected_port.parent not in elements:
self._new_selected_port = None
- #update highlighting
+ # update highlighting
for element in elements:
- element.set_highlighted(element in selected_elements)
+ element.highlighted = element in selected_elements
- def update(self):
- """
- Call the top level rewrite and validate.
- Call the top level create labels and shapes.
- """
- self.rewrite()
- self.validate()
- self.create_labels()
- self.create_shapes()
+ ###########################################################################
+ # Draw stuff
+ ###########################################################################
- def reload(self):
- """
- Reload flow-graph (with updated blocks)
+ def update_elements_to_draw(self):
+ hide_disabled_blocks = Actions.TOGGLE_HIDE_DISABLED_BLOCKS.get_active()
+ hide_variables = Actions.TOGGLE_HIDE_VARIABLES.get_active()
- Args:
- page: the page to reload (None means current)
- Returns:
- False if some error occurred during import
- """
- success = False
- data = self.export_data()
- if data:
- self.unselect()
- success = self.import_data(data)
- self.update()
- return success
+ def draw_order(elem):
+ return elem.highlighted, elem.is_block, elem.enabled
+
+ elements = sorted(self.get_elements(), key=draw_order)
+ del self._elements_to_draw[:]
+
+ for element in elements:
+ if hide_disabled_blocks and not element.enabled:
+ continue # skip hidden disabled blocks and connections
+ if hide_variables and (element.is_variable or element.is_import):
+ continue # skip hidden disabled blocks and connections
+ self._elements_to_draw.append(element)
+
+ def create_labels(self, cr=None):
+ for element in self._elements_to_draw:
+ element.create_labels(cr)
+
+ def create_shapes(self):
+ for element in self._elements_to_draw:
+ element.create_shapes()
+
+ def _drawables(self):
+ # todo: cache that
+ show_comments = Actions.TOGGLE_SHOW_BLOCK_COMMENTS.get_active()
+ for element in self._elements_to_draw:
+ if element.is_block and show_comments and element.enabled:
+ yield element.draw_comment
+ if self._new_connection is not None:
+ yield self._new_connection.draw
+ for element in self._elements_to_draw:
+ yield element.draw
+
+ def draw(self, cr):
+ """Draw blocks connections comment and select rectangle"""
+ for draw_element in self._drawables():
+ cr.save()
+ draw_element(cr)
+ cr.restore()
+
+ draw_multi_select_rectangle = (
+ self.mouse_pressed and
+ (not self.selected_elements or self.drawing_area.ctrl_mask) and
+ not self._new_connection
+ )
+ if draw_multi_select_rectangle:
+ x1, y1 = self.press_coor
+ x2, y2 = self.coordinate
+ x, y = int(min(x1, x2)), int(min(y1, y2))
+ w, h = int(abs(x1 - x2)), int(abs(y1 - y2))
+ cr.set_source_rgba(
+ colors.HIGHLIGHT_COLOR[0],
+ colors.HIGHLIGHT_COLOR[1],
+ colors.HIGHLIGHT_COLOR[2],
+ 0.5,
+ )
+ cr.rectangle(x, y, w, h)
+ cr.fill()
+ cr.rectangle(x, y, w, h)
+ cr.stroke()
##########################################################################
- ## Get Selected
+ # selection handling
##########################################################################
- def unselect(self):
+ def update_selected_elements(self):
"""
- Set selected elements to an empty set.
+ 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.
"""
- self._selected_elements = []
+ selected_elements = None
+ if self.mouse_pressed:
+ new_selections = self.what_is_selected(self.coordinate)
+ # update the selections if the new selection is not in the current selections
+ # allows us to move entire selected groups of elements
+ if not new_selections:
+ selected_elements = set()
+ elif self.drawing_area.ctrl_mask or self.selected_elements.isdisjoint(new_selections):
+ selected_elements = new_selections
- def select_all(self):
- """Select all blocks in the flow graph"""
- self._selected_elements = list(self.get_elements())
+ if self._old_selected_port:
+ self._old_selected_port.force_show_label = False
+ self.create_shapes()
+ self.drawing_area.queue_draw()
+ elif self._new_selected_port:
+ self._new_selected_port.force_show_label = True
+
+ else: # called from a mouse release
+ if not self.element_moved and (not self.selected_elements or self.drawing_area.ctrl_mask) and not self._new_connection:
+ selected_elements = self.what_is_selected(self.coordinate, self.press_coor)
+
+ # this selection and the last were ports, try to connect them
+ if self.make_connection():
+ return
+
+ # update selected elements
+ if selected_elements is None:
+ return
+
+ # if ctrl, set the selected elements to the union - intersection of old and new
+ if self.drawing_area.ctrl_mask:
+ self.selected_elements ^= selected_elements
+ else:
+ self.selected_elements.clear()
+ self.selected_elements.update(selected_elements)
+ Actions.ELEMENT_SELECT()
def what_is_selected(self, coor, coor_m=None):
"""
@@ -524,68 +581,60 @@ class FlowGraph(Element, _Flowgraph):
"""
selected_port = None
selected = set()
- #check the elements
- hide_disabled_blocks = Actions.TOGGLE_HIDE_DISABLED_BLOCKS.get_active()
- hide_variables = Actions.TOGGLE_HIDE_VARIABLES.get_active()
- for element in reversed(self.get_elements()):
- if hide_disabled_blocks and not element.get_enabled():
- continue # skip hidden disabled blocks and connections
- if hide_variables and (element.is_variable or element.is_import):
- continue # skip hidden disabled blocks and connections
+ # check the elements
+ for element in reversed(self._elements_to_draw):
selected_element = element.what_is_selected(coor, coor_m)
if not selected_element:
continue
- #update the selected port information
+ # update the selected port information
if selected_element.is_port:
- if not coor_m: selected_port = selected_element
- selected_element = selected_element.get_parent()
+ if not coor_m:
+ selected_port = selected_element
+ selected_element = selected_element.parent_block
+
selected.add(selected_element)
- #place at the end of the list
- self.get_elements().remove(element)
- self.get_elements().append(element)
- #single select mode, break
- if not coor_m: break
- #update selected ports
+ if not coor_m:
+ break
+
+ if selected_port and selected_port.is_source:
+ selected.remove(selected_port.parent_block)
+ self._new_connection = DummyConnection(selected_port, coordinate=coor)
+ self.drawing_area.queue_draw()
+ # update selected ports
if selected_port is not self._new_selected_port:
self._old_selected_port = self._new_selected_port
self._new_selected_port = selected_port
- return list(selected)
+ return selected
- def get_selected_connections(self):
+ def unselect(self):
"""
- Get a group of selected connections.
-
- Returns:
- sub set of connections in this flow graph
+ Set selected elements to an empty set.
"""
- selected = set()
- for selected_element in self.get_selected_elements():
- if selected_element.is_connection:
- selected.add(selected_element)
- return list(selected)
+ self.selected_elements.clear()
+
+ def select_all(self):
+ """Select all blocks in the flow graph"""
+ self.selected_elements.clear()
+ self.selected_elements.update(self._elements_to_draw)
- def get_selected_blocks(self):
+ def selected_blocks(self):
"""
Get a group of selected blocks.
Returns:
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)
+ return (e for e in self.selected_elements if e.is_block)
- def get_selected_block(self):
+ @property
+ def selected_block(self):
"""
Get the selected block when a block or port is selected.
Returns:
a block or None
"""
- selected_blocks = self.get_selected_blocks()
- return selected_blocks[0] if selected_blocks else None
+ return next(self.selected_blocks(), None)
def get_selected_elements(self):
"""
@@ -594,7 +643,7 @@ class FlowGraph(Element, _Flowgraph):
Returns:
sub set of elements in this flow graph
"""
- return self._selected_elements
+ return self.selected_elements
def get_selected_element(self):
"""
@@ -603,60 +652,10 @@ class FlowGraph(Element, _Flowgraph):
Returns:
a block, port, or connection or None
"""
- selected_elements = self.get_selected_elements()
- return selected_elements[0] if selected_elements else 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_selections = 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_selections and new_selections[0] in self.get_selected_elements()
- ): selected_elements = new_selections
- if self._old_selected_port:
- self._old_selected_port.force_label_unhidden(False)
- self.create_shapes()
- self.queue_draw()
- elif self._new_selected_port:
- self._new_selected_port.force_label_unhidden()
- 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:
- try:
- self.connect(self._old_selected_port, self._new_selected_port)
- Actions.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)
- )
- Actions.ELEMENT_SELECT()
+ return next(iter(self.selected_elements), None)
##########################################################################
- ## Event Handlers
+ # Event Handlers
##########################################################################
def handle_mouse_context_press(self, coordinate, event):
"""
@@ -665,12 +664,15 @@ class FlowGraph(Element, _Flowgraph):
Then, show the context menu at the mouse click location.
"""
selections = self.what_is_selected(coordinate)
- if not set(selections).intersection(self.get_selected_elements()):
- self.set_coordinate(coordinate)
+ if not selections.intersection(self.selected_elements):
+ self.coordinate = coordinate
self.mouse_pressed = True
self.update_selected_elements()
self.mouse_pressed = False
- self._context_menu.popup(None, None, None, event.button, event.time)
+ if self._new_connection:
+ self._new_connection = None
+ self.drawing_area.queue_draw()
+ self._context_menu.popup(None, None, None, None, event.button, event.time)
def handle_mouse_selector_press(self, double_click, coordinate):
"""
@@ -680,13 +682,13 @@ class FlowGraph(Element, _Flowgraph):
Update the selection state of the flow graph.
"""
self.press_coor = coordinate
- self.set_coordinate(coordinate)
- self.time = 0
+ self.coordinate = coordinate
self.mouse_pressed = True
- if double_click: self.unselect()
+ if double_click:
+ self.unselect()
self.update_selected_elements()
- #double click detected, bring up params dialog if possible
- if double_click and self.get_selected_block():
+
+ if double_click and self.selected_block:
self.mouse_pressed = False
Actions.BLOCK_PARAM_MODIFY()
@@ -696,13 +698,15 @@ class FlowGraph(Element, _Flowgraph):
Update the state, handle motion (dragging).
And update the selected flowgraph elements.
"""
- self.set_coordinate(coordinate)
- self.time = 0
+ self.coordinate = coordinate
self.mouse_pressed = False
if self.element_moved:
Actions.BLOCK_MOVE()
self.element_moved = False
self.update_selected_elements()
+ if self._new_connection:
+ self._new_connection = None
+ self.drawing_area.queue_draw()
def handle_mouse_motion(self, coordinate):
"""
@@ -710,56 +714,70 @@ class FlowGraph(Element, _Flowgraph):
Move a selected element to the new coordinate.
Auto-scroll the scroll bars at the boundaries.
"""
- #to perform a movement, the mouse must be pressed
- # (no longer checking pending events via gtk.events_pending() - always true in Windows)
- if not self.mouse_pressed:
- # only continue if mouse-over stuff is enabled (just the auto-hide port label stuff for now)
- if not Actions.TOGGLE_AUTO_HIDE_PORT_LABELS.get_active(): return
- redraw = False
- for element in reversed(self.get_elements()):
- over_element = element.what_is_selected(coordinate)
- if not over_element: continue
- if over_element != self.element_under_mouse: # over sth new
- if self.element_under_mouse:
- redraw |= self.element_under_mouse.mouse_out() or False
- self.element_under_mouse = over_element
- redraw |= over_element.mouse_over() or False
- break
- else:
+ # to perform a movement, the mouse must be pressed
+ # (no longer checking pending events via Gtk.events_pending() - always true in Windows)
+ redraw = False
+ if not self.mouse_pressed or self._new_connection:
+ redraw = self._handle_mouse_motion_move(coordinate)
+ if self.mouse_pressed:
+ redraw = redraw or self._handle_mouse_motion_drag(coordinate)
+ if redraw:
+ self.drawing_area.queue_draw()
+
+ def _handle_mouse_motion_move(self, coordinate):
+ # only continue if mouse-over stuff is enabled (just the auto-hide port label stuff for now)
+ if not Actions.TOGGLE_AUTO_HIDE_PORT_LABELS.get_active():
+ return
+ redraw = False
+ for element in self._elements_to_draw:
+ over_element = element.what_is_selected(coordinate)
+ if not over_element:
+ continue
+ if over_element != self.element_under_mouse: # over sth new
if self.element_under_mouse:
redraw |= self.element_under_mouse.mouse_out() or False
- self.element_under_mouse = None
- if redraw:
- #self.create_labels()
- self.create_shapes()
- self.queue_draw()
+ self.element_under_mouse = over_element
+ redraw |= over_element.mouse_over() or False
+ break
else:
- #perform auto-scrolling
- 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')
- #remove the connection if selected in drag event
- if len(self.get_selected_elements()) == 1 and self.get_selected_element().is_connection:
- Actions.ELEMENT_DELETE()
- #move the selected elements and record the new coordinate
- if not self.get_ctrl_mask():
- X, Y = self.get_coordinate()
- dX, dY = int(x - X), int(y - Y)
- active = Actions.TOGGLE_SNAP_TO_GRID.get_active() or self.get_mod1_mask()
- if not active or abs(dX) >= Utils.CANVAS_GRID_SIZE or abs(dY) >= Utils.CANVAS_GRID_SIZE:
- self.move_selected((dX, dY))
- self.set_coordinate((x, y))
- #queue draw for animation
- self.queue_draw()
+ if self.element_under_mouse:
+ redraw |= self.element_under_mouse.mouse_out() or False
+ self.element_under_mouse = None
+ if redraw:
+ # self.create_labels()
+ self.create_shapes()
+ return redraw
+
+ def _handle_mouse_motion_drag(self, coordinate):
+ redraw = False
+ # remove the connection if selected in drag event
+ if len(self.selected_elements) == 1 and self.get_selected_element().is_connection:
+ Actions.ELEMENT_DELETE()
+ redraw = True
+
+ if self._new_connection:
+ e = self.element_under_mouse
+ if e and e.is_port and e.is_sink:
+ self._new_connection.update(sink_port=self.element_under_mouse)
+ else:
+ self._new_connection.update(coordinate=coordinate, rotation=0)
+ return True
+ # move the selected elements and record the new coordinate
+ x, y = coordinate
+ if not self.drawing_area.ctrl_mask:
+ X, Y = self.coordinate
+ dX, dY = int(x - X), int(y - Y)
+ active = Actions.TOGGLE_SNAP_TO_GRID.get_active() or self.drawing_area.mod1_mask
+ if not active or abs(dX) >= Constants.CANVAS_GRID_SIZE or abs(dY) >= Constants.CANVAS_GRID_SIZE:
+ self.move_selected((dX, dY))
+ self.coordinate = (x, y)
+ redraw = True
+ return redraw
+
+ def get_extents(self):
+ extent = 100000, 100000, 0, 0
+ for element in self._elements_to_draw:
+ extent = (min_or_max(xy, e_xy) for min_or_max, xy, e_xy in zip(
+ (min, min, max, max), extent, element.get_extents()
+ ))
+ return tuple(extent)
diff --git a/grc/gui/canvas/param.py b/grc/gui/canvas/param.py
new file mode 100644
index 0000000000..e2c335d9cf
--- /dev/null
+++ b/grc/gui/canvas/param.py
@@ -0,0 +1,162 @@
+# Copyright 2007-2016 Free Software Foundation, Inc.
+# This file is part of GNU Radio
+#
+# GNU Radio Companion is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# GNU Radio Companion is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
+
+from __future__ import absolute_import
+
+from .drawable import Drawable
+
+from .. import ParamWidgets, Utils, Constants
+
+from ...core.Param import Param as CoreParam
+
+
+class Param(CoreParam):
+ """The graphical parameter."""
+
+ make_cls_with_base = classmethod(Drawable.make_cls_with_base.__func__)
+
+ def get_input(self, *args, **kwargs):
+ """
+ Get the graphical gtk class to represent this parameter.
+ An enum requires and combo parameter.
+ A non-enum with options gets a combined entry/combo parameter.
+ All others get a standard entry parameter.
+
+ Returns:
+ gtk input class
+ """
+ dtype = self.dtype
+ if dtype in ('file_open', 'file_save'):
+ input_widget_cls = ParamWidgets.FileParam
+
+ elif dtype == 'enum':
+ input_widget_cls = ParamWidgets.EnumParam
+
+ elif self.options:
+ input_widget_cls = ParamWidgets.EnumEntryParam
+
+ elif dtype == '_multiline':
+ input_widget_cls = ParamWidgets.MultiLineEntryParam
+
+ elif dtype == '_multiline_python_external':
+ input_widget_cls = ParamWidgets.PythonEditorParam
+
+ else:
+ input_widget_cls = ParamWidgets.EntryParam
+
+ return input_widget_cls(self, *args, **kwargs)
+
+ def format_label_markup(self, have_pending_changes=False):
+ block = self.parent
+ # fixme: using non-public attribute here
+ has_callback = \
+ hasattr(block, 'templates') and \
+ any(self.key in callback for callback in block.templates.get('callbacks', ''))
+
+ return '<span {underline} {foreground} font_desc="Sans 9">{label}</span>'.format(
+ underline='underline="low"' if has_callback else '',
+ foreground='foreground="blue"' if have_pending_changes else
+ 'foreground="red"' if not self.is_valid() else '',
+ label=Utils.encode(self.name)
+ )
+
+ def format_tooltip_text(self):
+ errors = self.get_error_messages()
+ tooltip_lines = ['Key: ' + self.key, 'Type: ' + self.dtype]
+ if self.is_valid():
+ value = str(self.get_evaluated())
+ if len(value) > 100:
+ value = '{}...{}'.format(value[:50], value[-50:])
+ tooltip_lines.append('Value: ' + value)
+ elif len(errors) == 1:
+ tooltip_lines.append('Error: ' + errors[0])
+ elif len(errors) > 1:
+ tooltip_lines.append('Error:')
+ tooltip_lines.extend(' * ' + msg for msg in errors)
+ return '\n'.join(tooltip_lines)
+
+ def pretty_print(self):
+ """
+ Get the repr (nice string format) for this param.
+
+ Returns:
+ the string representation
+ """
+ ##################################################
+ # Truncate helper method
+ ##################################################
+ def _truncate(string, style=0):
+ max_len = max(27 - len(self.name), 3)
+ if len(string) > max_len:
+ if style < 0: # Front truncate
+ string = '...' + string[3-max_len:]
+ elif style == 0: # Center truncate
+ string = string[:max_len//2 - 3] + '...' + string[-max_len//2:]
+ elif style > 0: # Rear truncate
+ string = string[:max_len-3] + '...'
+ return string
+
+ ##################################################
+ # Simple conditions
+ ##################################################
+ value = self.get_value()
+ if not self.is_valid():
+ return _truncate(value)
+ if value in self.options:
+ return self.options[value] # its name
+
+ ##################################################
+ # Split up formatting by type
+ ##################################################
+ # Default center truncate
+ truncate = 0
+ e = self.get_evaluated()
+ t = self.dtype
+ if isinstance(e, bool):
+ return str(e)
+ elif isinstance(e, Constants.COMPLEX_TYPES):
+ dt_str = Utils.num_to_str(e)
+ elif isinstance(e, Constants.VECTOR_TYPES):
+ # Vector types
+ if len(e) > 8:
+ # Large vectors use code
+ dt_str = self.get_value()
+ truncate = 1
+ else:
+ # Small vectors use eval
+ dt_str = ', '.join(map(Utils.num_to_str, e))
+ elif t in ('file_open', 'file_save'):
+ dt_str = self.get_value()
+ truncate = -1
+ else:
+ # Other types
+ dt_str = str(e)
+
+ # Done
+ return _truncate(dt_str, truncate)
+
+ def format_block_surface_markup(self):
+ """
+ Get the markup for this param.
+
+ Returns:
+ a pango markup string
+ """
+ return '<span {foreground} font_desc="{font}"><b>{label}:</b> {value}</span>'.format(
+ foreground='foreground="red"' if not self.is_valid() else '', font=Constants.PARAM_FONT,
+ label=Utils.encode(self.name), value=Utils.encode(self.pretty_print().replace('\n', ' '))
+ )
diff --git a/grc/gui/canvas/port.py b/grc/gui/canvas/port.py
new file mode 100644
index 0000000000..2ea35f3dd3
--- /dev/null
+++ b/grc/gui/canvas/port.py
@@ -0,0 +1,227 @@
+"""
+Copyright 2007, 2008, 2009 Free Software Foundation, Inc.
+This file is part of GNU Radio
+
+GNU Radio Companion is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation; either version 2
+of the License, or (at your option) any later version.
+
+GNU Radio Companion is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
+"""
+
+from __future__ import absolute_import, division
+
+import math
+
+from gi.repository import Gtk, PangoCairo, Pango
+
+from . import colors
+from .drawable import Drawable
+from .. import Actions, Utils, Constants
+
+from ...core.utils.descriptors import nop_write
+from ...core.ports import Port as CorePort
+
+
+class Port(CorePort, Drawable):
+ """The graphical port."""
+
+ def __init__(self, parent, direction, **n):
+ """
+ Port constructor.
+ Create list of connector coordinates.
+ """
+ super(self.__class__, self).__init__(parent, direction, **n)
+ Drawable.__init__(self)
+ self._connector_coordinate = (0, 0)
+ self._hovering = False
+ self.force_show_label = False
+
+ self._area = []
+ self._bg_color = self._border_color = 0, 0, 0, 0
+ self._font_color = list(colors.FONT_COLOR)
+
+ self._line_width_factor = 1.0
+ self._label_layout_offsets = 0, 0
+
+ self.width_with_label = self.height = 0
+
+ self.label_layout = None
+
+ @property
+ def width(self):
+ return self.width_with_label if self._show_label else Constants.PORT_LABEL_HIDDEN_WIDTH
+
+ @width.setter
+ def width(self, value):
+ self.width_with_label = value
+ self.label_layout.set_width(value * Pango.SCALE)
+
+ def _update_colors(self):
+ """
+ Get the color that represents this port's type.
+ Codes differ for ports where the vec length is 1 or greater than 1.
+
+ Returns:
+ a hex color code.
+ """
+ if not self.parent_block.enabled:
+ self._font_color[-1] = 0.4
+ color = colors.BLOCK_DISABLED_COLOR
+ elif self.domain == Constants.GR_MESSAGE_DOMAIN:
+ color = colors.PORT_TYPE_TO_COLOR.get('message')
+ else:
+ self._font_color[-1] = 1.0
+ color = colors.PORT_TYPE_TO_COLOR.get(self.dtype) or colors.PORT_TYPE_TO_COLOR.get('')
+ if self.vlen > 1:
+ dark = (0, 0, 30 / 255.0, 50 / 255.0, 70 / 255.0)[min(4, self.vlen)]
+ color = tuple(max(c - dark, 0) for c in color)
+ self._bg_color = color
+ self._border_color = tuple(max(c - 0.3, 0) for c in color)
+
+ def create_shapes(self):
+ """Create new areas and labels for the port."""
+ if self.is_horizontal():
+ self._area = (0, 0, self.width, self.height)
+ elif self.is_vertical():
+ self._area = (0, 0, self.height, self.width)
+ self.bounds_from_area(self._area)
+
+ self._connector_coordinate = {
+ 0: (self.width, self.height / 2),
+ 90: (self.height / 2, 0),
+ 180: (0, self.height / 2),
+ 270: (self.height / 2, self.width)
+ }[self.connector_direction]
+
+ def create_labels(self, cr=None):
+ """Create the labels for the socket."""
+ self.label_layout = Gtk.DrawingArea().create_pango_layout('')
+ self.label_layout.set_alignment(Pango.Alignment.CENTER)
+
+ if cr:
+ PangoCairo.update_layout(cr, self.label_layout)
+
+ if self.domain in (Constants.GR_MESSAGE_DOMAIN, Constants.GR_STREAM_DOMAIN):
+ self._line_width_factor = 1.0
+ else:
+ self._line_width_factor = 2.0
+
+ self._update_colors()
+
+ layout = self.label_layout
+ layout.set_markup('<span font_desc="{font}">{name}</span>'.format(
+ name=Utils.encode(self.name), font=Constants.PORT_FONT
+ ))
+ label_width, label_height = self.label_layout.get_size()
+
+ self.width = 2 * Constants.PORT_LABEL_PADDING + label_width / Pango.SCALE
+ self.height = 2 * Constants.PORT_LABEL_PADDING + label_height / Pango.SCALE
+ self._label_layout_offsets = [0, Constants.PORT_LABEL_PADDING]
+ # if self.dtype == 'bus':
+ # self.height += Constants.PORT_EXTRA_BUS_HEIGHT
+ # self._label_layout_offsets[1] += Constants.PORT_EXTRA_BUS_HEIGHT / 2
+ self.height += self.height % 2 # uneven height
+
+ def draw(self, cr):
+ """
+ Draw the socket with a label.
+ """
+ border_color = self._border_color
+ cr.set_line_width(self._line_width_factor * cr.get_line_width())
+ cr.translate(*self.coordinate)
+
+ cr.rectangle(*self._area)
+ cr.set_source_rgba(*self._bg_color)
+ cr.fill_preserve()
+ cr.set_source_rgba(*border_color)
+ cr.stroke()
+
+ if not self._show_label:
+ return # this port is folded (no label)
+
+ if self.is_vertical():
+ cr.rotate(-math.pi / 2)
+ cr.translate(-self.width, 0)
+ cr.translate(*self._label_layout_offsets)
+
+ cr.set_source_rgba(*self._font_color)
+ PangoCairo.update_layout(cr, self.label_layout)
+ PangoCairo.show_layout(cr, self.label_layout)
+
+ @property
+ def connector_coordinate_absolute(self):
+ """the coordinate where connections may attach to"""
+ return [sum(c) for c in zip(
+ self._connector_coordinate, # relative to port
+ self.coordinate, # relative to block
+ self.parent_block.coordinate # abs
+ )]
+
+ @property
+ def connector_direction(self):
+ """Get the direction that the socket points: 0,90,180,270."""
+ if self.is_source:
+ return self.rotation
+ elif self.is_sink:
+ return (self.rotation + 180) % 360
+
+ @nop_write
+ @property
+ def rotation(self):
+ return self.parent_block.rotation
+
+ def rotate(self, direction):
+ """
+ Rotate the parent rather than self.
+
+ Args:
+ direction: degrees to rotate
+ """
+ self.parent_block.rotate(direction)
+
+ def move(self, delta_coor):
+ """Move the parent rather than self."""
+ self.parent_block.move(delta_coor)
+
+ @property
+ def highlighted(self):
+ return self.parent_block.highlighted
+
+ @highlighted.setter
+ def highlighted(self, value):
+ self.parent_block.highlighted = value
+
+ @property
+ def _show_label(self):
+ """
+ Figure out if the label should be hidden
+
+ Returns:
+ true if the label should not be shown
+ """
+ return self._hovering or self.force_show_label or not Actions.TOGGLE_AUTO_HIDE_PORT_LABELS.get_active()
+
+ def mouse_over(self):
+ """
+ Called from flow graph on mouse-over
+ """
+ changed = not self._show_label
+ self._hovering = True
+ return changed
+
+ def mouse_out(self):
+ """
+ Called from flow graph on mouse-out
+ """
+ label_was_shown = self._show_label
+ self._hovering = False
+ return label_was_shown != self._show_label
diff --git a/grc/gui/external_editor.py b/grc/gui/external_editor.py
index 010bd71d1a..155b0915c5 100644
--- a/grc/gui/external_editor.py
+++ b/grc/gui/external_editor.py
@@ -17,6 +17,8 @@ along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
"""
+from __future__ import absolute_import, print_function
+
import os
import sys
import time
@@ -67,7 +69,7 @@ class ExternalEditor(threading.Thread):
time.sleep(1)
except Exception as e:
- print >> sys.stderr, "file monitor crashed:", str(e)
+ print("file monitor crashed:", str(e), file=sys.stderr)
finally:
try:
os.remove(self.filename)
@@ -76,10 +78,7 @@ class ExternalEditor(threading.Thread):
if __name__ == '__main__':
- def p(data):
- print data
-
- e = ExternalEditor('/usr/bin/gedit', "test", "content", p)
+ e = ExternalEditor('/usr/bin/gedit', "test", "content", print)
e.open_editor()
e.start()
time.sleep(15)
diff --git a/grc/main.py b/grc/main.py
index 0edab40769..4f4cd68704 100755
--- a/grc/main.py
+++ b/grc/main.py
@@ -15,13 +15,12 @@
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
-import argparse
+import argparse, logging, sys
-import gtk
-from gnuradio import gr
-
-from .gui.Platform import Platform
-from .gui.ActionHandler import ActionHandler
+import gi
+gi.require_version('Gtk', '3.0')
+gi.require_version('PangoCairo', '1.0')
+from gi.repository import Gtk
VERSION_AND_DISCLAIMER_TEMPLATE = """\
@@ -32,24 +31,63 @@ GRC comes with ABSOLUTELY NO WARRANTY.
This is free software, and you are welcome to redistribute it.
"""
+LOG_LEVELS = {
+ 'debug': logging.DEBUG,
+ 'info': logging.INFO,
+ 'warning': logging.WARNING,
+ 'error': logging.ERROR,
+ 'critical': logging.CRITICAL,
+}
+
def main():
+ from gnuradio import gr
parser = argparse.ArgumentParser(
description=VERSION_AND_DISCLAIMER_TEMPLATE % gr.version())
parser.add_argument('flow_graphs', nargs='*')
+ parser.add_argument('--log', choices=['debug', 'info', 'warning', 'error', 'critical'], default='warning')
args = parser.parse_args()
try:
- gtk.window_set_default_icon(gtk.IconTheme().load_icon('gnuradio-grc', 256, 0))
+ Gtk.window_set_default_icon(Gtk.IconTheme().load_icon('gnuradio-grc', 256, 0))
except:
pass
+ # Enable logging
+ # Note: All other modules need to use the 'grc.<module>' convention
+ log = logging.getLogger('grc')
+ log.setLevel(logging.DEBUG)
+
+ # Console formatting
+ console = logging.StreamHandler()
+ console.setLevel(LOG_LEVELS[args.log])
+
+ #msg_format = '[%(asctime)s - %(levelname)8s] --- %(message)s (%(filename)s:%(lineno)s)'
+ msg_format = '[%(levelname)s] %(message)s (%(filename)s:%(lineno)s)'
+ date_format = '%I:%M'
+ formatter = logging.Formatter(msg_format, datefmt=date_format)
+
+ #formatter = utils.log.ConsoleFormatter()
+ console.setFormatter(formatter)
+ log.addHandler(console)
+
+ py_version = sys.version.split()[0]
+ log.debug("Starting GNU Radio Companion ({})".format(py_version))
+
+ # Delay importing until the logging is setup
+ from .gui.Platform import Platform
+ from .gui.Application import Application
+
+ log.debug("Loading platform")
platform = Platform(
- prefs_file=gr.prefs(),
version=gr.version(),
version_parts=(gr.major_version(), gr.api_version(), gr.minor_version()),
+ prefs=gr.prefs(),
install_prefix=gr.prefix()
)
- ActionHandler(args.flow_graphs, platform)
- gtk.main()
+ platform.build_library()
+ log.debug("Loading application")
+ app = Application(args.flow_graphs, platform)
+ log.debug("Running")
+ sys.exit(app.run())
diff --git a/grc/scripts/gnuradio-companion b/grc/scripts/gnuradio-companion
index bacbbe2334..55e66cc761 100755
--- a/grc/scripts/gnuradio-companion
+++ b/grc/scripts/gnuradio-companion
@@ -37,11 +37,13 @@ Is the library path environment variable set correctly?
def die(error, message):
msg = "{0}\n\n({1})".format(message, error)
try:
- import gtk
- d = gtk.MessageDialog(
- type=gtk.MESSAGE_ERROR,
- buttons=gtk.BUTTONS_CLOSE,
- message_format=msg,
+ import gi
+ gi.require_version('Gtk', '3.0')
+ from gi.repository import Gtk
+ d = Gtk.MessageDialog(
+ message_type=Gtk.MessageType.ERROR,
+ buttons=Gtk.ButtonsType.CLOSE,
+ text=msg,
)
d.set_title(type(error).__name__)
d.run()
@@ -53,10 +55,13 @@ def die(error, message):
def check_gtk():
try:
warnings.filterwarnings("error")
- import pygtk
- pygtk.require('2.0')
- import gtk
- gtk.init_check()
+ import gi
+ gi.require_version('Gtk', '3.0')
+ gi.require_version('PangoCairo', '1.0')
+ gi.require_foreign('cairo', 'Context')
+
+ from gi.repository import Gtk
+ Gtk.init_check()
warnings.filterwarnings("always")
except Exception as err:
die(err, "Failed to initialize GTK. If you are running over ssh, "
diff --git a/grc/tests/__init__.py b/grc/tests/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/grc/tests/__init__.py
diff --git a/grc/tests/resources/file1.block.yml b/grc/tests/resources/file1.block.yml
new file mode 100644
index 0000000000..f486c89ea8
--- /dev/null
+++ b/grc/tests/resources/file1.block.yml
@@ -0,0 +1,38 @@
+id: block_key
+label: testname
+
+parameters:
+- id: vlen
+ label: Vec Length
+ category: test
+ dtype: int
+ default: '1'
+ hide: ${ 'part' if vlen == 1 else 'none' }
+- id: out_type
+ label: Vec Length
+ dtype: string
+ default: complex
+ hide: part
+- id: a
+ label: Alpha
+ dtype: ${ (out_type) }
+ default: '0'
+
+inputs:
+- domain: stream
+ dtype: complex
+ vlen: ${ 2 * vlen }
+- domain: message
+ id: in2
+
+outputs:
+- domain: stream
+ dtype: ${ out_type }
+ vlen: ${ vlen }
+asserts:
+- ${ vlen > 0 }
+
+templates:
+ make: blocks.complex_to_mag_squared(${vlen})
+
+file_format: 1
diff --git a/grc/tests/resources/file2.block.yml b/grc/tests/resources/file2.block.yml
new file mode 100644
index 0000000000..459527260c
--- /dev/null
+++ b/grc/tests/resources/file2.block.yml
@@ -0,0 +1,31 @@
+id: blocks_and_const_xx
+label: And Const
+
+parameters:
+- id: type
+ label: IO Type
+ dtype: enum
+ options: [int, short, byte]
+ option_attributes:
+ fcn: [ii, ss, bb]
+ hide: part
+- id: const
+ label: Constant
+ dtype: int
+ default: '0'
+
+inputs:
+- domain: stream
+ dtype: ${ type }
+
+outputs:
+- domain: stream
+ dtype: ${ type }
+
+templates:
+ imports: from gnuradio import blocks
+ make: blocks.and_const_${type.fcn}(${const})
+ callbacks:
+ - set_k(${const})
+
+file_format: 1
diff --git a/grc/tests/resources/file3.block.yml b/grc/tests/resources/file3.block.yml
new file mode 100644
index 0000000000..e53515d134
--- /dev/null
+++ b/grc/tests/resources/file3.block.yml
@@ -0,0 +1,66 @@
+id: variable_qtgui_check_box
+label: QT GUI Check Box
+
+parameters:
+- id: label
+ label: Label
+ dtype: string
+ hide: ${ ('none' if label else 'part') }
+- id: type
+ label: Type
+ dtype: enum
+ default: int
+ options: [real, int, string, bool, raw]
+ option_labels: [Float, Integer, String, Boolean, Any]
+ option_attributes:
+ conv: [float, int, str, bool, eval]
+ hide: part
+- id: value
+ label: Default Value
+ dtype: ${ type }
+ default: 'True'
+- id: 'true'
+ label: 'True'
+ dtype: ${ type }
+ default: 'True'
+- id: 'false'
+ label: 'False'
+ dtype: ${ type }
+ default: 'False'
+- id: gui_hint
+ label: GUI Hint
+ dtype: gui_hint
+ hide: part
+value: ${ value }
+
+asserts:
+- ${value in (true, false)}
+
+templates:
+ imports: from PyQt4 import Qt
+ var_make: self.${id} = ${id} = ${value}
+ callbacks:
+ - self.set_${id}(${value})
+ - self._${id}_callback(${id})
+ make: |-
+ <%
+ win = '_%s_check_box'%id
+ if not label:
+ label = id
+ %>
+ ${win} = Qt.QCheckBox(${label})
+ self._${id}_choices = {True: ${true}, False: ${false}}
+ self._${id}_choices_inv = dict((v,k) for k,v in self._${id}_choices.iteritems())
+ self._${id}_callback = lambda i: Qt.QMetaObject.invokeMethod(${win}, "setChecked", Qt.Q_ARG("bool", self._${id}_choices_inv[i]))
+ self._${id}_callback(self.${id})
+ ${win}.stateChanged.connect(lambda i: self.set_${id}(self._${id}_choices[bool(i)]))
+ ${gui_hint()(win)}
+
+documentation: |-
+ This block creates a variable check box. Leave the label blank to use the variable id as the label.
+
+ A check box selects between two values of similar type. Te values do not necessarily need to be of boolean type.
+
+ The GUI hint can be used to position the widget within the application. The hint is of the form [tab_id@tab_index]: [row, col, row_span, col_span]. Both the tab specification and the grid position are optional.
+
+file_format: 1
diff --git a/grc/tests/test_block_flags.py b/grc/tests/test_block_flags.py
new file mode 100644
index 0000000000..9eecaf20d7
--- /dev/null
+++ b/grc/tests/test_block_flags.py
@@ -0,0 +1,26 @@
+
+from grc.core.blocks._flags import Flags
+
+
+def test_simple():
+ assert 'test' in Flags('_test_')
+
+
+def test_deprecated():
+ assert Flags.DEPRECATED == 'deprecated'
+ assert Flags('this is deprecated').deprecated is True
+
+
+def test_extend():
+ f = Flags('a')
+ f += 'b'
+ assert isinstance(f, Flags)
+ f += u'b'
+ assert isinstance(f, Flags)
+ f = Flags(u'a')
+ f += 'b'
+ assert isinstance(f, Flags)
+ f += u'b'
+ assert isinstance(f, Flags)
+
+ assert str(f) == 'abb'
diff --git a/grc/tests/test_block_templates.py b/grc/tests/test_block_templates.py
new file mode 100644
index 0000000000..df9ab37550
--- /dev/null
+++ b/grc/tests/test_block_templates.py
@@ -0,0 +1,45 @@
+import pytest
+
+from grc.core.blocks._templates import MakoTemplates
+from grc.core.errors import TemplateError
+
+
+class Block(object):
+ namespace_templates = {}
+
+ templates = MakoTemplates(None)
+
+ def __init__(self, **kwargs):
+ self.namespace_templates.update(kwargs)
+
+
+def test_simple():
+ t = MakoTemplates(_bind_to=Block(num='123'), test='abc${num}')
+ assert t['test'] == 'abc${num}'
+ assert t.render('test') == 'abc123'
+ assert 'abc${num}' in t._template_cache
+
+
+def test_instance():
+ block = Block(num='123')
+ block.templates['test'] = 'abc${num}'
+ assert block.templates.render('test') == 'abc123'
+ assert block.templates is block.__dict__['templates']
+
+
+def test_list():
+ templates = ['abc${num}', '${2 * num}c']
+ t = MakoTemplates(_bind_to=Block(num='123'), test=templates)
+ assert t['test'] == templates
+ assert t.render('test') == ['abc123', '123123c']
+ assert set(templates) == set(t._template_cache.keys())
+
+
+def test_parse_error():
+ with pytest.raises(TemplateError):
+ MakoTemplates(_bind_to=Block(num='123'), test='abc${num NOT CLOSING').render('test')
+
+
+def test_parse_error2():
+ with pytest.raises(TemplateError):
+ MakoTemplates(_bind_to=Block(num='123'), test='abc${ WRONG_VAR }').render('test')
diff --git a/grc/tests/test_cheetah_converter.py b/grc/tests/test_cheetah_converter.py
new file mode 100644
index 0000000000..7999955436
--- /dev/null
+++ b/grc/tests/test_cheetah_converter.py
@@ -0,0 +1,132 @@
+""""""
+
+import functools
+import grc.converter.cheetah_converter as parser
+
+
+def test_basic():
+ c = parser.Converter(names={'abc'})
+ for convert in (c.convert_simple, c.convert_hard, c.to_python):
+ assert 'abc' == convert('$abc')
+ assert 'abc' == convert('$abc()')
+ assert 'abc' == convert('$(abc)')
+ assert 'abc' == convert('$(abc())')
+ assert 'abc' == convert('${abc}')
+ assert 'abc' == convert('${abc()}')
+
+ assert c.stats['simple'] == 2 * 6
+ assert c.stats['hard'] == 1 * 6
+
+
+def test_simple():
+ convert = parser.Converter(names={'abc': {'def'}})
+ assert 'abc' == convert.convert_simple('$abc')
+ assert 'abc.def' == convert.convert_simple('$abc.def')
+ assert 'abc.def' == convert.convert_simple('$(abc.def)')
+ assert 'abc.def' == convert.convert_simple('${abc.def}')
+ try:
+ convert.convert_simple('$abc.not_a_sub_key')
+ except NameError:
+ assert True
+ else:
+ assert False
+
+
+def test_conditional():
+ convert = parser.Converter(names={'abc'})
+ assert '(asb_asd_ if abc > 0 else __not__)' == convert.convert_inline_conditional(
+ '#if $abc > 0 then asb_$asd_ else __not__')
+
+
+def test_simple_format_string():
+ convert = functools.partial(parser.Converter(names={'abc'}).convert_simple, spec=parser.FormatString)
+ assert '{abc}' == convert('$abc')
+ assert '{abc:eval}' == convert('$abc()')
+ assert '{abc}' == convert('$(abc)')
+ assert '{abc:eval}' == convert('$(abc())')
+ assert '{abc}' == convert('${abc}')
+ assert '{abc:eval}' == convert('${abc()}')
+
+
+def test_hard_format_string():
+ names = {'abc': {'ff'}, 'param1': {}, 'param2': {}}
+ convert = functools.partial(parser.Converter(names).convert_hard, spec=parser.FormatString)
+ assert 'make_a_cool_block_{abc.ff}({param1}, {param2})' == \
+ convert('make_a_cool_block_${abc.ff}($param1, $param2)')
+
+
+converter = parser.Converter(names={'abc'})
+c2p = converter.to_python
+
+
+def test_opts():
+ assert 'abc abc abc' == c2p('$abc $(abc) ${abc}')
+ assert 'abc abc.abc abc' == c2p('$abc $abc.abc ${abc}')
+ assert 'abc abc[''].abc abc' == c2p('$abc $abc[''].abc() ${abc}')
+
+
+def test_nested():
+ assert 'abc(abc) abc + abc abc[abc]' == c2p('$abc($abc) $(abc + $abc) ${abc[$abc]}')
+ assert '(abc_abc_)' == c2p('(abc_$(abc)_)')
+
+
+def test_nested2():
+ class Other(parser.Python):
+ nested_start = '{'
+ nested_end = '}'
+ assert 'abc({abc})' == converter.convert('$abc($abc)', spec=Other)
+
+
+def test_nested3():
+ class Other(parser.Python):
+ start = '{'
+ end = '}'
+ assert '{abc(abc)}' == converter.convert('$abc($abc)', spec=Other)
+
+
+def test_with_string():
+ assert 'abc "$(abc)" abc' == c2p('$abc "$(abc)" ${abc}')
+ assert 'abc \'$(abc)\' abc' == c2p('$abc \'$(abc)\' ${abc}')
+ assert 'abc "\'\'$(abc)" abc' == c2p('$abc "\'\'$(abc)" ${abc}')
+
+
+def test_if():
+ result = converter.to_mako("""
+ #if $abc > 0
+ test
+ #else if $abc < 0
+ test
+ #else
+ bla
+ #end if
+ """)
+
+ expected = """
+ % if abc > 0:
+ test
+ % elif abc < 0:
+ test
+ % else:
+ bla
+ % endif
+ """
+ assert result == expected
+
+
+def test_hash_end():
+ result = converter.to_mako('$abc#slurp')
+ assert result == '${abc}\\'
+
+
+def test_slurp_if():
+ result = converter.to_mako("""
+ $abc#slurp
+ #if $abc
+ """)
+
+ expected = """
+ ${abc}
+ % if abc:
+ """
+ assert result == expected
+
diff --git a/grc/tests/test_evaled_property.py b/grc/tests/test_evaled_property.py
new file mode 100644
index 0000000000..27957cd291
--- /dev/null
+++ b/grc/tests/test_evaled_property.py
@@ -0,0 +1,104 @@
+import collections
+import numbers
+
+from grc.core.utils.descriptors import Evaluated, EvaluatedEnum, EvaluatedPInt
+
+
+class A(object):
+ def __init__(self, **kwargs):
+ self.called = collections.defaultdict(int)
+ self.errors = []
+ self.namespace = kwargs
+
+ def add_error_message(self, msg):
+ self.errors.append(msg)
+
+ @property
+ def parent_block(self):
+ return self
+
+ def evaluate(self, expr):
+ self.called['evaluate'] += 1
+ return eval(expr, self.namespace)
+
+ @Evaluated(int, 1)
+ def foo(self):
+ self.called['foo'] += 1
+ return eval(self._foo)
+
+ bar = Evaluated(numbers.Real, 1.0, name='bar')
+
+ test = EvaluatedEnum(['a', 'b'], 'a', name='test')
+
+ lala = EvaluatedPInt()
+
+
+def test_fixed_value():
+ a = A()
+ a.foo = 10
+
+ assert not hasattr(a, '_foo')
+ assert a.foo == 10
+ assert a.called['foo'] == 0
+ delattr(a, 'foo')
+ assert a.foo == 10
+ assert a.called['foo'] == 0
+
+
+def test_evaled():
+ a = A()
+ a.foo = '${ 10 + 1 }'
+ assert getattr(a, '_foo') == '10 + 1'
+ assert a.foo == 11 and a.foo == 11
+ assert a.called['foo'] == 1
+ assert a.called['evaluate'] == 0
+ delattr(a, 'foo')
+ assert a.foo == 11 and a.foo == 11
+ assert a.called['foo'] == 2
+ assert not a.errors
+
+
+def test_evaled_with_default():
+ a = A()
+ a.bar = '${ 10 + 1 }'
+ assert getattr(a, '_bar') == '10 + 1'
+ assert a.bar == 11.0 and type(a.bar) == int
+ assert a.called['evaluate'] == 1
+ assert not a.errors
+
+
+def test_evaled_int_with_default():
+ a = A(ll=10)
+ a.lala = '${ ll * 2 }'
+ assert a.lala == 20
+ a.namespace['ll'] = -10
+ assert a.lala == 20
+ del a.lala
+ assert a.lala == 1
+ assert not a.errors
+
+
+def test_evaled_enum_fixed_value():
+ a = A()
+ a.test = 'a'
+ assert not hasattr(a, '_test')
+ assert a.test == 'a' and type(a.test) == str
+ assert not a.errors
+
+
+def test_evaled_enum():
+ a = A(bla=False)
+ a.test = '${ "a" if bla else "b" }'
+ assert a.test == 'b'
+ a.namespace['bla'] = True
+ assert a.test == 'b'
+ del a.test
+ assert a.test == 'a'
+ assert not a.errors
+
+
+def test_class_access():
+ a = A()
+ a.foo = '${ meme }'
+ descriptor = getattr(a.__class__, 'foo')
+ assert descriptor.name_raw == '_foo'
diff --git a/grc/tests/test_expr_utils.py b/grc/tests/test_expr_utils.py
new file mode 100644
index 0000000000..4f25477bf1
--- /dev/null
+++ b/grc/tests/test_expr_utils.py
@@ -0,0 +1,41 @@
+import operator
+
+import pytest
+
+from grc.core.utils import expr_utils
+
+id_getter = operator.itemgetter(0)
+expr_getter = operator.itemgetter(1)
+
+
+def test_simple():
+ objects = [
+ ['c', '2 * a + b'],
+ ['a', '1'],
+ ['b', '2 * a + unknown * d'],
+ ['d', '5'],
+ ]
+
+ expected = [
+ ['a', '1'],
+ ['d', '5'],
+ ['b', '2 * a + unknown * d'],
+ ['c', '2 * a + b'],
+ ]
+
+ out = expr_utils.sort_objects2(objects, id_getter, expr_getter)
+
+ assert out == expected
+
+
+def test_other():
+ test = [
+ ['c', '2 * a + b'],
+ ['a', '1'],
+ ['b', '2 * c + unknown'],
+ ]
+
+ expr_utils.sort_objects2(test, id_getter, expr_getter, check_circular=False)
+
+ with pytest.raises(RuntimeError):
+ expr_utils.sort_objects2(test, id_getter, expr_getter)
diff --git a/grc/tests/test_generator.py b/grc/tests/test_generator.py
new file mode 100644
index 0000000000..4c79ce4bd3
--- /dev/null
+++ b/grc/tests/test_generator.py
@@ -0,0 +1,46 @@
+# Copyright 2016 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.
+
+from os import path
+import tempfile
+
+from grc.core.platform import Platform
+
+
+def test_generator():
+ # c&p form compiler code.
+ # todo: make this independent from installed GR
+ grc_file = path.join(path.dirname(__file__), 'resources', 'test_compiler.grc')
+ out_dir = tempfile.gettempdir()
+
+ platform = Platform(
+ name='GNU Radio Companion Compiler',
+ prefs=None,
+ version='0.0.0',
+ )
+ platform.build_library()
+
+ flow_graph = platform.make_flow_graph(grc_file)
+ flow_graph.rewrite()
+ flow_graph.validate()
+
+ assert flow_graph.is_valid()
+
+ generator = platform.Generator(flow_graph, path.join(path.dirname(__file__), 'resources'))
+ generator.write()
diff --git a/grc/core/Element.pyi b/grc/tests/test_xml_parser.py
index c81180a33e..c68b6cdc5a 100644
--- a/grc/core/Element.pyi
+++ b/grc/tests/test_xml_parser.py
@@ -1,4 +1,4 @@
-# Copyright 2008, 2009, 2015, 2016 Free Software Foundation, Inc.
+# Copyright 2017 Free Software Foundation, Inc.
# This file is part of GNU Radio
#
# GNU Radio Companion is free software; you can redistribute it and/or
@@ -15,40 +15,25 @@
# 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 Platform, FlowGraph, Block
+from os import path
+import sys
-def lazy_property(func):
- return func
+from grc.converter import flow_graph
-class Element(object):
+def test_flow_graph_converter():
+ filename = path.join(path.dirname(__file__), 'resources', 'test_compiler.grc')
- def __init__(self, parent=None):
- ...
+ data = flow_graph.from_xml(filename)
- @property
- def parent(self):
- ...
+ flow_graph.dump(data, sys.stdout)
- def get_parent_by_type(self, cls):
- parent = self.parent
- if parent is None:
- return None
- elif isinstance(parent, cls):
- return parent
- else:
- return parent.get_parent_by_type(cls)
- @lazy_property
- def parent_platform(self): -> Platform.Platform
- ...
+def test_flow_graph_converter_with_fp():
+ filename = path.join(path.dirname(__file__), 'resources', 'test_compiler.grc')
- @lazy_property
- def parent_flowgraph(self): -> FlowGraph.FlowGraph
- ...
-
- @lazy_property
- def parent_block(self): -> Block.Block
- ...
+ with open(filename) as fp:
+ data = flow_graph.from_xml(fp)
+ flow_graph.dump(data, sys.stdout)
diff --git a/grc/tests/test_yaml_checker.py b/grc/tests/test_yaml_checker.py
new file mode 100644
index 0000000000..e6b466e511
--- /dev/null
+++ b/grc/tests/test_yaml_checker.py
@@ -0,0 +1,84 @@
+# Copyright 2016 Free Software Foundation, Inc.
+# This file is part of GNU Radio
+#
+# GNU Radio Companion is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by the
+# Free Software Foundation; either version 2 of the License, or (at your
+# option) any later version.
+#
+# GNU Radio Companion is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+# details.
+#
+# You should have received a copy of the GNU General Public License along with
+# this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
+
+import yaml
+
+from grc.core.schema_checker import Validator, BLOCK_SCHEME
+
+
+BLOCK1 = """
+id: block_key
+label: testname
+
+parameters:
+- id: vlen
+ label: Vec Length
+ dtype: int
+ default: 1
+- id: out_type
+ label: Vec Length
+ dtype: string
+ default: complex
+- id: a
+ label: Alpha
+ dtype: ${ out_type }
+ default: '0'
+
+inputs:
+- label: in
+ domain: stream
+ dtype: complex
+ vlen: ${ 2 * vlen }
+- name: in2
+ domain: message
+ id: in2
+
+outputs:
+- label: out
+ domain: stream
+ dtype: ${ out_type }
+ vlen: ${ vlen }
+
+templates:
+ make: blocks.complex_to_mag_squared(${ vlen })
+
+file_format: 1
+"""
+
+
+def test_min():
+ checker = Validator(BLOCK_SCHEME)
+ assert checker.run({'id': 'test', 'file_format': 1}), checker.messages
+ assert not checker.run({'name': 'test', 'file_format': 1})
+
+
+def test_extra_keys():
+ checker = Validator(BLOCK_SCHEME)
+ assert checker.run({'id': 'test', 'abcdefg': 'nonsense', 'file_format': 1})
+ assert checker.messages == [('block', 'warn', "Ignoring extra key 'abcdefg'")]
+
+
+def test_checker():
+ checker = Validator(BLOCK_SCHEME)
+ data = yaml.load(BLOCK1)
+ passed = checker.run(data)
+ if not passed:
+ print()
+ for msg in checker.messages:
+ print(msg)
+
+ assert passed, checker.messages