540 lines
19 KiB
Python

# Copyright 2014-2020 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
# Standard modules
import logging
import functools
from qtpy import QtGui, QtCore, QtWidgets, QT6
from qtpy.QtCore import Qt
from qtpy.QtWidgets import QApplication
from itertools import count
# Custom modules
from ....core.base import Element
from .connection import DummyConnection
from .port import GUIPort
from ... import base
from ....core.FlowGraph import FlowGraph as CoreFlowgraph
from ... import Utils
from ...external_editor import ExternalEditor
from .block import GUIBlock
from .connection import GUIConnection
# Logging
log = logging.getLogger(f"grc.application.{__name__}")
DEFAULT_MAX_X = 400
DEFAULT_MAX_Y = 300
class Flowgraph(CoreFlowgraph):
def __init__(self, gui, platform, *args, **kwargs):
self.gui = gui
CoreFlowgraph.__init__(self, platform)
class FlowgraphScene(QtWidgets.QGraphicsScene, base.Component):
itemMoved = QtCore.Signal([QtCore.QPointF])
newElement = QtCore.Signal([Element])
deleteElement = QtCore.Signal([Element])
blockPropsChange = QtCore.Signal([Element])
def __init__(self, view, platform, *args, **kwargs):
self.core = Flowgraph(self, platform)
super(FlowgraphScene, self).__init__()
self.setParent(view)
self.view = view
self.isPanning = False
self.mousePressed = False
self.moving_blocks = False
self.dummy_arrow = None
self.start_port = None
self.end_port = None
self._elements_to_draw = []
self._external_updaters = {}
self.qsettings = QApplication.instance().qsettings
if QT6:
self.undoStack = QtGui.QUndoStack(self)
else:
self.undoStack = QtWidgets.QUndoStack(self)
self.undoAction = self.undoStack.createUndoAction(self, "Undo")
self.redoAction = self.undoStack.createRedoAction(self, "Redo")
self.filename = None
self.clickPos = None
self.saved = False
self.save_allowed = True
def set_saved(self, saved):
self.saved = saved
def update(self):
"""
Call the top level rewrite and validate.
Call the top level create labels and shapes.
"""
self.core.rewrite()
self.core.validate()
for block in self.core.blocks:
block.gui.create_shapes_and_labels()
self.update_elements_to_draw()
self.app.VariableEditor.update_gui(self.core.blocks)
def update_elements_to_draw(self):
# hide_disabled_blocks = Actions.TOGGLE_HIDE_DISABLED_BLOCKS.get_active()
hide_disabled_blocks = False
# hide_variables = Actions.TOGGLE_HIDE_VARIABLES.get_active()
hide_variables = False
def draw_order(elem):
return elem.gui.isSelected(), elem.is_block, elem.enabled
elements = sorted(self.core.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 dragEnterEvent(self, event):
if event.mimeData().hasUrls:
event.setDropAction(Qt.CopyAction)
event.accept()
else:
event.ignore()
def dragMoveEvent(self, event):
if event.mimeData().hasUrls:
event.setDropAction(Qt.CopyAction)
event.accept()
else:
event.ignore()
def decode_data(self, bytearray):
data = []
item = {}
ds = QtCore.QDataStream(bytearray)
while not ds.atEnd():
row = ds.readInt32()
column = ds.readInt32()
map_items = ds.readInt32()
for i in range(map_items):
key = ds.readInt32()
value = QtCore.QVariant()
ds >> value
item[Qt.ItemDataRole(key)] = value
data.append(item)
return data
def _get_unique_id(self, base_id=""):
"""
Get a unique id starting with the base id.
Args:
base_id: the id starts with this and appends a count
Returns:
a unique id
"""
block_ids = set(b.name for b in self.core.blocks)
for index in count():
block_id = "{}_{}".format(base_id, index)
if block_id not in block_ids:
break
return block_id
def dropEvent(self, event):
QtWidgets.QGraphicsScene.dropEvent(self, event)
if event.mimeData().hasUrls:
data = event.mimeData()
if data.hasFormat("application/x-qabstractitemmodeldatalist"):
bytearray = data.data("application/x-qabstractitemmodeldatalist")
data_items = self.decode_data(bytearray)
# Find block in tree so that we can pull out label
block_key = data_items[0][QtCore.Qt.UserRole].value()
# Add block of this key at the cursor position
cursor_pos = event.scenePos()
pos = (cursor_pos.x(), cursor_pos.y())
self.add_block(block_key, pos)
event.setDropAction(Qt.CopyAction)
event.accept()
else:
return QtGui.QStandardItemModel.dropMimeData(
self, data, action, row, column, parent
)
else:
event.ignore()
def add_block(self, block_key, pos=(0, 0)):
block = self.platform.blocks[block_key]
# Pull out its params (keep in mind we still havent added the dialog box that lets you change param values so this is more for show)
params = []
for (
p
) in (
block.parameters_data
): # block.parameters_data is a list of dicts, one per param
if "label" in p: # for now let's just show it as long as it has a label
key = p["label"]
value = p.get("default", "") # just show default value for now
params.append((key, value))
id = self._get_unique_id(block_key)
c_block = self.core.new_block(block_key)
g_block = c_block.gui
c_block.states["coordinate"] = pos
g_block.setPos(*pos)
c_block.params["id"].set_value(id)
self.addItem(g_block)
g_block.moveToTop()
self.update()
self.newElement.emit(c_block)
return id
def selected_blocks(self) -> list[GUIBlock]:
blocks = []
for item in self.selectedItems():
if isinstance(item, GUIBlock):
blocks.append(item)
return blocks
def selected_connections(self):
conns = []
for item in self.selectedItems():
if isinstance(item, GUIConnection):
conns.append(item)
return conns
def delete_selected(self):
for item in self.selectedItems():
self.remove_element(item)
def select_all(self):
for block in self.core.blocks:
block.gui.setSelected(True)
for conn in self.core.connections:
conn.gui.setSelected(True)
def rotate_selected(self, rotation):
"""
Rotate the selected blocks by multiples of 90 degrees.
Args:
rotation: the rotation in degrees
Returns:
true if changed, otherwise false.
"""
selected_blocks = self.selected_blocks()
if not any(selected_blocks):
return False
# initialize min and max coordinates
min_x, min_y = max_x, max_y = selected_blocks[0].x(), selected_blocks[0].y()
# rotate each selected block, and find min/max coordinate
for selected_block in selected_blocks:
selected_block.rotate(rotation)
# update the min/max coordinate
x, y = selected_block.x(), selected_block.y()
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 selected 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 selected_blocks:
x, y = selected_block.x(), selected_block.y()
x, y = Utils.get_rotated_coordinate((x - ctr_x, y - ctr_y), rotation)
selected_block.setPos(x + ctr_x, y + ctr_y)
return True
def mousePressEvent(self, event):
g_item = self.itemAt(event.scenePos(), QtGui.QTransform())
if not g_item: # Nothing selected
event.ignore()
return
self.clickPos = event.scenePos()
self.moving_blocks = False
if g_item and not isinstance(g_item, DummyConnection):
c_item = g_item.core
if c_item.is_block:
self.moving_blocks = True
elif c_item.is_port:
if c_item.is_source:
self.start_port = g_item
elif c_item.is_sink:
self.end_port = g_item
if event.button() == Qt.LeftButton:
self.mousePressed = True
super(FlowgraphScene, self).mousePressEvent(event)
def mouseMoveEvent(self, event):
if self.mousePressed:
if not self.dummy_arrow and not self.moving_blocks and self.start_port:
self.dummy_arrow = DummyConnection(self, self.start_port.connection_point, self.clickPos)
self.addItem(self.dummy_arrow)
self.view.setSceneRect(self.itemsBoundingRect())
if self.dummy_arrow:
self.dummy_arrow.update(event.scenePos())
if self.mousePressed and self.isPanning:
new_pos = event.pos()
self.dragPos = new_pos
event.accept()
else:
super(FlowgraphScene, self).mouseMoveEvent(event)
def mouseReleaseEvent(self, event):
if self.dummy_arrow: # We are currently dragging a DummyConnection
self.removeItem(self.dummy_arrow)
g_item = self.itemAt(event.scenePos(), QtGui.QTransform())
if isinstance(g_item, GUIPort):
c_item = g_item.core
if g_item != self.start_port:
log.debug("Created connection (drag)")
new_con = self.core.connect(self.start_port.core, c_item)
self.addItem(new_con.gui)
self.newElement.emit(new_con)
self.update()
self.start_port = None
self.end_port = None
self.dummy_arrow = None
else:
if self.clickPos != event.scenePos() and self.moving_blocks:
self.itemMoved.emit(event.scenePos() - self.clickPos)
elif (self.start_port != None) and (self.end_port != None):
log.debug("Created connection (click)")
new_con = self.core.connect(self.start_port.core, self.end_port.core)
self.addItem(new_con.gui)
self.newElement.emit(new_con)
self.update()
self.start_port = None
self.end_port = None
self.mousePressed = False
self.moving_blocks = False
super(FlowgraphScene, self).mouseReleaseEvent(event)
def createActions(self, actions):
log.debug("Creating actions")
"""
# File Actions
actions['save'] = Action(Icons("document-save"), _("save"), self,
shortcut=Keys.New, statusTip=_("save-tooltip"))
actions['clear'] = Action(Icons("document-close"), _("clear"), self,
shortcut=Keys.Open, statusTip=_("clear-tooltip"))
"""
def createMenus(self, actions, menus):
log.debug("Creating menus")
def createToolbars(self, actions, toolbars):
log.debug("Creating toolbars")
def import_data(self, data):
self.core.import_data(data)
for conn in self.core.connections:
self.addItem(conn.gui)
for block in self.core.blocks:
self.addItem(block.gui)
def getMaxZValue(self):
z_values = []
for block in self.core.blocks:
z_values.append(block.gui.zValue())
return max(z_values)
def remove_element(self, element: GUIBlock):
self.removeItem(element)
self.core.remove_element(element.core)
def get_extents(self):
# show_comments = Actions.TOGGLE_SHOW_BLOCK_COMMENTS.get_active()
show_comments = True
def sub_extents():
for element in self._elements_to_draw:
yield element.get_extents()
if element.is_block and show_comments and element.enabled:
yield element.get_extents_comment()
extent = 10000000, 10000000, 0, 0
cmps = (min, min, max, max)
for sub_extent in sub_extents():
extent = [cmp(xy, e_xy) for cmp, xy, e_xy in zip(cmps, extent, sub_extent)]
return tuple(extent)
def copy_to_clipboard(self):
"""
Copy the selected blocks and connections into the clipboard.
Returns:
the clipboard
"""
# get selected blocks
g_blocks = list(self.selected_blocks())
if not g_blocks:
return None
# calc x and y min
x_min, y_min = g_blocks[0].core.states["coordinate"]
for g_block in g_blocks:
x, y = g_block.core.states["coordinate"]
x_min = min(x, x_min)
y_min = min(y, y_min)
# get connections between selected blocks
connections = list(
filter(
lambda c: c.source_block.gui in g_blocks and c.sink_block.gui in g_blocks,
self.core.connections,
)
)
clipboard = (
(x_min, y_min),
[g_block.core.export_data() for g_block in g_blocks],
[c_connection.export_data() for c_connection in connections],
)
return clipboard
def paste_from_clipboard(self, clipboard):
"""
Paste the blocks and connections from the clipboard.
Args:
clipboard: the nested data of blocks, connections
"""
self.clearSelection()
(x_min, y_min), blocks_n, connections_n = clipboard
"""
# 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
"""
x_off, y_off = 10, 10
# create blocks
pasted_blocks = {}
for block_n in blocks_n:
block_key = block_n.get("id")
if block_key == "options":
continue
block_name = block_n.get("name")
# Verify whether a block with this name exists before adding it
if block_name in (blk.name for blk in self.core.blocks):
block_n = block_n.copy()
block_n["name"] = self._get_unique_id(block_name)
block = self.core.new_block(block_key)
if not block:
continue # unknown block was pasted (e.g. dummy block)
block.import_data(**block_n)
pasted_blocks[block_name] = block # that is before any rename
block.gui.moveBy(x_off, y_off)
self.addItem(block.gui)
block.gui.moveToTop()
block.gui.setSelected(True)
"""
while any(Utils.align_to_grid(block.states['coordinate']) == Utils.align_to_grid(other.states['coordinate'])
for other in self.blocks if other is not block):
block.moveBy(Constants.CANVAS_GRID_SIZE,
Constants.CANVAS_GRID_SIZE)
# shift all following blocks
x_off += Constants.CANVAS_GRID_SIZE
y_off += Constants.CANVAS_GRID_SIZE
"""
# update before creating connections
self.update()
# create connections
for src_block, src_port, dst_block, dst_port in connections_n:
source = pasted_blocks[src_block].get_source(src_port)
sink = pasted_blocks[dst_block].get_sink(dst_port)
connection = self.core.connect(source, sink)
self.addItem(connection.gui)
connection.gui.setSelected(True)
def itemsBoundingRect(self):
rect = QtWidgets.QGraphicsScene.itemsBoundingRect(self)
return QtCore.QRectF(0.0, 0.0, rect.right(), rect.bottom())
def install_external_editor(self, param, parent=None):
target = (param.parent_block.name, param.key)
if target in self._external_updaters:
editor = self._external_updaters[target]
else:
editor = self.qsettings.value("grc/editor", "")
if not editor:
return
updater = functools.partial(
self.handle_external_editor_change, target=target)
editor = self._external_updaters[target] = ExternalEditor(
editor=editor,
name=target[0], value=param.get_value(),
callback=updater
)
editor.start()
try:
editor.open_editor()
except Exception as e:
# Problem launching the editor. Need to select a new editor.
log.error('Error opening an external editor. Please select a different editor.\n')
# Reset the editor to force the user to select a new one.
self.parent_platform.config.editor = ''
self.remove_external_editor(target=target)
def remove_external_editor(self, target=None, param=None):
if target is None:
target = (param.parent_block.name, param.key)
if target in self._external_updaters:
self._external_updaters[target].stop()
del self._external_updaters[target]
def handle_external_editor_change(self, new_value, target):
try:
block_id, param_key = target
self.core.get_block(block_id).params[param_key].set_value(new_value)
except (IndexError, ValueError): # block no longer exists
self.remove_external_editor(target=target)
return