grc: Add GRC Qt

Signed-off-by: Håkon Vågsether <hakon.vagsether@gmail.com>
This commit is contained in:
Håkon Vågsether 2024-01-17 20:33:35 +01:00 committed by Jeff Long
parent 3ff7a1ef17
commit ee6fc4a419
60 changed files with 10743 additions and 56 deletions

View File

@ -95,9 +95,11 @@ if(ENABLE_GRC)
# Create and install the grc and grc-docs conf file
########################################################################
file(TO_NATIVE_PATH ${CMAKE_INSTALL_PREFIX}/${GRC_BLOCKS_DIR} blocksdir)
file(TO_NATIVE_PATH ${CMAKE_INSTALL_PREFIX}/${GRC_EXAMPLES_DIR} examplesdir)
if(CMAKE_INSTALL_PREFIX STREQUAL "/usr")
# linux binary installs: append blocks dir with prefix /usr/local
# linux binary installs: append blocks and examples dir with prefix /usr/local
set(blocksdir ${blocksdir}:/usr/local/${GRC_BLOCKS_DIR})
set(examplesdir ${examplesdir}:/usr/local/${GRC_EXAMPLES_DIR})
endif(CMAKE_INSTALL_PREFIX STREQUAL "/usr")
if(UNIX)
@ -154,6 +156,12 @@ GR_PYTHON_INSTALL(
FILES_MATCHING REGEX "\\.(py|dtd|grc|tmpl|png|mako)$"
)
GR_PYTHON_INSTALL(
DIRECTORY gui_qt
DESTINATION "${GR_PYTHON_DIR}/gnuradio/grc"
FILES_MATCHING REGEX "\\.(py|yml|grc|mo|png|ui)$"
)
GR_PYTHON_INSTALL(
DIRECTORY converter
DESTINATION "${GR_PYTHON_DIR}/gnuradio/grc"

View File

@ -50,6 +50,10 @@ class Config(object):
return valid_paths
@property
def example_paths(self):
return [self._gr_prefs.get_string('grc', 'examples_path', '')]
@property
def default_flow_graph(self):
user_default = (

View File

@ -23,6 +23,7 @@ DEFAULT_HIER_BLOCK_LIB_DIR = os.path.expanduser('~/.grc_gnuradio')
DEFAULT_FLOW_GRAPH_ID = 'default'
CACHE_FILE = os.path.expanduser('~/.cache/grc_gnuradio/cache_v2.json')
EXAMPLE_CACHE_FILE = os.path.expanduser('~/.cache/grc_gnuradio/example_cache.json')
BLOCK_DESCRIPTION_FILE_FORMAT_VERSION = 1
# File format versions:

View File

@ -612,6 +612,13 @@ class Block(Element):
def children(self):
return itertools.chain(self.params.values(), self.ports())
def connections(self):
block_connections = []
for port in self.ports():
block_connections = block_connections + list(port.connections())
return block_connections
##############################################
# Access
##############################################

View File

@ -16,9 +16,10 @@ logger = logging.getLogger(__name__)
class Cache(object):
def __init__(self, filename, version=None):
def __init__(self, filename, version=None, log=True):
self.cache_file = filename
self.version = version
self.log = log
self.cache = {}
self._cachetime = None
self.need_cache_write = True
@ -35,16 +36,20 @@ class Cache(object):
def load(self):
try:
self.need_cache_write = False
logger.debug(f"Loading block cache from: {self.cache_file}")
if self.log:
logger.debug(f"Loading cache from: {self.cache_file}")
with open(self.cache_file, encoding='utf-8') as cache_file:
cache = json.load(cache_file)
cacheversion = cache.get("version", None)
if self.log:
logger.debug(f"Cache version {cacheversion}")
self._cachetime = cache.get("cached-at", 0)
if cacheversion == self.version:
logger.debug("Loaded block cache")
if self.log:
logger.debug("Loaded cache")
self.cache = cache["cache"]
else:
if self.log:
logger.info(f"Outdated cache {self.cache_file} found, "
"will be overwritten.")
raise ValueError()
@ -59,6 +64,7 @@ class Cache(object):
cached = self.cache[filename]
if int(cached["cached-at"] + 0.5) >= modtime:
return cached["data"]
if self.log:
logger.info(f"Cache for {filename} outdated, loading yaml")
except KeyError:
pass
@ -76,6 +82,7 @@ class Cache(object):
if not self.need_cache_write:
return
if self.log:
logger.debug('Saving %d entries to json cache', len(self.cache))
# Dumping to binary file is only supported for Python3 >= 3.6
with open(self.cache_file, 'w', encoding='utf8') as cache_file:

View File

@ -22,7 +22,7 @@ blocks:
value: '32000'
states:
coordinate:
- 184
- 200
- 12
rotation: 0
state: enabled

View File

@ -48,6 +48,7 @@ class Platform(Element):
self.blocks = self.block_classes
self.domains = {}
self.examples_dict = {}
self.connection_templates = {}
self.cpp_connection_templates = {}
self.connection_params = {}
@ -115,6 +116,9 @@ class Platform(Element):
return flow_graph, generator.file_path
def build_example_library(self, path=None):
self.examples = list(self._iter_files_in_example_path())
def build_library(self, path=None):
"""load the blocks and block tree from the search paths

View File

@ -5,6 +5,7 @@
[grc]
global_blocks_path = @blocksdir@
local_blocks_path =
examples_path = @examplesdir@
default_flow_graph =
xterm_executable = @GRC_XTERM_EXE@
canvas_font_size = 8

19
gui_qt/Config.py Normal file
View File

@ -0,0 +1,19 @@
import os
from ..core.Config import Config as CoreConfig
class Config(CoreConfig):
name = 'GNU Radio Companion'
gui_prefs_file = os.environ.get(
'GRC_QT_PREFS_PATH', os.path.expanduser('~/.gnuradio/grc_qt.conf'))
def __init__(self, install_prefix, *args, **kwargs):
CoreConfig.__init__(self, *args, **kwargs)
self.install_prefix = install_prefix
@property
def wiki_block_docs_url_prefix(self):
return self._gr_prefs.get_string('grc-docs', 'wiki_block_docs_url_prefix', '')

121
gui_qt/Constants.py Normal file
View File

@ -0,0 +1,121 @@
#TODO: This file is a modified copy of the old gui/Platform.py
"""
Copyright 2008, 2009 Free Software Foundation, Inc.
This file is part of GNU Radio
SPDX-License-Identifier: GPL-2.0-or-later
"""
import os
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_EXTENSION = '.grc'
# name for new/unsaved flow graphs
NEW_FLOGRAPH_TITLE = 'untitled'
# main window constraints
MIN_WINDOW_WIDTH = 600
MIN_WINDOW_HEIGHT = 400
# dialog constraints
MIN_DIALOG_WIDTH = 600
MIN_DIALOG_HEIGHT = 500
# default sizes
DEFAULT_BLOCKS_WINDOW_WIDTH = 100
DEFAULT_CONSOLE_WINDOW_WIDTH = 100
FONT_SIZE = DEFAULT_FONT_SIZE = 8
FONT_FAMILY = "Sans"
BLOCK_FONT = PORT_FONT = "Sans 8"
PARAM_FONT = "Sans 7.5"
# size of the state saving cache in the flow graph (undo/redo functionality)
STATE_CACHE_SIZE = 42
'''
# Shared targets for drag and drop of blocks
DND_TARGETS = [('STRING', Gtk.TargetFlags.SAME_APP, 0)]
'''
# label constraint dimensions
LABEL_SEPARATION = 3
BLOCK_LABEL_PADDING = 7
PORT_LABEL_PADDING = 2
# canvas grid size
CANVAS_GRID_SIZE = 8
# port constraint dimensions
PORT_BORDER_SEPARATION = 8
PORT_SPACING = 2 * PORT_BORDER_SEPARATION
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
# increment length for connector
CONNECTOR_EXTENSION_INCREMENT = 11
# connection arrow dimensions
CONNECTOR_ARROW_BASE = 10
CONNECTOR_ARROW_HEIGHT = 13
# possible rotations in degrees
POSSIBLE_ROTATIONS = (0, 90, 180, 270)
# How close the mouse can get to the edge of the visible window before scrolling is invoked.
SCROLL_PROXIMITY_SENSITIVITY = 50
# When the window has to be scrolled, move it this distance in the required direction.
SCROLL_DISTANCE = 15
# How close the mouse click can be to a line and register a connection select.
LINE_SELECT_SENSITIVITY = 5
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
'''
# Gtk-themes classified as dark
GTK_DARK_THEMES = [
'Adwaita-dark',
'HighContrastInverse',
]
GTK_SETTINGS_INI_PATH = '~/.config/gtk-3.0/settings.ini'
GTK_INI_PREFER_DARK_KEY = 'gtk-application-prefer-dark-theme'
GTK_INI_THEME_NAME_KEY = 'gtk-theme-name'
'''
def update_font_size(font_size):
global PORT_SEPARATION, BLOCK_FONT, PORT_FONT, PARAM_FONT, FONT_SIZE
FONT_SIZE = font_size
BLOCK_FONT = "%s %f" % (FONT_FAMILY, font_size)
PORT_FONT = BLOCK_FONT
PARAM_FONT = "%s %f" % (FONT_FAMILY, font_size - 0.5)
PORT_SEPARATION = PORT_SPACING + 2 * PORT_LABEL_PADDING + int(1.5 * font_size)
PORT_SEPARATION += -PORT_SEPARATION % (2 * CANVAS_GRID_SIZE) # even multiple
update_font_size(DEFAULT_FONT_SIZE)

69
gui_qt/Platform.py Normal file
View File

@ -0,0 +1,69 @@
#TODO: This file is a modified copy of the old gui/Platform.py
"""
Copyright 2008, 2009 Free Software Foundation, Inc.
This file is part of GNU Radio
SPDX-License-Identifier: GPL-2.0-or-later
"""
import sys
import os
from collections import ChainMap
from .Config import Config
from .components.canvas.flowgraph import Flowgraph
from .components.canvas.block import Block
from .components.canvas.port import Port
from .components.canvas.connection import Connection
from ..core.platform import Platform as CorePlatform
class Platform(CorePlatform):
def __init__(self, *args, **kwargs):
CorePlatform.__init__(self, *args, **kwargs)
# Ensure conf directories
gui_prefs_file = self.config.gui_prefs_file
if not os.path.exists(os.path.dirname(gui_prefs_file)):
os.mkdir(os.path.dirname(gui_prefs_file))
self._move_old_pref_file()
def get_prefs_file(self):
return self.config.gui_prefs_file
def _move_old_pref_file(self):
gui_prefs_file = self.config.gui_prefs_file
old_gui_prefs_file = os.environ.get(
'GRC_PREFS_PATH', os.path.expanduser('~/.grc'))
if gui_prefs_file == old_gui_prefs_file:
return # prefs file overridden with env var
if os.path.exists(old_gui_prefs_file) and not os.path.exists(gui_prefs_file):
try:
import shutil
shutil.move(old_gui_prefs_file, gui_prefs_file)
except Exception as e:
print(e, file=sys.stderr)
##############################################
# Factories
##############################################
Config = Config
FlowGraph = Flowgraph
Connection = Connection
def new_block_class(self, **data):
cls = CorePlatform.new_block_class(self, **data)
return Block.make_cls_with_base(cls)
block_classes_build_in = {key: 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: Port.make_cls_with_base(cls)
for key, cls in CorePlatform.port_classes.items()}
#param_classes = {key: Param.make_cls_with_base(cls)
# for key, cls in CorePlatform.param_classes.items()}

110
gui_qt/Utils.py Normal file
View File

@ -0,0 +1,110 @@
import numbers
import os
import logging
from pathlib import Path
from qtpy import QtGui, QtCore
from . import Constants
log = logging.getLogger(f"grc.application.{__name__}")
def get_rotated_coordinate(coor, rotation):
"""
Rotate the coordinate by the given rotation.
Args:
coor: the coordinate x, y tuple
rotation: the angle in degrees
Returns:
the rotated coordinates
"""
# handles negative angles
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 = {
0: (1, 0),
90: (0, 1),
180: (-1, 0),
270: (0, -1),
}[rotation]
x, y = coor
return x * cos_r + y * sin_r, -x * sin_r + y * cos_r
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, numbers.Complex):
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)
_nproc = None
def get_cmake_nproc():
""" Get number of cmake processes for C++ flowgraphs """
global _nproc # Cached result
if _nproc:
return _nproc
try:
# See https://docs.python.org/3.8/library/os.html#os.cpu_count
_nproc = len(os.sched_getaffinity(0))
except:
_nproc = os.cpu_count()
if not _nproc:
_nproc = 1
_nproc = max(_nproc // 2 - 1, 1)
return _nproc
def make_screenshot(fg_view, file_path, transparent_bg=False):
if not file_path:
return
file_path = Path(file_path)
if file_path.suffix == ".png":
rect = fg_view.viewport().rect()
pixmap = QtGui.QPixmap(rect.size())
painter = QtGui.QPainter(pixmap)
fg_view.render(painter, QtCore.QRectF(pixmap.rect()), rect)
pixmap.save(str(file_path), "PNG")
painter.end()
elif file_path.suffix == ".svg":
try:
from qtpy import QtSvg
except ImportError:
log.error("Missing (Python-)QtSvg! Please install it or export as PNG instead.")
return
rect = fg_view.viewport().rect()
generator = QtSvg.QSvgGenerator()
generator.setFileName(str(file_path))
generator.setSize(rect.size())
painter = QtGui.QPainter(generator)
fg_view.render(painter)
painter.end()
elif file_path.suffix == ".pdf":
log.warning("PDF screen capture not implemented")
return # TODO

0
gui_qt/__init__.py Normal file
View File

147
gui_qt/base.py Normal file
View File

@ -0,0 +1,147 @@
# 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 abc
import logging
import weakref
# Third-party modules
from qtpy import QtWidgets
# Logging
log = logging.getLogger(f"grc.application.{__name__}")
class Component(object):
''' Abstract base class for all grc components. '''
__metaclass__ = abc.ABCMeta
def __init__(self):
'''
Initializes the component's base class.
Sets up the references to the QApplication and the GNU Radio platform.
Calls createActions() to initialize the component's actions.
'''
log.debug("Initializing {}".format(self.__class__.__name__))
# Application reference - Use weak references to avoid issues with circular references
# Platform and settings properties are accessed through this reference
self._app = weakref.ref(QtWidgets.QApplication.instance())
# Automatically create the actions, menus and toolbars.
# Child controllers need to call the register functions to integrate into the mainwindow
self.actions = {}
self.menus = {}
self.toolbars = {}
'''
self.createActions(self.actions)
self.createMenus(self.actions, self.menus)
self.createToolbars(self.actions, self.toolbars)
'''
log.debug("Connecting signals")
self.connectSlots()
### Properties
@property
def app(self):
return self._app()
@property
def settings(self):
return self._app().settings
@property
def platform(self):
return self._app().platform
### Required methods
@abc.abstractmethod
def createActions(self, actions):
''' Add actions to the component. '''
raise NotImplementedError()
@abc.abstractmethod
def createMenus(self, actions, menus):
''' Add menus to the component. '''
raise NotImplementedError()
@abc.abstractmethod
def createToolbars(self, actions, toolbars):
''' Add toolbars to the component. '''
raise NotImplementedError()
### Base methods
def connectSlots(self, useToggled=True, toggledHandler='_toggled',
triggeredHandler="_triggered"):
'''
Handles connecting signals from given actions to handlers
self - Calling class
actions - Dictionary of a QAction and unique key
Dynamically build the connections for the signals by finding the correct
function to handle an action. Default behavior is to connect checkable actions to
the 'toggled' signal and normal actions to the 'triggered' signal. If 'toggled' is
not avaliable or useToggled is set to False then try to connect it to triggered.
Both toggled and triggered are called for checkable items, so there is no need for
both to be connected.
void QAction::toggled ( bool checked ) [signal]
void QAction::triggered ( bool checked = false ) [signal]
- Checked is set for checkable actions
Essentially the same as QMetaObject::connectSlotsByName, except the actions
and slots can be separated into a view and controller class
'''
actions = self.actions
for key in actions:
if useToggled and actions[key].isCheckable():
# Try to use toggled rather than triggered
try:
handler = key + toggledHandler
actions[key].toggled.connect(getattr(self, handler))
log.debug("<{0}.toggled> connected to handler <{1}>".format(key, handler))
# Successful connection. Jump to the next action.
continue
except:
# Default to the triggered handler
log.warning("Could not connect <{0}.toggled> to handler <{1}>".format(key, handler))
# Try and bind the 'triggered' signal to a handler.
try:
handler = key + triggeredHandler
actions[key].triggered.connect(getattr(self, handler))
log.debug("<{0}.triggered> connected to handler <{1}>".format(key, handler))
except:
try:
log.warning("Handler not implemented for <{0}.triggered> in {1}".format(
key, type(self).__name__))
actions[key].triggered.connect(getattr(self, 'notImplemented'))
except:
# This should never happen
log.error("Class cannot handle <{0}.triggered>".format(key))
def notImplemented(self):
log.warning('Not implemented')

View File

@ -0,0 +1,11 @@
from .block_library import BlockLibrary
from .example_browser import ExampleBrowser, Worker
from .wiki_tab import WikiTab
from .console import Console
from .flowgraph_view import FlowgraphView
from .undoable_actions import ChangeStateAction
from .variable_editor import VariableEditor
# Import last since there are dependencies
from .window import MainWindow

View File

@ -0,0 +1,322 @@
# 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
# Standard modules
import logging
from qtpy.QtCore import QUrl, Qt, QVariant
from qtpy.QtWidgets import (QLineEdit, QTreeView, QMenu, QDockWidget, QWidget,
QAction, QVBoxLayout, QAbstractItemView, QCompleter)
from qtpy.QtGui import QStandardItem, QStandardItemModel
# Custom modules
from .. import base
from .canvas.block import Block
# Logging
log = logging.getLogger(f"grc.application.{__name__}")
class BlockSearchBar(QLineEdit):
def __init__(self, parent):
QLineEdit.__init__(self)
self.parent = parent
self.setObjectName("block_library::search_bar")
self.returnPressed.connect(self.on_return_pressed)
def on_return_pressed(self):
label = self.text()
if label in self.parent._block_tree_flat:
block_key = self.parent._block_tree_flat[label].key
self.parent.add_block(block_key)
self.setText("")
self.parent.populate_tree(self.parent._block_tree)
else:
log.info(f"No block named {label}")
def get_items(model):
items = []
for i in range(0, model.rowCount()):
index = model.index(i, 0)
items.append(model.data(index))
return items
class LibraryView(QTreeView):
def __init__(self, parent):
QTreeView.__init__(self, parent)
self.library = parent.parent()
self.contextMenu = QMenu()
self.example_action = QAction("Examples...")
self.add_to_fg_action = QAction("Add to flowgraph")
self.example_action.triggered.connect(self.view_examples)
self.add_to_fg_action.triggered.connect(self.add_block)
self.contextMenu.addAction(self.example_action)
self.contextMenu.addAction(self.add_to_fg_action)
# TODO: Use selectionChanged() or something instead
# so we can use arrow keys too
def updateDocTab(self):
label = self.model().data(self.currentIndex())
if label in self.parent().parent()._block_tree_flat:
prefix = str(
self.parent().parent().platform.config.wiki_block_docs_url_prefix
)
self.parent().parent().app.WikiTab.setURL(
QUrl(prefix + label.replace(" ", "_"))
)
def handle_clicked(self):
if self.isExpanded(self.currentIndex()):
self.collapse(self.currentIndex())
else:
self.expand(self.currentIndex())
def contextMenuEvent(self, event):
key = self.model().data(self.currentIndex(), Qt.UserRole)
if key: # Modules and categories don't have UserRole data
self.contextMenu.exec_(self.mapToGlobal(event.pos()))
def view_examples(self):
key = self.model().data(self.currentIndex(), Qt.UserRole)
self.library.app.MainWindow.example_browser_triggered(key_filter=key)
def add_block(self) -> None:
key = self.model().data(self.currentIndex(), Qt.UserRole)
self.library.add_block(key)
class BlockLibrary(QDockWidget, base.Component):
def __init__(self):
super(BlockLibrary, self).__init__()
self.setObjectName("block_library")
self.setWindowTitle("Block Library")
# TODO: Pull from preferences and revert to default if not found?
self.resize(400, 300)
self.setFloating(False)
### GUI Widgets
# Create the layout widget
container = QWidget(self)
container.setObjectName("block_library::container")
self._container = container
layout = QVBoxLayout(container)
layout.setObjectName("block_library::layout")
layout.setSpacing(0)
layout.setContentsMargins(5, 0, 5, 5)
self._layout = layout
# Setup the model for holding block data
self._model = QStandardItemModel()
library = LibraryView(container)
library.setObjectName("block_library::library")
library.setModel(self._model)
library.setDragEnabled(True)
library.setDragDropMode(QAbstractItemView.DragOnly)
# library.setColumnCount(1)
library.setHeaderHidden(True)
# Expand categories with a single click
library.clicked.connect(library.handle_clicked)
library.selectionModel().selectionChanged.connect(library.updateDocTab)
# library.headerItem().setText(0, "Blocks")
library.doubleClicked.connect(
lambda block: self.add_block(block.data(Qt.UserRole))
)
self._library = library
search_bar = BlockSearchBar(self)
search_bar.setPlaceholderText("Find a block")
self._search_bar = search_bar
# Add widgets to the component
layout.addWidget(search_bar)
layout.addSpacing(5)
layout.addWidget(library)
container.setLayout(layout)
self.setWidget(container)
### Translation support
# self.setWindowTitle(_translate("blockLibraryDock", "Library", None))
# library.headerItem().setText(0, _translate("blockLibraryDock", "Blocks", None))
# QMetaObject.connectSlotsByName(blockLibraryDock)
### Loading blocks
# Keep as a separate function so it can be called at a later point (Reloading blocks)
self._block_tree_flat = {}
self.load_blocks()
self.populate_tree(self._block_tree)
completer = QCompleter(self._block_tree_flat.keys())
completer.setCompletionMode(QCompleter.InlineCompletion)
completer.setCaseSensitivity(Qt.CaseInsensitive)
completer.setFilterMode(Qt.MatchContains)
self._search_bar.setCompleter(completer)
self._search_bar.textChanged.connect(
lambda x: self.populate_tree(
self._block_tree, get_items(completer.completionModel())
)
)
# TODO: Move to the base controller and set actions as class attributes
# Automatically create the actions, menus and toolbars.
# Child controllers need to call the register functions to integrate into the mainwindow
self.actions = {}
self.menus = {}
self.toolbars = {}
self.createActions(self.actions)
self.createMenus(self.actions, self.menus)
self.createToolbars(self.actions, self.toolbars)
self.connectSlots()
# Register the dock widget through the AppController.
# The AppController then tries to find a saved dock location from the preferences
# before calling the MainWindow Controller to add the widget.
self.app.registerDockWidget(
self, location=self.settings.window.BLOCK_LIBRARY_DOCK_LOCATION
)
# Register the menus
# self.app.registerMenu(self.menus["library"])
# Dict representing which examples contain various blocks
self.examples_w_block = {}
def createActions(self, actions):
pass
def createMenus(self, actions, menus):
pass
def createToolbars(self, actions, toolbars):
pass
def load_blocks(self):
"""Load the block tree from the platform and populate the widget."""
# Loop through all of the blocks and create the nested hierarchy (this can be unlimited nesting)
# This takes advantage of Python's use of references to move through the nested layers
log.info("Loading blocks")
block_tree = {}
for block in self.platform.blocks.values():
if block.category:
# Blocks with None category should be left out for whatever reason (e.g. not installed)
# print(block.category) # in list form, e.g. ['Core', 'Digital Television', 'ATSC']
# print(block.label) # label GRC uses to name block
# print(block.key) # actual block name (i.e. class name)
# Create a copy of the category list so things can be removed without changing the original list
category = block.category[:]
# Get a reference to the main block tree.
# As nested categories are added, this is updated to point to the proper sub-tree in the next layer
sub_tree = block_tree
while category:
current = category.pop(0)
if current not in sub_tree.keys():
# Create the new sub-tree
sub_tree[current] = {}
# Move to the next layer in the block tree
sub_tree = sub_tree[current]
# Sub_tree should now point at the final node of the block_tree that contains the block
# Add a reference to the block object to the proper subtree
sub_tree[block.label] = block
self._block_tree_flat[block.label] = block
# Save a reference to the block tree in case it is needed later
self._block_tree = block_tree
def add_block(self, block_key: str):
"""Add a block by its key."""
if block_key is None:
return
scene = self.app.MainWindow.currentFlowgraphScene
view = self.app.MainWindow.currentView
pos_ = view.mapToScene(view.viewport().rect().center())
scene.add_block(block_key, pos=(pos_.x(), pos_.y()))
def populate_tree(self, block_tree, v_blocks=None):
"""Populate the item model and tree view with the hierarchical block tree."""
# Recursive method of populating the QStandardItemModel
# Since the _model.invisibleRootItem is the initial parent, this will populate
# the model which is used for the TreeView.
self._model.removeRows(0, self._model.rowCount())
def _populate(blocks, parent):
found = False
for name, obj in sorted(blocks.items()):
child_item = QStandardItem()
child_item.setEditable(False)
if type(obj) is dict: # It's a category
child_item.setText(name)
child_item.setDragEnabled(
False
) # categories should not be draggable
if not _populate(obj, child_item):
continue
else:
found = True
else: # It's a block
if v_blocks and not name in v_blocks:
continue
else:
found = True
child_item.setText(obj.label)
child_item.setDragEnabled(True)
child_item.setSelectable(True)
child_item.setData(
QVariant(obj.key),
role=Qt.UserRole,
)
parent.appendRow(child_item)
return found
# Call the nested function recursively to populate the block tree
log.debug("Populating the treeview")
_populate(block_tree, self._model.invisibleRootItem())
self._library.expand(
self._model.item(0, 0).index()
) # TODO: Should be togglable in prefs
if v_blocks:
self._library.expandAll()
def populate_w_examples(self, examples_w_block: dict[str, set[str]], designated_examples_w_block: dict[str, set[str]]):
"""
Store the examples in the block library.
See the ExampleBrowser for more info.
"""
self.examples_w_block = examples_w_block
self.designated_examples_w_block = designated_examples_w_block
def get_examples(self, block_key: str) -> list[Block]:
"""Get the example flowgraphs that contain a certain block"""
try:
return self.designated_examples_w_block[block_key]
except:
if block_key in self.examples_w_block:
return self.examples_w_block[block_key]
else:
return []

View File

420
gui_qt/components/canvas/block.py Executable file
View File

@ -0,0 +1,420 @@
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()

View File

@ -0,0 +1,54 @@
#TODO: This file is a modified copy of the old gui/Platform.py
"""
Copyright 2008,2013 Free Software Foundation, Inc.
This file is part of GNU Radio
SPDX-License-Identifier: GPL-2.0-or-later
"""
from qtpy import QtGui
from ... import Constants
def get_color(rgb):
color = QtGui.QColor(rgb)
return color
#################################################################################
# fg colors
#################################################################################
HIGHLIGHT_COLOR = get_color('#00FFFF')
BORDER_COLOR = get_color('#616161')
BORDER_COLOR_DISABLED = get_color('#888888')
FONT_COLOR = get_color('#000000')
# Deprecated blocks
# a light warm yellow
BLOCK_DEPRECATED_BACKGROUND_COLOR = get_color('#FED86B')
# orange
BLOCK_DEPRECATED_BORDER_COLOR = get_color('#FF540B')
# Missing blocks
MISSING_BLOCK_BACKGROUND_COLOR = get_color('#FFF2F2')
MISSING_BLOCK_BORDER_COLOR = get_color('#FF0000')
# Block color constants
BLOCK_ENABLED_COLOR = get_color('#F1ECFF')
BLOCK_DISABLED_COLOR = get_color('#CCCCCC')
BLOCK_BYPASSED_COLOR = get_color('#F4FF81')
CONNECTION_ENABLED_COLOR = get_color('#616161')
CONNECTION_DISABLED_COLOR = get_color('#BBBBBB')
CONNECTION_ERROR_COLOR = get_color('#FF0000')
DARK_FLOWGRAPH_BACKGROUND_COLOR = get_color('#19232D')
LIGHT_FLOWGRAPH_BACKGROUND_COLOR = get_color('#FFFFFF')
#################################################################################
# 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())

View File

@ -0,0 +1,146 @@
from qtpy.QtGui import QPainterPath, QPainter, QPen
from qtpy.QtWidgets import QGraphicsPathItem
from qtpy.QtCore import QPointF
from ....core.Connection import Connection as CoreConnection
from . import colors
from ...Constants import (
CONNECTOR_ARROW_BASE,
CONNECTOR_ARROW_HEIGHT
)
class DummyConnection(QGraphicsPathItem):
"""
Dummy connection used for when the user drags a connection
between two ports.
"""
def __init__(self, parent, start_point, end_point):
super(DummyConnection, self).__init__()
self.start_point = start_point
self.end_point = end_point
self._line = QPainterPath()
self._arrowhead = QPainterPath()
self._path = QPainterPath()
self.update(end_point)
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
self.setFlag(QGraphicsPathItem.ItemIsSelectable)
def update(self, end_point):
"""User moved the mouse, redraw with new end point"""
self.end_point = end_point
self._line.clear()
self._line.moveTo(self.start_point)
c1 = self.start_point + QPointF(200, 0)
c2 = self.end_point - QPointF(200, 0)
self._line.cubicTo(c1, c2, self.end_point)
self._arrowhead.clear()
self._arrowhead.moveTo(self.end_point)
self._arrowhead.lineTo(self.end_point + QPointF(-CONNECTOR_ARROW_HEIGHT, - CONNECTOR_ARROW_BASE / 2))
self._arrowhead.lineTo(self.end_point + QPointF(-CONNECTOR_ARROW_HEIGHT, CONNECTOR_ARROW_BASE / 2))
self._arrowhead.lineTo(self.end_point)
self._path.clear()
self._path.addPath(self._line)
self._path.addPath(self._arrowhead)
self.setPath(self._path)
def paint(self, painter, option, widget):
painter.setRenderHint(QPainter.Antialiasing)
color = colors.BORDER_COLOR
pen = QPen(color)
pen.setWidth(2)
painter.setPen(pen)
painter.drawPath(self._line)
painter.setBrush(color)
painter.drawPath(self._arrowhead)
class Connection(CoreConnection):
def __init__(self, parent, source, sink):
super(Connection, self).__init__(parent, source, sink)
self.gui = GUIConnection(self, parent, source, sink)
class GUIConnection(QGraphicsPathItem):
def __init__(self, core, parent, source, sink):
self.core = core
super(GUIConnection, self).__init__()
self.source = source
self.sink = sink
self._line = QPainterPath()
self._arrowhead = QPainterPath()
self._path = QPainterPath()
self.update()
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
self.setFlag(QGraphicsPathItem.ItemIsSelectable)
def update(self):
"""
Source and sink moved in relation to each other, redraw with new end points
"""
self._line.clear()
self._line.moveTo(self.source.gui.connection_point)
c1 = self.source.gui.ctrl_point
c2 = self.sink.gui.ctrl_point
self._line.cubicTo(c1, c2, self.sink.gui.connection_point)
self._arrowhead.clear()
self._arrowhead.moveTo(self.sink.gui.connection_point + QPointF(10.0, 0))
self._arrowhead.lineTo(self.sink.gui.connection_point + QPointF(10.0, 0) + QPointF(-CONNECTOR_ARROW_HEIGHT, - CONNECTOR_ARROW_BASE / 2))
self._arrowhead.lineTo(self.sink.gui.connection_point + QPointF(10.0, 0) + QPointF(-CONNECTOR_ARROW_HEIGHT, CONNECTOR_ARROW_BASE / 2))
self._arrowhead.lineTo(self.sink.gui.connection_point + QPointF(10.0, 0))
self._path.clear()
self._path.addPath(self._line)
self._path.addPath(self._arrowhead)
self.setPath(self._path)
def paint(self, painter, option, widget):
painter.setRenderHint(QPainter.Antialiasing)
color = colors.CONNECTION_ENABLED_COLOR
if self.isSelected():
color = colors.HIGHLIGHT_COLOR
elif not self.core.enabled:
color = colors.CONNECTION_DISABLED_COLOR
elif not self.core.is_valid():
color = colors.CONNECTION_ERROR_COLOR
pen = QPen(color)
pen.setWidth(2)
painter.setPen(pen)
painter.drawPath(self._line)
painter.setBrush(color)
painter.drawPath(self._arrowhead)
def mouseDoubleClickEvent(self, e):
self.parent.connections.remove(self)
self.scene().removeItem(self)

View File

@ -0,0 +1,531 @@
# 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._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)
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())
self.clickPos = event.scenePos()
selected = self.selectedItems()
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:
new_con = None
if len(selected) == 1:
if selected[0].core.is_port and selected[0] != g_item:
if selected[0].core.is_source and c_item.is_sink:
new_con = self.core.connect(selected[0].core, c_item)
elif selected[0].is_sink and c_item.is_source:
new_con = self.core.connect(c_item, selected[0].core)
if new_con:
log.debug("Created connection (click)")
self.addItem(new_con.gui)
self.newElement.emit(new_con)
self.update()
else:
self.start_port = g_item
if c_item.is_source:
self.dummy_arrow = DummyConnection(self, g_item.connection_point, event.scenePos())
self.addItem(self.dummy_arrow)
if event.button() == Qt.LeftButton:
self.mousePressed = True
super(FlowgraphScene, self).mousePressEvent(event)
def mouseMoveEvent(self, event):
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
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.removeItem(self.dummy_arrow)
self.dummy_arrow = None
else:
if self.clickPos != event.scenePos() and self.moving_blocks:
self.itemMoved.emit(event.scenePos() - self.clickPos)
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

View File

@ -0,0 +1,147 @@
from qtpy.QtGui import QPen, QPainter, QBrush, QFont, QFontMetrics
from qtpy.QtCore import Qt, QPointF, QRectF
from qtpy.QtWidgets import QGraphicsItem
from . import colors
from ... import Constants
from ....core.ports import Port as CorePort
class Port(CorePort):
@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, direction, **n):
super(self.__class__, self).__init__(parent, direction, **n)
self.gui = GUIPort(self, direction)
def remove_clone(self, port):
self.gui.scene().removeItem(port.gui)
super(self.__class__, self).remove_clone(port)
class GUIPort(QGraphicsItem):
"""
The graphical port. Interfaces with its underlying Port object using self.core.
The GUIPort is always instantiated in the Port constructor.
Note that this constructor is called before its parent GUIBlock is instantiated,
which is why we call setParentItem() in create_shapes_and_labels().
"""
def __init__(self, core, direction, **n):
self.core = core
QGraphicsItem.__init__(self)
self.y_offset = 0
self.height = 3 * 15.0 if self.core.dtype == 'bus' else 15.0
self.width = 15.0
if self.core._dir == "sink":
self.connection_point = self.mapToScene(QPointF(0.0, self.height / 2.0))
self.ctrl_point = self.mapToScene(QPointF(0.0, self.height / 2.0) - QPointF(5.0, 0.0))
else:
self.connection_point = self.mapToScene(QPointF(self.width, self.height / 2.0))
self.ctrl_point = self.mapToScene(QPointF(self.width, self.height / 2.0) + QPointF(5.0, 0.0))
self.setFlag(QGraphicsItem.ItemSendsScenePositionChanges)
self._border_color = self._bg_color = colors.BLOCK_ENABLED_COLOR
self.setFlag(QGraphicsItem.ItemStacksBehindParent)
self.setFlag(QGraphicsItem.ItemIsSelectable)
self.setAcceptHoverEvents(True)
self._hovering = False
self.auto_hide_port_labels = False
self.font = QFont('Helvetica', 8)
self.fm = QFontMetrics(self.font)
# TODO: Move somewhere else? Not necessarily
self.core.parent_flowgraph.gui.addItem(self)
def update_connections(self):
if self.core._dir == "sink":
self.connection_point = self.mapToScene(QPointF(-10.0, self.height / 2.0))
self.ctrl_point = self.mapToScene(QPointF(-10.0, self.height / 2.0) - QPointF(100.0, 0.0))
else:
self.connection_point = self.mapToScene(QPointF(self.width, self.height / 2.0))
self.ctrl_point = self.mapToScene(QPointF(self.width, self.height / 2.0) + QPointF(100.0, 0.0))
for conn in self.core.connections():
conn.gui.update()
def itemChange(self, change, value):
self.update_connections()
return QGraphicsItem.itemChange(self, change, value)
def create_shapes_and_labels(self):
self.auto_hide_port_labels = self.core.parent.parent.gui.app.qsettings.value('grc/auto_hide_port_labels', type=bool)
if not self.parentItem():
self.setParentItem(self.core.parent_block.gui)
self.width = max(15, self.fm.width(self.core.name) * 1.5)
self._update_colors()
self.update_connections()
@property
def _show_label(self) -> bool:
return self._hovering or not self.auto_hide_port_labels
def _update_colors(self):
"""
Get the color that represents this port's type.
TODO: Codes differ for ports where the vec length is 1 or greater than 1.
"""
if not self.core.parent.enabled:
color = colors.BLOCK_DISABLED_COLOR
elif self.core.domain == Constants.GR_MESSAGE_DOMAIN:
color = colors.PORT_TYPE_TO_COLOR.get('message')
else:
color = colors.PORT_TYPE_TO_COLOR.get(self.core.dtype) or colors.PORT_TYPE_TO_COLOR.get('')
self._bg_color = color
self._border_color = color
def paint(self, painter, option, widget):
if self.core.hidden:
return
painter.setRenderHint(QPainter.Antialiasing)
pen = QPen(self._border_color)
painter.setPen(pen)
painter.setBrush(QBrush(self._bg_color))
if self.core._dir == "sink":
rect = QRectF(-max(0, self.width - 15), 0, self.width, self.height)
else:
rect = QRectF(0, 0, self.width, self.height)
painter.drawRect(rect)
if self._show_label:
painter.setPen(QPen(1))
painter.setFont(self.font)
if self.core._dir == "sink":
painter.drawText(QRectF(-max(0, self.width - 15), 0, self.width, self.height), Qt.AlignCenter, self.core.name)
else:
painter.drawText(QRectF(0, 0, self.width, self.height), Qt.AlignCenter, self.core.name)
def center(self):
return QPointF(self.x() + self.width / 2, self.y() + self.height / 2)
def boundingRect(self):
if self.core._dir == "sink":
return QRectF(-max(0, self.width - 15), 0, self.width, self.height)
else:
return QRectF(0, 0, self.width, self.height)
def hoverEnterEvent(self, event):
self._hovering = True
return QGraphicsItem.hoverEnterEvent(self, event)
def hoverLeaveEvent(self, event):
self._hovering = False
return QGraphicsItem.hoverLeaveEvent(self, event)

View File

@ -0,0 +1,270 @@
# 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 html
import logging
import textwrap
# Third-party modules
from qtpy import QtCore, QtGui, QtWidgets
# Custom modules
from .. import base
# Shortcuts
Action = QtWidgets.QAction
Menu = QtWidgets.QMenu
Toolbar = QtWidgets.QToolBar
Icons = QtGui.QIcon.fromTheme
Keys = QtGui.QKeySequence
# Logging
log = logging.getLogger(f"grc.application.{__name__}")
HTML = '''
<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.0//EN\"
\"http://www.w3.org/TR/REC-html40/strict.dtd\">
<html>
<head>
<meta name=\"qrichtext\" content=\"1\" />
<style type=\"text/css\">
p, li {
white-space: pre-wrap;
}
/* Not currently used because entries are wrapped with <pre> */
body {
/* font-family: \'Ubuntu\'; */
font-family: \'MS Shell Dlg 2\';
font-size: 10pt;
font-weight: 400;
font-style: normal;
font-color: black;
margin: 0px;
text-indent: 0px;
-qt-block-indent: 0;
}
pre {
display: block;
font-family: monospace;
white-space: pre;
font-color: black;
margin: 1em 0;
}
</style>
</head>
</html>
'''
class Console(QtWidgets.QDockWidget, base.Component):
def __init__(self, level):
super(Console, self).__init__()
self.setObjectName('console')
self.setWindowTitle('Console')
self.level = level
### GUI Widgets
# Create the layout widget
container = QtWidgets.QWidget(self)
container.setObjectName('console::container')
self._container = container
layout = QtWidgets.QHBoxLayout(container)
layout.setObjectName('console::layout')
layout.setSpacing(0)
layout.setContentsMargins(5, 0, 5, 5)
self._layout = layout
# Console output widget
text = QtWidgets.QTextEdit(container)
text.setObjectName('console::text')
text.setUndoRedoEnabled(False)
text.setReadOnly(True)
text.setCursorWidth(0)
text.setTextInteractionFlags(QtCore.Qt.TextSelectableByKeyboard | QtCore.Qt.TextSelectableByMouse)
text.setHtml(textwrap.dedent(HTML))
self._text = text
# Add widgets to the component
layout.addWidget(text)
container.setLayout(layout)
self.setWidget(container)
### Translation support
#self.setWindowTitle(_translate("", "Library", None))
#library.headerItem().setText(0, _translate("", "Blocks", None))
#QtCore.QMetaObject.connectSlotsByName(blockLibraryDock)
### Setup actions
# TODO: Move to the base controller and set actions as class attributes
# Automatically create the actions, menus and toolbars.
# Child controllers need to call the register functions to integrate into the mainwindow
self.actions = {}
self.menus = {}
self.toolbars = {}
self.createActions(self.actions)
self.createMenus(self.actions, self.menus)
self.createToolbars(self.actions, self.toolbars)
self.connectSlots()
# Register the dock widget through the AppController.
# The AppController then tries to find a saved dock location from the preferences
# before calling the MainWindow Controller to add the widget.
self.app.registerDockWidget(self, location=self.settings.window.CONSOLE_DOCK_LOCATION)
# Register the menus
self.app.registerMenu(self.menus["console"])
# Register a new handler for the root logger that outputs messages of
# INFO and HIGHER to the reports view
handler = ReportsHandler(self.add_line)
handler.setLevel(self.level)
# Need to add this handler to the parent of the controller's logger
log.parent.addHandler(handler)
self.handler = handler
self.actions['show_level'].setChecked = True
self.handler.show_level = True
self.enabled = False
def enable(self):
self.enabled = True
### Actions
def createActions(self, actions):
''' Defines all actions for this view. '''
log.debug("Creating actions")
# File Actions
actions['save'] = Action(Icons("document-save"), _("save"), self, statusTip=_("save-tooltip"))
actions['clear'] = Action(Icons("document-close"), _("clear"), self, statusTip=_("clear-tooltip"))
actions['show_level'] = Action(_("show-level"), self, statusTip=_("show-level"), checkable=True, checked=True)
actions['auto_scroll'] = Action(_("auto-scroll"), self, statusTip=_("auto-scroll"), checkable=True, checked=True)
def createMenus(self, actions, menus):
''' Setup the view's menus '''
log.debug("Creating menus")
console_menu = QtWidgets.QMenu("&Console")
console_menu.setObjectName("console::menu")
# Not needed, we have FileHandler logging in main.py
#console_menu.addAction(actions["save"])
console_menu.addAction(actions["clear"])
console_menu.addAction(actions["show_level"])
console_menu.addAction(actions["auto_scroll"])
menus["console"] = console_menu
def createToolbars(self, actions, toolbars):
log.debug("Creating toolbars")
def add_line(self, line):
# TODO: Support multiple columns for the HTML. DO better with the spacing
# and margins in the output
if self.enabled:
self._text.append(line)
if self.actions["auto_scroll"].isChecked():
self._text.verticalScrollBar().setValue(
self._text.verticalScrollBar().maximum())
# Handlers for the view actions
def clear_triggered(self):
self._text.clear()
def save_triggered(self):
log.warning("Save reports not implemented")
def show_level_toggled(self, checked):
self.handler.show_level = checked
class ReportsHandler(logging.Handler): # Inherit from logging.Handler
''' Writes out logs to the reporst window '''
def __init__(self, add_line, show_level=True, short_level=True):
# run the regular Handler __init__
logging.Handler.__init__(self)
self.add_line = add_line # Function for adding a line to the view
self.show_level = show_level # Dynamically show levels
self.short_level = short_level # Default to true, changed by properties
self.formatLevelLength = self.formatLevelShort
if not short_level:
self.formatLevelLength = self.formatLevelLong
def emit(self, record):
# Just handle all formatting here
if self.show_level:
level = self.formatLevel(record.levelname)
message = html.escape(record.msg)
output = self.formatOutput()
self.add_line(output.format(level, message))
else:
message = html.escape(record.msg)
output = self.formatOutput()
self.add_line(output.format(message))
def formatLevel(self, levelname):
output = "{0}{1}{2}"
level = self.formatLevelLength(levelname)
if levelname == "INFO":
return output.format("<font color=\"Green\"><b>", level, "</b></font>")
elif levelname == "WARNING":
return output.format("<font color=\"Orange\"><b>", level, "</b></font>")
elif levelname == "ERROR":
return output.format("<font color=\"Red\"><b>", level, "</b></font>")
elif levelname == "CRITICAL":
return output.format("<font color=\"Red\"><b>", level, "</b></font>")
else:
return output.format("<font color=\"Blue\"><b>", level, "</b></font>")
def formatLevelShort(self, levelname):
return f'[{levelname[0:1]}]'
def formatLevelLong(self, levelname):
output = "{0:<10}"
if levelname in ["DEBUG", "INFO", "WARNING"]:
return output.format(f'[{levelname.capitalize()}]')
else:
return output.format(f'[{levelname.upper()}]')
def formatOutput(self):
''' Returns the correct output format based on internal settings '''
if self.show_level:
if self.short_level:
return "<tr><td width=\"25\">{0}</td><td><pre>{1}</pre></td></tr>"
return "<tr><td width=\"75\">{0}</td><td><pre>{1}</pre></td></tr>"
return "<tr><td><pre>{0}</pre></td></tr>"

View File

@ -0,0 +1,196 @@
from __future__ import absolute_import, print_function
from copy import copy
from ..Constants import MIN_DIALOG_HEIGHT, DEFAULT_PARAM_TAB
from qtpy.QtCore import Qt
from qtpy.QtGui import QStandardItem, QStandardItemModel
from qtpy.QtWidgets import (QLineEdit, QDialog, QDialogButtonBox, QTreeView,
QVBoxLayout, QTabWidget, QGridLayout, QWidget, QLabel,
QPushButton, QListWidget, QComboBox, QPlainTextEdit, QHBoxLayout,
QFileDialog, QApplication)
class ErrorsDialog(QDialog):
def __init__(self, flowgraph):
super().__init__()
self.flowgraph = flowgraph
self.store = []
self.setModal(True)
self.resize(700, MIN_DIALOG_HEIGHT)
self.setWindowTitle("Errors and Warnings")
buttons = QDialogButtonBox.Close
self.buttonBox = QDialogButtonBox(buttons)
self.buttonBox.rejected.connect(self.reject) # close
self.treeview = QTreeView()
self.model = QStandardItemModel()
self.treeview.setModel(self.model)
self.layout = QVBoxLayout()
self.layout.addWidget(self.treeview)
self.layout.addWidget(self.buttonBox)
self.setLayout(self.layout)
self.update()
def update(self):
# TODO: Make sure the columns are wide enough
self.model = QStandardItemModel()
self.model.setHorizontalHeaderLabels(['Source', 'Aspect', 'Message'])
for element, message in self.flowgraph.iter_error_messages():
if element.is_block:
src, aspect = QStandardItem(element.name), QStandardItem('')
elif element.is_connection:
src = QStandardItem(element.source_block.name)
aspect = QStandardItem("Connection to '{}'".format(element.sink_block.name))
elif element.is_port:
src = QStandardItem(element.parent_block.name)
aspect = QStandardItem("{} '{}'".format(
'Sink' if element.is_sink else 'Source', element.name))
elif element.is_param:
src = QStandardItem(element.parent_block.name)
aspect = QStandardItem("Param '{}'".format(element.name))
else:
src = aspect = QStandardItem('')
self.model.appendRow([src, aspect, QStandardItem(message)])
self.treeview.setModel(self.model)
class PropsDialog(QDialog):
def __init__(self, parent_block, force_show_id):
super().__init__()
self.setMinimumSize(600, 400)
self._block = parent_block
self.qsettings = QApplication.instance().qsettings
self.setModal(True)
self.force_show_id = force_show_id
self.setWindowTitle(f"Properties: {self._block.label}")
categories = (p.category for p in self._block.params.values())
def unique_categories():
seen = {DEFAULT_PARAM_TAB}
yield DEFAULT_PARAM_TAB
for cat in categories:
if cat in seen:
continue
yield cat
seen.add(cat)
self.edit_params = []
self.tabs = QTabWidget()
ignore_dtype_labels = ["_multiline", "_multiline_python_external"]
for cat in unique_categories():
qvb = QGridLayout()
qvb.setAlignment(Qt.AlignTop)
qvb.setVerticalSpacing(5)
qvb.setHorizontalSpacing(20)
i = 0
for param in self._block.params.values():
if force_show_id and param.dtype == 'id':
param.hide = 'none'
if param.category == cat and param.hide != "all":
dtype_label = None
if param.dtype not in ignore_dtype_labels:
dtype_label = QLabel(f"[{param.dtype}]")
qvb.addWidget(QLabel(param.name), i, 0)
if param.dtype == "enum" or param.options:
dropdown = QComboBox()
for opt in param.options.values():
dropdown.addItem(opt)
dropdown.param_values = list(param.options)
dropdown.param = param
qvb.addWidget(dropdown, i, 1)
self.edit_params.append(dropdown)
if param.dtype == "enum":
dropdown.setCurrentIndex(
dropdown.param_values.index(param.get_value())
)
else:
dropdown.setEditable(True)
value_label = (
param.options[param.value]
if param.value in param.options
else param.value
)
dropdown.setCurrentText(value_label)
elif param.dtype == "_multiline":
line_edit = QPlainTextEdit(param.value)
line_edit.param = param
qvb.addWidget(line_edit, i, 1)
self.edit_params.append(line_edit)
elif param.dtype == "_multiline_python_external":
ext_param = copy(param)
def open_editor(widget=None):
self._block.parent_flowgraph.gui.install_external_editor(
ext_param)
def open_chooser(widget=None):
self._block.parent_flowgraph.gui.remove_external_editor(param=ext_param)
editor, filtr = QFileDialog.getOpenFileName(
self,
)
self.qsettings.setValue("grc/editor", editor)
editor_widget = QWidget()
editor_widget.setLayout(QHBoxLayout())
open_editor_button = QPushButton("Open in Editor")
open_editor_button.clicked.connect(open_editor)
choose_editor_button = QPushButton("Choose Editor")
choose_editor_button.clicked.connect(open_chooser)
editor_widget.layout().addWidget(open_editor_button)
editor_widget.layout().addWidget(choose_editor_button)
line_edit = QPlainTextEdit(param.value)
line_edit.param = param
qvb.addWidget(editor_widget, i, 1)
#self.edit_params.append(line_edit)
else:
line_edit = QLineEdit(param.value)
line_edit.param = param
qvb.addWidget(line_edit, i, 1)
self.edit_params.append(line_edit)
if dtype_label:
qvb.addWidget(dtype_label, i, 2)
i += 1
tab = QWidget()
tab.setLayout(qvb)
self.tabs.addTab(tab, cat)
# Add example tab
self.example_tab = QWidget()
self.example_layout = QVBoxLayout()
self.example_tab.setLayout(self.example_layout)
self.example_list = QListWidget()
self.example_list.itemDoubleClicked.connect(lambda ex: self.open_example(ex))
buttons = QDialogButtonBox.Ok | QDialogButtonBox.Cancel
self.buttonBox = QDialogButtonBox(buttons)
self.buttonBox.accepted.connect(self.accept)
self.buttonBox.rejected.connect(self.reject)
self.layout = QVBoxLayout()
self.layout.addWidget(self.tabs)
self.layout.addWidget(self.buttonBox)
self.setLayout(self.layout)
def accept(self):
super().accept()
self._block.old_data = self._block.export_data()
for par in self.edit_params:
if isinstance(par, QLineEdit):
par.param.set_value(par.text())
else: # Dropdown/ComboBox
for key, val in par.param.options.items():
if val == par.currentText():
par.param.set_value(key)
self._block.rewrite()
self._block.validate()
self._block.gui.create_shapes_and_labels()
self._block.parent.gui.blockPropsChange.emit(self._block)
def open_example(self, ex=None):
# example is None if the "Open examples" button was pushed
if ex is None:
ex = self.example_list.currentItem()
self._block.parent.gui.app.MainWindow.open_example(ex.text())
self.close()

View File

@ -0,0 +1,311 @@
import logging
import os
import traceback
from qtpy import uic
from qtpy.QtCore import QObject, Signal, Slot, QRunnable, QVariant, Qt
from qtpy.QtGui import QPixmap, QStandardItem, QStandardItemModel
from qtpy.QtWidgets import QDialog, QListWidgetItem, QTreeWidgetItem, QWidget, QVBoxLayout
from ...core.cache import Cache
from .. import base, Constants
from ..properties import Paths
# Logging
log = logging.getLogger(f"grc.application.{__name__}")
class WorkerSignals(QObject):
error = Signal(tuple)
result = Signal(object)
progress = Signal(tuple)
class Worker(QRunnable):
"""
This is the Worker that will gather/parse examples as a background task
"""
def __init__(self, fn, *args, **kwargs):
super(Worker, self).__init__()
self.fn = fn
self.args = args
self.kwargs = kwargs
self.signals = WorkerSignals()
self.kwargs['progress_callback'] = self.signals.progress
@Slot()
def run(self):
try:
result = self.fn(*self.args, **self.kwargs)
except:
print("Error in background task:")
traceback.print_exc()
else:
self.signals.result.emit(result)
class ExampleBrowserDialog(QDialog):
def __init__(self, browser):
super(ExampleBrowserDialog, self).__init__()
self.setMinimumSize(600, 400)
self.setModal(True)
self.setWindowTitle("GRC Examples")
self.browser = browser
self.layout = QVBoxLayout()
self.setLayout(self.layout)
self.layout.addWidget(browser)
self.browser.connect_dialog(self)
class ExampleBrowser(QWidget, base.Component):
file_to_open = Signal(str)
data_role = Qt.UserRole
lang_dict = {
'python': 'Python',
'cpp': 'C++',
}
gen_opts_dict = {
'no_gui': 'No GUI',
'qt_gui': 'Qt GUI',
'bokeh_gui': 'Bokeh GUI',
'hb': 'Hier block ',
'hb_qt_gui': 'Hier block (Qt GUI)'
}
def __init__(self):
super(ExampleBrowser, self).__init__()
uic.loadUi(Paths.RESOURCES + "/example_browser_widget.ui", self)
self.library = None
self.dialog = None
self.tree_widget.setHeaderHidden(True)
self.tree_widget.clicked.connect(self.handle_clicked)
self.cpp_qt_fg = QPixmap(Paths.RESOURCES + "/cpp_qt_fg.png")
self.cpp_cmd_fg = QPixmap(Paths.RESOURCES + "/cpp_cmd_fg.png")
self.py_qt_fg = QPixmap(Paths.RESOURCES + "/py_qt_fg.png")
self.py_cmd_fg = QPixmap(Paths.RESOURCES + "/py_cmd_fg.png")
self.examples_dict = self.platform.examples_dict
self.dir_items = {}
self.tree_widget.currentItemChanged.connect(self.populate_preview)
self.tree_widget.itemDoubleClicked.connect(self.open_file)
self.open_button.clicked.connect(self.open_file)
def set_library(self, library):
self.library = library
def handle_clicked(self):
if self.tree_widget.isExpanded(self.tree_widget.currentIndex()):
self.tree_widget.collapse(self.tree_widget.currentIndex())
else:
self.tree_widget.expand(self.tree_widget.currentIndex())
def connect_dialog(self, dialog: QDialog):
if self.dialog:
pass # disconnect?
self.dialog = dialog
if isinstance(dialog, ExampleBrowserDialog):
self.close_button.setHidden(False)
self.close_button.clicked.connect(dialog.reject)
self.open_button.clicked.connect(self.done)
self.tree_widget.itemDoubleClicked.connect(self.done)
else:
raise Exception
def done(self, _=None):
self.dialog.done(0)
def populate(self, examples_dict):
self.examples_dict = examples_dict
self.tree_widget.clear()
for path, examples in examples_dict.items():
for ex in examples:
rel_path = os.path.relpath(os.path.dirname(ex['path']), path)
split_rel_path = os.path.normpath(rel_path).split(os.path.sep)
parent_path = "/".join(split_rel_path[0:-1])
if rel_path not in self.dir_items:
dir_ = None
if parent_path:
try:
dir_ = QTreeWidgetItem(self.dir_items[parent_path])
except KeyError:
i = 0
while i <= len(split_rel_path):
partial_path = "/".join(split_rel_path[0:i+1])
split_partial_path = os.path.normpath(partial_path).split(os.path.sep)
if not partial_path in self.dir_items:
if i == 0: # Top level
dir_ = QTreeWidgetItem(self.tree_widget)
dir_.setText(0, partial_path)
self.dir_items[partial_path] = dir_
else:
dir_ = QTreeWidgetItem(self.dir_items["/".join(split_partial_path[:-1])])
dir_.setText(0, split_partial_path[-1])
self.dir_items[partial_path] = dir_
i += 1
else:
dir_ = QTreeWidgetItem(self.tree_widget)
dir_.setText(0, split_rel_path[-1])
self.dir_items[rel_path] = dir_
item = QTreeWidgetItem(self.dir_items[rel_path])
item.setText(0, ex["title"] if ex["title"] else ex["name"])
item.setData(0, self.data_role, QVariant(ex))
def reset_preview(self):
self.title_label.setText(f"<b>Title:</b> ")
self.author_label.setText(f"<b>Author:</b> ")
self.language_label.setText(f"<b>Output language:</b> ")
self.gen_opts_label.setText(f"<b>Type:</b> ")
self.desc_label.setText('')
self.image_label.setPixmap(QPixmap())
def populate_preview(self):
ex = self.tree_widget.currentItem().data(0, self.data_role)
self.title_label.setText(f"<b>Title:</b> {ex['title'] if ex else ''}")
self.author_label.setText(f"<b>Author:</b> {ex['author'] if ex else ''}")
try:
self.language_label.setText(f"<b>Output language:</b> {self.lang_dict[ex['output_language']] if ex else ''}")
self.gen_opts_label.setText(f"<b>Type:</b> {self.gen_opts_dict[ex['generate_options']] if ex else ''}")
except KeyError:
self.language_label.setText(f"<b>Output language:</b> ")
self.gen_opts_label.setText(f"<b>Type:</b> ")
self.desc_label.setText(ex["desc"] if ex else '')
if ex:
if ex["output_language"] == "python":
if ex["generate_options"] == "qt_gui":
self.image_label.setPixmap(self.py_qt_fg)
else:
self.image_label.setPixmap(self.py_cmd_fg)
else:
if ex["generate_options"] == "qt_gui":
self.image_label.setPixmap(self.cpp_qt_fg)
else:
self.image_label.setPixmap(self.cpp_cmd_fg)
else:
self.image_label.setPixmap(QPixmap())
def open_file(self):
ex = self.tree_widget.currentItem().data(0, self.data_role)
self.file_to_open.emit(ex["path"])
def filter_(self, key: str):
"""
Only display examples that contain a specific block. (Hide the others)
Parameters:
key: The key of the block to search for
"""
found = False
ex_paths = self.library.get_examples(key)
for i in range(self.tree_widget.topLevelItemCount()):
top = self.tree_widget.topLevelItem(i)
if self.show_selective(top, ex_paths):
found = True
return found
def show_selective(self, item, path):
item.setHidden(True)
if item.childCount(): # is a directory
for i in range(item.childCount()):
if self.show_selective(item.child(i), path):
item.setHidden(False)
return not item.isHidden()
else: # is an example
ex = item.data(0, self.data_role)
if ex['path'] in path:
item.setHidden(False)
return True
else:
return False
def show_all(self, item):
item.setHidden(False)
for i in range(item.childCount()):
self.show_all(item.child(i))
def reset(self):
"""Reset the filter, collapse all."""
self.tree_widget.collapseAll()
self.reset_preview()
for i in range(self.tree_widget.topLevelItemCount()):
top = self.tree_widget.topLevelItem(i)
self.show_all(top)
def find_examples(self, progress_callback, ext="grc"):
"""Iterate through the example flowgraph directories and parse them."""
examples_dict = {}
with Cache(Constants.EXAMPLE_CACHE_FILE, log=False) as cache:
for entry in self.platform.config.example_paths:
if entry == '':
log.error("Empty example path!")
break
examples_dict[entry] = []
if os.path.isdir(entry):
subdirs = 0
current_subdir = 0
for dirpath, dirnames, filenames in os.walk(entry):
subdirs += 1 # Loop through once to see how many there are
for dirpath, dirnames, filenames in os.walk(entry):
dirnames.sort()
current_subdir += 1
progress_callback.emit((int(100 * current_subdir / subdirs), "Indexing examples"))
for filename in sorted(filter(lambda f: f.endswith('.' + ext), filenames)):
file_path = os.path.join(dirpath, filename)
try:
data = cache.get_or_load(file_path)
example = {}
example["name"] = os.path.basename(file_path)
example["generate_options"] = data["options"]["parameters"].get("generate_options") or "no_gui"
example["output_language"] = data["options"]["parameters"].get("output_language") or "python"
example["example_filter"] = data["metadata"].get("example_filter") or []
example["title"] = data["options"]["parameters"]["title"] or ""
example["desc"] = data["options"]["parameters"]["description"] or ""
example["author"] = data["options"]["parameters"]["author"] or ""
example["path"] = file_path
example["module"] = os.path.dirname(file_path).replace(entry, "")
if example["module"].startswith("/"):
example["module"] = example["module"][1:]
example["blocks"] = set()
for block in data["blocks"]:
example["blocks"].add(block["id"])
examples_dict[entry].append(example)
except Exception:
continue
examples_w_block: dict[str, set[str]] = {}
designated_examples_w_block: dict[str, set[str]] = {}
for path, examples in examples_dict.items():
for example in examples:
if example["example_filter"]:
for block in example["example_filter"]:
try:
designated_examples_w_block[block].append(example["path"])
except KeyError:
designated_examples_w_block[block] = [example["path"]]
continue
else:
for block in example["blocks"]:
try:
examples_w_block[block].append(example["path"])
except KeyError:
examples_w_block[block] = [example["path"]]
return (examples_dict, examples_w_block, designated_examples_w_block)

View File

@ -0,0 +1,120 @@
import os
import shlex
import subprocess
import threading
import time
from pathlib import Path
from shutil import which as find_executable
from ..Utils import get_cmake_nproc
from ...core import Messages
class ExecFlowGraphThread(threading.Thread):
"""Execute the flow graph as a new process and wait on it to finish."""
def __init__(self, view, flowgraph, xterm_executable, callback):
"""
ExecFlowGraphThread constructor.
"""
threading.Thread.__init__(self)
self.view = view
self.flow_graph = flowgraph
self.xterm_executable = xterm_executable
self.update_callback = callback
try:
if self.flow_graph.get_option('output_language') == 'python':
self.process = self.view.process = self._popen()
elif self.flow_graph.get_option('output_language') == 'cpp':
self.process = self.view.process = self._cpp_popen()
self.update_callback()
self.start()
except Exception as e:
Messages.send_verbose_exec(str(e))
Messages.send_end_exec()
def _popen(self):
"""
Execute this python flow graph.
"""
generator = self.view.get_generator()
run_command = self.flow_graph.get_run_command(generator.file_path)
run_command_args = shlex.split(run_command)
# When in no gui mode on linux, use a graphical terminal (looks nice)
xterm_executable = find_executable(self.xterm_executable)
if generator.generate_options == 'no_gui' and xterm_executable:
if ('gnome-terminal' in xterm_executable):
run_command_args = [xterm_executable, '--'] + run_command_args
else:
run_command_args = [xterm_executable, '-e', run_command]
# this does not reproduce a shell executable command string, if a graphical
# terminal is used. Passing run_command though shlex_quote would do it but
# it looks really ugly and confusing in the console panel.
Messages.send_start_exec(' '.join(run_command_args))
dirname = Path(generator.file_path).parent
return subprocess.Popen(
args=run_command_args,
cwd=dirname,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
shell=False, universal_newlines=True
)
def _cpp_popen(self):
"""
Execute this C++ flow graph after generating and compiling it.
"""
generator = self.view.get_generator()
run_command = generator.file_path + \
'/build/' + self.flow_graph.get_option('id')
dirname = generator.file_path
builddir = os.path.join(dirname, 'build')
if os.path.isfile(run_command):
os.remove(run_command)
xterm_executable = find_executable(self.xterm_executable)
nproc = get_cmake_nproc()
run_command_args = f'cmake .. && cmake --build . -j{nproc} && cd ../.. && {xterm_executable} -e {run_command}'
Messages.send_start_exec(run_command_args)
return subprocess.Popen(
args=run_command_args,
cwd=builddir,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
shell=True, universal_newlines=True
)
def run(self):
"""
Wait on the executing process by reading from its stdout.
Use GObject.idle_add when calling functions that modify gtk objects.
"""
# handle completion
r = "\n"
while r:
Messages.send_verbose_exec(r)
r = self.process.stdout.read(1)
# Properly close pipe before thread is terminated
self.process.stdout.close()
while self.process.poll() is None:
# Wait for the process to fully terminate
time.sleep(0.05)
self.done
def done(self):
"""Perform end of execution tasks."""
Messages.send_end_exec(self.process.returncode)
self.view.process = None
self.update_callback()

View File

@ -0,0 +1,188 @@
from __future__ import absolute_import, print_function
# Standard modules
import logging
import xml.etree.ElementTree as ET
from ast import literal_eval
from qtpy import QtGui, QtCore, QtWidgets
from qtpy.QtCore import Qt
# Custom modules
from .canvas.block import Block
from .. import base
from .canvas.flowgraph import FlowgraphScene, Flowgraph
from .canvas.colors import LIGHT_FLOWGRAPH_BACKGROUND_COLOR, DARK_FLOWGRAPH_BACKGROUND_COLOR
from ...core.generator import Generator
# Logging
log = logging.getLogger(f"grc.application.{__name__}")
DEFAULT_MAX_X = 400
DEFAULT_MAX_Y = 300
class FlowgraphView(
QtWidgets.QGraphicsView, base.Component
): # added base.Component so it can see platform
def __init__(self, parent, platform, filename=None):
super(FlowgraphView, self).__init__()
self.setParent(parent)
self.setAlignment(Qt.AlignLeft | Qt.AlignTop)
self.setScene(FlowgraphScene(self, platform))
self.scalefactor = 0.8
self.scale(self.scalefactor, self.scalefactor)
self.setSceneRect(0, 0, DEFAULT_MAX_X, DEFAULT_MAX_Y)
if filename is not None:
self.readFile(filename)
else:
self.initEmpty()
self.fitInView(self.scene().sceneRect(), QtCore.Qt.KeepAspectRatio)
if self.app.theme == "dark":
self.setBackgroundBrush(QtGui.QBrush(DARK_FLOWGRAPH_BACKGROUND_COLOR))
else:
self.setBackgroundBrush(QtGui.QBrush(LIGHT_FLOWGRAPH_BACKGROUND_COLOR))
self.isPanning = False
self.mousePressed = False
self.setDragMode(self.RubberBandDrag)
self.generator = None
self.process = None
def createActions(self, actions):
log.debug("Creating actions")
def contextMenuEvent(self, event):
super(FlowgraphView, self).contextMenuEvent(event)
def createMenus(self, actions, menus):
log.debug("Creating menus")
def createToolbars(self, actions, toolbars):
log.debug("Creating toolbars")
def get_generator(self) -> Generator:
return self.generator
def process_is_done(self) -> bool:
if self.process is None:
return True
else:
return (False if self.process.returncode is None else True)
def readFile(self, filename):
tree = ET.parse(filename)
for xml_block in tree.findall("block"):
attrib = {}
params = []
block_key = xml_block.find("key").text
for param in xml_block.findall("param"):
key = param.find("key").text
value = param.find("value").text
if key.startswith("_"):
attrib[key] = literal_eval(value)
else:
params.append((key, value))
# Find block in tree so that we can pull out label
try:
block = self.platform.blocks[block_key]
new_block = Block(block_key, block.label, attrib, params)
self.scene.addItem(new_block)
self.newElement.emit(new_block)
except:
log.warning("Block '{}' was not found".format(block_key))
# This part no longer works now that we are using a Scene with GraphicsItems, but I'm sure there's still some way to do it
# bounds = self.scene.itemsBoundingRect()
# self.setSceneRect(bounds)
# self.fitInView(bounds)
def initEmpty(self):
self.setSceneRect(0, 0, DEFAULT_MAX_X, DEFAULT_MAX_Y)
def zoom(self, factor: float, anchor=QtWidgets.QGraphicsView.AnchorViewCenter):
new_scalefactor = self.scalefactor * factor
if new_scalefactor > 0.25 and new_scalefactor < 2.5:
self.scalefactor = new_scalefactor
self.setTransformationAnchor(anchor)
self.setResizeAnchor(anchor)
self.scale(factor, factor)
def zoomOriginal(self):
# TODO: Original scale factor as a constant?
self.zoom(0.8 / self.scalefactor)
def wheelEvent(self, event):
# TODO: Support multi touch drag and drop for scrolling through the view
if event.modifiers() == Qt.ControlModifier:
factor = 1.1 if event.angleDelta().y() > 0 else (1.0 / 1.1)
self.zoom(factor, anchor=QtWidgets.QGraphicsView.AnchorUnderMouse)
# if new_scalefactor > 0.25 and new_scalefactor < 2.5:
# self.scalefactor = new_scalefactor
# self.setTransformationAnchor(QtWidgets.QGraphicsView.NoAnchor)
# self.setResizeAnchor(QtWidgets.QGraphicsView.NoAnchor)
# old_pos = self.mapToScene(event.pos())
# self.scale(factor, factor)
# new_pos = self.mapToScene(event.pos())
# delta = new_pos - old_pos
# self.translate(delta.x(), delta.y())
elif event.modifiers() == Qt.ShiftModifier:
self.horizontalScrollBar().setValue(
self.horizontalScrollBar().value() - event.angleDelta().y())
else:
QtWidgets.QGraphicsView.wheelEvent(self, event)
def mousePressEvent(self, event):
if event.button() == Qt.LeftButton:
self.mousePressed = True
# This will pass the mouse move event to the scene
super(FlowgraphView, self).mousePressEvent(event)
def mouseMoveEvent(self, event):
if self.mousePressed and self.isPanning:
new_pos = event.pos()
diff = new_pos - self.dragPos
self.dragPos = new_pos
self.horizontalScrollBar().setValue(
self.horizontalScrollBar().value() - diff.x()
)
self.verticalScrollBar().setValue(
self.verticalScrollBar().value() - diff.y()
)
event.accept()
else:
# This will pass the mouse move event to the scene
super(FlowgraphView, self).mouseMoveEvent(event)
def mouseReleaseEvent(self, event):
if event.button() == Qt.LeftButton:
self.mousePressed = False
super(FlowgraphView, self).mouseReleaseEvent(event)
def keyPressEvent(self, event):
super(FlowgraphView, self).keyPressEvent(event)
def keyReleaseEvent(self, event):
super(FlowgraphView, self).keyPressEvent(event)
def mouseDoubleClickEvent(self, event):
# This will pass the double click event to the scene
super(FlowgraphView, self).mouseDoubleClickEvent(event)

View File

@ -0,0 +1,108 @@
from __future__ import absolute_import, print_function
# Standard modules
import logging
import os
import yaml
from qtpy import QtCore, QtWidgets, uic
from .. import base
from ..properties import Paths
# Logging
log = logging.getLogger(f"grc.application.{__name__}")
class OOTBrowser(QtWidgets.QDialog, base.Component):
data_role = QtCore.Qt.UserRole
def __init__(self):
super().__init__()
uic.loadUi(Paths.RESOURCES + "/oot_browser.ui", self)
self.setMinimumSize(600, 450)
self.setModal(True)
self.setWindowTitle("GRC OOT Modules")
self.left_list.currentItemChanged.connect(self.populate_right_view)
self.manifest_dir = os.path.join(Paths.RESOURCES, "manifests")
for f in os.listdir(self.manifest_dir):
with open(os.path.join(self.manifest_dir, f), 'r', encoding='utf8') as manifest:
text = manifest.read()
yml, desc = text.split("---")
data = yaml.safe_load(yml)
data["description"] = desc
self.validate(data)
item = QtWidgets.QListWidgetItem()
item.setText(data["title"])
item.setData(self.data_role, data)
self.left_list.addItem(item)
self.left_list.setCurrentRow(0)
def validate(self, module) -> bool:
type_dict = {
'title': str,
'brief': str,
'website': str,
'dependencies': list,
'repo': str,
'copyright_owner': list,
'gr_supported_version': list,
'tags': list,
'license': str,
'description': str,
'author': list
}
valid = True
for key, val in type_dict.items():
if key in module:
if not type(module.get(key)) == val:
log.error(f"OOT module {module.get('title')} has field {key}, but it's not the correct type. Expected {val}, got {type(module.get(key))}. Ignoring")
valid = False
else:
log.error(f"OOT module {module.get('title')} is missing field {key}. Ignoring")
valid = False
return valid
def populate_right_view(self):
module = self.left_list.currentItem().data(self.data_role)
self.title_label.setText(f"{module.get('title')}")
self.version_label.setText(f"<b>Version:</b> {module.get('version')}")
self.brief_label.setText(module.get("brief"))
if module.get('website'):
self.website_label.setText(f"**Website:** {module.get('website')}")
else:
self.website_label.setText(f'**Website:** None')
if module.get("dependencies"):
self.dep_label.setText(f"<b>Dependencies:</b> {'; '.join(module.get('dependencies'))}")
else:
self.dep_label.setText("<b>Dependencies:</b> None")
if module.get('repo'):
self.repo_label.setText(f"**Repository:** {module.get('repo')}")
else:
self.repo_label.setText(f'**Repository:** None')
if module.get("copyright_owner"):
self.copyright_label.setText(f"<b>Copyright Owner:</b> {', '.join(module.get('copyright_owner'))}")
else:
self.copyright_label.setText("<b>Copyright Owner:</b> None")
if type(module.get('gr_supported_version')) == list:
self.supp_ver_label.setText(f"<b>Supported GNU Radio Versions:</b> {', '.join(module.get('gr_supported_version'))}")
else:
self.supp_ver_label.setText("<b>Supported GNU Radio Versions:</b> N/A")
log.error(f"module {module.get('title')} has invalid manifest field gr_supported_version")
self.tags_label.setText(f"<b>Tags:</b> {'; '.join(module.get('tags'))}")
self.license_label.setText(f"<b>License:</b> {module.get('license')}")
self.desc_label.setMarkdown("\n" + module.get("description").replace("\t", ""))
self.author_label.setText(f"**Author(s):** {', '.join(module.get('author'))}")

View File

@ -0,0 +1,172 @@
from __future__ import absolute_import, print_function
# Standard modules
import logging
import yaml
from qtpy.QtCore import Qt, QSettings
from qtpy.QtWidgets import (QLineEdit, QTabWidget, QDialog,
QScrollArea, QVBoxLayout, QCheckBox,
QComboBox, QHBoxLayout, QDialogButtonBox,
QLabel, QWidget, QFormLayout)
from ..properties import Paths
from gnuradio import gr
# Logging
log = logging.getLogger(f"grc.application.{__name__}")
class PreferencesDialog(QDialog):
pref_dict = {}
def __init__(self, qsettings):
super(QDialog, self).__init__()
self.qsettings = qsettings
self.setMinimumSize(600, 400)
self.setModal(True)
self.setWindowTitle("GRC Preferences")
self.tab_widget = QTabWidget()
self.rt_prefs = QSettings(gr.userconf_path() + "/config.conf", QSettings.IniFormat)
with open(Paths.AVAILABLE_PREFS_YML) as available_prefs_yml:
self.pref_dict = yaml.safe_load(available_prefs_yml)
self.populate_tabs()
buttons = QDialogButtonBox.Save | QDialogButtonBox.Cancel
self.buttonBox = QDialogButtonBox(buttons)
self.buttonBox.accepted.connect(self.accept)
self.buttonBox.rejected.connect(self.reject)
self.layout = QVBoxLayout()
self.layout.addWidget(self.tab_widget)
self.layout.addWidget(self.buttonBox)
self.setLayout(self.layout)
def new_tab_widget(self) -> QWidget:
widget = QWidget()
vbox = QVBoxLayout()
scroll = QScrollArea()
scroll.setWidgetResizable(True)
content = QWidget()
widget.form = QFormLayout(content)
widget.setLayout(vbox)
vbox.addWidget(scroll)
scroll.setWidget(content)
return widget
def populate_tabs(self) -> None:
for cat in self.pref_dict['categories']:
tab = cat['tab'] = self.new_tab_widget()
self.tab_widget.addTab(tab, cat['name'])
for item in cat['items']:
full_key = cat['key'] + '/' + item['key']
item['_label'] = QLabel(item['name'])
if item['dtype'] == 'bool':
item['_edit'] = QCheckBox()
if self.qsettings.contains(full_key):
value = self.qsettings.value(full_key, False, type=bool)
item['_edit'].setChecked(value)
else:
item['_edit'].setChecked(item['default'])
self.qsettings.setValue(full_key, item['default'])
elif item['dtype'] == 'enum':
item['_edit'] = QComboBox()
for opt in item['option_labels']:
item['_edit'].addItem(opt)
index = item['options'].index(self.qsettings.value(full_key, item['default'], type=str))
item['_edit'].setCurrentIndex(index)
else:
if self.qsettings.contains(full_key):
item['_edit'] = QLineEdit(self.qsettings.value(full_key))
else:
item['_edit'] = QLineEdit(str(item['default']))
self.qsettings.setValue(full_key, item['default'])
item['_line'] = QHBoxLayout()
if 'tooltip' in item.keys():
item['_label'].setToolTip(item['tooltip'])
item['_edit'].setToolTip(item['tooltip'])
tab.form.addRow(item['_label'], item['_edit'])
rt_tab = self.new_tab_widget()
for rt_cat in self.pref_dict['runtime']:
for item in rt_cat['items']:
full_key = rt_cat['key'] + '/' + item['key']
item['_label'] = QLabel(item['name'])
if item['dtype'] == 'bool':
item['_edit'] = QCheckBox()
if self.rt_prefs.contains(full_key):
value = self.rt_prefs.value(full_key, False, type=bool)
item['_edit'].setChecked(value)
else:
item['_edit'].setChecked(item['default'])
self.rt_prefs.setValue(full_key, item['default'])
elif item['dtype'] == 'enum':
item['_edit'] = QComboBox()
for opt in item['option_labels']:
item['_edit'].addItem(opt)
index = item['options'].index(self.rt_prefs.value(full_key, item['default'], type=str))
item['_edit'].setCurrentIndex(index)
else:
if self.rt_prefs.contains(full_key):
item['_edit'] = QLineEdit(self.rt_prefs.value(full_key))
else:
item['_edit'] = QLineEdit(str(item['default']))
self.rt_prefs.setValue(full_key, item['default'])
item['_line'] = QHBoxLayout()
if 'tooltip' in item.keys():
item['_label'].setToolTip(item['tooltip'])
item['_edit'].setToolTip(item['tooltip'])
rt_tab.form.addRow(item['_label'], item['_edit'])
self.tab_widget.addTab(rt_tab, 'Runtime')
def save_all(self):
log.debug(f'Writing changes to {self.qsettings.fileName()}')
for cat in self.pref_dict['categories']:
for item in cat['items']:
full_key = cat['key'] + '/' + item['key']
if item['dtype'] == 'bool':
self.qsettings.setValue(full_key, item['_edit'].isChecked())
elif item['dtype'] == 'enum':
self.qsettings.setValue(full_key, item['options'][item['_edit'].currentIndex()])
else:
self.qsettings.setValue(full_key, item['_edit'].text())
self.qsettings.sync()
for rt_cat in self.pref_dict['runtime']:
for item in rt_cat['items']:
full_key = rt_cat['key'] + '/' + item['key']
if item['dtype'] == 'bool':
self.rt_prefs.setValue(full_key, item['_edit'].isChecked())
elif item['dtype'] == 'enum':
self.rt_prefs.setValue(full_key, item['options'][item['_edit'].currentIndex()])
else:
self.rt_prefs.setValue(full_key, item['_edit'].text())
self.rt_prefs.sync()

View File

@ -0,0 +1,233 @@
from qtpy.QtWidgets import QUndoCommand
import logging
from copy import copy
from qtpy.QtCore import QPointF
from .canvas.flowgraph import FlowgraphScene
from .canvas.block import Block
from ...core.base import Element
log = logging.getLogger(f"grc.application.{__name__}")
# Movement, rotation, enable/disable/bypass, bus ports,
# change params, toggle type.
# Basically anything that's not cut/paste or new/delete
class ChangeStateAction(QUndoCommand):
def __init__(self, scene: FlowgraphScene):
QUndoCommand.__init__(self)
log.debug("init ChangeState")
self.old_states = []
self.old_params = []
self.new_states = []
self.new_params = []
self.scene = scene
self.g_blocks = scene.selected_blocks()
for g_block in self.g_blocks:
self.old_states.append(copy(g_block.core.states))
self.new_states.append(copy(g_block.core.states))
self.old_params.append(copy(g_block.core.params))
self.new_params.append(copy(g_block.core.params))
def redo(self):
for i in range(len(self.g_blocks)):
self.g_blocks[i].set_states(self.new_states[i])
self.g_blocks[i].core.params = (self.new_params[i])
self.scene.update()
def undo(self):
for i in range(len(self.g_blocks)):
self.g_blocks[i].set_states(self.old_states[i])
self.g_blocks[i].params = (self.old_params[i])
self.scene.update()
class RotateAction(QUndoCommand):
def __init__(self, scene: FlowgraphScene, delta_angle: int):
QUndoCommand.__init__(self)
log.debug("init RotateAction")
self.setText('Rotate')
self.g_blocks = scene.selected_blocks()
self.scene = scene
self.delta_angle = delta_angle
def redo(self):
self.scene.rotate_selected(self.delta_angle)
def undo(self):
self.scene.rotate_selected(-self.delta_angle)
class MoveAction(QUndoCommand):
def __init__(self, scene: FlowgraphScene, diff: QPointF):
QUndoCommand.__init__(self)
log.debug("init MoveAction")
self.setText('Move')
self.g_blocks = scene.selected_blocks()
self.scene = scene
self.x = diff.x()
self.y = diff.y()
for block in self.g_blocks:
block.core.states["coordinate"] = (block.x(), block.y())
self.first = True
# redo() is called when the MoveAction is first created.
# At this point, the item is already at the correct position.
# Therefore, do nothing.
def redo(self):
if self.first:
self.first = False
return
for g_block in self.g_blocks:
g_block.move(self.x, self.y)
self.scene.update()
def undo(self):
for g_block in self.g_blocks:
g_block.move(-self.x, -self.y)
self.scene.update()
class EnableAction(ChangeStateAction):
def __init__(self, scene: FlowgraphScene):
ChangeStateAction.__init__(self, scene)
log.debug("init EnableAction")
self.setText('Enable')
for i in range(len(self.g_blocks)):
self.new_states[i]['state'] = 'enabled'
class DisableAction(ChangeStateAction):
def __init__(self, scene: FlowgraphScene):
ChangeStateAction.__init__(self, scene)
log.debug("init DisableAction")
self.setText('Disable')
for i in range(len(self.g_blocks)):
self.new_states[i]['state'] = 'disabled'
class BypassAction(ChangeStateAction):
def __init__(self, scene: FlowgraphScene):
ChangeStateAction.__init__(self, scene)
log.debug("init BypassAction")
self.setText('Bypass')
for i in range(len(self.g_blocks)):
self.new_states[i]['state'] = 'bypassed'
# Change properties
# This can only be performed on one block at a time
class BlockPropsChangeAction(QUndoCommand):
def __init__(self, scene: FlowgraphScene, c_block: Block):
QUndoCommand.__init__(self)
log.debug("init BlockPropsChangeAction")
self.setText(f'{c_block.name} block: Change properties')
self.scene = scene
self.c_block = c_block
self.old_data = copy(c_block.old_data)
self.new_data = copy(c_block.export_data())
self.first = True
def redo(self):
if self.first:
self.first = False
return
try:
name = self.new_data['name']
except KeyError:
name = self.new_data['parameters']['id']
self.c_block.import_data(name, self.new_data['states'], self.new_data['parameters'])
self.c_block.rewrite()
self.c_block.validate()
self.c_block.gui.create_shapes_and_labels()
self.scene.update()
def undo(self):
try:
name = self.old_data['name']
except KeyError:
name = self.old_data['parameters']['id']
self.c_block.import_data(name, self.old_data['states'], self.old_data['parameters'])
self.c_block.rewrite()
self.c_block.validate()
self.c_block.gui.create_shapes_and_labels()
self.scene.update()
class BussifyAction(QUndoCommand):
def __init__(self, scene: FlowgraphScene, direction: str): # direction is either "sink" or "source"
QUndoCommand.__init__(self)
log.debug("init BussifyAction")
self.setText(f'Toggle bus {direction}')
self.scene = scene
self.direction = direction
self.g_blocks = scene.selected_blocks()
def bussify(self):
for g_block in self.g_blocks:
g_block.core.bussify(self.direction)
self.scene.update()
def redo(self):
self.bussify()
def undo(self):
self.bussify()
# Blocks and connections
class NewElementAction(QUndoCommand):
def __init__(self, scene: FlowgraphScene, element: Element):
QUndoCommand.__init__(self)
log.debug("init NewElementAction")
self.setText('New')
self.scene = scene
self.element = element
self.first = True
def redo(self):
if self.first:
self.first = False
return
if self.element.is_block:
self.scene.core.blocks.append(self.element)
elif self.element.is_connection:
self.scene.core.connections.add(self.element)
self.scene.addItem(self.element.gui)
self.scene.update()
def undo(self):
self.scene.remove_element(self.element.gui)
self.scene.update()
class DeleteElementAction(QUndoCommand):
def __init__(self, scene: FlowgraphScene):
QUndoCommand.__init__(self)
log.debug("init DeleteElementAction")
self.setText('Delete')
self.scene = scene
self.g_connections = scene.selected_connections()
self.g_blocks = scene.selected_blocks()
for block in self.g_blocks:
for conn in block.core.connections():
self.g_connections = self.g_connections + [conn.gui]
def redo(self):
for con in self.g_connections:
self.scene.remove_element(con)
for block in self.g_blocks:
self.scene.remove_element(block)
self.scene.update()
def undo(self):
for block in self.g_blocks:
self.scene.core.blocks.append(block.core)
self.scene.addItem(block)
for con in self.g_connections:
self.scene.core.connections.add(con.core)
self.scene.addItem(con)
self.scene.update()

View File

@ -0,0 +1,224 @@
from __future__ import absolute_import, print_function
# Standard modules
import logging
from enum import Enum
from qtpy import QtGui
from qtpy.QtWidgets import QMenu, QAction, QDockWidget, QTreeWidget, QTreeWidgetItem
from qtpy.QtCore import Slot, Signal, QPointF, Qt, QVariant
# Custom modules
from .. import base
from ...core.base import Element
from .canvas.flowgraph import FlowgraphScene
# Logging
log = logging.getLogger(f"grc.application.{__name__}")
class VariableEditorAction(Enum):
# Actions that are handled by the editor
ADD_IMPORT = 0
ADD_VARIABLE = 1
OPEN_PROPERTIES = 2
DELETE_BLOCK = 3
DELETE_CONFIRM = 4
ENABLE_BLOCK = 5
DISABLE_BLOCK = 6
class VariableEditor(QDockWidget, base.Component):
new_block = Signal([str])
delete_block = Signal([str])
def __init__(self):
super(VariableEditor, self).__init__()
self.setObjectName('variable_editor')
self.setWindowTitle('Variable Editor')
self.right_click_menu = VariableEditorContextMenu(self)
self.scene = None
### GUI Widgets
self._tree = QTreeWidget()
self._model = self._tree.model()
self._tree.setObjectName('variable_editor::tree_widget')
self._tree.setHeaderLabels(["ID", "Value", ""])
self.setWidget(self._tree)
self.currently_rebuilding = True
self._model.dataChanged.connect(self.handle_change)
imports = QTreeWidgetItem(self._tree)
imports.setText(0, "Imports")
imports.setIcon(2, QtGui.QIcon.fromTheme("list-add"))
variables = QTreeWidgetItem(self._tree)
variables.setText(0, "Variables")
variables.setIcon(2, QtGui.QIcon.fromTheme("list-add"))
self._tree.expandAll()
# TODO: Move to the base controller and set actions as class attributes
# Automatically create the actions, menus and toolbars.
# Child controllers need to call the register functions to integrate into the mainwindow
self.actions = {}
self.menus = {}
self.toolbars = {}
self.createActions(self.actions)
self.createMenus(self.actions, self.menus)
self.createToolbars(self.actions, self.toolbars)
self.connectSlots()
# Register the dock widget through the AppController.
# The AppController then tries to find a saved dock location from the preferences
# before calling the MainWindow Controller to add the widget.
self.app.registerDockWidget(self, location=self.settings.window.VARIABLE_EDITOR_DOCK_LOCATION)
self.currently_rebuilding = False
### Actions
def createActions(self, actions):
log.debug("Creating actions")
def createMenus(self, actions, menus):
log.debug("Creating menus")
def createToolbars(self, actions, toolbars):
log.debug("Creating toolbars")
def contextMenuEvent(self, event):
self.right_click_menu.exec_(self.mapToGlobal(event.pos()))
def keyPressEvent(self, event):
super(VariableEditor, self).keyPressEvent(event)
def set_scene(self, scene: FlowgraphScene):
self.scene = scene
self.update_gui(self.scene.core.blocks)
def handle_change(self, tl, br): # TODO: Why are there two arguments?
if self.currently_rebuilding:
return
c_block = self._tree.model().data(tl, role=Qt.UserRole)
new_text = self._tree.model().data(tl)
c_block.old_data = c_block.export_data()
if tl.column() == 0: # The name (id) changed
c_block.params['id'].set_value(new_text)
else: # column == 1, i.e. the value changed
if c_block.is_import:
c_block.params['import'].set_value(new_text)
else:
c_block.params['value'].set_value(new_text)
self.scene.blockPropsChange.emit(c_block)
def _rebuild(self):
# TODO: The way we update block params here seems suboptimal
self.currently_rebuilding = True
self._tree.clear()
imports = QTreeWidgetItem(self._tree)
imports.setText(0, "Imports")
imports.setIcon(2, QtGui.QIcon.fromTheme("list-add"))
for block in self._imports:
import_ = QTreeWidgetItem(imports)
import_.setText(0, block.name)
import_.setData(0, Qt.UserRole, block)
import_.setText(1, block.params['imports'].get_value())
import_.setData(1, Qt.UserRole, block)
import_.setIcon(2, QtGui.QIcon.fromTheme("list-remove"))
if block.enabled:
import_.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEditable | Qt.ItemIsEnabled)
else:
import_.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEditable)
variables = QTreeWidgetItem(self._tree)
variables.setText(0, "Variables")
variables.setIcon(2, QtGui.QIcon.fromTheme("list-add"))
for block in sorted(self._variables, key=lambda v: v.name):
variable_ = QTreeWidgetItem(variables)
variable_.setText(0, block.name)
variable_.setData(0, Qt.UserRole, block)
if block.key == 'variable':
variable_.setText(1, block.params['value'].get_value())
variable_.setData(1, Qt.UserRole, block)
if block.enabled:
variable_.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEditable | Qt.ItemIsEnabled)
else:
variable_.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEditable)
else:
variable_.setText(1, '<Open Properties>')
variable_.setFlags(Qt.ItemIsSelectable)
variable_.setIcon(2, QtGui.QIcon.fromTheme("list-remove"))
self.currently_rebuilding = False
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._tree.expandAll()
Slot(VariableEditorAction)
def handle_action(self, action):
log.debug(f"{action} triggered!")
"""
Single handler for the different actions that can be triggered by the context menu,
key presses or mouse clicks. Also triggers an update of the flow graph and editor.
"""
if action == VariableEditorAction.ADD_IMPORT:
self.new_block.emit("import")
elif action == VariableEditorAction.ADD_VARIABLE:
self.new_block.emit("variable")
elif action == VariableEditorAction.OPEN_PROPERTIES:
# TODO: Disabled in GRC Gtk. Enable?
pass
elif action == VariableEditorAction.DELETE_BLOCK:
self.delete_block.emit(self._block.name)
elif action == VariableEditorAction.DELETE_CONFIRM:
pass # TODO: Handle this
elif action == VariableEditorAction.ENABLE_BLOCK:
self._block.state = 'enabled'
elif action == VariableEditorAction.DISABLE_BLOCK:
self._block.state = 'disabled'
#Actions.VARIABLE_EDITOR_UPDATE() # TODO: Fix this
class VariableEditorContextMenu(QMenu):
def __init__(self, var_edit: VariableEditor):
super(QMenu, self).__init__()
self.imports = QAction(_("variable_editor_add_import"), self)
self.imports.triggered.connect(lambda: var_edit.handle_action(VariableEditorAction.ADD_IMPORT))
self.addAction(self.imports)
self.variables = QAction(_("variable_editor_add_variable"), self)
self.variables.triggered.connect(lambda: var_edit.handle_action(VariableEditorAction.ADD_VARIABLE))
self.addAction(self.variables)
self.addSeparator()
self.enable = QAction(_("variable_editor_enable"), self)
self.enable.triggered.connect(lambda: var_edit.handle_action(VariableEditorAction.ENABLE_BLOCK))
self.addAction(self.enable)
self.disable = QAction(_("variable_editor_disable"), self)
self.disable.triggered.connect(lambda: var_edit.handle_action(VariableEditorAction.DISABLE_BLOCK))
self.addAction(self.disable)
self.addSeparator()
self.delete = QAction(_("variable_editor_delete"), self)
self.delete.triggered.connect(lambda: var_edit.handle_action(VariableEditorAction.DELETE_BLOCK))
self.addAction(self.delete)
self.addSeparator()
self.properties = QAction(_("variable_editor_properties"), self)
self.properties.triggered.connect(lambda: var_edit.handle_action(VariableEditorAction.OPEN_PROPERTIES))
self.addAction(self.properties)
def update_enabled(self, selected, enabled=False):
self.delete.setEnabled(selected)
self.properties.setEnabled(selected)
self.enable.setEnabled(selected and not enabled)
self.disable.setEnabled(selected and enabled)

View File

@ -0,0 +1,115 @@
# Copyright 2014-2022 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
from qtpy import QtWidgets
# Custom modules
from .. import base
# Logging
log = logging.getLogger(f"grc.application.{__name__}")
class WikiTab(QtWidgets.QDockWidget, base.Component):
def __init__(self, argv_enabled=False):
super(WikiTab, self).__init__()
self.qsettings = self.app.qsettings
self.setObjectName('wiki_tab')
self.setWindowTitle('Wiki')
self.setFloating(False)
active = None
if argv_enabled:
active = True
else:
if self.qsettings.value("appearance/display_wiki", False, type=bool) == True:
active = True
else:
active = False
if active:
try:
from qtpy.QtWebEngineWidgets import QWebEngineView
self.hidden = False
except ImportError:
log.error("PyQt QWebEngine missing!")
self.hide()
self.hidden = True
return
else:
self.hide()
self.hidden = True
return
### GUI Widgets
# Create the layout widget
container = QtWidgets.QWidget(self)
container.setObjectName('wiki_tab::container')
self._container = container
layout = QtWidgets.QVBoxLayout(container)
layout.setObjectName('wiki_tab::layout')
layout.setSpacing(0)
layout.setContentsMargins(5, 0, 5, 5)
self._text = QWebEngineView()
self._text.setZoomFactor(0.5)
layout.addWidget(self._text)
self._layout = layout
container.setLayout(layout)
self.setWidget(container)
# TODO: Move to the base controller and set actions as class attributes
# Automatically create the actions, menus and toolbars.
# Child controllers need to call the register functions to integrate into the mainwindow
self.actions = {}
self.menus = {}
self.toolbars = {}
self.createActions(self.actions)
self.createMenus(self.actions, self.menus)
self.createToolbars(self.actions, self.toolbars)
self.connectSlots()
# Register the dock widget through the AppController.
# The AppController then tries to find a saved dock location from the preferences
# before calling the MainWindow Controller to add the widget.
self.app.registerDockWidget(self, location=self.settings.window.WIKI_TAB_DOCK_LOCATION)
def setURL(self, url):
if not self.hidden:
self._text.load(url)
self._text.show()
### Actions
def createActions(self, actions):
pass
def createMenus(self, actions, menus):
pass
def createToolbars(self, actions, toolbars):
pass

1519
gui_qt/components/window.py Normal file

File diff suppressed because it is too large Load Diff

74
gui_qt/external_editor.py Normal file
View File

@ -0,0 +1,74 @@
"""
Copyright 2015 Free Software Foundation, Inc.
This file is part of GNU Radio
SPDX-License-Identifier: GPL-2.0-or-later
"""
import os
import sys
import time
import threading
import tempfile
import subprocess
class ExternalEditor(threading.Thread):
def __init__(self, editor, name, value, callback):
threading.Thread.__init__(self)
self.daemon = True
self._stop_event = threading.Event()
self.editor = editor
self.callback = callback
self.filename = self._create_tempfile(name, value)
def _create_tempfile(self, name, value):
with tempfile.NamedTemporaryFile(
mode='wb', prefix=name + '_', suffix='.py', delete=False,
) as fp:
fp.write(value.encode('utf-8'))
return fp.name
def open_editor(self):
proc = subprocess.Popen(args=(self.editor, self.filename))
proc.poll()
return proc
def stop(self):
self._stop_event.set()
def run(self):
filename = self.filename
# print "file monitor: started for", filename
last_change = os.path.getmtime(filename)
try:
while not self._stop_event.is_set():
mtime = os.path.getmtime(filename)
if mtime > last_change:
# print "file monitor: reload trigger for", filename
last_change = mtime
with open(filename, 'rb') as fp:
data = fp.read().decode('utf-8')
self.callback(data)
time.sleep(1)
except Exception as e:
print("file monitor crashed:", str(e), file=sys.stderr)
finally:
try:
os.remove(self.filename)
except OSError:
pass
if __name__ == '__main__':
e = ExternalEditor('/usr/bin/gedit', "test", "content", print)
e.open_editor()
e.start()
time.sleep(15)
e.stop()
e.join()

165
gui_qt/grc.py Normal file
View File

@ -0,0 +1,165 @@
# 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 textwrap
import os
from qtpy import QtCore, QtWidgets, PYQT_VERSION, PYSIDE_VERSION
# Custom modules
from . import components
from .helpers.profiling import StopWatch
# Logging
# Setup the logger to use a different name than the file name
log = logging.getLogger("grc.application")
class Application(QtWidgets.QApplication):
"""
This is the main QT application for GRC.
It handles setting up the application components and actions and handles communication between different components in the system.
"""
def __init__(self, settings, platform):
# Note. Logger must have the correct naming convention to share handlers
log.debug("__init__")
self.settings = settings
self.platform = platform
config = platform.config
self.qsettings = QtCore.QSettings(config.gui_prefs_file, QtCore.QSettings.IniFormat)
log.debug(f"Using QSettings from {config.gui_prefs_file}")
os.environ["QT_SCALE_FACTOR"] = self.qsettings.value('appearance/qt_scale_factor', "1.0", type=str)
log.debug("Creating QApplication instance")
QtWidgets.QApplication.setAttribute(QtCore.Qt.AA_ShareOpenGLContexts, True)
QtWidgets.QApplication.__init__(self, settings.argv)
self.theme = "light"
if self.qsettings.value("appearance/theme", "dark") == "dark":
try:
import qdarkstyle
self.setStyleSheet(qdarkstyle.load_stylesheet())
self.theme = "dark"
except ImportError:
log.warning("Did not find QDarkstyle. Dark mode disabled")
# Load the main view class and initialize QMainWindow
log.debug("ARGV - {0}".format(settings.argv))
log.debug("INSTALL_DIR - {0}".format(settings.path.INSTALL))
# Global signals
self.signals = {}
# Setup the main application window
log.debug("Creating main application window")
stopwatch = StopWatch()
self.MainWindow = components.MainWindow()
stopwatch.lap("mainwindow")
level_str = self.qsettings.value('grc/console_log_level', "info", type=str)
if level_str == "info":
self.Console = components.Console(logging.INFO)
else: # level_str == "debug"
self.Console = components.Console(logging.DEBUG)
stopwatch.lap("console")
self.BlockLibrary = components.BlockLibrary()
stopwatch.lap("blocklibrary")
# self.DocumentationTab = components.DocumentationTab()
# stopwatch.lap('documentationtab')
self.WikiTab = components.WikiTab("--wiki" in settings.argv)
stopwatch.lap("wikitab")
self.VariableEditor = components.VariableEditor()
stopwatch.lap("variable_editor")
self.VariableEditor.set_scene(self.MainWindow.currentFlowgraphScene)
self.MainWindow.ExampleBrowser.set_library(self.BlockLibrary)
# Debug times
log.debug(
"Loaded MainWindow controller - {:.4f}s".format(
stopwatch.elapsed("mainwindow")
)
)
log.debug(
"Loaded Console component - {:.4f}s".format(stopwatch.elapsed("console"))
)
log.debug(
"Loaded BlockLibrary component - {:.4}s".format(
stopwatch.elapsed("blocklibrary")
)
)
# log.debug("Loaded DocumentationTab component - {:.4}s".format(stopwatch.elapsed("documentationtab")))
# Print Startup information once everything has loaded
self.Console.enable()
paths = "\n\t".join(config.block_paths)
welcome = (
f"<<< Welcome to {config.name} {config.version} >>>\n\n"
f"{('PyQt ' + PYQT_VERSION) if PYQT_VERSION else ('PySide ' + PYSIDE_VERSION)}\n"
f"GUI preferences file: {self.qsettings.fileName()}\n"
f"Block paths:\n\t{paths}\n"
)
log.info(textwrap.dedent(welcome))
log.debug(f'devicePixelRatio {self.MainWindow.screen().devicePixelRatio()}')
if (self.qsettings.value("appearance/theme", "dark") == "dark") and (self.theme == "light"):
log.warning("Could not apply dark theme. Is QDarkStyle installed?")
# Global registration functions
# - Handles the majority of child controller interaciton
def registerSignal(self, signal):
pass
def registerDockWidget(self, widget, location=0):
"""Allows child controllers to register a widget that can be docked in the main window"""
# TODO: Setup the system to automatically add new "Show <View Name>" menu items when a new
# dock widget is added.
log.debug(
"Registering widget ({0}, {1})".format(widget.__class__.__name__, location)
)
self.MainWindow.registerDockWidget(location, widget)
def registerMenu(self, menu):
"""Allows child controllers to register an a menu rather than just a single action"""
# TODO: Need to have several capabilities:
# - Menu's need the ability to have a priority for ordering
# - When registering, the controller needs to specific target menu
# - Automatically add seporators (unless it is the first or last items)
# - Have the ability to add it as a sub menu
# - MainWindow does not need to call register in the app controller. It can call directly
# - Possibly view sidebars and toolbars as submenu
# - Have the ability to create an entirely new menu
log.debug("Registering menu ({0})".format(menu.title()))
self.MainWindow.registerMenu(menu)
def registerAction(self, action, menu):
"""Allows child controllers to register a global action shown in the main window"""
pass
def run(self):
"""Launches the main QT event loop"""
# Show the main window after everything is initialized.
self.MainWindow.show()
return self.exec_()

View File

@ -0,0 +1,2 @@
from . import logging
from . import qt

131
gui_qt/helpers/logging.py Normal file
View File

@ -0,0 +1,131 @@
import logging
class GRCHandler(logging.Handler): # Inherit from logging.Handler
''' Custom log handler for GRC. Stores log entries to be viewed using the GRC debug window. '''
def __init__(self, maxLength=256):
# run the regular Handler __init__
logging.Handler.__init__(self)
# Our custom argument
self.log = collections.deque(maxlen=maxLength)
def emit(self, record):
self.log.append(record)
def getLogs(self, level):
pass
class ConsoleFormatter(logging.Formatter):
'''
Custom log formatter that nicely truncates the log message and log levels
- Verbose mode outputs: time, level, message, name, filename, and line number
- Normal mode output varies based on terminal size:
w < 80 - Level, Message (min length 40)
80 < w < 120 - Level, Message, File, Line (25)
120 < w - Level, Message, Name, File, Line
- Color mode ouptuts the same variable sizes and uses the blessings module
to add color
'''
# TODO: Better handle multi line messages. Need to indent them or something
def __init__(self, verbose=False, short_level=True):
# Test for blessings formatter
try:
from blessings import Terminal
self.terminal = Terminal()
self.formatLevel = self.formatLevelColor
except:
self.terminal = None
self.formatLevel = self.formatLevelPlain
# Default to short
self.formatLevelLength = self.formatLevelShort
if not short_level:
self.formatLevelLength = self.formatLevelLong
# Setup the format function as a pointer to the correct formatting function
# Determine size and mode
# TODO: Need to update the sizes depending on short or long outputs
import shutil
size = shutil.get_terminal_size()
width = max(40, size.columns - 10)
if size.columns < 80:
self.format = self.short
self.width = width
elif size.columns < 120:
self.format = self.medium
self.width = width - 30
elif size.columns >= 120:
self.format = self.long
self.width = width - 45
# Check if verbose mode. If so override other options
if verbose:
self.format = self.verbose
# Normal log formmatters
def short(self, record):
message = self.formatMessage(record.msg, self.width)
level = self.formatLevel(record.levelname)
return "{0} -- {1}".format(level, message)
def medium(self, record):
message = self.formatMessage(record.msg, self.width)
level = self.formatLevel(record.levelname)
output = '{0} -- {1:<' + str(self.width) + '} ({2}:{3})'
return output.format(level, message, record.filename, record.lineno)
def long(self, record):
message = self.formatMessage(record.msg, self.width)
level = self.formatLevel(record.levelname)
output = '{0} -- {1:<' + str(self.width) + '} {2} ({3}:{4})'
return output.format(level, message, record.name, record.filename, record.lineno)
''' Verbose formatter '''
def verbose(self, record):
# TODO: Still need to implement this
pass
''' Level and message formatters '''
# Nicely format the levelname
# Level name can be formated to either short or long, and also with color
def formatLevelColor(self, levelname):
term = self.terminal
output = "{0}{1}{2}{3}"
level = self.formatLevelLength(levelname)
if levelname == "DEBUG":
return output.format(term.blue, "", level, term.normal)
elif levelname == "INFO":
return output.format(term.green, "", level, term.normal)
elif levelname == "WARNING":
return output.format(term.yellow, "", level, term.normal)
elif levelname == "ERROR":
return output.format(term.red, term.bold, level, term.normal)
elif levelname == "CRITICAL":
return output.format(term.red, term.bold, level, term.normal)
else:
return output.format(term.blue, "", level, term.normal)
def formatLevelPlain(self, levelname):
''' Format the level name without color. formatLevelLength points to the right function '''
return self.formatLevelLength(levelname)
def formatLevelShort(self, levelname):
return "[{0}]".format(levelname[0:1])
def formatLevelLong(self, levelname):
output = "{0:<10}"
if levelname in ["DEBUG", "INFO", "WARNING"]:
return output.format("[{0}]".format(levelname.capitalize()))
else:
return output.format("[{0}]".format(levelname.upper()))
def formatMessage(self, message, width):
# First, strip out any newline for console output
message = message.rstrip()
if len(message) > width:
return (message[:(width - 3)] + "...")
return message

View File

@ -0,0 +1,26 @@
import time
class StopWatch(object):
'''
Tool for tracking/profiling application execution. Once initialized, this tracks elapsed
time between "laps" which can be given a name and accessed at a later point.
'''
def __init__(self):
self._laps = {}
self._previous = time.time()
def lap(self, name):
# Do as little as possible since this is timing things.
# Save the previous and current time, then overwrite the previous
lap = (self._previous, time.time())
self._previous = lap[1]
self._laps[name] = lap
def elapsed(self, name):
# If the lap isn't defined, this should throw a key error
# Don't worry about catching it here
start, stop = self._laps[name]
return stop - start

0
gui_qt/helpers/qt.py Normal file
View File

263
gui_qt/properties.py Normal file
View File

@ -0,0 +1,263 @@
import os
import stat
from qtpy.QtCore import Qt
class Properties(object):
''' Stores global properties for GRC. '''
APP_NAME = 'grc'
DEFAULT_LANGUAGE = ['en_US']
def __init__(self, argv):
self.argv = argv
# Setup sub-categories
self.path = Paths()
self.system = System()
self.window = Window()
self.colors = Colors()
self.types = Types()
class Paths(object):
''' Initialize GRC paths relative to current file. '''
# Flow graph
DEFAULT_FILE = os.getcwd()
IMAGE_FILE_EXTENSION = '.png'
TEXT_FILE_EXTENSION = '.txt'
NEW_FLOGRAPH_TITLE = 'untitled'
SEPARATORS = {'/': ':', '\\': ';'}[os.path.sep]
# Setup all the install paths
p = os.path
PREFERENCES = p.expanduser('~/.grc')
INSTALL = p.abspath(p.join(p.dirname(__file__), '..'))
RESOURCES = p.join(INSTALL, 'gui_qt/resources')
LANGUAGE = p.join(INSTALL, 'gui_qt/resources/language')
LOGO = p.join(INSTALL, 'gui_qt/resources/logo')
ICON = p.join(LOGO, 'gnuradio_logo_icon-square.png')
AVAILABLE_PREFS_YML = p.join(RESOURCES, 'available_preferences.yml')
# Model Paths
MODEL = p.join(INSTALL, 'model')
BLOCK_TREE_DTD = p.join(MODEL, 'block_tree.dtd')
FLOW_GRAPH_DTD = p.join(MODEL, 'flow_graph.dtd')
FLOW_GRAPH_TEMPLATE = p.join(MODEL, 'flow_graph.tmpl')
DEFAULT_FLOW_GRAPH = os.path.join(MODEL, 'default_flow_graph.grc')
# File creation modes
HIER_BLOCK_FILE_MODE = stat.S_IRUSR | stat.S_IWUSR | stat.S_IWGRP | stat.S_IROTH | stat.S_IRGRP
TOP_BLOCK_FILE_MODE = HIER_BLOCK_FILE_MODE | stat.S_IXUSR | stat.S_IXGRP
# Setup paths
'''
HIER_BLOCKS_LIB_DIR = os.environ.get('GRC_HIER_PATH',
os.path.expanduser('~/.grc_gnuradio'))
PREFS_FILE = os.environ.get('GRC_PREFS_PATH', os.path.join(os.path.expanduser('~/.grc')))
BLOCKS_DIRS = filter( #filter blank strings
lambda x: x, PATH_SEP.join([
os.environ.get('GRC_BLOCKS_PATH', ''),
_gr_prefs.get_string('grc', 'local_blocks_path', ''),
_gr_prefs.get_string('grc', 'global_blocks_path', ''),
]).split(PATH_SEP),
) + [HIER_BLOCKS_LIB_DIR]
'''
class System(object):
''' System specific properties '''
OS = 'Unknown'
#XTERM_EXECUTABLE = _gr_prefs.get_string('grc', 'xterm_executable', 'xterm')
class Window(object):
''' Properties for the main window '''
# Default window properties
MIN_WINDOW_WIDTH = 600
MIN_WINDOW_HEIGHT = 400
MIN_DIALOG_WIDTH = 500
MIN_DIALOG_HEIGHT = 500
# 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
# When the window has to be scrolled, move it this distance in the required direction.
SCROLL_DISTANCE = 15
# By default Always show the menubar
NATIVE_MENUBAR = False
# Default sizes
DEFAULT_BLOCKS_WINDOW_WIDTH = 100
DEFAULT_CONSOLE_WINDOW_WIDTH = 100
DEFAULT_PARAM_TAB = 'General'
ADVANCED_PARAM_TAB = 'Advanced'
CONSOLE_DOCK_LOCATION = Qt.BottomDockWidgetArea
BLOCK_LIBRARY_DOCK_LOCATION = Qt.LeftDockWidgetArea
DOCUMENTATION_TAB_DOCK_LOCATION = Qt.RightDockWidgetArea
WIKI_TAB_DOCK_LOCATION = Qt.RightDockWidgetArea
VARIABLE_EDITOR_DOCK_LOCATION = Qt.BottomDockWidgetArea
# Define the top level menus.
# This does not actually define the menus; it simply defines a list of constants that
# to be used as keys for the actual menu dictionaries
class menus(object):
FILE = "file"
EDIT = "edit"
VIEW = "view"
BUILD = "build"
TOOLS = "tools"
#PLUGINS = "plugins"
HELP = "help"
class Flowgraph(object):
''' Flow graph specific properites '''
# File format
FILE_FORMAT_VERSION = 1
# Fonts
FONT_FAMILY = 'Sans'
FONT_SIZE = 8
BLOCK_FONT = "%s %f" % (FONT_FAMILY, FONT_SIZE)
PORT_FONT = BLOCK_FONT
PARAM_FONT = "%s %f" % (FONT_FAMILY, FONT_SIZE - 0.5)
# The size of the state saving cache in the flow graph (for undo/redo functionality)
STATE_CACHE_SIZE = 42
# Shared targets for drag and drop of blocks
#DND_TARGETS = [('STRING', gtk.TARGET_SAME_APP, 0)]
# Label constraints
LABEL_SEPARATION = 3
BLOCK_LABEL_PADDING = 7
PORT_LABEL_PADDING = 2
# Port constraints
PORT_SEPARATION = 32
PORT_BORDER_SEPARATION = 9
PORT_MIN_WIDTH = 20
PORT_LABEL_HIDDEN_WIDTH = 10
# Connector lengths
CONNECTOR_EXTENSION_MINIMAL = 11
CONNECTOR_EXTENSION_INCREMENT = 11
# Connection arrows
CONNECTOR_ARROW_BASE = 13
CONNECTOR_ARROW_HEIGHT = 17
# Rotations
POSSIBLE_ROTATIONS = (0, 90, 180, 270)
# How close the mouse click can be to a line and register a connection select.
LINE_SELECT_SENSITIVITY = 5
# canvas grid size
CANVAS_GRID_SIZE = 8
class Colors(object):
''' Color definitions '''
# Graphics stuff
HIGHLIGHT = '#00FFFF'
BORDER = 'black'
MISSING_BLOCK_BACKGROUND = '#FFF2F2'
MISSING_BLOCK_BORDER = 'red'
PARAM_ENTRY_TEXT = 'black'
ENTRYENUM_CUSTOM = '#EEEEEE'
FLOWGRAPH_BACKGROUND = '#FFF9FF'
BLOCK_ENABLED = '#F1ECFF'
BLOCK_DISABLED = '#CCCCCC'
CONNECTION_ENABLED = 'black'
CONNECTION_DISABLED = '#999999'
CONNECTION_ERROR = 'red'
# Alias Colors
COMPLEX = '#3399FF'
FLOAT = '#FF8C69'
INT = '#00FF99'
SHORT = '#FFFF66'
BYTE = '#FF66FF'
# Type Colors
COMPLEX_FLOAT_64 = '#CC8C69'
COMPLEX_FLOAT_32 = '#3399FF'
COMPLEX_INTEGER_64 = '#66CC00'
COMPLEX_INTEGER_32 = '#33cc66'
COMPLEX_INTEGER_16 = '#cccc00'
COMPLEX_INTEGER_8 = '#cc00cc'
FLOAT_64 = '#66CCCC'
FLOAT_32 = '#FF8C69'
INTEGER_64 = '#99FF33'
INTEGER_32 = '#00FF99'
INTEGER_16 = '#FFFF66'
INTEGER_8 = '#FF66FF'
MESSAGE_QUEUE = '#777777'
ASYNC_MESSAGE = '#C0C0C0'
BUS_CONNECTION = '#FFFFFF'
WILDCARD = '#FFFFFF'
COMPLEX_VECTOR = '#3399AA'
FLOAT_VECTOR = '#CC8C69'
INT_VECTOR = '#00CC99'
SHORT_VECTOR = '#CCCC33'
BYTE_VECTOR = '#CC66CC'
ID = '#DDDDDD'
WILDCARD = '#FFFFFF'
MSG = '#777777'
class Types(object):
''' Setup types then map them to the conversion dictionaries '''
CORE_TYPES = { # Key: (Size, Color, Name)
'fc64': (16, Colors.COMPLEX_FLOAT_64, 'Complex Float 64'),
'fc32': (8, Colors.COMPLEX_FLOAT_32, 'Complex Float 32'),
'sc64': (16, Colors.COMPLEX_INTEGER_64, 'Complex Integer 64'),
'sc32': (8, Colors.COMPLEX_INTEGER_32, 'Complex Integer 32'),
'sc16': (4, Colors.COMPLEX_INTEGER_16, 'Complex Integer 16'),
'sc8': (2, Colors.COMPLEX_INTEGER_8, 'Complex Integer 8',),
'f64': (8, Colors.FLOAT_64, 'Float 64'),
'f32': (4, Colors.FLOAT_32, 'Float 32'),
's64': (8, Colors.INTEGER_64, 'Integer 64'),
's32': (4, Colors.INTEGER_32, 'Integer 32'),
's16': (2, Colors.INTEGER_16, 'Integer 16'),
's8': (1, Colors.INTEGER_8, 'Integer 8'),
'msg': (0, Colors.MESSAGE_QUEUE, 'Message Queue'),
'message': (0, Colors.ASYNC_MESSAGE, 'Async Message'),
'bus': (0, Colors.BUS_CONNECTION, 'Bus Connection'),
'': (0, Colors.WILDCARD, 'Wildcard')
}
ALIAS_TYPES = {
'complex': (8, Colors.COMPLEX),
'float': (4, Colors.FLOAT),
'int': (4, Colors.INT),
'short': (2, Colors.SHORT),
'byte': (1, Colors.BYTE),
}
# Setup conversion dictionaries
TYPE_TO_COLOR = {}
TYPE_TO_SIZEOF = {}
for key, (size, color, name) in CORE_TYPES.items():
TYPE_TO_COLOR[key] = color
TYPE_TO_SIZEOF[key] = size
for key, (sizeof, color) in ALIAS_TYPES.items():
TYPE_TO_COLOR[key] = color
TYPE_TO_SIZEOF[key] = size

View File

@ -0,0 +1,178 @@
# This file specifies which settings are available
# in the Preferences window.
categories:
- key: grc
name: GRC
items:
- key: editor
name: Editor
tooltip: Choose the editor
dtype: str
default: /usr/bin/gedit
- key: xterm_executable
name: Terminal
tooltip: Choose the Terminal app
dtype: str
default: /usr/bin/xterm
- key: hide_variables
name: Hide variables
dtype: bool
default: False
- key: show_param_expr
name: Show parameter expressions in block
dtype: bool
default: False
- key: show_param_val
name: Show parameter value in block
dtype: bool
default: False
- key: hide_disabled_blocks
name: Hide disabled blocks
dtype: bool
default: False
- key: auto_hide_port_labels
name: Auto-hide port labels
dtype: bool
default: False
- key: snap_to_grid
name: Snap to grid
dtype: bool
default: False
- key: show_block_comments
name: Show block comments
dtype: bool
default: True
- key: show_block_ids
name: Show all block IDs
dtype: bool
default: False
- key: generated_code_preview
name: Generated code preview
dtype: bool
default: False
- key: show_complexity
name: Show flowgraph complexity
dtype: bool
default: False
- key: custom_block_paths
name: Custom block paths
dtype: str
default: ""
- key: default_grc
name: Default GRC version
dtype: enum
default: grc_gtk
options:
- grc_gtk
- grc_qt
option_labels:
- GRC Gtk
- GRC Qt
- key: console_log_level
name: Console log level (Requires restart)
dtype: enum
default: info
options:
- info
- debug
option_labels:
- Info
- Debug
- key: config_file_path
name: Config file path
dtype: str
default: ""
- key: appearance
name: Appearance
items:
- key: theme
name: Theme (requires restart)
tooltip: Choose the theme
dtype: enum
default: dark
options:
- light
- dark
option_labels:
- Light
- Dark
- key: qt_scale_factor
name: Qt scale factor (experimental, requires restart)
tooltip: Scaling factor of Qt GUI elements. Note that the effective device pixel ratio will be a product of this value and the native device pixel ratio.
dtype: str
default: 1.0
- key: default_qt_gui_theme
name: Default Qt GUI theme
dtype: str
default: ""
- key: display_wiki
name: Display Wiki tab
dtype: bool
default: False
# Runtime preferences typically end up in config.conf. They are grouped in a single tab.
runtime:
- key: log
items:
- key: debug_file
name: Debug file
dtype: str
default: stderr
- key: debug_level
name: Debug level
dtype: str
default: crit
- key: log_file
name: Log file
dtype: str
default: stdout
- key: log_level
name: Log level
dtype: str
default: info
- key: perfcounters
items:
- key: clock
name: Perfcounters clock
dtype: str
default: thread
- key: export
name: Perfcounters export
dtype: bool
default: False
- key: enabled
name: Perfcounters enabled
dtype: bool
default: False
- key: controlport
items:
- key: edges_list
name: Controlport edges list
dtype: bool
default: False
- key: enabled
name: Controlport enabled
dtype: bool
default: False
- key: default
items:
- key: max_messages
name: Max messages
dtype: int
default: 8192
- key: verbose
name: Verbose
dtype: bool
default: False
# Window settings, these are not meant to be changed by the user.
# TODO: Is it necessary to have these in here?
window:
- key: current_file
dtype: str
default: ""
- key: recent_files
dtype: list
default: []

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

BIN
gui_qt/resources/cpp_fg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

1711
gui_qt/resources/data/rx_logo.grc Executable file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,184 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Dialog</class>
<widget class="QDialog" name="Dialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>754</width>
<height>407</height>
</rect>
</property>
<property name="windowTitle">
<string>Dialog</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QSplitter" name="splitter">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<widget class="QTreeWidget" name="tree_widget">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>200</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>400</width>
<height>16777215</height>
</size>
</property>
<column>
<property name="text">
<string>example_flowgraph.grc</string>
</property>
</column>
</widget>
<widget class="QWidget" name="right_widget" native="true">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>250</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>600</width>
<height>16777215</height>
</size>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QLabel" name="image_label">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>0</width>
<height>150</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>150</width>
<height>150</height>
</size>
</property>
<property name="text">
<string>Image</string>
</property>
<property name="scaledContents">
<bool>true</bool>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="title_label">
<property name="text">
<string>Title</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="author_label">
<property name="text">
<string>Author</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="gen_opts_label">
<property name="text">
<string>Generate options</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="language_label">
<property name="text">
<string>Language</string>
</property>
</widget>
</item>
<item>
<widget class="Line" name="separator">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="desc_label">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>2</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Description</string>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_3">
<item>
<widget class="QPushButton" name="close_button">
<property name="text">
<string>Close</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="open_button">
<property name="text">
<string>Open</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@ -0,0 +1,202 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>example_browser_widget</class>
<widget class="QWidget" name="example_browser_widget">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>786</width>
<height>430</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QSplitter" name="splitter">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="handleWidth">
<number>20</number>
</property>
<widget class="QTreeWidget" name="tree_widget">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>200</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>400</width>
<height>16777215</height>
</size>
</property>
<column>
<property name="text">
<string notr="true">1</string>
</property>
</column>
</widget>
<widget class="QWidget" name="right_widget" native="true">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>250</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>300</width>
<height>16777215</height>
</size>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QLabel" name="image_label">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>0</width>
<height>150</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>150</width>
<height>150</height>
</size>
</property>
<property name="text">
<string>Image</string>
</property>
<property name="scaledContents">
<bool>true</bool>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="title_label">
<property name="text">
<string>Title</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="author_label">
<property name="text">
<string>Author</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="gen_opts_label">
<property name="text">
<string>Generate options</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="language_label">
<property name="text">
<string>Language</string>
</property>
</widget>
</item>
<item>
<widget class="Line" name="separator">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="desc_label">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>2</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Description</string>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_3">
<property name="spacing">
<number>20</number>
</property>
<item>
<widget class="QPushButton" name="close_button">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Close</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="open_button">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Open</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@ -0,0 +1,17 @@
# Adding a Translation
1. Add new locale directory - <locale>/LC_MESSAGES
2. Copy grc.pot to <locale>/LC_MESSAGES/grc.po
3. Update fields
4. Convert to compiled version:
cd <locale>/LC_MESSAGES/; msgfmt -o grc.mo grc.po
# Updating Translatable Strings
1. Go to project root
2. Mark translatable strings with _():
print ("example") --> print(_("example"))
3. Run xgettext:
xgettext -L Python --keyword=_ -d grc -o grc/gui_qt/resources/language/grc.pot `find . -name "*.py"`
4. Open grc/gui_qt/resources/language/en_US/LC_MESSAGES/grc.po using Poedit or similar
5. Translation -> Update from POT file -> choose grc/gui_qt/resources/language/grc.pot

Binary file not shown.

View File

@ -0,0 +1,416 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
msgid ""
msgstr ""
"Project-Id-Version: GNU Radio Companion\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-11-26 20:00+0300\n"
"PO-Revision-Date: 2023-11-26 20:03+0300\n"
"Last-Translator: Håkon Vågsether <hakon.vagsether@gmail.com>\n"
"Language-Team: US English <sdhitefield@gmail.com>\n"
"Language: grc\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: Poedit 3.4.1\n"
#: grc/gui_qt/components/window.py:76
msgid "window-title"
msgstr "GNU Radio Companion"
#: grc/gui_qt/components/window.py:244
msgid "new"
msgstr "&New"
#: grc/gui_qt/components/window.py:247
msgid "new-tooltip"
msgstr "Create a new flow graph"
#: grc/gui_qt/components/window.py:252
msgid "open"
msgstr "&Open..."
#: grc/gui_qt/components/window.py:255
msgid "open-tooltip"
msgstr "Open an existing flow graph"
#: grc/gui_qt/components/window.py:259
msgid "example_browser"
msgstr "&Examples..."
#: grc/gui_qt/components/window.py:261
msgid "example_browser-tooltip"
msgstr "Show example browser"
#: grc/gui_qt/components/window.py:266
msgid "close"
msgstr "&Close"
#: grc/gui_qt/components/window.py:269
msgid "close-tooltip"
msgstr "Close the current flow graph"
#: grc/gui_qt/components/window.py:274
msgid "close_all"
msgstr "C&lose All"
#: grc/gui_qt/components/window.py:276
msgid "close_all-tooltip"
msgstr "Close all open flow graphs"
#: grc/gui_qt/components/window.py:280
msgid "save"
msgstr "&Save"
#: grc/gui_qt/components/window.py:283
msgid "save-tooltip"
msgstr "Save the current flow graph"
#: grc/gui_qt/components/window.py:288
msgid "save_as"
msgstr "Save &As..."
#: grc/gui_qt/components/window.py:291
msgid "save_as-tooltip"
msgstr "Save the current flow graph under a new name"
#: grc/gui_qt/components/window.py:294
msgid "save_copy"
msgstr "Save Cop&y"
#: grc/gui_qt/components/window.py:298
msgid "screen_capture"
msgstr "Screen ca&pture"
#: grc/gui_qt/components/window.py:301
msgid "screen_capture-tooltip"
msgstr "Create a screen capture of the current flow graph"
#: grc/gui_qt/components/window.py:306
msgid "exit"
msgstr "E&xit"
#: grc/gui_qt/components/window.py:309
msgid "exit-tooltip"
msgstr "Exit GNU Radio Companion"
#: grc/gui_qt/components/window.py:315
msgid "undo"
msgstr "Undo"
#: grc/gui_qt/components/window.py:318
msgid "undo-tooltip"
msgstr "Undo the last change"
#: grc/gui_qt/components/window.py:323
msgid "redo"
msgstr "Redo"
#: grc/gui_qt/components/window.py:326
msgid "redo-tooltip"
msgstr "Redo the last change"
#: grc/gui_qt/components/window.py:333
msgid "cut"
msgstr "Cu&t"
#: grc/gui_qt/components/window.py:336
msgid "cut-tooltip"
msgstr "Cut the current selection's contents to the clipboard"
#: grc/gui_qt/components/window.py:341
msgid "copy"
msgstr "&Copy"
#: grc/gui_qt/components/window.py:344
msgid "copy-tooltip"
msgstr "Copy the current selection's contents to the clipboard"
#: grc/gui_qt/components/window.py:349
msgid "paste"
msgstr "&Paste"
#: grc/gui_qt/components/window.py:352
msgid "paste-tooltip"
msgstr "Paste the clipboard's contents into the current selection"
#: grc/gui_qt/components/window.py:357
msgid "delete"
msgstr "&Delete"
#: grc/gui_qt/components/window.py:360
msgid "delete-tooltip"
msgstr "Delete the selected blocks"
#: grc/gui_qt/components/window.py:372
msgid "select_all"
msgstr "Select all"
#: grc/gui_qt/components/window.py:375
msgid "select_all-tooltip"
msgstr "Select all items in the canvas"
#: grc/gui_qt/components/window.py:379
msgid "Select None"
msgstr ""
#: grc/gui_qt/components/window.py:379
#, fuzzy
#| msgid "select_all-tooltip"
msgid "select_none-tooltip"
msgstr "Select all items in the canvas"
#: grc/gui_qt/components/window.py:384
msgid "rotate_ccw"
msgstr "Rotate counterclockwise"
#: grc/gui_qt/components/window.py:387
msgid "rotate_ccw-tooltip"
msgstr "Rotate the selected block(s) 90 degrees counterclockwise"
#: grc/gui_qt/components/window.py:392
msgid "rotate_cw"
msgstr "Rotate clockwise"
#: grc/gui_qt/components/window.py:395
msgid "rotate_cw-tooltip"
msgstr "Rotate the selected block(s) 90 degrees clockwise"
#: grc/gui_qt/components/window.py:401
msgid "enable"
msgstr "Enable"
#: grc/gui_qt/components/window.py:402
msgid "disable"
msgstr "Disable"
#: grc/gui_qt/components/window.py:403
msgid "bypass"
msgstr "Bypass"
#: grc/gui_qt/components/window.py:409
msgid "vertical_align_top"
msgstr "Vertical align top"
#: grc/gui_qt/components/window.py:410
msgid "vertical_align_middle"
msgstr "Vertical align middle"
#: grc/gui_qt/components/window.py:411
msgid "vertical_align_bottom"
msgstr "Vertical align bottom"
#: grc/gui_qt/components/window.py:417
msgid "horizontal_align_left"
msgstr "Horizontal align left"
#: grc/gui_qt/components/window.py:418
msgid "horizontal_align_center"
msgstr "Horizontal align center"
#: grc/gui_qt/components/window.py:419
msgid "horizontal_align_right"
msgstr "Horizontal align right"
#: grc/gui_qt/components/window.py:425
msgid "create_hier_block"
msgstr "Create hierarchical block"
#: grc/gui_qt/components/window.py:426
msgid "open_hier_block"
msgstr "Open hierarchical block"
#: grc/gui_qt/components/window.py:427
msgid "toggle_source_bus"
msgstr "Toggle source bus"
#: grc/gui_qt/components/window.py:428
msgid "toggle_sink_bus"
msgstr "Toggle sink bus"
#: grc/gui_qt/components/window.py:437
msgid "flowgraph-properties"
msgstr "Properties"
#: grc/gui_qt/components/window.py:439
msgid "flowgraph-properties-tooltip"
msgstr "Show the properties for a flow graph"
#: grc/gui_qt/components/window.py:443
msgid "search-in-examples"
msgstr ""
#: grc/gui_qt/components/window.py:445
msgid "search-in-examples-tooltip"
msgstr ""
#: grc/gui_qt/components/window.py:453
msgid "Zoom &in"
msgstr "Zoom &in"
#: grc/gui_qt/components/window.py:459
msgid "Zoom &out"
msgstr "Zoom &out"
#: grc/gui_qt/components/window.py:465
msgid "O&riginal size"
msgstr "O&riginal size"
#: grc/gui_qt/components/window.py:470
msgid "snap_to_grid"
msgstr "Snap to grid"
#: grc/gui_qt/components/window.py:474
msgid "toggle_grid"
msgstr "Toggle grid"
#: grc/gui_qt/components/window.py:474
msgid "toggle_grid-tooltip"
msgstr "Toggle the canvas grid"
#: grc/gui_qt/components/window.py:478
msgid "errors"
msgstr "&Errors"
#: grc/gui_qt/components/window.py:478
msgid "errors-tooltip"
msgstr "View errors in the current flow graph"
#: grc/gui_qt/components/window.py:483
msgid "find"
msgstr "&Find Block"
#: grc/gui_qt/components/window.py:486
msgid "find-tooltip"
msgstr "Search for a block by name (and key)"
#: grc/gui_qt/components/window.py:491
msgid "about"
msgstr "&About"
#: grc/gui_qt/components/window.py:491 grc/gui_qt/components/window.py:498
msgid "about-tooltip"
msgstr "Show the about window"
#: grc/gui_qt/components/window.py:496
msgid "about-qt"
msgstr "About &Qt"
#: grc/gui_qt/components/window.py:503
msgid "process-generate"
msgstr "&Generate"
#: grc/gui_qt/components/window.py:506
msgid "process-generate-tooltip"
msgstr "Generate a python flow graph"
#: grc/gui_qt/components/window.py:511
msgid "process-execute"
msgstr "&Execute"
#: grc/gui_qt/components/window.py:514
msgid "process-execute-tooltip"
msgstr "Execute a python flow graph"
#: grc/gui_qt/components/window.py:519
msgid "process-kill"
msgstr "&Kill"
#: grc/gui_qt/components/window.py:522
msgid "process-kill-tooltip"
msgstr "Kill the current flow graph"
#: grc/gui_qt/components/window.py:527
msgid "help"
msgstr "&Help"
#: grc/gui_qt/components/window.py:530
msgid "help-tooltip"
msgstr "Show the help window"
#: grc/gui_qt/components/window.py:535
#, fuzzy
#| msgid "filter_design_tool"
msgid "&Filter Design Tool"
msgstr "Filter Design Tool"
#: grc/gui_qt/components/window.py:538
msgid "Set Default &Qt GUI Theme"
msgstr ""
#: grc/gui_qt/components/window.py:541
#, fuzzy
#| msgid "module_browser"
msgid "&OOT Module Browser"
msgstr "OOT Module Browser"
#: grc/gui_qt/components/window.py:544
msgid "show_flowgraph_complexity"
msgstr "Show Flowgraph Complexity"
#: grc/gui_qt/components/window.py:550
msgid "&Types"
msgstr ""
#: grc/gui_qt/components/window.py:552
msgid "&Keys"
msgstr ""
#: grc/gui_qt/components/window.py:556
msgid "&Get Involved"
msgstr ""
#: grc/gui_qt/components/window.py:560
msgid "preferences"
msgstr "Pre&ferences"
#: grc/gui_qt/components/window.py:562
msgid "preferences-tooltip"
msgstr "Show GRC preferences"
#: grc/gui_qt/components/window.py:566
msgid "reload"
msgstr ""
#: grc/gui_qt/components/window.py:566
#, fuzzy
#| msgid "redo-tooltip"
msgid "reload-tooltip"
msgstr "Redo the last change"
#: grc/gui_qt/components/window.py:822
msgid "ready-message"
msgstr "Ready"
#~ msgid "zoom_in"
#~ msgstr "Zoom in"
#~ msgid "zoom_out"
#~ msgstr "Zoom out"
#~ msgid "zoom_reset"
#~ msgstr "Reset zoom level"
#~ msgid "clear"
#~ msgstr "Clear"
#~ msgid "clear-tooltip"
#~ msgstr "Clear the console"
#~ msgid "show-level"
#~ msgstr "Show message level"
#~ msgid "set_default_qt_gui_theme"
#~ msgstr "Set Default Qt GUI Theme"
#~ msgid "print"
#~ msgstr "&Print"
#~ msgid "print-tooltip"
#~ msgstr "Print the current flow graph"
#~ msgid "block-library-tooltip"
#~ msgstr "Show the block library "

View File

@ -0,0 +1,378 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-11-26 20:00+0300\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=CHARSET\n"
"Content-Transfer-Encoding: 8bit\n"
#: grc/gui_qt/components/window.py:76
msgid "window-title"
msgstr ""
#: grc/gui_qt/components/window.py:244
msgid "new"
msgstr ""
#: grc/gui_qt/components/window.py:247
msgid "new-tooltip"
msgstr ""
#: grc/gui_qt/components/window.py:252
msgid "open"
msgstr ""
#: grc/gui_qt/components/window.py:255
msgid "open-tooltip"
msgstr ""
#: grc/gui_qt/components/window.py:259
msgid "example_browser"
msgstr ""
#: grc/gui_qt/components/window.py:261
msgid "example_browser-tooltip"
msgstr ""
#: grc/gui_qt/components/window.py:266
msgid "close"
msgstr ""
#: grc/gui_qt/components/window.py:269
msgid "close-tooltip"
msgstr ""
#: grc/gui_qt/components/window.py:274
msgid "close_all"
msgstr ""
#: grc/gui_qt/components/window.py:276
msgid "close_all-tooltip"
msgstr ""
#: grc/gui_qt/components/window.py:280
msgid "save"
msgstr ""
#: grc/gui_qt/components/window.py:283
msgid "save-tooltip"
msgstr ""
#: grc/gui_qt/components/window.py:288
msgid "save_as"
msgstr ""
#: grc/gui_qt/components/window.py:291
msgid "save_as-tooltip"
msgstr ""
#: grc/gui_qt/components/window.py:294
msgid "save_copy"
msgstr ""
#: grc/gui_qt/components/window.py:298
msgid "screen_capture"
msgstr ""
#: grc/gui_qt/components/window.py:301
msgid "screen_capture-tooltip"
msgstr ""
#: grc/gui_qt/components/window.py:306
msgid "exit"
msgstr ""
#: grc/gui_qt/components/window.py:309
msgid "exit-tooltip"
msgstr ""
#: grc/gui_qt/components/window.py:315
msgid "undo"
msgstr ""
#: grc/gui_qt/components/window.py:318
msgid "undo-tooltip"
msgstr ""
#: grc/gui_qt/components/window.py:323
msgid "redo"
msgstr ""
#: grc/gui_qt/components/window.py:326
msgid "redo-tooltip"
msgstr ""
#: grc/gui_qt/components/window.py:333
msgid "cut"
msgstr ""
#: grc/gui_qt/components/window.py:336
msgid "cut-tooltip"
msgstr ""
#: grc/gui_qt/components/window.py:341
msgid "copy"
msgstr ""
#: grc/gui_qt/components/window.py:344
msgid "copy-tooltip"
msgstr ""
#: grc/gui_qt/components/window.py:349
msgid "paste"
msgstr ""
#: grc/gui_qt/components/window.py:352
msgid "paste-tooltip"
msgstr ""
#: grc/gui_qt/components/window.py:357
msgid "delete"
msgstr ""
#: grc/gui_qt/components/window.py:360
msgid "delete-tooltip"
msgstr ""
#: grc/gui_qt/components/window.py:372
msgid "select_all"
msgstr ""
#: grc/gui_qt/components/window.py:375
msgid "select_all-tooltip"
msgstr ""
#: grc/gui_qt/components/window.py:379
msgid "Select None"
msgstr ""
#: grc/gui_qt/components/window.py:379
msgid "select_none-tooltip"
msgstr ""
#: grc/gui_qt/components/window.py:384
msgid "rotate_ccw"
msgstr ""
#: grc/gui_qt/components/window.py:387
msgid "rotate_ccw-tooltip"
msgstr ""
#: grc/gui_qt/components/window.py:392
msgid "rotate_cw"
msgstr ""
#: grc/gui_qt/components/window.py:395
msgid "rotate_cw-tooltip"
msgstr ""
#: grc/gui_qt/components/window.py:401
msgid "enable"
msgstr ""
#: grc/gui_qt/components/window.py:402
msgid "disable"
msgstr ""
#: grc/gui_qt/components/window.py:403
msgid "bypass"
msgstr ""
#: grc/gui_qt/components/window.py:409
msgid "vertical_align_top"
msgstr ""
#: grc/gui_qt/components/window.py:410
msgid "vertical_align_middle"
msgstr ""
#: grc/gui_qt/components/window.py:411
msgid "vertical_align_bottom"
msgstr ""
#: grc/gui_qt/components/window.py:417
msgid "horizontal_align_left"
msgstr ""
#: grc/gui_qt/components/window.py:418
msgid "horizontal_align_center"
msgstr ""
#: grc/gui_qt/components/window.py:419
msgid "horizontal_align_right"
msgstr ""
#: grc/gui_qt/components/window.py:425
msgid "create_hier_block"
msgstr ""
#: grc/gui_qt/components/window.py:426
msgid "open_hier_block"
msgstr ""
#: grc/gui_qt/components/window.py:427
msgid "toggle_source_bus"
msgstr ""
#: grc/gui_qt/components/window.py:428
msgid "toggle_sink_bus"
msgstr ""
#: grc/gui_qt/components/window.py:437
msgid "flowgraph-properties"
msgstr ""
#: grc/gui_qt/components/window.py:439
msgid "flowgraph-properties-tooltip"
msgstr ""
#: grc/gui_qt/components/window.py:443
msgid "search-in-examples"
msgstr ""
#: grc/gui_qt/components/window.py:445
msgid "search-in-examples-tooltip"
msgstr ""
#: grc/gui_qt/components/window.py:453
msgid "Zoom &in"
msgstr ""
#: grc/gui_qt/components/window.py:459
msgid "Zoom &out"
msgstr ""
#: grc/gui_qt/components/window.py:465
msgid "O&riginal size"
msgstr ""
#: grc/gui_qt/components/window.py:470
msgid "snap_to_grid"
msgstr ""
#: grc/gui_qt/components/window.py:474
msgid "toggle_grid"
msgstr ""
#: grc/gui_qt/components/window.py:474
msgid "toggle_grid-tooltip"
msgstr ""
#: grc/gui_qt/components/window.py:478
msgid "errors"
msgstr ""
#: grc/gui_qt/components/window.py:478
msgid "errors-tooltip"
msgstr ""
#: grc/gui_qt/components/window.py:483
msgid "find"
msgstr ""
#: grc/gui_qt/components/window.py:486
msgid "find-tooltip"
msgstr ""
#: grc/gui_qt/components/window.py:491
msgid "about"
msgstr ""
#: grc/gui_qt/components/window.py:491 grc/gui_qt/components/window.py:498
msgid "about-tooltip"
msgstr ""
#: grc/gui_qt/components/window.py:496
msgid "about-qt"
msgstr ""
#: grc/gui_qt/components/window.py:503
msgid "process-generate"
msgstr ""
#: grc/gui_qt/components/window.py:506
msgid "process-generate-tooltip"
msgstr ""
#: grc/gui_qt/components/window.py:511
msgid "process-execute"
msgstr ""
#: grc/gui_qt/components/window.py:514
msgid "process-execute-tooltip"
msgstr ""
#: grc/gui_qt/components/window.py:519
msgid "process-kill"
msgstr ""
#: grc/gui_qt/components/window.py:522
msgid "process-kill-tooltip"
msgstr ""
#: grc/gui_qt/components/window.py:527
msgid "help"
msgstr ""
#: grc/gui_qt/components/window.py:530
msgid "help-tooltip"
msgstr ""
#: grc/gui_qt/components/window.py:535
msgid "&Filter Design Tool"
msgstr ""
#: grc/gui_qt/components/window.py:538
msgid "Set Default &Qt GUI Theme"
msgstr ""
#: grc/gui_qt/components/window.py:541
msgid "&OOT Module Browser"
msgstr ""
#: grc/gui_qt/components/window.py:544
msgid "show_flowgraph_complexity"
msgstr ""
#: grc/gui_qt/components/window.py:550
msgid "&Types"
msgstr ""
#: grc/gui_qt/components/window.py:552
msgid "&Keys"
msgstr ""
#: grc/gui_qt/components/window.py:556
msgid "&Get Involved"
msgstr ""
#: grc/gui_qt/components/window.py:560
msgid "preferences"
msgstr ""
#: grc/gui_qt/components/window.py:562
msgid "preferences-tooltip"
msgstr ""
#: grc/gui_qt/components/window.py:566
msgid "reload"
msgstr ""
#: grc/gui_qt/components/window.py:566
msgid "reload-tooltip"
msgstr ""
#: grc/gui_qt/components/window.py:822
msgid "ready-message"
msgstr ""

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

View File

@ -0,0 +1,21 @@
title: Example OOT Module
brief: GNU Radio components to do various things
tags:
- sdr
author:
- Håkon Vågsether
copyright_owner:
- Håkon Vågsether
license: GPLv3
gr_supported_version:
- v3.9
- v3.10
repo: https://github.com/haakov/gnuradio
website: https://www.example.org
#icon: <icon_url> # Put a URL to a square image here that will be used as an icon on CGRAN
---
This module contains blocks to do things.
Please se the [website](https://github.com/haakov/gnuradio) for more

View File

@ -0,0 +1,275 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Dialog</class>
<widget class="QDialog" name="Dialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>600</width>
<height>456</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="windowTitle">
<string>Dialog</string>
</property>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QListWidget" name="left_list">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Expanding">
<horstretch>1</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="maximumSize">
<size>
<width>125</width>
<height>16777215</height>
</size>
</property>
</widget>
</item>
<item>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="sizeConstraint">
<enum>QLayout::SetFixedSize</enum>
</property>
<item>
<widget class="QLabel" name="title_label">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="font">
<font>
<pointsize>16</pointsize>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="text">
<string>gr-example</string>
</property>
<property name="wordWrap">
<bool>false</bool>
</property>
<property name="textInteractionFlags">
<set>Qt::TextBrowserInteraction</set>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="brief_label">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="font">
<font>
<pointsize>11</pointsize>
</font>
</property>
<property name="text">
<string>Brief</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
<property name="textInteractionFlags">
<set>Qt::TextBrowserInteraction</set>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_3">
<item alignment="Qt::AlignTop">
<widget class="QLabel" name="icon">
<property name="maximumSize">
<size>
<width>100</width>
<height>100</height>
</size>
</property>
<property name="text">
<string/>
</property>
<property name="pixmap">
<pixmap>cgran_logo.png</pixmap>
</property>
<property name="scaledContents">
<bool>true</bool>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QLabel" name="version_label">
<property name="text">
<string>Version</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="tags_label">
<property name="text">
<string>Tags</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
<property name="textInteractionFlags">
<set>Qt::TextBrowserInteraction</set>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="author_label">
<property name="text">
<string>Author(s)</string>
</property>
<property name="textFormat">
<enum>Qt::MarkdownText</enum>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
<property name="textInteractionFlags">
<set>Qt::TextBrowserInteraction</set>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="dep_label">
<property name="text">
<string>Dependencies</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
<property name="textInteractionFlags">
<set>Qt::TextBrowserInteraction</set>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="repo_label">
<property name="text">
<string>Repository</string>
</property>
<property name="textFormat">
<enum>Qt::MarkdownText</enum>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
<property name="openExternalLinks">
<bool>true</bool>
</property>
<property name="textInteractionFlags">
<set>Qt::TextBrowserInteraction</set>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="copyright_label">
<property name="text">
<string>Copyright Owner</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
<property name="textInteractionFlags">
<set>Qt::TextBrowserInteraction</set>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="supp_ver_label">
<property name="text">
<string>Supported GNU Radio Versions</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
<property name="textInteractionFlags">
<set>Qt::TextBrowserInteraction</set>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="license_label">
<property name="text">
<string>License</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
<property name="textInteractionFlags">
<set>Qt::TextBrowserInteraction</set>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="website_label">
<property name="text">
<string>Website</string>
</property>
<property name="textFormat">
<enum>Qt::MarkdownText</enum>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
<property name="openExternalLinks">
<bool>true</bool>
</property>
<property name="textInteractionFlags">
<set>Qt::TextBrowserInteraction</set>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</item>
<item>
<widget class="QTextBrowser" name="desc_label">
<property name="enabled">
<bool>true</bool>
</property>
<property name="openExternalLinks">
<bool>true</bool>
</property>
<property name="openLinks">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</item>
</layout>
</widget>
<resources>
<include location="resource.qrc"/>
</resources>
<connections/>
</ui>

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

BIN
gui_qt/resources/py_fg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

260
main.py
View File

@ -1,23 +1,24 @@
# Copyright 2009-2016 Free Software Foundation, Inc.
#!/usr/bin/env python3
# Copyright 2009-2020 Free Software Foundation, Inc.
# This file is part of GNU Radio
#
# SPDX-License-Identifier: GPL-2.0-or-later
#
import argparse
import gettext
import locale
import logging
import logging.handlers
import os
import platform
import sys
import gi
gi.require_version('Gtk', '3.0')
gi.require_version('PangoCairo', '1.0')
from gi.repository import Gtk
VERSION_AND_DISCLAIMER_TEMPLATE = """\
GNU Radio Companion %s
This program is part of GNU Radio
GNU Radio Companion (%s) -
This program is part of GNU Radio.
GRC comes with ABSOLUTELY NO WARRANTY.
This is free software, and you are welcome to redistribute it.
"""
@ -30,56 +31,57 @@ LOG_LEVELS = {
'critical': logging.CRITICAL,
}
def main():
### Load GNU Radio
# Do this globally so it is available for both run_gtk() and run_qt()
try:
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()
except ImportError as ex:
# Throw a new exception with more information
print ("Cannot find GNU Radio! (Have you sourced the environment file?)", file=sys.stderr)
# Enable logging
# Note: All other modules need to use the same '<prefix>.<module>' convention.
# This should automatically be handled by logging.getLogger(__name__), but
# the prefix can change depending on how grc is launched (either by calling
# gnuradio-companion once installed or python -m grc). The prefix for main.py
# should be the same as other modules, so use that dynamically.
# Strip '.main' from the module name to get the prefix
logger_prefix = __name__[0:__name__.rindex('.')]
log = logging.getLogger(logger_prefix)
# If this is a background session (not launched through a script), show a Tkinter error dialog.
# Tkinter should already be installed default with Python, so this shouldn't add new dependencies
if not sys.stdin.isatty():
import tkinter
from tkinter import messagebox
# Hide the main window
root = tkinter.Tk()
root.withdraw()
# Show the error dialog
# TODO: Have a more helpful dialog here. Maybe a link to the wiki pages?
messagebox.showerror("Cannot find GNU Radio", "Cannot find GNU Radio!")
# NOTE: This sets the log level to what was requested for the logger on the
# command line, but this may not be the correct approach if multiple handlers
# are intended to be used. The logger level shown here indicates all the log
# messages that are captured and the handler levels indicate messages each
# handler will output. A better approach may be resetting this to logging.DEBUG
# to catch everything and making sure the handlers have the correct levels set.
# This would be useful for a future GUI logging window that can filter messages
# independently of the console output. In this case, this should be DEBUG.
log.setLevel(LOG_LEVELS[args.log])
# Throw the new exception
raise Exception("Cannot find GNU Radio!") from None
# 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)
### Enable Logging
# Do this globally so it is available for both run_gtk() and run_qt()
# TODO: Advanced logging - https://docs.python.org/3/howto/logging-cookbook.html#formatting-styles
# Note: All other modules need to use the 'grc.<module>' convention
log = logging.getLogger('grc')
# Set the root log name
# Since other files are in the 'grc' module, they automatically get a child logger when using:
# log = logging.getLogger(__name__)
# This log level should be set to DEBUG so the logger itself catches everything.
# The StreamHandler level can be set independently to choose what messages are sent to the console.
# The default console logging should be WARNING
log.setLevel(logging.DEBUG)
#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))
def run_gtk(args, log):
''' Runs the GTK version of GNU Radio Companion '''
import gi
gi.require_version('Gtk', '3.0')
gi.require_version('PangoCairo', '1.0')
from gi.repository import Gtk
# Delay importing until the logging is setup
from .gui.Platform import Platform
from .gui.Application import Application
# The platform is loaded differently between QT and GTK, so this is required both places
log.debug("Loading platform")
platform = Platform(
version=gr.version(),
@ -94,3 +96,167 @@ def main():
app = Application(args.flow_graphs, platform)
log.debug("Running")
sys.exit(app.run())
def run_qt(args, log):
''' Runs the Qt version of GNU Radio Companion '''
import platform
import locale
import gettext
from .gui_qt import grc
from .gui_qt import helpers
from .gui_qt import properties
# Delay importing until the logging is setup
from .gui_qt.Platform import Platform
''' Global Settings/Constants '''
# Initialize a class with all of the default settings and properties
# TODO: Move argv to separate argument parsing class that overrides default properties?
# TODO: Split settings/constants into separate classes rather than a single properites class?
settings = properties.Properties(sys.argv)
''' Translation Support '''
# Try to get the current locale. Always add English
lc, encoding = locale.getdefaultlocale()
if lc:
languages = [lc]
languages += settings.DEFAULT_LANGUAGE
log.debug("Using locale - %s" % str(languages))
# Still run even if the english translation isn't found
language = gettext.translation(settings.APP_NAME, settings.path.LANGUAGE, languages=languages,
fallback=True)
if type(language) == gettext.NullTranslations:
log.error("Unable to find any translation")
log.error("Default English translation missing")
else:
log.info("Using translation - %s" % language.info()["language"])
# Still need to install null translation to let the system handle calls to _()
language.install()
''' OS Platform '''
# Figure out system specific properties and setup defaults.
# Some properties can be overridden by preferences
# Get the current OS
if platform.system() == "Linux":
log.debug("Detected Linux")
settings.system.OS = "Linux"
# Determine if Unity is running....
try:
#current_desktop = os.environ['DESKTOP_SESSION']
current_desktop = os.environ['XDG_CURRENT_DESKTOP']
log.debug("Desktop Session - %s" % current_desktop)
if current_desktop == "Unity":
log.debug("Detected GRC is running under unity")
# Use the native menubar rather than leaving it in the window
settings.window.NATIVE_MENUBAR = True
except:
log.warning("Unable to determine the Linux desktop system")
elif platform.system() == "Darwin":
log.debug("Detected Mac OS X")
settings.system.OS = "OS X"
# Setup Mac specific QT elements
settings.window.NATIVE_MENUBAR = True
elif platform.system() == "Windows":
log.warning("Detected Windows")
settings.system.OS = "Windows"
else:
log.warning("Unknown operating system")
''' Preferences '''
# TODO: Move earlier? Need to load user preferences and override the default properties/settings
# The platform is loaded differently between QT and GTK, so this is required both places
log.debug("Loading platform")
# TODO: Might be beneficial to rename Platform to avoid confusion with the builtin Python module
# Possible names: internal, model?
model = Platform(
version=gr.version(),
version_parts=(gr.major_version(), gr.api_version(), gr.minor_version()),
prefs=gr.prefs(),
install_prefix=gr.prefix()
)
model.build_library()
# Launch GRC
app = grc.Application(settings, model)
sys.exit(app.run())
def main():
grc_version_from_config = ""
grc_qt_config_file = os.path.expanduser('~/.gnuradio/grc_qt.conf')
if os.path.isfile(grc_qt_config_file):
try:
from qtpy.QtCore import QSettings
qsettings = QSettings(grc_qt_config_file, QSettings.IniFormat)
grc_version_from_config = qsettings.value('grc/default_grc', "", type=str)
except Exception as e:
log.warning("main.py could not read grc_qt.conf")
log.warning(e)
### Argument parsing
parser = argparse.ArgumentParser(
description=VERSION_AND_DISCLAIMER_TEMPLATE % gr.version())
parser.add_argument('flow_graphs', nargs='*')
# Custom Configurations
# TODO: parser.add_argument('--config')
# Logging support
parser.add_argument('--log', choices=['debug', 'info', 'warning', 'error', 'critical'], default='info')
parser.add_argument('--wiki', action='store_true')
# TODO: parser.add_argument('--log-output')
# Graphics framework (QT or GTK)
gui_group = parser.add_argument_group('Framework')
gui_group_exclusive = gui_group.add_mutually_exclusive_group()
gui_group_exclusive.add_argument("--qt", dest='framework', action='store_const', const='qt',
help="GNU Radio Companion (QT)")
gui_group_exclusive.add_argument("--gtk", dest='framework', action='store_const', const='gtk',
help="GNU Radio Companion (GTK)")
# Default options if not already set with add_argument()
args = parser.parse_args()
# Print the startup message
py_version = sys.version.split()[0]
log.info("Starting GNU Radio Companion {} (Python {})".format(gr.version(), py_version))
# File logging
log_file = os.path.expanduser('~') + "/.gnuradio/grc.log"
try:
fileHandler = logging.FileHandler(log_file)
file_msg_format = '%(asctime)s [%(levelname)s] %(message)s'
if args.log == 'debug':
file_msg_format += ' (%(name)s:%(lineno)s)'
fileHandler.setLevel(logging.DEBUG)
log.info(f'Logging to {log_file} (DEBUG and higher)')
else:
fileHandler.setLevel(logging.INFO)
log.info(f'Logging to {log_file} (INFO and higher)')
file_formatter = logging.Formatter(file_msg_format)
fileHandler.setFormatter(file_formatter)
log.addHandler(fileHandler)
except (PermissionError, FileNotFoundError) as e:
log.error(f'Cannot write to {log_file} - {e}')
### GUI Framework
if args.framework == 'qt':
run_qt(args, log)
elif args.framework == 'gtk':
run_gtk(args, log)
else: # args.framework == None
if grc_version_from_config == 'grc_qt':
run_qt(args, log)
else:
run_gtk(args, log)

2
tests/pytest.ini Normal file
View File

@ -0,0 +1,2 @@
[pytest]
addopts = --ignore=test_qtbot.py

891
tests/test_qtbot.py Normal file
View File

@ -0,0 +1,891 @@
import pytest
import gettext
import locale
import threading
import sys
from pytestqt.plugin import qapp
import time
import pyautogui as pag
import logging
from qtpy import QtTest, QtCore, QtGui, QtWidgets, QT6
from os import path, remove
from gnuradio import gr
from grc.gui_qt import properties
from grc.gui_qt.grc import Application
from grc.gui_qt.components.window import MainWindow
from grc.gui_qt.Platform import Platform
log = logging.getLogger("grc")
@pytest.fixture(scope="session")
def qapp_cls_():
settings = properties.Properties([])
settings.argv = [""]
""" Translation Support """
# Try to get the current locale. Always add English
lc, encoding = locale.getlocale()
if lc:
languages = [lc]
languages += settings.DEFAULT_LANGUAGE
log.debug("Using locale - %s" % str(languages))
# Still run even if the english translation isn't found
language = gettext.translation(
settings.APP_NAME, settings.path.LANGUAGE, languages=languages, fallback=True
)
if type(language) == gettext.NullTranslations:
log.error("Unable to find any translation")
log.error("Default English translation missing")
else:
log.info("Using translation - %s" % language.info()["language"])
# Still need to install null translation to let the system handle calls to _()
language.install()
model = Platform(
version=gr.version(),
version_parts=(gr.major_version(), gr.api_version(), gr.minor_version()),
prefs=gr.prefs(),
install_prefix=gr.prefix(),
)
model.build_library()
app = Application(settings, model)
app.MainWindow.showMaximized()
return app
def global_pos(block, view):
scene_pos = block.mapToScene(block.boundingRect().center())
global_pos = view.viewport().mapToGlobal(view.mapFromScene(scene_pos))
return global_pos
def type_text(qtbot, app, keys):
for key in keys:
# Each sequence contains a single key.
# That's why we use the first element
keycode = QtGui.QKeySequence(key)[0]
if QT6:
qtbot.keyClick(app.focusWidget(), keycode.key(), QtCore.Qt.NoModifier)
else:
qtbot.keyClick(app.focusWidget(), keycode, QtCore.Qt.NoModifier)
def keystroke(qtbot, app, key):
qtbot.keyClick(app.focusWidget(), key, QtCore.Qt.NoModifier)
qtbot.wait(100)
def ctrl_keystroke(qtbot, app, key):
qtbot.keyClick(app.focusWidget(), key, QtCore.Qt.ControlModifier)
qtbot.wait(100)
def gather_menu_items(menu):
ret = {}
for act in menu.actions():
ret[act.text()] = act
return ret
def add_block_from_query(qtbot, app, query):
qtbot.keyClick(app.focusWidget(), QtCore.Qt.Key_F, QtCore.Qt.ControlModifier)
type_text(qtbot, app, query)
keystroke(qtbot, app, QtCore.Qt.Key_Enter)
def find_blocks(flowgraph, block_type):
blocks = []
for b in flowgraph.blocks:
if b.key == block_type:
blocks.append(b)
if len(blocks) == 0:
return None
if len(blocks) == 1:
return blocks[0]
return blocks
def click_on(qtbot, app, item, button="left"):
scaling = app.MainWindow.screen().devicePixelRatio()
view = app.MainWindow.currentView
click_pos = scaling * global_pos(item.gui, view)
pag.click(click_pos.x(), click_pos.y(), button=button)
qtbot.wait(100)
def undo(qtbot, app):
qtbot.keyClick(app.focusWidget(), QtCore.Qt.Key_Z, QtCore.Qt.ControlModifier)
qtbot.wait(100)
def redo(qtbot, app):
qtbot.keyClick(
app.focusWidget(),
QtCore.Qt.Key_Z,
QtCore.Qt.ControlModifier | QtCore.Qt.ShiftModifier,
)
qtbot.wait(100)
def delete_block(qtbot, app, block):
view = app.MainWindow.currentView
scaling = app.MainWindow.screen().devicePixelRatio()
click_pos = scaling * global_pos(block.gui, view)
pag.click(click_pos.x(), click_pos.y(), button="left")
qtbot.wait(100)
qtbot.keyClick(app.focusWidget(), QtCore.Qt.Key_Delete)
qtbot.wait(100)
def menu_shortcut(qtbot, app, menu_name, menu_key, shortcut_key):
menu = app.MainWindow.menus[menu_name]
qtbot.keyClick(app.focusWidget(), menu_key, QtCore.Qt.AltModifier)
qtbot.wait(100)
qtbot.keyClick(menu, shortcut_key)
qtbot.wait(100)
# Start by closing the flowgraph that pops up on start
def test_file_close_init(qtbot, qapp_cls_, monkeypatch):
win = qapp_cls_.MainWindow
monkeypatch.setattr(
QtWidgets.QMessageBox,
"question",
lambda *args: QtWidgets.QMessageBox.Discard,
)
qtbot.wait(100)
assert win.tabWidget.count() == 1
menu_shortcut(qtbot, qapp_cls_, "file", QtCore.Qt.Key_F, QtCore.Qt.Key_L)
assert win.tabWidget.count() == 1
def test_delete_block(qtbot, qapp_cls_):
qtbot.wait(100)
var = find_blocks(qapp_cls_.MainWindow.currentFlowgraph, "variable")
assert var is not None
delete_block(qtbot, qapp_cls_, var)
qtbot.wait(100)
assert len(qapp_cls_.MainWindow.currentFlowgraph.blocks) == 1
undo(qtbot, qapp_cls_)
assert len(qapp_cls_.MainWindow.currentFlowgraph.blocks) == 2
def test_add_null_sink(qtbot, qapp_cls_):
qtbot.wait(100)
add_block_from_query(qtbot, qapp_cls_, "null sin")
qtbot.wait(100)
n_sink = find_blocks(qapp_cls_.MainWindow.currentFlowgraph, "blocks_null_sink")
assert n_sink is not None
delete_block(qtbot, qapp_cls_, n_sink)
def test_add_null_source(qtbot, qapp_cls_):
qtbot.wait(100)
add_block_from_query(qtbot, qapp_cls_, "null sou")
n_sou = find_blocks(qapp_cls_.MainWindow.currentFlowgraph, "blocks_null_source")
assert n_sou is not None
delete_block(qtbot, qapp_cls_, n_sou)
def test_add_throttle(qtbot, qapp_cls_):
qtbot.wait(100)
add_block_from_query(qtbot, qapp_cls_, "throttle")
throttle = find_blocks(qapp_cls_.MainWindow.currentFlowgraph, "blocks_throttle")
assert throttle is not None
delete_block(qtbot, qapp_cls_, throttle)
def test_right_click(qtbot, qapp_cls_):
qtbot.wait(100)
add_block_from_query(qtbot, qapp_cls_, "throttle")
throttle = find_blocks(qapp_cls_.MainWindow.currentFlowgraph, "blocks_throttle")
assert throttle is not None
qtbot.wait(100)
def close():
qtbot.keyClick(throttle.gui.right_click_menu, QtCore.Qt.Key_Escape)
QtCore.QTimer.singleShot(200, close)
click_on(qtbot, qapp_cls_, throttle, button="right")
qtbot.wait(100)
delete_block(qtbot, qapp_cls_, throttle)
def test_errors(qtbot, qapp_cls_):
menu = qapp_cls_.MainWindow.menus["build"]
def assert_and_close():
assert qapp_cls_.activeWindow() != qapp_cls_.MainWindow
qtbot.keyClick(qapp_cls_.activeWindow(), QtCore.Qt.Key_Escape)
qtbot.wait(100)
add_block_from_query(qtbot, qapp_cls_, "throttle")
qtbot.wait(100)
qtbot.keyClick(qapp_cls_.focusWidget(), QtCore.Qt.Key_B, QtCore.Qt.AltModifier)
qtbot.wait(100)
QtCore.QTimer.singleShot(200, assert_and_close)
#qtbot.keyClick(menu, QtCore.Qt.Key_E) # Not necessary since it's already selected (it's the first item)
qtbot.keyClick(menu, QtCore.Qt.Key_Enter)
qtbot.wait(300)
throttle = find_blocks(qapp_cls_.MainWindow.currentFlowgraph, "blocks_throttle")
assert throttle is not None
delete_block(qtbot, qapp_cls_, throttle)
def test_open_properties(qtbot, qapp_cls_):
qtbot.wait(100)
qtbot.mouseDClick(
qapp_cls_.MainWindow.currentView.viewport(),
QtCore.Qt.LeftButton,
pos=qapp_cls_.MainWindow.currentView.mapFromScene(
qapp_cls_.MainWindow.currentFlowgraph.options_block.gui.pos()
+ QtCore.QPointF(15.0, 15.0)
),
)
qtbot.wait(100)
assert qapp_cls_.MainWindow.currentFlowgraph.options_block.gui.props_dialog.isVisible()
keystroke(qtbot, qapp_cls_, QtCore.Qt.Key_Escape)
assert (
not qapp_cls_.MainWindow.currentFlowgraph.options_block.gui.props_dialog.isVisible()
)
def test_change_id(qtbot, qapp_cls_):
opts = find_blocks(qapp_cls_.MainWindow.currentFlowgraph, "options")
assert opts.params["title"].value == "Not titled yet"
qtbot.mouseDClick(
qapp_cls_.MainWindow.currentView.viewport(),
QtCore.Qt.LeftButton,
pos=qapp_cls_.MainWindow.currentView.mapFromScene(
opts.gui.pos() + QtCore.QPointF(15.0, 15.0)
),
)
qtbot.wait(100)
qtbot.mouseDClick(
opts.gui.props_dialog.edit_params[1],
QtCore.Qt.LeftButton,
)
type_text(qtbot, qapp_cls_, "changed")
qtbot.wait(100)
keystroke(qtbot, qapp_cls_, QtCore.Qt.Key_Enter)
assert opts.params["title"].value == "Not titled changed"
qtbot.wait(100)
undo(qtbot, qapp_cls_)
assert opts.params["title"].value == "Not titled yet"
redo(qtbot, qapp_cls_)
assert opts.params["title"].value == "Not titled changed"
def test_rotate_block(qtbot, qapp_cls_):
qtbot.wait(100)
opts = find_blocks(qapp_cls_.MainWindow.currentFlowgraph, "options")
# Still running into issues with what has focus depending on the order of tests run.
# This is a problem! Tests should be able to run independently without affecting other
# tests. Need to track this down, but for those tests that are failing for this reason,
# something like below seems to be a workaround
click_on(qtbot, qapp_cls_, opts)
qtbot.wait(400)
click_on(qtbot, qapp_cls_, opts)
old_rotation = opts.states["rotation"]
keystroke(qtbot, qapp_cls_, QtCore.Qt.Key_Left)
new_rotation = opts.states["rotation"]
assert new_rotation == old_rotation - 90
undo(qtbot, qapp_cls_)
new_rotation = opts.states["rotation"]
assert new_rotation == old_rotation
keystroke(qtbot, qapp_cls_, QtCore.Qt.Key_Right)
new_rotation = opts.states["rotation"]
assert new_rotation == old_rotation + 90
undo(qtbot, qapp_cls_)
new_rotation = opts.states["rotation"]
assert new_rotation == old_rotation
def test_disable_enable(qtbot, qapp_cls_):
qtbot.wait(100)
var = find_blocks(qapp_cls_.MainWindow.currentFlowgraph, "variable")
click_on(qtbot, qapp_cls_, var)
assert var is not None
assert var.state == "enabled"
keystroke(qtbot, qapp_cls_, QtCore.Qt.Key_D)
assert var.state == "disabled"
keystroke(qtbot, qapp_cls_, QtCore.Qt.Key_E)
assert var.state == "enabled"
def test_move_blocks(qtbot, qapp_cls_):
fg = qapp_cls_.MainWindow.currentFlowgraph
view = qapp_cls_.MainWindow.currentView
scaling = qapp_cls_.MainWindow.screen().devicePixelRatio()
qtbot.wait(100)
add_block_from_query(qtbot, qapp_cls_, "throttle")
throttle = find_blocks(fg, "blocks_throttle")
variable = find_blocks(fg, "variable")
assert throttle is not None
click_on(qtbot, qapp_cls_, variable)
qtbot.wait(100)
start_throttle = scaling * global_pos(throttle.gui, view)
start_variable = scaling * global_pos(variable.gui, view)
pag.moveTo(start_throttle.x(), start_throttle.y())
pag.mouseDown()
def drag():
for i in range(20):
pag.move(0, scaling * 10)
drag_t = threading.Thread(target=drag)
drag_t.start()
while drag_t.is_alive():
qtbot.wait(50)
pag.mouseUp()
qtbot.wait(100)
assert scaling * global_pos(throttle.gui, view) != start_throttle
undo(qtbot, qapp_cls_)
assert scaling * global_pos(throttle.gui, view) == start_throttle
redo(qtbot, qapp_cls_)
assert scaling * global_pos(throttle.gui, view) != start_throttle
# Variable shouldn't move
assert scaling * global_pos(variable.gui, view) == start_variable
delete_block(qtbot, qapp_cls_, throttle)
def test_connection(qtbot, qapp_cls_):
fg = qapp_cls_.MainWindow.currentFlowgraph
view = qapp_cls_.MainWindow.currentView
scaling = qapp_cls_.MainWindow.screen().devicePixelRatio()
qtbot.wait(100)
for block in ["null sou", "null sin"]:
add_block_from_query(qtbot, qapp_cls_, block)
n_src = find_blocks(fg, "blocks_null_source")
n_sink = find_blocks(fg, "blocks_null_sink")
assert len(fg.connections) == 0
start = scaling * global_pos(n_sink.gui, view)
pag.moveTo(start.x(), start.y())
pag.mouseDown()
def drag():
for i in range(20):
pag.move(scaling * 10, 0)
drag_t = threading.Thread(target=drag)
drag_t.start()
while drag_t.is_alive():
qtbot.wait(50)
pag.mouseUp()
click_on(qtbot, qapp_cls_, n_src.sources[0])
click_on(qtbot, qapp_cls_, n_sink.sinks[0])
assert len(fg.connections) == 1
undo(qtbot, qapp_cls_)
assert len(fg.connections) == 0
redo(qtbot, qapp_cls_)
assert len(fg.connections) == 1
for block in [n_src, n_sink]:
delete_block(qtbot, qapp_cls_, block)
def test_num_inputs(qtbot, qapp_cls_):
fg = qapp_cls_.MainWindow.currentFlowgraph
view = qapp_cls_.MainWindow.currentView
scaling = qapp_cls_.MainWindow.screen().devicePixelRatio()
qtbot.wait(100)
for block in ["null sou", "null sin"]:
add_block_from_query(qtbot, qapp_cls_, block)
n_src = find_blocks(fg, "blocks_null_source")
n_sink = find_blocks(fg, "blocks_null_sink")
assert len(n_sink.sinks) == 1
start = scaling * global_pos(n_sink.gui, view)
pag.moveTo(start.x(), start.y())
pag.mouseDown()
def drag():
for i in range(20):
pag.move(scaling * 10, 0)
drag_t = threading.Thread(target=drag)
drag_t.start()
while drag_t.is_alive():
qtbot.wait(50)
pag.mouseUp()
click_on(qtbot, qapp_cls_, n_src.sources[0])
click_on(qtbot, qapp_cls_, n_sink.sinks[0])
qtbot.wait(100)
click_pos = scaling * global_pos(n_sink.gui, view)
pag.doubleClick(click_pos.x(), click_pos.y(), button="left")
qtbot.wait(100)
param_index = 0
for i in range(len(n_sink.gui.props_dialog.edit_params)):
if n_sink.gui.props_dialog.edit_params[i].param.key == 'num_inputs':
param_index = i
qtbot.mouseDClick(n_sink.gui.props_dialog.edit_params[param_index], QtCore.Qt.LeftButton)
type_text(qtbot, qapp_cls_, "2")
qtbot.wait(100)
keystroke(qtbot, qapp_cls_, QtCore.Qt.Key_Enter)
assert len(n_sink.sinks) == 2
assert len(fg.connections) == 1
click_pos = scaling * global_pos(n_sink.gui, view)
pag.doubleClick(click_pos.x(), click_pos.y(), button="left")
qtbot.wait(100)
qtbot.mouseDClick(n_sink.gui.props_dialog.edit_params[param_index], QtCore.Qt.LeftButton)
type_text(qtbot, qapp_cls_, "1")
qtbot.wait(100)
keystroke(qtbot, qapp_cls_, QtCore.Qt.Key_Enter)
qtbot.wait(100)
assert len(n_sink.sinks) == 1
assert len(fg.connections) == 1
# I think loses focus makes delete_fail the first time. This makes it work, but is a hack
#click_on(qtbot, qapp_cls_, n_src)
pag.click(click_pos.x()+50, click_pos.y()+50, button="left")
for block in [n_src, n_sink]:
delete_block(qtbot, qapp_cls_, block)
qtbot.wait(100)
assert len(fg.blocks) == 2
def test_bus(qtbot, qapp_cls_):
fg = qapp_cls_.MainWindow.currentFlowgraph
view = qapp_cls_.MainWindow.currentView
scaling = qapp_cls_.MainWindow.screen().devicePixelRatio()
qtbot.wait(100)
add_block_from_query(qtbot, qapp_cls_, "null sin")
n_sink = find_blocks(fg, "blocks_null_sink")
assert len(n_sink.sinks) == 1
click_pos = scaling * global_pos(n_sink.gui, view)
pag.doubleClick(click_pos.x(), click_pos.y(), button="left")
qtbot.wait(100)
param_index = 0
for i in range(len(n_sink.gui.props_dialog.edit_params)):
if n_sink.gui.props_dialog.edit_params[i].param.key == 'num_inputs':
param_index = i
qtbot.mouseDClick(n_sink.gui.props_dialog.edit_params[param_index], QtCore.Qt.LeftButton)
type_text(qtbot, qapp_cls_, "2")
qtbot.wait(100)
keystroke(qtbot, qapp_cls_, QtCore.Qt.Key_Enter)
qtbot.wait(100)
assert len(n_sink.sinks) == 2
# Enable bus port
qtbot.wait(100)
more_menu = qapp_cls_.MainWindow.menus["more"]
menu_shortcut(qtbot, qapp_cls_, "edit", QtCore.Qt.Key_E, QtCore.Qt.Key_M)
qtbot.wait(100)
qtbot.keyClick(more_menu, QtCore.Qt.Key_Up)
qtbot.wait(100)
qtbot.keyClick(more_menu, QtCore.Qt.Key_Enter)
qtbot.wait(100)
assert len(n_sink.sinks) == 3
assert n_sink.sinks[2].dtype == 'bus'
# Disable bus port
qtbot.wait(100)
more_menu = qapp_cls_.MainWindow.menus["more"]
menu_shortcut(qtbot, qapp_cls_, "edit", QtCore.Qt.Key_E, QtCore.Qt.Key_M)
qtbot.wait(100)
qtbot.keyClick(more_menu, QtCore.Qt.Key_Up)
qtbot.wait(100)
qtbot.keyClick(more_menu, QtCore.Qt.Key_Enter)
qtbot.wait(100)
assert len(n_sink.sinks) == 2
# Test undo
undo(qtbot, qapp_cls_)
qtbot.wait(100)
assert len(n_sink.sinks) == 3
qtbot.wait(100)
undo(qtbot, qapp_cls_)
qtbot.wait(100)
assert len(n_sink.sinks) == 2
delete_block(qtbot, qapp_cls_, n_sink)
qtbot.wait(100)
def test_bypass(qtbot, qapp_cls_):
scaling = qapp_cls_.MainWindow.screen().devicePixelRatio()
fg = qapp_cls_.MainWindow.currentFlowgraph
view = qapp_cls_.MainWindow.currentView
qtbot.wait(100)
for block in ["null sou", "throttle"]:
add_block_from_query(qtbot, qapp_cls_, block)
n_src = find_blocks(fg, "blocks_null_source")
throttle = find_blocks(fg, "blocks_throttle")
# Bypass the throttle block
click_on(qtbot, qapp_cls_, throttle)
keystroke(qtbot, qapp_cls_, QtCore.Qt.Key_B)
assert throttle.state == "bypassed"
undo(qtbot, qapp_cls_)
assert throttle.state == "enabled"
redo(qtbot, qapp_cls_)
assert throttle.state == "bypassed"
qtbot.wait(100)
keystroke(qtbot, qapp_cls_, QtCore.Qt.Key_E)
assert throttle.state == "enabled"
# Try to bypass the null source, this shouldn't work
click_on(qtbot, qapp_cls_, n_src)
keystroke(qtbot, qapp_cls_, QtCore.Qt.Key_B)
assert n_src.state == "enabled"
for block in [throttle, n_src]:
delete_block(qtbot, qapp_cls_, block)
def test_file_save(qtbot, qapp_cls_, monkeypatch, tmp_path):
fg_path = tmp_path / "test_save.grc"
monkeypatch.setattr(
QtWidgets.QFileDialog, "getSaveFileName", lambda *args, **kargs: (fg_path, "")
)
assert not fg_path.exists(), "File/Save (setup): File already exists"
ctrl_keystroke(qtbot, qapp_cls_, QtCore.Qt.Key_S)
assert fg_path.exists(), "File/Save: Could not save file"
def test_file_save_as(qtbot, qapp_cls_, monkeypatch, tmp_path):
fg_path = tmp_path / "test.grc"
monkeypatch.setattr(
QtWidgets.QFileDialog, "getSaveFileName", lambda *args, **kargs: (fg_path, "")
)
qtbot.wait(100)
menu_shortcut(qtbot, qapp_cls_, "file", QtCore.Qt.Key_F, QtCore.Qt.Key_A)
assert fg_path.exists()
def test_file_save_copy(qtbot, qapp_cls_, monkeypatch, tmp_path):
fg_path = tmp_path / "test_copy.grc"
monkeypatch.setattr(
QtWidgets.QFileDialog, "getSaveFileName", lambda *args, **kargs: (fg_path, "")
)
qtbot.wait(100)
assert not fg_path.exists(), "File/Save Copy (setup): File already exists"
menu_shortcut(qtbot, qapp_cls_, "file", QtCore.Qt.Key_F, QtCore.Qt.Key_Y)
assert fg_path.exists(), "File/Save Copy: Could not save file"
# TODO: File/Open
@pytest.mark.xfail()
def test_file_screen_capture_pdf(qtbot, qapp_cls_, monkeypatch, tmp_path):
fg_path = tmp_path / "test.pdf"
monkeypatch.setattr(
QtWidgets.QFileDialog, "getSaveFileName", lambda *args, **kargs: (fg_path, "")
)
qtbot.wait(100)
assert not fg_path.exists(), "File/Screen Capture (setup): PDF already exists"
ctrl_keystroke(qtbot, qapp_cls_, QtCore.Qt.Key_P)
assert fg_path.exists(), "File/Screen Capture: Could not create PDF"
def test_file_screen_capture_png(qtbot, qapp_cls_, monkeypatch, tmp_path):
fg_path = tmp_path / "test.png"
assert not fg_path.exists()
monkeypatch.setattr(
QtWidgets.QFileDialog, "getSaveFileName", lambda *args, **kargs: (fg_path, "")
)
qtbot.wait(100)
assert not fg_path.exists(), "File/Screen Capture (setup): PNG already exists"
ctrl_keystroke(qtbot, qapp_cls_, QtCore.Qt.Key_P)
assert fg_path.exists(), "File/Screen Capture: Could not create PNG"
def test_file_screen_capture_svg(qtbot, qapp_cls_, monkeypatch, tmp_path):
fg_path = tmp_path / "test.svg"
assert not fg_path.exists()
monkeypatch.setattr(
QtWidgets.QFileDialog, "getSaveFileName", lambda *args, **kargs: (fg_path, "")
)
qtbot.wait(100)
assert not fg_path.exists(), "File/Screen Capture (setup): SVG already exists"
ctrl_keystroke(qtbot, qapp_cls_, QtCore.Qt.Key_P)
assert fg_path.exists(), "File/Screen Capture: Could not create SVG"
def test_file_preferences(qtbot, qapp_cls_):
menu = qapp_cls_.MainWindow.menus["file"]
items = gather_menu_items(menu)
def assert_and_close():
assert qapp_cls_.activeWindow() != qapp_cls_.MainWindow
qtbot.keyClick(qapp_cls_.activeWindow(), QtCore.Qt.Key_Enter)
qtbot.keyClick(qapp_cls_.focusWidget(), QtCore.Qt.Key_F, QtCore.Qt.AltModifier)
qtbot.wait(100)
QtCore.QTimer.singleShot(200, assert_and_close)
qtbot.keyClick(menu, QtCore.Qt.Key_F)
qtbot.wait(600)
assert qapp_cls_.activeWindow() == qapp_cls_.MainWindow
qtbot.wait(100)
def test_file_examples(qtbot, qapp_cls_):
menu = qapp_cls_.MainWindow.menus["file"]
items = gather_menu_items(menu)
def assert_and_close():
assert qapp_cls_.activeWindow() != qapp_cls_.MainWindow
qtbot.keyClick(qapp_cls_.activeWindow(), QtCore.Qt.Key_Escape)
qtbot.keyClick(qapp_cls_.focusWidget(), QtCore.Qt.Key_F, QtCore.Qt.AltModifier)
qtbot.wait(100)
QtCore.QTimer.singleShot(200, assert_and_close)
qtbot.keyClick(menu, QtCore.Qt.Key_E)
qtbot.wait(600)
assert qapp_cls_.activeWindow() == qapp_cls_.MainWindow
qtbot.wait(100)
def test_edit_actions(qtbot, qapp_cls_):
pass
def test_edit_select_all(qtbot, qapp_cls_):
qtbot.keyClick(qapp_cls_.focusWidget(), QtCore.Qt.Key_A, QtCore.Qt.ControlModifier)
qtbot.wait(100)
def test_edit_cut_paste(qtbot, qapp_cls_):
fg = qapp_cls_.MainWindow.currentFlowgraph
qtbot.wait(100)
var = find_blocks(fg, "variable")
assert var is not None, "Edit/Cut and paste (setup): Could not find variable block"
click_on(qtbot, qapp_cls_, var)
ctrl_keystroke(qtbot, qapp_cls_, QtCore.Qt.Key_X)
qtbot.wait(100)
var = find_blocks(fg, "variable")
assert var is None, "Edit/Cut and paste: Could not cut variable block"
ctrl_keystroke(qtbot, qapp_cls_, QtCore.Qt.Key_V)
qtbot.wait(100)
var = find_blocks(fg, "variable")
assert var is not None, "Edit/Cut and paste: Could not paste variable block"
qtbot.wait(100)
def test_edit_copy_paste(qtbot, qapp_cls_):
fg = qapp_cls_.MainWindow.currentFlowgraph
qtbot.wait(100)
var = find_blocks(fg, "variable")
assert var is not None, "Edit/Copy and paste (setup): Could not find variable block"
click_on(qtbot, qapp_cls_, var)
ctrl_keystroke(qtbot, qapp_cls_, QtCore.Qt.Key_C)
qtbot.wait(100)
ctrl_keystroke(qtbot, qapp_cls_, QtCore.Qt.Key_V)
vars = find_blocks(fg, "variable")
assert isinstance(vars, list), "Edit/Copy and paste: Could not paste variable block"
assert len(vars) == 2, "Edit/Copy and paste: Could not paste variable block"
assert (
vars[0].name != vars[1].name
), "Edit/Copy and paste: Newly pasted variable block's ID is the same as the original block's ID"
delete_block(qtbot, qapp_cls_, vars[1])
def test_view_actions(qtbot, qapp_cls_):
pass
def test_build_actions(qtbot, qapp_cls_):
pass
def test_tools_actions(qtbot, qapp_cls_):
pass
def test_tools_oot_browser(qtbot, qapp_cls_):
menu = qapp_cls_.MainWindow.menus["tools"]
items = gather_menu_items(menu)
def assert_open():
assert qapp_cls_.activeWindow() != qapp_cls_.MainWindow
qtbot.keyClick(qapp_cls_.focusWidget(), QtCore.Qt.Key_T, QtCore.Qt.AltModifier)
qtbot.wait(100)
QtCore.QTimer.singleShot(100, assert_open)
qtbot.keyClick(menu, QtCore.Qt.Key_O)
qtbot.wait(200)
qtbot.keyClick(qapp_cls_.activeWindow(), QtCore.Qt.Key_Escape)
qtbot.wait(200)
def test_reports_actions(qtbot, qapp_cls_):
pass
def test_help_windows(qtbot, qapp_cls_):
def assert_and_close():
assert qapp_cls_.activeWindow() != qapp_cls_.MainWindow
qtbot.keyClick(qapp_cls_.activeWindow(), QtCore.Qt.Key_Enter)
def test_help_window(menu_key):
qtbot.keyClick(qapp_cls_.focusWidget(), QtCore.Qt.Key_H, QtCore.Qt.AltModifier)
qtbot.wait(300)
QtCore.QTimer.singleShot(200, assert_and_close)
qtbot.keyClick(menu, menu_key)
qtbot.wait(600)
assert qapp_cls_.activeWindow() == qapp_cls_.MainWindow
qtbot.wait(100)
menu = qapp_cls_.MainWindow.menus["help"]
qtbot.wait(100)
for key in [
QtCore.Qt.Key_H,
QtCore.Qt.Key_T,
QtCore.Qt.Key_K,
QtCore.Qt.Key_G,
QtCore.Qt.Key_A,
QtCore.Qt.Key_Q,
]:
test_help_window(key)
def test_file_new_close(qtbot, qapp_cls_, monkeypatch):
win = qapp_cls_.MainWindow
monkeypatch.setattr(
QtWidgets.QMessageBox,
"question",
lambda *args: QtWidgets.QMessageBox.Discard,
)
qtbot.wait(100)
menu_shortcut(qtbot, qapp_cls_, "file", QtCore.Qt.Key_F, QtCore.Qt.Key_N)
assert win.tabWidget.count() == 2, "File/New"
for i in range(3, 5):
ctrl_keystroke(qtbot, qapp_cls_, QtCore.Qt.Key_N)
assert win.tabWidget.count() == i, "File/New"
for i in range(1, 4):
ctrl_keystroke(qtbot, qapp_cls_, QtCore.Qt.Key_W)
assert win.tabWidget.count() == 4 - i, "File/Close"
def test_generate(qtbot, qapp_cls_, monkeypatch, tmp_path):
fg = qapp_cls_.MainWindow.currentFlowgraph
view = qapp_cls_.MainWindow.currentView
scaling = qapp_cls_.MainWindow.screen().devicePixelRatio()
fg_path = tmp_path / "test_generate.grc"
py_path = tmp_path / "default.py"
monkeypatch.setattr(
QtWidgets.QFileDialog, "getSaveFileName", lambda *args, **kargs: (fg_path, "")
)
qtbot.wait(100)
for block in ["null sou", "null sin"]:
add_block_from_query(qtbot, qapp_cls_, block)
n_src = find_blocks(fg, "blocks_null_source")
n_sink = find_blocks(fg, "blocks_null_sink")
assert len(fg.connections) == 0
start = scaling * global_pos(n_sink.gui, view)
pag.moveTo(start.x(), start.y())
pag.mouseDown()
def drag():
for i in range(20):
pag.move(scaling * 10, 0)
drag_t = threading.Thread(target=drag)
drag_t.start()
while drag_t.is_alive():
qtbot.wait(50)
pag.mouseUp()
click_on(qtbot, qapp_cls_, n_src.sources[0])
click_on(qtbot, qapp_cls_, n_sink.sinks[0])
assert not fg_path.exists(), "File/Save (setup): .grc file already exists"
assert not py_path.exists(), "File/Save (setup): Python file already exists"
menu_shortcut(qtbot, qapp_cls_, "build", QtCore.Qt.Key_B, QtCore.Qt.Key_G)
qtbot.wait(500)
assert fg_path.exists(), "File/Save: Could not save .grc file"
assert py_path.exists(), "File/Save: Could not save Python file"
def test_file_close_all(qtbot, qapp_cls_, monkeypatch):
win = qapp_cls_.MainWindow
monkeypatch.setattr(
QtWidgets.QMessageBox,
"question",
lambda *args: QtWidgets.QMessageBox.Discard,
)
qtbot.wait(100)
for i in range(1, 4):
ctrl_keystroke(qtbot, qapp_cls_, QtCore.Qt.Key_N)
assert win.tabWidget.count() == 4, "File/Close All"
menu_shortcut(qtbot, qapp_cls_, "file", QtCore.Qt.Key_F, QtCore.Qt.Key_L)
assert win.tabWidget.count() == 1, "File/Close All"
def test_quit(qtbot, qapp_cls_, monkeypatch):
monkeypatch.setattr(
QtWidgets.QMessageBox,
"question",
lambda *args: QtWidgets.QMessageBox.Discard,
)
qapp_cls_.MainWindow.actions["exit"].trigger()
assert True
time.sleep(1)