# 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, QToolButton) 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) QLineEdit.setClearButtonEnabled(self, True) self.parent = parent self.setObjectName("block_library::search_bar") # We must use a queued connection here, otherwise # the searchbar entry will not be deleted if we selected an item from # the popup window by key_down and key_enter self.returnPressed.connect(self.on_return_pressed, Qt.QueuedConnection) # Find clear Button inside QLineEdit clr_btn = self.findChildren(QToolButton) if len(clr_btn) > 0: clr_btn[0].triggered.connect(self.clr) def clr(self): self.parent.reset() def first_block_in_results(self): model = self.completer().completionModel() index = model.index(0, 0) return model.data(index) def on_return_pressed(self): block_key = None label = self.text() if label in self.parent._block_tree_flat: block_key = self.parent._block_tree_flat[label].key else: label = self.first_block_in_results() if label in self.parent._block_tree_flat: block_key = self.parent._block_tree_flat[label].key if block_key: self.parent.add_block(block_key) else: log.info(f"No block named {label}") self.clr() 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 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.qsettings = self.app.qsettings self.blocklibrary_expanded = self.qsettings.value('grc/blocklibrary_expanded', type=bool) 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 is build in 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.PopupCompletion) completer.setModelSorting(QCompleter.CaseInsensitivelySortedModel) completer.setCaseSensitivity(Qt.CaseInsensitive) completer.setFilterMode(Qt.MatchContains) self._search_bar.setCompleter(completer) self._search_bar.textChanged.connect(self.show_search_results) # 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 = {} if not self.qsettings.value("appearance/display_blocklibrary", True, type=bool): self.hide() def createActions(self, actions): pass def createMenus(self, actions, menus): pass def createToolbars(self, actions, toolbars): pass def show_search_results(self): if self._search_bar.text() == '': self.populate_tree(self._block_tree) else: self.populate_tree(self._block_tree, get_items(self._search_bar.completer().completionModel())) def reset(self): """Reset the filter (show all blocks in the tree view)""" self._search_bar.clear() self.populate_tree(self._block_tree) 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 reload_blocks(self): self.platform.build_library() self.load_blocks() self.populate_tree(self._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()) if self.blocklibrary_expanded: for row in range(0, self._model.rowCount()): self._library.expand(self._model.item(row, 0).index()) else: self._library.expand( self._model.item(0, 0).index() ) 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 []