# 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