mirror of
https://github.com/gnuradio/gnuradio-companion.git
synced 2025-12-10 00:42:30 -06:00
421 lines
15 KiB
Python
Executable File
421 lines
15 KiB
Python
Executable File
import logging
|
|
|
|
from qtpy.QtGui import QPen, QPainter, QBrush, QFont, QFontMetrics
|
|
from qtpy.QtCore import Qt, QPointF, QRectF, QUrl
|
|
from qtpy.QtWidgets import QGraphicsItem, QApplication, QAction
|
|
|
|
from . import colors
|
|
from ... import Constants
|
|
from ... import Utils
|
|
from ....core.blocks.block import Block as CoreBlock
|
|
from ....core.utils import flow_graph_complexity
|
|
|
|
from ..dialogs import PropsDialog
|
|
|
|
# Logging
|
|
log = logging.getLogger(f"grc.application.{__name__}")
|
|
|
|
LONG_VALUE = 20 # maximum length of a param string.
|
|
# if exceeded, '...' will be displayed
|
|
|
|
|
|
class Block(CoreBlock):
|
|
"""
|
|
A block. Accesses its graphical representation with self.gui.
|
|
"""
|
|
@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, parent, **n):
|
|
super(self.__class__, self).__init__(parent, **n)
|
|
|
|
self.width = 50 # will change immediately after the first paint
|
|
self.block_label = self.key
|
|
|
|
self.old_data = None
|
|
|
|
if "rotation" not in self.states.keys():
|
|
self.states["rotation"] = 0.0
|
|
|
|
self.gui = GUIBlock(self, parent)
|
|
|
|
def import_data(self, name, states, parameters, **_):
|
|
super(self.__class__, self).import_data(name, states, parameters, **_)
|
|
self.gui.setPos(*self.states["coordinate"])
|
|
self.gui.setRotation(self.states["rotation"])
|
|
self.rewrite()
|
|
self.gui.create_shapes_and_labels()
|
|
|
|
def update_bus_logic(self):
|
|
for direc in {'source', 'sink'}:
|
|
if direc == 'source':
|
|
ports = self.sources
|
|
ports_gui = self.filter_bus_port(self.sources)
|
|
else:
|
|
ports = self.sinks
|
|
ports_gui = self.filter_bus_port(self.sinks)
|
|
if 'bus' in map(lambda a: a.dtype, ports):
|
|
for port in ports_gui:
|
|
self.parent_flowgraph.gui.removeItem(port.gui)
|
|
super(self.__class__, self).update_bus_logic()
|
|
|
|
|
|
class GUIBlock(QGraphicsItem):
|
|
"""
|
|
The graphical representation of a block. Accesses the actual block with self.core.
|
|
"""
|
|
def __init__(self, core, parent, **n):
|
|
super(GUIBlock, self).__init__()
|
|
self.core = core
|
|
self.parent = self.scene()
|
|
self.font = QFont("Helvetica", 10)
|
|
|
|
self.create_shapes_and_labels()
|
|
|
|
if "coordinate" not in self.core.states.keys():
|
|
self.core.states["coordinate"] = (500, 300)
|
|
self.setPos(*self.core.states["coordinate"])
|
|
|
|
self.old_pos = (self.x(), self.y())
|
|
self.new_pos = (self.x(), self.y())
|
|
self.moving = False
|
|
|
|
self.props_dialog = None
|
|
self.right_click_menu = None
|
|
|
|
self.setFlag(QGraphicsItem.ItemIsMovable)
|
|
self.setFlag(QGraphicsItem.ItemIsSelectable)
|
|
self.setFlag(QGraphicsItem.ItemSendsScenePositionChanges)
|
|
|
|
self.markups_height = 0.0
|
|
self.markups_width = 0.0
|
|
self.markups = []
|
|
self.markup_text = ""
|
|
|
|
def create_shapes_and_labels(self):
|
|
qsettings = QApplication.instance().qsettings
|
|
self.force_show_id = qsettings.value('grc/show_block_ids', type=bool)
|
|
self.hide_variables = qsettings.value('grc/hide_variables', type=bool)
|
|
self.hide_disabled_blocks = qsettings.value('grc/hide_disabled_blocks', type=bool)
|
|
self.snap_to_grid = qsettings.value('grc/snap_to_grid', type=bool)
|
|
self.show_complexity = qsettings.value('grc/show_complexity', type=bool)
|
|
self.show_block_comments = qsettings.value('grc/show_block_comments', type=bool)
|
|
self.show_param_expr = qsettings.value('grc/show_param_expr', type=bool)
|
|
self.show_param_val = qsettings.value('grc/show_param_val', type=bool)
|
|
self.prepareGeometryChange()
|
|
self.font.setBold(True)
|
|
|
|
# figure out height of block based on how many params there are
|
|
i = 35
|
|
|
|
# Check if we need to increase the height to fit all the ports
|
|
for key, item in self.core.params.items():
|
|
value = item.value
|
|
if (value is not None and item.hide == "none") or (item.dtype == 'id' and self.force_show_id):
|
|
i += 20
|
|
|
|
self.height = i
|
|
|
|
def get_min_height_for_ports(ports):
|
|
min_height = (
|
|
2 * Constants.PORT_BORDER_SEPARATION +
|
|
len(ports) * Constants.PORT_SEPARATION
|
|
)
|
|
# If any of the ports are bus ports - make the min height larger
|
|
if any([p.dtype == "bus" for p in ports]):
|
|
min_height = (
|
|
2 * Constants.PORT_BORDER_SEPARATION +
|
|
sum(
|
|
port.gui.height + Constants.PORT_SPACING
|
|
for port in ports
|
|
if port.dtype == "bus"
|
|
) - Constants.PORT_SPACING
|
|
)
|
|
|
|
else:
|
|
if ports:
|
|
min_height -= ports[-1].gui.height
|
|
return min_height
|
|
|
|
self.height = max(
|
|
self.height,
|
|
get_min_height_for_ports(self.core.active_sinks),
|
|
get_min_height_for_ports(self.core.active_sources),
|
|
)
|
|
|
|
# Figure out width of block based on widest line of text
|
|
fm = QFontMetrics(self.font)
|
|
largest_width = fm.width(self.core.label)
|
|
for key, item in self.core.params.items():
|
|
name = item.name
|
|
value = item.value
|
|
if (value is not None and item.hide == "none") or (item.dtype == 'id' and self.force_show_id):
|
|
if len(value) > LONG_VALUE:
|
|
value = value[:LONG_VALUE - 3] + '...'
|
|
|
|
value_label = item.options[value] if value in item.options else value
|
|
full_line = name + ": " + value_label
|
|
|
|
if fm.width(full_line) > largest_width:
|
|
largest_width = fm.width(full_line)
|
|
self.width = largest_width + 15
|
|
|
|
self.markups = []
|
|
self.markup_text = ""
|
|
self.markups_width = 0.0
|
|
|
|
if self.show_complexity and self.core.key == "options":
|
|
complexity = flow_graph_complexity.calculate(self.parent)
|
|
self.markups.append('Complexity: {num} bal'.format(
|
|
num=Utils.num_to_str(complexity)))
|
|
|
|
if self.show_block_comments and self.core.comment:
|
|
self.markups.append(self.core.comment)
|
|
|
|
self.markup_text = "\n".join(self.markups).strip()
|
|
|
|
self.markups_height = fm.height() * (self.markup_text.count("\n") + 1)
|
|
for line in self.markup_text.split("\n"):
|
|
if fm.width(line) > self.markups_width:
|
|
self.markups_width = fm.width(line)
|
|
|
|
# Update the position and size of all the ports
|
|
bussified = (
|
|
self.core.current_bus_structure["source"],
|
|
self.core.current_bus_structure["sink"],
|
|
)
|
|
for ports, has_busses in zip(
|
|
(self.core.active_sources, self.core.active_sinks), bussified
|
|
):
|
|
if not ports:
|
|
continue
|
|
port_separation = (
|
|
Constants.PORT_SEPARATION
|
|
if not has_busses
|
|
else ports[0].gui.height + Constants.PORT_SPACING
|
|
)
|
|
offset = (
|
|
self.height - (len(ports) - 1) * port_separation - ports[0].gui.height
|
|
) / 2
|
|
for port in ports:
|
|
if port._dir == "sink":
|
|
port.gui.setPos(-15, offset)
|
|
else:
|
|
port.gui.setPos(self.width, offset)
|
|
port.gui.create_shapes_and_labels()
|
|
|
|
offset += (
|
|
Constants.PORT_SEPARATION
|
|
if not has_busses
|
|
else port.gui.height + Constants.PORT_SPACING
|
|
)
|
|
|
|
self._update_colors()
|
|
self.create_port_labels()
|
|
self.setTransformOriginPoint(self.width / 2, self.height / 2)
|
|
|
|
def create_port_labels(self):
|
|
for ports in (self.core.active_sinks, self.core.active_sources):
|
|
for port in ports:
|
|
port.gui.create_shapes_and_labels()
|
|
|
|
def _update_colors(self):
|
|
def get_bg():
|
|
"""
|
|
Get the background color for this block
|
|
Explicit is better than a chain of if/else expressions,
|
|
so this was extracted into a nested function.
|
|
"""
|
|
if self.core.is_dummy_block:
|
|
return colors.MISSING_BLOCK_BACKGROUND_COLOR
|
|
if self.core.state == "bypassed":
|
|
return colors.BLOCK_BYPASSED_COLOR
|
|
if self.core.state == "enabled":
|
|
if self.core.deprecated:
|
|
return colors.BLOCK_DEPRECATED_BACKGROUND_COLOR
|
|
return colors.BLOCK_ENABLED_COLOR
|
|
return colors.BLOCK_DISABLED_COLOR
|
|
|
|
self._bg_color = get_bg()
|
|
|
|
def move(self, x, y):
|
|
self.moveBy(x, y)
|
|
self.core.states["coordinate"] = (self.x(), self.y())
|
|
|
|
def paint(self, painter, option, widget):
|
|
if (self.hide_variables and (self.core.is_variable or self.core.is_import)) or (self.hide_disabled_blocks and not self.core.enabled):
|
|
return
|
|
|
|
painter.setRenderHint(QPainter.Antialiasing)
|
|
self.font.setBold(True)
|
|
|
|
# TODO: Make sure this is correct
|
|
border_color = colors.BORDER_COLOR
|
|
|
|
if self.isSelected():
|
|
border_color = colors.HIGHLIGHT_COLOR
|
|
else:
|
|
if self.core.is_dummy_block:
|
|
border_color = colors.MISSING_BLOCK_BORDER_COLOR
|
|
elif self.core.deprecated:
|
|
border_color = colors.BLOCK_DEPRECATED_BORDER_COLOR
|
|
elif self.core.state == "disabled":
|
|
border_color = colors.BORDER_COLOR_DISABLED
|
|
|
|
pen = QPen(1)
|
|
pen = QPen(border_color)
|
|
|
|
pen.setWidth(3)
|
|
painter.setPen(pen)
|
|
painter.setBrush(QBrush(self._bg_color))
|
|
rect = QRectF(0, 0, self.width, self.height)
|
|
|
|
painter.drawRect(rect)
|
|
painter.setPen(QPen(1))
|
|
|
|
# Draw block label text
|
|
painter.setFont(self.font)
|
|
if self.core.is_valid():
|
|
painter.setPen(Qt.black)
|
|
else:
|
|
painter.setPen(Qt.red)
|
|
painter.drawText(
|
|
QRectF(0, 0 - self.height / 2 + 15, self.width, self.height),
|
|
Qt.AlignCenter,
|
|
self.core.label,
|
|
)
|
|
|
|
# Draw param text
|
|
y_offset = 30 # params start 30 down from the top of the box
|
|
for key, item in self.core.params.items():
|
|
name = item.name
|
|
value = item.value
|
|
is_evaluated = item.value != str(item.get_evaluated())
|
|
|
|
display_value = ""
|
|
|
|
# Include the value defined by the user (after evaluation)
|
|
if not is_evaluated or self.show_param_val or not self.show_param_expr:
|
|
display_value += item.options[value] if value in item.options else value # TODO: pretty_print
|
|
|
|
# Include the expression that was evaluated to get the value
|
|
if is_evaluated and self.show_param_expr:
|
|
expr_string = value # TODO: Truncate
|
|
|
|
if display_value: # We are already displaying the value
|
|
display_value = expr_string + "=" + display_value
|
|
else:
|
|
display_value = expr_string
|
|
|
|
if len(display_value) > LONG_VALUE:
|
|
display_value = display_value[:LONG_VALUE - 3] + '...'
|
|
|
|
value_label = display_value
|
|
if (value is not None and item.hide == "none") or (item.dtype == 'id' and self.force_show_id):
|
|
if item.is_valid():
|
|
painter.setPen(QPen(1))
|
|
else:
|
|
painter.setPen(Qt.red)
|
|
|
|
self.font.setBold(True)
|
|
painter.setFont(self.font)
|
|
painter.drawText(
|
|
QRectF(7.5, 0 + y_offset, self.width, self.height),
|
|
Qt.AlignLeft,
|
|
name + ": ",
|
|
)
|
|
fm = QFontMetrics(self.font)
|
|
self.font.setBold(False)
|
|
painter.setFont(self.font)
|
|
painter.drawText(
|
|
QRectF(
|
|
7.5 + fm.width(name + ": "),
|
|
0 + y_offset,
|
|
self.width,
|
|
self.height,
|
|
),
|
|
Qt.AlignLeft,
|
|
value_label,
|
|
)
|
|
y_offset += 20
|
|
|
|
if self.markup_text:
|
|
painter.setPen(Qt.gray)
|
|
painter.drawText(
|
|
QRectF(0, self.height + 5, self.markups_width, self.markups_height),
|
|
Qt.AlignLeft,
|
|
self.markup_text,
|
|
)
|
|
|
|
def boundingRect(self):
|
|
# TODO: Comments should be a separate QGraphicsItem
|
|
return QRectF(
|
|
-2.5, -2.5, self.width + 5 + self.markups_width, self.height + 5 + self.markups_height
|
|
)
|
|
|
|
def set_states(self, states):
|
|
for k, v in states.items():
|
|
self.core.states[k] = v
|
|
|
|
self.setPos(*self.core.states["coordinate"])
|
|
self.setRotation(self.core.states["rotation"])
|
|
|
|
def mousePressEvent(self, e):
|
|
super(self.__class__, self).mousePressEvent(e)
|
|
log.debug(f"{self} clicked")
|
|
url_prefix = str(self.core.parent_platform.config.wiki_block_docs_url_prefix)
|
|
QApplication.instance().WikiTab.setURL(QUrl(url_prefix + self.core.label.replace(" ", "_")))
|
|
|
|
self.moveToTop()
|
|
|
|
def contextMenuEvent(self, e):
|
|
if not self.isSelected():
|
|
self.scene().clearSelection()
|
|
self.setSelected(True)
|
|
|
|
self.right_click_menu = self.scene()._app().MainWindow.menus["edit"]
|
|
example_action = QAction("Examples...")
|
|
example_action.triggered.connect(self.view_examples)
|
|
self.right_click_menu.addAction(example_action)
|
|
self.right_click_menu.exec_(e.screenPos())
|
|
|
|
def view_examples(self):
|
|
self.scene().app.MainWindow.example_browser_triggered(key_filter=self.core.key)
|
|
|
|
def mouseDoubleClickEvent(self, e):
|
|
self.open_properties()
|
|
super(self.__class__, self).mouseDoubleClickEvent(e)
|
|
|
|
def itemChange(self, change, value):
|
|
if change == QGraphicsItem.ItemPositionChange and self.scene() and self.snap_to_grid:
|
|
grid_size = 10
|
|
value.setX(round(value.x() / grid_size) * grid_size)
|
|
value.setY(round(value.y() / grid_size) * grid_size)
|
|
return value
|
|
else:
|
|
return QGraphicsItem.itemChange(self, change, value)
|
|
|
|
def rotate(self, rotation):
|
|
log.debug(f"Rotating {self.core.name}")
|
|
new_rotation = self.rotation() + rotation
|
|
self.setRotation(new_rotation)
|
|
self.core.states["rotation"] = new_rotation
|
|
self.create_shapes_and_labels()
|
|
for con in self.core.connections():
|
|
con.gui.update()
|
|
|
|
def moveToTop(self):
|
|
# TODO: Is there a simpler way to do this?
|
|
self.setZValue(self.scene().getMaxZValue() + 1)
|
|
|
|
def center(self):
|
|
return QPointF(self.x() + self.width / 2, self.y() + self.height / 2)
|
|
|
|
def open_properties(self):
|
|
self.props_dialog = PropsDialog(self.core, self.force_show_id)
|
|
self.props_dialog.show()
|