mirror of
https://github.com/gnuradio/gnuradio-companion.git
synced 2025-12-10 00:42:30 -06:00
grc: Add edge (connection) properties
This allows connections (graph edges) to have properties associated with them. In order to do so, the edge types must have parameters associated with them in the *.domain.yml file. For edges that have properties/parameters associated with them, a property dialog can be summoned by right-clicking a connector arrow. Signed-off-by: Martin Braun <martin.braun@ettus.com>
This commit is contained in:
parent
829e21f0fd
commit
486e7a5edb
@ -6,6 +6,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
"""
|
||||
|
||||
import collections
|
||||
|
||||
from .base import Element
|
||||
from .Constants import ALIASES_OF
|
||||
@ -13,15 +14,23 @@ from .utils.descriptors import lazy_property
|
||||
|
||||
|
||||
class Connection(Element):
|
||||
"""
|
||||
Stores information about a connection between two block ports. This class
|
||||
knows:
|
||||
- Where the source and sink ports are (on which blocks)
|
||||
- The domain (message, stream, ...)
|
||||
- Which parameters are associated with this connection
|
||||
"""
|
||||
|
||||
is_connection = True
|
||||
documentation = {'': ''}
|
||||
|
||||
def __init__(self, parent, source, sink):
|
||||
"""
|
||||
Make a new connection given the parent and 2 ports.
|
||||
|
||||
Args:
|
||||
flow_graph: the parent of this element
|
||||
parent: the parent of this element (a flow graph)
|
||||
source: a port (any direction)
|
||||
sink: a port (any direction)
|
||||
@throws Error cannot make connection
|
||||
@ -41,6 +50,16 @@ class Connection(Element):
|
||||
self.source_port = source
|
||||
self.sink_port = sink
|
||||
|
||||
# Unlike the blocks, connection parameters are defined in the connection
|
||||
# domain definition files, as all connections within the same domain
|
||||
# share the same properties.
|
||||
param_factory = self.parent_platform.make_param
|
||||
conn_parameters = self.parent_platform.connection_params.get(self.type, {})
|
||||
self.params = collections.OrderedDict(
|
||||
(data['id'], param_factory(parent=self, **data))
|
||||
for data in conn_parameters
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return 'Connection (\n\t{}\n\t\t{}\n\t{}\n\t\t{}\n)'.format(
|
||||
self.source_block, self.source_port, self.sink_block, self.sink_port,
|
||||
@ -57,6 +76,10 @@ class Connection(Element):
|
||||
def __iter__(self):
|
||||
return iter((self.source_port, self.sink_port))
|
||||
|
||||
def children(self):
|
||||
""" This includes the connection parameters """
|
||||
return self.params.values()
|
||||
|
||||
@lazy_property
|
||||
def source_block(self):
|
||||
return self.source_port.parent_block
|
||||
@ -79,6 +102,18 @@ class Connection(Element):
|
||||
"""
|
||||
return self.source_block.enabled and self.sink_block.enabled
|
||||
|
||||
@property
|
||||
def label(self):
|
||||
""" Returns a label for dialogs """
|
||||
src_domain, sink_domain = [
|
||||
self.parent_platform.domains[d].name for d in self.type]
|
||||
return f'Connection ({src_domain} → {sink_domain})'
|
||||
|
||||
@property
|
||||
def namespace_templates(self):
|
||||
"""Returns everything we want to have available in the template rendering"""
|
||||
return {key: param.template_arg for key, param in self.params.items()}
|
||||
|
||||
def validate(self):
|
||||
"""
|
||||
Validate the connections.
|
||||
@ -113,9 +148,33 @@ class Connection(Element):
|
||||
Export this connection's info.
|
||||
|
||||
Returns:
|
||||
a nested data odict
|
||||
A tuple with connection info, and parameters.
|
||||
"""
|
||||
return (
|
||||
# See if we need to use file format version 2:
|
||||
if self.params:
|
||||
return {
|
||||
'src_blk_id': self.source_block.name,
|
||||
'src_port_id': self.source_port.key,
|
||||
'snk_blk_id': self.sink_block.name,
|
||||
'snk_port_id': self.sink_port.key,
|
||||
'params': collections.OrderedDict(sorted(
|
||||
(param_id, param.value)
|
||||
for param_id, param in self.params.items())),
|
||||
}
|
||||
|
||||
# If there's no reason to do otherwise, we can export info as
|
||||
# FLOW_GRAPH_FILE_FORMAT_VERSION 1 format:
|
||||
return [
|
||||
self.source_block.name, self.source_port.key,
|
||||
self.sink_block.name, self.sink_port.key
|
||||
)
|
||||
self.sink_block.name, self.sink_port.key,
|
||||
]
|
||||
|
||||
def import_data(self, params):
|
||||
"""
|
||||
Import connection parameters.
|
||||
"""
|
||||
for key, value in params.items():
|
||||
try:
|
||||
self.params[key].set_value(value)
|
||||
except KeyError:
|
||||
continue
|
||||
|
||||
@ -26,11 +26,12 @@ CACHE_FILE = os.path.expanduser('~/.cache/grc_gnuradio/cache_v2.json')
|
||||
|
||||
BLOCK_DESCRIPTION_FILE_FORMAT_VERSION = 1
|
||||
# File format versions:
|
||||
# 0: undefined / legacy
|
||||
# 1: non-numeric message port keys (label is used instead)
|
||||
# This constant is the max known version. If a version higher than this shows
|
||||
# up, we assume we can't handle it.
|
||||
FLOW_GRAPH_FILE_FORMAT_VERSION = 1
|
||||
# 0: undefined / legacy
|
||||
# 1: non-numeric message port keys (label is used instead)
|
||||
# 2: connection info is stored as dictionary
|
||||
FLOW_GRAPH_FILE_FORMAT_VERSION = 2
|
||||
|
||||
# Param tabs
|
||||
DEFAULT_PARAM_TAB = "General"
|
||||
|
||||
@ -334,7 +334,7 @@ class FlowGraph(Element):
|
||||
block = None
|
||||
return block
|
||||
|
||||
def connect(self, porta, portb):
|
||||
def connect(self, porta, portb, params=None):
|
||||
"""
|
||||
Create a connection between porta and portb.
|
||||
|
||||
@ -348,6 +348,8 @@ class FlowGraph(Element):
|
||||
"""
|
||||
connection = self.parent_platform.Connection(
|
||||
parent=self, source=porta, sink=portb)
|
||||
if params:
|
||||
connection.import_data(params)
|
||||
self.connections.add(connection)
|
||||
|
||||
return connection
|
||||
@ -393,13 +395,31 @@ class FlowGraph(Element):
|
||||
def block_order(b):
|
||||
return not b.is_variable, b.name # todo: vars still first ?!?
|
||||
|
||||
def get_file_format_version(data):
|
||||
"""Determine file format version based on available data"""
|
||||
if any(isinstance(c, dict) for c in data['connections']):
|
||||
return 2
|
||||
return 1
|
||||
|
||||
def sort_connection_key(connection_info):
|
||||
if isinstance(connection_info, dict):
|
||||
return [
|
||||
connection_info.get('src_blk_id'),
|
||||
connection_info.get('src_port_id'),
|
||||
connection_info.get('snk_blk_id'),
|
||||
connection_info.get('snk_port_id'),
|
||||
]
|
||||
return connection_info
|
||||
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['connections'] = sorted(
|
||||
(c.export_data() for c in self.connections),
|
||||
key=sort_connection_key)
|
||||
|
||||
data['metadata'] = {
|
||||
'file_format': FLOW_GRAPH_FILE_FORMAT_VERSION,
|
||||
'file_format': get_file_format_version(data),
|
||||
'grc_version': self.parent_platform.config.version
|
||||
}
|
||||
return data
|
||||
@ -470,7 +490,23 @@ class FlowGraph(Element):
|
||||
_blocks = {block.name: block for block in self.blocks}
|
||||
|
||||
# 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', []):
|
||||
for connection_info in data.get('connections', []):
|
||||
# First unpack the connection info, which can be in different formats.
|
||||
# FLOW_GRAPH_FILE_FORMAT_VERSION 1: Connection info is a 4-tuple
|
||||
if isinstance(connection_info, (list, tuple)) and len(connection_info) == 4:
|
||||
src_blk_id, src_port_id, snk_blk_id, snk_port_id = connection_info
|
||||
conn_params = {}
|
||||
# FLOW_GRAPH_FILE_FORMAT_VERSION 2: Connection info is a dictionary
|
||||
elif isinstance(connection_info, dict):
|
||||
src_blk_id = connection_info.get('src_blk_id')
|
||||
src_port_id = connection_info.get('src_port_id')
|
||||
snk_blk_id = connection_info.get('snk_blk_id')
|
||||
snk_port_id = connection_info.get('snk_port_id')
|
||||
conn_params = connection_info.get('params', {})
|
||||
else:
|
||||
Messages.send_error_load(f'Invalid connection format detected!')
|
||||
had_connect_errors = True
|
||||
continue
|
||||
try:
|
||||
source_block = _blocks[src_blk_id]
|
||||
sink_block = _blocks[snk_blk_id]
|
||||
@ -486,7 +522,7 @@ class FlowGraph(Element):
|
||||
sink_port = verify_and_get_port(
|
||||
snk_port_id, sink_block, 'sink')
|
||||
|
||||
self.connect(source_port, sink_port)
|
||||
self.connect(source_port, sink_port, conn_params)
|
||||
|
||||
except (KeyError, LookupError) as e:
|
||||
Messages.send_error_load(
|
||||
|
||||
@ -184,6 +184,13 @@ class TopBlockGenerator(object):
|
||||
return output
|
||||
|
||||
def _blocks(self):
|
||||
"""
|
||||
Returns a list of tuples: (block, block_make)
|
||||
|
||||
'block' contains a reference to the block object.
|
||||
'block_make' contains the pre-rendered string for the 'make' part of the
|
||||
block.
|
||||
"""
|
||||
fg = self._flow_graph
|
||||
parameters = fg.get_parameters()
|
||||
|
||||
@ -319,7 +326,10 @@ class TopBlockGenerator(object):
|
||||
template = templates[con.type]
|
||||
if con.source_port.dtype != 'bus':
|
||||
code = template.render(
|
||||
make_port_sig=make_port_sig, source=con.source_port, sink=con.sink_port)
|
||||
make_port_sig=make_port_sig,
|
||||
source=con.source_port,
|
||||
sink=con.sink_port,
|
||||
**con.namespace_templates)
|
||||
rendered.append(code)
|
||||
else:
|
||||
# Bus ports need to iterate over the underlying connections and then render
|
||||
@ -337,7 +347,10 @@ class TopBlockGenerator(object):
|
||||
connection = fg.parent_platform.Connection(
|
||||
parent=self, source=hidden_porta, sink=hidden_portb)
|
||||
code = template.render(
|
||||
make_port_sig=make_port_sig, source=hidden_porta, sink=hidden_portb)
|
||||
make_port_sig=make_port_sig,
|
||||
source=hidden_porta,
|
||||
sink=hidden_portb,
|
||||
**con.namespace_templates)
|
||||
rendered.append(code)
|
||||
|
||||
return rendered
|
||||
|
||||
@ -47,6 +47,7 @@ class Platform(Element):
|
||||
self.domains = {}
|
||||
self.connection_templates = {}
|
||||
self.cpp_connection_templates = {}
|
||||
self.connection_params = {}
|
||||
|
||||
self._block_categories = {}
|
||||
self._auto_hier_block_generate_chain = set()
|
||||
@ -279,6 +280,7 @@ class Platform(Element):
|
||||
'connect', '')
|
||||
self.cpp_connection_templates[connection_id] = connection.get(
|
||||
'cpp_connect', '')
|
||||
self.connection_params[connection_id] = connection.get('parameters', {})
|
||||
|
||||
def load_category_tree_description(self, data, file_path):
|
||||
"""Parse category tree file and add it to list"""
|
||||
@ -358,8 +360,10 @@ class Platform(Element):
|
||||
data = flow_graph.export_data()
|
||||
|
||||
try:
|
||||
data['connections'] = [yaml.ListFlowing(
|
||||
i) for i in data['connections']]
|
||||
data['connections'] = [
|
||||
yaml.ListFlowing(conn) if isinstance(conn, (list, tuple)) else conn
|
||||
for conn in data['connections']
|
||||
]
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
|
||||
@ -1,9 +1,11 @@
|
||||
from .utils import Spec, expand
|
||||
from .block import PARAM_SCHEME
|
||||
|
||||
DOMAIN_CONNECTION = expand(
|
||||
type=Spec(types=list, required=True, item_scheme=None),
|
||||
connect=str,
|
||||
cpp_connect=str,
|
||||
parameters=Spec(types=list, required=False, item_scheme=PARAM_SCHEME),
|
||||
)
|
||||
|
||||
DOMAIN_SCHEME = expand(
|
||||
|
||||
@ -21,6 +21,8 @@ from .MainWindow import MainWindow
|
||||
from .PropsDialog import PropsDialog
|
||||
|
||||
from ..core import Messages
|
||||
from ..core.Connection import Connection
|
||||
from ..core.blocks import Block
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
@ -551,7 +553,8 @@ class Application(Gtk.Application):
|
||||
##################################################
|
||||
elif action == Actions.BLOCK_PARAM_MODIFY:
|
||||
selected_block = args[0] if args[0] else flow_graph.selected_block
|
||||
if selected_block:
|
||||
selected_conn = args[0] if args[0] else flow_graph.selected_connection
|
||||
if selected_block and isinstance(selected_block, Block):
|
||||
self.dialog = PropsDialog(self.main_window, selected_block)
|
||||
response = Gtk.ResponseType.APPLY
|
||||
while response == Gtk.ResponseType.APPLY: # rerun the dialog if Apply was hit
|
||||
@ -571,6 +574,26 @@ class Application(Gtk.Application):
|
||||
Actions.ELEMENT_SELECT()
|
||||
self.dialog.destroy()
|
||||
self.dialog = None
|
||||
elif selected_conn and isinstance(selected_conn, Connection):
|
||||
self.dialog = PropsDialog(self.main_window, selected_conn)
|
||||
response = Gtk.ResponseType.APPLY
|
||||
while response == Gtk.ResponseType.APPLY: # rerun the dialog if Apply was hit
|
||||
response = self.dialog.run()
|
||||
if response in (Gtk.ResponseType.APPLY, Gtk.ResponseType.ACCEPT):
|
||||
page.state_cache.save_new_state(
|
||||
flow_graph.export_data())
|
||||
# Following line forces a complete update of io ports
|
||||
flow_graph_update()
|
||||
page.saved = False
|
||||
if response in (Gtk.ResponseType.REJECT, Gtk.ResponseType.ACCEPT):
|
||||
curr_state = page.state_cache.get_current_state()
|
||||
flow_graph.import_data(curr_state)
|
||||
flow_graph_update()
|
||||
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.state_cache.save_new_state(flow_graph.export_data())
|
||||
flow_graph_update()
|
||||
@ -809,11 +832,18 @@ class Application(Gtk.Application):
|
||||
|
||||
selected_blocks = list(flow_graph.selected_blocks())
|
||||
selected_block = selected_blocks[0] if selected_blocks else None
|
||||
# See if a connection has modifiable parameters or grey out the entry
|
||||
# in the menu
|
||||
selected_connections = list(flow_graph.selected_connections())
|
||||
selected_connection = selected_connections[0] \
|
||||
if len(selected_connections) == 1 \
|
||||
else None
|
||||
selected_conn_has_params = selected_connection and bool(len(selected_connection.params))
|
||||
|
||||
# update general buttons
|
||||
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_PARAM_MODIFY.set_enabled(bool(selected_block) or bool(selected_conn_has_params))
|
||||
Actions.BLOCK_ROTATE_CCW.set_enabled(bool(selected_blocks))
|
||||
Actions.BLOCK_ROTATE_CW.set_enabled(bool(selected_blocks))
|
||||
# update alignment options
|
||||
|
||||
@ -42,6 +42,8 @@ class PropsDialog(Gtk.Dialog):
|
||||
(Constants.MIN_DIALOG_WIDTH, Constants.MIN_DIALOG_HEIGHT)
|
||||
))
|
||||
|
||||
# Careful: 'block' can also be a connection! The naming is because
|
||||
# property dialogs for connections were added much later.
|
||||
self._block = block
|
||||
self._hash = 0
|
||||
self._config = parent.config
|
||||
@ -212,7 +214,9 @@ class PropsDialog(Gtk.Dialog):
|
||||
pos = buf.get_end_iter()
|
||||
|
||||
# Add link to wiki page for this block, at the top, as long as it's not an OOT block
|
||||
if self._block.category and self._block.category[0] == "Core":
|
||||
if self._block.is_connection:
|
||||
self._docs_link.set_markup('Connection')
|
||||
elif self._block.category and self._block.category[0] == "Core":
|
||||
note = "Wiki Page for this Block: "
|
||||
prefix = self._config.wiki_block_docs_url_prefix
|
||||
suffix = self._block.label.replace(" ", "_")
|
||||
@ -236,11 +240,13 @@ class PropsDialog(Gtk.Dialog):
|
||||
buf.insert(pos, '\n')
|
||||
|
||||
# if given the current parameters an exact match can be made
|
||||
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]}
|
||||
block_templates = getattr(self._block, 'templates', None)
|
||||
if block_templates:
|
||||
block_constructor = 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.items():
|
||||
|
||||
@ -679,6 +679,15 @@ class FlowGraph(CoreFlowgraph, Drawable):
|
||||
"""
|
||||
return (e for e in self.selected_elements.copy() if e.is_block)
|
||||
|
||||
def selected_connections(self):
|
||||
"""
|
||||
Get a group of selected connections.
|
||||
|
||||
Returns:
|
||||
sub set of connections in this flow graph
|
||||
"""
|
||||
return (e for e in self.selected_elements.copy() if e.is_connection)
|
||||
|
||||
@property
|
||||
def selected_block(self):
|
||||
"""
|
||||
@ -689,6 +698,16 @@ class FlowGraph(CoreFlowgraph, Drawable):
|
||||
"""
|
||||
return next(self.selected_blocks(), None)
|
||||
|
||||
@property
|
||||
def selected_connection(self):
|
||||
"""
|
||||
Get the selected connection
|
||||
|
||||
Returns:
|
||||
a connection or None
|
||||
"""
|
||||
return next(self.selected_connections(), None)
|
||||
|
||||
def get_selected_elements(self):
|
||||
"""
|
||||
Get the group of selected elements.
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user