grc: Add GRC Qt
Signed-off-by: Håkon Vågsether <hakon.vagsether@gmail.com>
@ -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"
|
||||
|
||||
@ -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 = (
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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
|
||||
##############################################
|
||||
|
||||
@ -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,17 +36,21 @@ 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)
|
||||
logger.debug(f"Cache version {cacheversion}")
|
||||
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:
|
||||
logger.info(f"Outdated cache {self.cache_file} found, "
|
||||
if self.log:
|
||||
logger.info(f"Outdated cache {self.cache_file} found, "
|
||||
"will be overwritten.")
|
||||
raise ValueError()
|
||||
except (IOError, ValueError):
|
||||
@ -59,7 +64,8 @@ class Cache(object):
|
||||
cached = self.cache[filename]
|
||||
if int(cached["cached-at"] + 0.5) >= modtime:
|
||||
return cached["data"]
|
||||
logger.info(f"Cache for {filename} outdated, loading yaml")
|
||||
if self.log:
|
||||
logger.info(f"Cache for {filename} outdated, loading yaml")
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
@ -76,7 +82,8 @@ class Cache(object):
|
||||
if not self.need_cache_write:
|
||||
return
|
||||
|
||||
logger.debug('Saving %d entries to json cache', len(self.cache))
|
||||
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:
|
||||
cache_content = {
|
||||
|
||||
@ -22,7 +22,7 @@ blocks:
|
||||
value: '32000'
|
||||
states:
|
||||
coordinate:
|
||||
- 184
|
||||
- 200
|
||||
- 12
|
||||
rotation: 0
|
||||
state: enabled
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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
@ -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
@ -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
@ -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
@ -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
147
gui_qt/base.py
Normal 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')
|
||||
11
gui_qt/components/__init__.py
Normal 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
|
||||
322
gui_qt/components/block_library.py
Normal 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 []
|
||||
0
gui_qt/components/canvas/__init__.py
Normal file
420
gui_qt/components/canvas/block.py
Executable 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()
|
||||
54
gui_qt/components/canvas/colors.py
Executable 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())
|
||||
146
gui_qt/components/canvas/connection.py
Executable 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)
|
||||
531
gui_qt/components/canvas/flowgraph.py
Normal 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
|
||||
147
gui_qt/components/canvas/port.py
Normal 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)
|
||||
270
gui_qt/components/console.py
Normal 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>"
|
||||
196
gui_qt/components/dialogs.py
Normal 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()
|
||||
311
gui_qt/components/example_browser.py
Normal 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)
|
||||
120
gui_qt/components/executor.py
Normal 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()
|
||||
188
gui_qt/components/flowgraph_view.py
Normal 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)
|
||||
108
gui_qt/components/oot_browser.py
Normal 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'))}")
|
||||
172
gui_qt/components/preferences.py
Normal 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()
|
||||
233
gui_qt/components/undoable_actions.py
Normal 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()
|
||||
224
gui_qt/components/variable_editor.py
Normal 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)
|
||||
115
gui_qt/components/wiki_tab.py
Normal 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
74
gui_qt/external_editor.py
Normal 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
@ -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_()
|
||||
2
gui_qt/helpers/__init__.py
Normal file
@ -0,0 +1,2 @@
|
||||
from . import logging
|
||||
from . import qt
|
||||
131
gui_qt/helpers/logging.py
Normal 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
|
||||
26
gui_qt/helpers/profiling.py
Normal 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
263
gui_qt/properties.py
Normal 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
|
||||
178
gui_qt/resources/available_preferences.yml
Normal 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: []
|
||||
BIN
gui_qt/resources/cpp_cmd_fg.png
Normal file
|
After Width: | Height: | Size: 26 KiB |
BIN
gui_qt/resources/cpp_fg.png
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
gui_qt/resources/cpp_qt_fg.png
Normal file
|
After Width: | Height: | Size: 32 KiB |
1711
gui_qt/resources/data/rx_logo.grc
Executable file
184
gui_qt/resources/example_browser.ui
Normal 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>
|
||||
202
gui_qt/resources/example_browser_widget.ui
Normal 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>
|
||||
17
gui_qt/resources/language/add-translation.txt
Normal 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
|
||||
BIN
gui_qt/resources/language/en_US/LC_MESSAGES/grc.mo
Normal file
416
gui_qt/resources/language/en_US/LC_MESSAGES/grc.po
Normal 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 "
|
||||
378
gui_qt/resources/language/grc.pot
Normal 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 ""
|
||||
BIN
gui_qt/resources/logo/gnuradio_logo_icon-square.png
Normal file
|
After Width: | Height: | Size: 4.9 KiB |
21
gui_qt/resources/manifests/gr-example_haakov.md
Normal 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
|
||||
|
||||
275
gui_qt/resources/oot_browser.ui
Normal 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>
|
||||
BIN
gui_qt/resources/py_cmd_fg.png
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
gui_qt/resources/py_fg.png
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
gui_qt/resources/py_qt_fg.png
Normal file
|
After Width: | Height: | Size: 31 KiB |
260
main.py
@ -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
@ -0,0 +1,2 @@
|
||||
[pytest]
|
||||
addopts = --ignore=test_qtbot.py
|
||||
891
tests/test_qtbot.py
Normal 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)
|
||||