# 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 os
import sys
import subprocess
import cProfile
import pstats

from typing import Union

from PyQt6.QtCore import QMetaObject, QPointF, QThreadPool, QUrl, Qt, pyqtSlot
from PyQt6.QtGui import QDesktopServices, QIcon, QKeySequence, QAction, QPalette
from PyQt6.QtWidgets import QApplication, QFileDialog, QMainWindow, QMenu, QMessageBox, QProgressBar, QStyle, QTabWidget, QToolBar, QUndoView

# Custom modules
from .flowgraph_view import FlowgraphView
from .canvas.flowgraph import FlowgraphScene
from .example_browser import ExampleBrowser, ExampleBrowserDialog, Worker
from .executor import ExecFlowGraphThread
from .. import base, Constants, Utils
from .variable_editor import VariableEditorAction
from .undoable_actions import (
    RotateAction,
    EnableAction,
    DisableAction,
    BypassAction,
    MoveAction,
    NewElementAction,
    DeleteElementAction,
    BlockPropsChangeAction,
    BussifyAction
)
from .preferences import PreferencesDialog
from .oot_browser import OOTBrowser
from .dialogs import ErrorsDialog
from ...core.base import Element
from ... import paths

# Logging
log = logging.getLogger(f"grc.application.{__name__}")

# Shortcuts
Action = QAction
Menu = QMenu
Toolbar = QToolBar
Icons = QIcon.fromTheme
Keys = QKeySequence.StandardKey
QStyle = QStyle


class MainWindow(QMainWindow, base.Component):
    def __init__(self, file_path=[]):
        QMainWindow.__init__(self)
        # base.Component.__init__(self)

        log.debug("Setting the main window")
        self.setObjectName("main")
        self.setWindowTitle(_("window-title"))
        self.setDockOptions(
            QMainWindow.DockOption.AllowNestedDocks |
            QMainWindow.DockOption.AllowTabbedDocks |
            QMainWindow.DockOption.AnimatedDocks
        )
        self.progress_bar = QProgressBar()
        self.statusBar().addPermanentWidget(self.progress_bar)
        self.progress_bar.hide()

        # Setup the window icon
        icon = QIcon(self.settings.path.ICON)
        log.debug("Setting window icon - ({0})".format(self.settings.path.ICON))
        self.setWindowIcon(icon)

        monitor = self.screen().availableGeometry()
        log.debug(
            "Setting window size - ({}, {})".format(monitor.width(), monitor.height())
        )
        self.resize(int(monitor.width() * 0.50), monitor.height())

        self.setCorner(Qt.Corner.BottomLeftCorner, Qt.DockWidgetArea.LeftDockWidgetArea)

        # Get max length of recently opened files list to be displayed in the menu
        self.max_recent_files = self.app.qsettings.value('appearance/max_recent_files', 10, type=int)
        recent_files = list(self.app.qsettings.value('window/files_recent', []))
        for file in recent_files:
            if not os.path.exists(file):
                recent_files.remove(file)
        self.recent_files = recent_files[:self.max_recent_files]

        self.menuBar().setNativeMenuBar(self.settings.window.NATIVE_MENUBAR)

        # TODO: Not sure about document mode
        # self.setDocumentMode(True)

        # Generate the rest of the window
        self.createStatusBar()

        self.profiler = cProfile.Profile()

        # actions['Quit.triggered.connect(self.close)
        # actions['Report.triggered.connect(self.reportDock.show)
        # QMetaObject.connectSlotsByName(self)

        # Translation support

        # self.setWindowTitle(_translate("blockLibraryDock", "Library", None))
        # library.headerItem().setText(0, _translate("blockLibraryDock", "Blocks", None))
        # QMetaObject.connectSlotsByName(blockLibraryDock)

        # 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()

        # Rest of the GUI widgets

        # Map some of the view's functions to the controller class
        self.registerDockWidget = self.addDockWidget
        self.registerMenu = self.addMenu
        self.registerToolBar = self.addToolBar

        # Do other initialization stuff. View should already be allocated and
        # actions dynamically connected to class functions. Also, the log
        # functionality should be also allocated
        log.debug("__init__")

        # Add the menus from the view
        menus = self.menus
        self.registerMenu(menus["file"])
        self.registerMenu(menus["edit"])
        self.registerMenu(menus["view"])
        self.registerMenu(menus["build"])
        self.registerMenu(menus["tools"])
        self.registerMenu(menus["help"])

        toolbars = self.toolbars
        self.registerToolBar(toolbars["file"])
        self.registerToolBar(toolbars["edit"])
        self.registerToolBar(toolbars["run"])
        self.registerToolBar(toolbars["misc"])

        self.tabWidget = QTabWidget()
        self.tabWidget.setTabsClosable(True)
        self.tabWidget.tabCloseRequested.connect(
            lambda index: self.close_triggered(index)
        )
        self.tabWidget.currentChanged.connect(
            lambda index: self.tab_triggered(index)
        )
        self.setCentralWidget(self.tabWidget)

        self.clipboard = None
        self.undoView = None

        files_open = list(self.app.qsettings.value('window/files_open', [])) + file_path
        grc_file_found = False
        if files_open:
            for file in files_open:
                if os.path.isfile(file):
                    try:
                        self.open_triggered(file)
                        grc_file_found = True
                    except Exception as e:
                        log.error(f"failed to load flowgraph file {file} with exception {e}")
                        log.debug(f"file {file}:", exc_info=True)
        if not grc_file_found:
            self.new_triggered()

        try:
            self.restoreGeometry(self.app.qsettings.value("window/geometry"))
            self.restoreState(self.app.qsettings.value("window/windowState"))
        except TypeError:
            log.warning("Could not restore window geometry and state.")

        self.examples_found = False
        self.ExampleBrowser = ExampleBrowser()
        self.ExampleBrowser.file_to_open.connect(self.open_example)
        self.OOTBrowser = OOTBrowser()

        self.threadpool = QThreadPool()
        self.threadpool.setMaxThreadCount(1)
        ExampleFinder = Worker(self.ExampleBrowser.find_examples)
        ExampleFinder.signals.result.connect(self.populate_libraries_w_examples)
        ExampleFinder.signals.progress.connect(self.update_progress_bar)
        self.threadpool.start(ExampleFinder)

    """def show(self):
        log.debug("Showing main window")
        self.show()
    """

    @pyqtSlot(VariableEditorAction)
    def handle_editor_action(self, key):
        # Calculate the position to insert a new block
        # Perhaps we should add a random component, as we may add several blocks
        pos = (self.currentFlowgraphScene.sceneRect().width() / 2, self.currentFlowgraphScene.sceneRect().height() / 2)
        if key == VariableEditorAction.DELETE_BLOCK:
            self.delete_triggered()
        elif key == VariableEditorAction.DISABLE_BLOCK:
            self.disable_triggered()
        elif key == VariableEditorAction.ENABLE_BLOCK:
            self.enable_triggered()
        elif key == VariableEditorAction.ADD_VARIABLE:
            self.currentFlowgraphScene.add_block('variable', pos)
        elif key == VariableEditorAction.ADD_IMPORT:
            self.currentFlowgraphScene.add_block('import', pos)
        elif key == VariableEditorAction.OPEN_PROPERTIES:
            self.currentFlowgraphScene.selected_blocks()[0].open_properties()
        else:
            log.debug(f"{key} not implemented yet")
        self.currentFlowgraphScene.clearSelection()

    @property
    def currentView(self):
        return self.tabWidget.currentWidget()

    @property
    def currentFlowgraphScene(self):
        return self.tabWidget.currentWidget().scene()

    @property
    def currentFlowgraph(self):
        return self.tabWidget.currentWidget().scene().core

    @pyqtSlot(QPointF)
    def registerMove(self, diff):
        self.currentFlowgraphScene.set_saved(False)
        self.tabWidget.tabBar().setTabTextColor(self.tabWidget.currentIndex(), Qt.GlobalColor.red)
        action = MoveAction(self.currentFlowgraphScene, diff)
        self.currentFlowgraphScene.undoStack.push(action)
        self.updateActions()

    @pyqtSlot(Element)
    def registerNewElement(self, elem):
        self.currentFlowgraphScene.set_saved(False)
        self.tabWidget.tabBar().setTabTextColor(self.tabWidget.currentIndex(), Qt.GlobalColor.red)
        action = NewElementAction(self.currentFlowgraphScene, elem)
        self.currentFlowgraphScene.undoStack.push(action)
        self.updateActions()
        self.currentFlowgraphScene.update()

    @pyqtSlot(Element)
    def registerDeleteElement(self, elem):
        self.currentFlowgraphScene.set_saved(False)
        self.tabWidget.tabBar().setTabTextColor(self.tabWidget.currentIndex(), Qt.GlobalColor.red)
        action = DeleteElementAction(self.currentFlowgraphScene, elem)
        self.currentFlowgraphScene.undoStack.push(action)
        self.updateActions()
        self.currentFlowgraphScene.update()

    @pyqtSlot(Element)
    def registerBlockPropsChange(self, elem):
        self.currentFlowgraphScene.set_saved(False)
        self.tabWidget.tabBar().setTabTextColor(self.tabWidget.currentIndex(), Qt.GlobalColor.red)
        action = BlockPropsChangeAction(self.currentFlowgraphScene, elem)
        self.currentFlowgraphScene.undoStack.push(action)
        self.updateActions()
        self.currentFlowgraphScene.update()

    def createActions(self, actions):
        """
        Defines all actions for this view.
        Controller manages connecting signals to slots implemented in the controller
        """
        log.debug("Creating actions")

        # File Actions
        actions["new"] = Action(
            Icons("document-new"),
            _("new"),
            self,
            shortcut=Keys.New,
            statusTip=_("new-tooltip"),
        )

        actions["open"] = Action(
            Icons("document-open"),
            _("open"),
            self,
            shortcut=Keys.Open,
            statusTip=_("open-tooltip"),
        )

        actions["open_recent"] = Action(
            Icons("document-open"),
            _("open_recent"),
            self,
            statusTip=_("open-recent-file"),
        )
        actions["open_recent"].setText("Open recent file")

        actions["example_browser"] = Action(
            _("example_browser"),
            self,
            statusTip=_("example_browser-tooltip"),
        )

        actions["close"] = Action(
            Icons("window-close"),
            _("close"),
            self,
            shortcut=Keys.Close,
            statusTip=_("close-tooltip"),
        )

        actions["close_all"] = Action(
            Icons("window-close"),
            _("close_all"),
            self,
            statusTip=_("close_all-tooltip"),
        )
        actions["save"] = Action(
            Icons("document-save"),
            _("save"),
            self,
            shortcut=Keys.Save,
            statusTip=_("save-tooltip"),
        )

        actions["save_as"] = Action(
            Icons("document-save-as"),
            _("save_as"),
            self,
            shortcut=Keys.SaveAs,
            statusTip=_("save_as-tooltip"),
        )

        actions["save_copy"] = Action(_("save_copy"), self)

        actions["screen_capture"] = Action(
            Icons("camera-photo"),
            _("screen_capture"),
            self,
            shortcut=Keys.Print,
            statusTip=_("screen_capture-tooltip"),
        )

        actions["exit"] = Action(
            Icons("application-exit"),
            _("exit"),
            self,
            shortcut=Keys.Quit,
            statusTip=_("exit-tooltip"),
        )

        # Edit Actions
        actions["undo"] = Action(
            Icons("edit-undo"),
            _("undo"),
            self,
            shortcut=Keys.Undo,
            statusTip=_("undo-tooltip"),
        )

        actions["redo"] = Action(
            Icons("edit-redo"),
            _("redo"),
            self,
            shortcut=Keys.Redo,
            statusTip=_("redo-tooltip"),
        )

        actions["view_undo_stack"] = Action("View undo stack", self)

        actions["cut"] = Action(
            Icons("edit-cut"),
            _("cut"),
            self,
            shortcut=Keys.Cut,
            statusTip=_("cut-tooltip"),
        )

        actions["copy"] = Action(
            Icons("edit-copy"),
            _("copy"),
            self,
            shortcut=Keys.Copy,
            statusTip=_("copy-tooltip"),
        )

        actions["paste"] = Action(
            Icons("edit-paste"),
            _("paste"),
            self,
            shortcut=Keys.Paste,
            statusTip=_("paste-tooltip"),
        )

        actions["delete"] = Action(
            Icons("edit-delete"),
            _("delete"),
            self,
            shortcut=Keys.Delete,
            statusTip=_("delete-tooltip"),
        )

        actions["undo"].setEnabled(False)
        actions["redo"].setEnabled(False)
        actions["cut"].setEnabled(False)
        actions["copy"].setEnabled(False)
        actions["paste"].setEnabled(False)
        actions["delete"].setEnabled(False)

        actions["select_all"] = Action(
            Icons("edit-select_all"),
            _("select_all"),
            self,
            shortcut=Keys.SelectAll,
            statusTip=_("select_all-tooltip"),
        )

        actions["select_none"] = Action(
            _("Select None"), self, statusTip=_("select_none-tooltip")
        )

        actions["rotate_ccw"] = Action(
            Icons("object-rotate-left"),
            _("rotate_ccw"),
            self,
            shortcut=Keys.MoveToPreviousChar,
            statusTip=_("rotate_ccw-tooltip"),
        )

        actions["rotate_cw"] = Action(
            Icons("object-rotate-right"),
            _("rotate_cw"),
            self,
            shortcut=Keys.MoveToNextChar,
            statusTip=_("rotate_cw-tooltip"),
        )

        actions["rotate_cw"].setEnabled(False)
        actions["rotate_ccw"].setEnabled(False)

        actions["enable"] = Action(_("enable"), self, shortcut="E")
        actions["disable"] = Action(_("disable"), self, shortcut="D")
        actions["bypass"] = Action(_("bypass"), self, shortcut="B")

        # TODO
        actions["block_inc_type"] = Action(_("block_inc_type"), self)
        actions["block_dec_type"] = Action(_("block_dec_type"), self)

        actions["enable"].setEnabled(False)
        actions["disable"].setEnabled(False)
        actions["bypass"].setEnabled(False)

        # TODO
        actions["vertical_align_top"] = Action(_("vertical_align_top"), self)
        actions["vertical_align_middle"] = Action(_("vertical_align_middle"), self)
        actions["vertical_align_bottom"] = Action(_("vertical_align_bottom"), self)

        actions["vertical_align_top"].setEnabled(False)
        actions["vertical_align_middle"].setEnabled(False)
        actions["vertical_align_bottom"].setEnabled(False)

        actions["horizontal_align_left"] = Action(_("horizontal_align_left"), self)
        actions["horizontal_align_center"] = Action(_("horizontal_align_center"), self)
        actions["horizontal_align_right"] = Action(_("horizontal_align_right"), self)

        actions["horizontal_align_left"].setEnabled(False)
        actions["horizontal_align_center"].setEnabled(False)
        actions["horizontal_align_right"].setEnabled(False)

        actions["create_hier"] = Action(_("create_hier_block"), self)
        actions["open_hier"] = Action(_("open_hier_block"), self)
        actions["toggle_source_bus"] = Action(_("toggle_source_bus"), self)
        actions["toggle_sink_bus"] = Action(_("toggle_sink_bus"), self)

        actions["create_hier"].setEnabled(True)
        actions["open_hier"].setEnabled(True)
        actions["toggle_source_bus"].setEnabled(False)
        actions["toggle_sink_bus"].setEnabled(False)

        actions["properties"] = Action(
            Icons("document-properties"),
            _("flowgraph-properties"),
            self,
            statusTip=_("flowgraph-properties-tooltip"),
        )

        actions["properties"].setEnabled(False)

        # View Actions
        actions["zoom_in"] = Action(
            Icons("zoom-in"),
            _("Zoom &in"),
            self
        )
        actions["zoom_in"].setShortcuts([Keys.ZoomIn, "Ctrl+="])
        actions["zoom_out"] = Action(
            Icons("zoom-out"),
            _("Zoom &out"),
            self,
            shortcut=Keys.ZoomOut,
        )
        actions["zoom_original"] = Action(
            Icons("zoom-original"),
            _("O&riginal size"),
            self,
            shortcut="Ctrl+0",
        )

        actions["toggle_grid"] = Action(
            _("toggle_grid"), self, shortcut="G", statusTip=_("toggle_grid-tooltip")
        )

        actions["errors"] = Action(
            Icons("dialog-error"), _("errors"), self, statusTip=_("errors-tooltip")
        )

        actions["find"] = Action(
            Icons("edit-find"),
            _("find"),
            self,
            shortcut=Keys.Find,
            statusTip=_("find-tooltip"),
        )

        # Help Actions
        actions["about"] = Action(
            Icons("help-about"), _("about"), self, statusTip=_("about-tooltip")
        )

        actions["about_qt"] = Action(
            self.style().standardIcon(QStyle.StandardPixmap.SP_TitleBarMenuButton),
            _("about-qt"),
            self,
            statusTip=_("about-tooltip"),
        )

        actions["generate"] = Action(
            Icons("system-run"),
            _("process-generate"),
            self,
            shortcut="F5",
            statusTip=_("process-generate-tooltip"),
        )

        actions["execute"] = Action(
            Icons("media-playback-start"),
            _("process-execute"),
            self,
            shortcut="F6",
            statusTip=_("process-execute-tooltip"),
        )

        actions["kill"] = Action(
            Icons("process-stop"),
            _("process-kill"),
            self,
            shortcut="F7",
            statusTip=_("process-kill-tooltip"),
        )

        actions["help"] = Action(
            Icons("help-browser"),
            _("help"),
            self,
            shortcut=Keys.HelpContents,
            statusTip=_("help-tooltip"),
        )

        # Tools Actions

        actions["filter_design_tool"] = Action(_("&Filter Design Tool"), self)

        actions["module_browser"] = Action(_("&OOT Module Browser"), self)

        actions["start_profiler"] = Action(_("Start profiler"), self)
        actions["stop_profiler"] = Action(_("Stop profiler"), self)

        # Help Actions

        actions["types"] = Action(_("&Types"), self)

        actions["keys"] = Action(_("&Keys"), self)

        actions["guided_tutorials"] = Action(
            Icons("help-browser"),
            _("Guided Tutorials"),
            self,
            statusTip=_("Open the GNU Radio guided tutorials"),
        )

        actions["get_involved"] = Action(_("&Get Involved"), self)

        actions["preferences"] = Action(
            Icons("preferences-system"),
            _("preferences"),
            self,
            statusTip=_("preferences-tooltip"),
        )

        actions["reload"] = Action(
            Icons("view-refresh"), _("reload"), self, statusTip=_("reload-tooltip")
        )

        # Disable some actions, by default
        actions["save"].setEnabled(True)
        actions["errors"].setEnabled(False)

    def updateDocTab(self):
        pass
        """
        doc_txt = self._app().DocumentationTab._text
        blocks = self.currentFlowgraphScene.selected_blocks()
        if len(blocks) == 1:
            #print(blocks[0].documentation)
            doc_string = blocks[0].documentation['']
            doc_txt.setText(doc_string)
        """

    def updateActions(self):
        """Update the available actions based on what is selected"""

        blocks = self.currentFlowgraphScene.selected_blocks()
        conns = self.currentFlowgraphScene.selected_connections()
        undoStack = self.currentFlowgraphScene.undoStack
        canUndo = undoStack.canUndo()
        canRedo = undoStack.canRedo()
        self.currentFlowgraph.validate()
        valid_fg = self.currentFlowgraph.is_valid()
        saved_fg = self.currentFlowgraphScene.saved

        self.actions["save"].setEnabled(not saved_fg)

        self.actions["undo"].setEnabled(canUndo)
        self.actions["redo"].setEnabled(canRedo)
        self.actions["generate"].setEnabled(valid_fg)
        self.actions["execute"].setEnabled(valid_fg)
        self.actions["errors"].setEnabled(not valid_fg)
        self.actions["kill"].setEnabled(self.currentView.process_is_done())

        self.actions["cut"].setEnabled(False)
        self.actions["copy"].setEnabled(False)
        self.actions["paste"].setEnabled(False)
        self.actions["delete"].setEnabled(False)
        self.actions["rotate_cw"].setEnabled(False)
        self.actions["rotate_ccw"].setEnabled(False)
        self.actions["enable"].setEnabled(False)
        self.actions["disable"].setEnabled(False)
        self.actions["bypass"].setEnabled(False)
        self.actions["properties"].setEnabled(False)
        self.actions["toggle_source_bus"].setEnabled(False)
        self.actions["toggle_sink_bus"].setEnabled(False)

        if self.clipboard:
            self.actions["paste"].setEnabled(True)

        if len(conns) > 0:
            self.actions["delete"].setEnabled(True)

        if len(blocks) > 0:
            self.actions["cut"].setEnabled(True)
            self.actions["copy"].setEnabled(True)
            self.actions["delete"].setEnabled(True)
            self.actions["rotate_cw"].setEnabled(True)
            self.actions["rotate_ccw"].setEnabled(True)
            self.actions["enable"].setEnabled(True)
            self.actions["disable"].setEnabled(True)
            self.actions["bypass"].setEnabled(True)
            self.actions["toggle_source_bus"].setEnabled(True)
            self.actions["toggle_sink_bus"].setEnabled(True)

            self.actions["vertical_align_top"].setEnabled(False)
            self.actions["vertical_align_middle"].setEnabled(False)
            self.actions["vertical_align_bottom"].setEnabled(False)

            self.actions["horizontal_align_left"].setEnabled(False)
            self.actions["horizontal_align_center"].setEnabled(False)
            self.actions["horizontal_align_right"].setEnabled(False)

            if len(blocks) == 1:
                self.actions["properties"].setEnabled(True)

            if len(blocks) > 1:
                self.actions["vertical_align_top"].setEnabled(True)
                self.actions["vertical_align_middle"].setEnabled(True)
                self.actions["vertical_align_bottom"].setEnabled(True)

                self.actions["horizontal_align_left"].setEnabled(True)
                self.actions["horizontal_align_center"].setEnabled(True)
                self.actions["horizontal_align_right"].setEnabled(True)

            for block in blocks:
                if not block.core.can_bypass():
                    self.actions["bypass"].setEnabled(False)
                    break

    def createMenus(self, actions, menus):
        """Setup the main menubar for the application"""
        log.debug("Creating menus")

        # Global menu options
        self.menuBar().setNativeMenuBar(True)

        open_recent = Menu("Open recent file")
        menus["open_recent"] = open_recent

        act_recent_len = len(self.recent_files)
        if act_recent_len > self.max_recent_files:
            act_recent_len = self.max_recent_files

        # Populate recent file list
        if act_recent_len == 0:
            for i in range(self.max_recent_files):
                # Setup invisible dummy entries, that can be filled later, if recently opened files are available
                action = open_recent.addAction("Dummy", self.open_recent_triggered)
                action.setVisible(False)
        else:
            for i in range(act_recent_len):
                open_recent.addAction(self.recent_files[i], self.open_recent_triggered)
            if act_recent_len < self.max_recent_files:
                for i in range(self.max_recent_files - act_recent_len):
                    action = open_recent.addAction("Dummy", self.open_recent_triggered)
                    action.setVisible(False)

        # Setup the file menu
        file = Menu("&File")
        file.addAction(actions["new"])
        file.addAction(actions["open"])
        file.addMenu(open_recent)
        file.addAction(actions["example_browser"])
        file.addAction(actions["close"])
        file.addAction(actions["close_all"])
        file.addSeparator()
        file.addAction(actions["save"])
        file.addAction(actions["save_as"])
        file.addAction(actions["save_copy"])
        file.addSeparator()
        file.addAction(actions["screen_capture"])
        file.addSeparator()
        file.addAction(actions["preferences"])
        file.addSeparator()
        file.addAction(actions["exit"])
        menus["file"] = file

        # Setup the edit menu
        edit = Menu("&Edit")
        edit.addAction(actions["undo"])
        edit.addAction(actions["redo"])
        edit.addAction(actions["view_undo_stack"])
        edit.addSeparator()
        edit.addAction(actions["cut"])
        edit.addAction(actions["copy"])
        edit.addAction(actions["paste"])
        edit.addAction(actions["delete"])
        edit.addAction(actions["select_all"])
        edit.addAction(actions["select_none"])
        edit.addSeparator()
        edit.addAction(actions["rotate_ccw"])
        edit.addAction(actions["rotate_cw"])

        align = Menu("&Align")
        menus["align"] = align
        align.addAction(actions["vertical_align_top"])
        align.addAction(actions["vertical_align_middle"])
        align.addAction(actions["vertical_align_bottom"])
        align.addSeparator()
        align.addAction(actions["horizontal_align_left"])
        align.addAction(actions["horizontal_align_center"])
        align.addAction(actions["horizontal_align_right"])

        edit.addMenu(align)
        edit.addSeparator()
        edit.addAction(actions["enable"])
        edit.addAction(actions["disable"])
        edit.addAction(actions["bypass"])
        edit.addSeparator()

        more = Menu("&More")
        menus["more"] = more
        more.addAction(actions["create_hier"])
        more.addAction(actions["open_hier"])
        more.addAction(actions["toggle_source_bus"])
        more.addAction(actions["toggle_sink_bus"])

        edit.addMenu(more)
        edit.addAction(actions["properties"])
        menus["edit"] = edit

        # Setup submenu
        panels = Menu("&Panels")
        menus["panels"] = panels
        panels.setEnabled(False)

        toolbars = Menu("&Toolbars")
        menus["toolbars"] = toolbars
        toolbars.setEnabled(False)

        # Setup the view menu
        view = Menu("&View")
        view.addMenu(panels)
        view.addMenu(toolbars)
        view.addSeparator()
        view.addAction(actions["zoom_in"])
        view.addAction(actions["zoom_out"])
        view.addAction(actions["zoom_original"])
        view.addSeparator()
        view.addAction(actions["toggle_grid"])
        view.addAction(actions["find"])
        menus["view"] = view

        # Setup the build menu
        build = Menu("&Build")
        build.addAction(actions["errors"])
        build.addAction(actions["generate"])
        build.addAction(actions["execute"])
        build.addAction(actions["kill"])
        menus["build"] = build

        # Setup the tools menu
        tools = Menu("&Tools")
        tools.addAction(actions["filter_design_tool"])
        tools.addAction(actions["module_browser"])
        tools.addSeparator()
        tools.addAction(actions["start_profiler"])
        tools.addAction(actions["stop_profiler"])
        menus["tools"] = tools

        # Setup the help menu
        help = Menu("&Help")
        help.addAction(actions["help"])
        help.addAction(actions["guided_tutorials"])
        help.addAction(actions["types"])
        help.addAction(actions["keys"])
        help.addSeparator()
        help.addAction(actions["get_involved"])
        help.addAction(actions["about"])
        help.addAction(actions["about_qt"])
        menus["help"] = help

    def createToolbars(self, actions, toolbars):
        log.debug("Creating toolbars")

        # Main toolbar
        file = Toolbar("File")
        file.setObjectName("_FileTb")
        file.addAction(actions["new"])
        file.addAction(actions["open"])
        file.addAction(actions["save"])
        file.addAction(actions["close"])
        toolbars["file"] = file

        # Edit toolbar
        edit = Toolbar("Edit")
        edit.setObjectName("_EditTb")
        edit.addAction(actions["undo"])
        edit.addAction(actions["redo"])
        edit.addSeparator()
        edit.addAction(actions["cut"])
        edit.addAction(actions["copy"])
        edit.addAction(actions["paste"])
        edit.addAction(actions["delete"])
        toolbars["edit"] = edit

        # Run Toolbar
        run = Toolbar("Run")
        run.setObjectName("_RunTb")
        run.addAction(actions["errors"])
        run.addAction(actions["generate"])
        run.addAction(actions["execute"])
        run.addAction(actions["kill"])
        toolbars["run"] = run

        # Misc Toolbar
        misc = Toolbar("Misc")
        misc.setObjectName("_MiscTb")
        misc.addAction(actions["reload"])
        toolbars["misc"] = misc

    def createStatusBar(self):
        log.debug("Creating status bar")
        self.statusBar().showMessage(_("ready-message"))

    def open(self):
        if self.currentFlowgraphScene.filename:
            dirname = os.path.dirname(self.currentFlowgraphScene.filename)
        else:
            dirname = os.getcwd()
        Open = QFileDialog.getOpenFileName
        filename, filtr = Open(
            self,  # parent
            self.actions["open"].statusTip(),  # caption
            dirname,  # dir
            filter="Flow Graph Files (*.grc);;All files (*.*)",
        )
        return filename

    def save(self):
        Save = QFileDialog.getSaveFileName
        filename, filtr = Save(
            self,
            self.actions["save"].statusTip(),
            filter="Flow Graph Files (*.grc);;All files (*.*)",
        )
        return filename

    # Overridden methods
    def addDockWidget(self, location, widget):
        """Adds a dock widget to the view."""
        # This overrides QT's addDockWidget so that a 'show' menu auto can automatically be
        # generated for this action.
        super().addDockWidget(location, widget)
        # This is the only instance where a controller holds a reference to a view it does not
        # actually control.
        name = widget.__class__.__name__
        log.debug("Generating show action item for widget: {0}".format(name))

        # Create the new action and wire it to the show/hide for the widget
        self.menus["panels"].addAction(widget.toggleViewAction())
        self.menus["panels"].setEnabled(True)

    def addToolBar(self, toolbar):
        """Adds a toolbar to the main window"""
        # This is also overridden so a show menu item can automatically be added
        super().addToolBar(toolbar)
        name = toolbar.windowTitle()
        log.debug("Generating show action item for toolbar: {0}".format(name))

        # Create the new action and wire it to the show/hide for the widget
        self.menus["toolbars"].addAction(toolbar.toggleViewAction())
        self.menus["toolbars"].setEnabled(True)

    def addMenu(self, menu):
        """Adds a menu to the main window"""
        help = self.menus["help"].menuAction()
        self.menuBar().insertMenu(help, menu)

    def populate_libraries_w_examples(self, example_tuple):
        examples, examples_w_block, designated_examples_w_block = example_tuple
        self.ExampleBrowser.populate(examples)
        self.app.BlockLibrary.populate_w_examples(examples_w_block, designated_examples_w_block)
        self.progress_bar.reset()
        self.progress_bar.hide()
        self.examples_found = True

    @pyqtSlot(tuple)
    def update_progress_bar(self, progress_tuple):
        progress, msg = progress_tuple
        self.progress_bar.show()
        self.progress_bar.setValue(progress)
        self.progress_bar.setTextVisible(True)
        self.progress_bar.setFormat(msg)

    def connect_fg_signals(self, scene: FlowgraphScene):
        scene.selectionChanged.connect(self.updateActions)
        scene.selectionChanged.connect(self.updateDocTab)
        scene.itemMoved.connect(self.registerMove)
        scene.newElement.connect(self.registerNewElement)
        scene.deleteElement.connect(self.registerDeleteElement)
        scene.blockPropsChange.connect(self.registerBlockPropsChange)

    # Action Handlers
    def new_triggered(self):
        log.debug("New")
        fg_view = FlowgraphView(self, self.platform)
        fg_view.centerOn(0, 0)
        initial_state = self.platform.parse_flow_graph("")
        fg_view.scene().import_data(initial_state)
        fg_view.scene().saved = False
        self.connect_fg_signals(fg_view.scene())
        log.debug("Adding flowgraph view")
        self.tabWidget.addTab(fg_view, "Untitled")
        self.tabWidget.setCurrentIndex(self.tabWidget.count() - 1)
        self.tabWidget.tabBar().setTabTextColor(self.tabWidget.currentIndex(), Qt.GlobalColor.red)

    def open_triggered(self, filename=None, save_allowed=True):
        log.debug("open")
        if not filename:
            filename = self.open()

        if filename:
            open_fgs = self.get_open_flowgraphs()
            if filename not in open_fgs:
                self.add_recent_file(filename)
                log.info("Opening flowgraph ({0})".format(filename))
                new_flowgraph = FlowgraphView(self, self.platform)
                initial_state = self.platform.parse_flow_graph(filename)
                self.tabWidget.addTab(new_flowgraph, os.path.basename(filename))
                self.tabWidget.setCurrentIndex(self.tabWidget.count() - 1)
                self.currentFlowgraphScene.import_data(initial_state)
                self.currentFlowgraphScene.filename = filename
                self.connect_fg_signals(self.currentFlowgraphScene)
                self.currentFlowgraphScene.saved = True
                self.currentFlowgraphScene.save_allowed = save_allowed
                self.currentFlowgraphScene.core.rewrite()
                self.currentFlowgraphScene.core.validate()
                for block in self.currentFlowgraphScene.core.blocks:
                    block.gui.create_shapes_and_labels()
                self.currentFlowgraphScene.update_elements_to_draw()
                if hasattr(self.app, 'VariableEditor'):
                    self.app.VariableEditor.set_scene(self.currentFlowgraphScene)
                self.updateActions()
            else:
                self.tabWidget.setCurrentIndex(open_fgs.index(filename))

    def open_example(self, example_path):
        log.debug("open example")
        if example_path:
            self.open_triggered(example_path, False)

    def save_triggered(self):
        if not self.currentFlowgraphScene.save_allowed:
            self.save_as_triggered()
            return
        log.debug("save")
        filename = self.currentFlowgraphScene.filename

        if filename:
            try:
                self.platform.save_flow_graph(filename, self.currentFlowgraph)
                self.currentFlowgraph.grc_file_path = filename
                self.add_recent_file(filename)
            except IOError:
                log.error("Save failed")
                return

            log.info(f"Saved {filename}")
            self.tabWidget.tabBar().setTabTextColor(self.tabWidget.currentIndex(),
                                                    self.palette().color(QPalette.ColorRole.WindowText))
            self.currentFlowgraphScene.set_saved(True)
        else:
            log.debug("Flowgraph does not have a filename")
            self.save_as_triggered()
        self.updateActions()

    def save_as_triggered(self):
        log.debug("Save As")
        file_dialog = QFileDialog()
        file_dialog.setWindowTitle(self.actions["save"].statusTip())
        file_dialog.setNameFilter('Flow Graph Files (*.grc)')
        file_dialog.setDefaultSuffix('grc')
        file_dialog.setAcceptMode(QFileDialog.AcceptMode.AcceptSave)
        if self.currentFlowgraphScene.filename:
            dirname = os.path.dirname(self.currentFlowgraphScene.filename)
        else:
            dirname = os.getcwd()
        file_dialog.setDirectory(dirname)
        filename = None
        if file_dialog.exec() == QFileDialog.DialogCode.Accepted:
            filename = file_dialog.selectedFiles()[0]

        if filename:
            self.currentFlowgraphScene.filename = filename
            try:
                self.platform.save_flow_graph(filename, self.currentFlowgraph)
            except IOError:
                log.error("Save (as) failed")
                return

            log.info(f"Saved (as) {filename}")
            self.tabWidget.tabBar().setTabTextColor(self.tabWidget.currentIndex(),
                                                    self.palette().color(QPalette.ColorRole.WindowText))
            self.currentFlowgraphScene.set_saved(True)
            self.tabWidget.setTabText(self.tabWidget.currentIndex(), os.path.basename(filename))
            self.add_recent_file(filename)
        else:
            log.debug("Cancelled Save As action")
        self.updateActions()

    def save_copy_triggered(self):
        log.debug("Save Copy")
        file_dialog = QFileDialog()
        file_dialog.setWindowTitle(self.actions["save"].statusTip())
        file_dialog.setNameFilter('Flow Graph Files (*.grc)')
        file_dialog.setDefaultSuffix('grc')
        file_dialog.setAcceptMode(QFileDialog.AcceptMode.AcceptSave)

        filename = None
        if file_dialog.exec() == QFileDialog.DialogCode.Accepted:
            filename = file_dialog.selectedFiles()[0]

        if filename:
            try:
                self.platform.save_flow_graph(filename, self.currentFlowgraph)
            except IOError:
                log.error("Save (copy) failed")

            log.info(f"Saved (copy) {filename}")
        else:
            log.debug("Cancelled Save Copy action")

    def tab_triggered(self, tab_index=None):
        """
        Switches to a tab.

        Parameters:
            tab_index: switches to tab(tab_index)
        """
        log.debug(f"Switching to tab (index {tab_index})")
        if tab_index < 0:
            return
        self.tabWidget.setCurrentIndex(tab_index)
        if hasattr(self.app, 'VariableEditor'):
            self.app.VariableEditor.set_scene(self.currentFlowgraphScene)
        self.updateActions()

    def close_triggered(self, tab_index=False) -> Union[str, bool]:
        """
        Closes a tab.

        Parameters:
            tab_index: specifies which tab to close. If none, close the open tab

        Returns:
            the file path OR True if a tab was closed (False otherwise)
        """
        log.debug(f"Closing a tab (index {tab_index})")

        file_path = self.currentFlowgraphScene.filename
        if tab_index == False:
            tab_index = self.tabWidget.currentIndex()

        if self.currentFlowgraphScene.saved:
            self.tabWidget.removeTab(tab_index)
        else:
            message = None
            if file_path:
                message = f"Save changes to {os.path.basename(file_path)} before closing? Your changes will be lost otherwise."
            else:
                message = "This flowgraph has not been saved"  # TODO: Revise text

            response = QMessageBox.question(
                None,
                "Save flowgraph?",
                message,
                QMessageBox.StandardButton.Discard |
                QMessageBox.StandardButton.Cancel |
                QMessageBox.StandardButton.Save,
            )

            if response == QMessageBox.StandardButton.Discard:
                file_path = self.currentFlowgraphScene.filename
                self.tabWidget.removeTab(tab_index)
            elif response == QMessageBox.StandardButton.Save:
                self.save_triggered()
                if self.currentFlowgraphScene.saved:
                    file_path = self.currentFlowgraphScene.filename
                    self.tabWidget.removeTab(tab_index)
                else:
                    return False
            else:  # Cancel
                return False

        if self.tabWidget.count() == 0:  # No tabs left
            self.new_triggered()
            return True
        else:
            return file_path

    def close_all_triggered(self):
        log.debug("close")

        while self.tabWidget.count() > 1:
            self.close_triggered()
        # Close the final tab
        self.close_triggered()

    def print_triggered(self):
        log.debug("print")

    def screen_capture_triggered(self):
        log.debug("screen capture")
        # TODO: Should be user-set somehow
        background_transparent = True

        if self.currentFlowgraphScene.filename:
            filename = self.currentFlowgraphScene.filename.split('.')[0] + '.pdf'
        else:
            filename = 'Untitled.pdf'
        Save = QFileDialog.getSaveFileName
        file_path, filtr = Save(
            self,
            self.actions["screen_capture"].statusTip(),
            filename,
            filter="PDF files (*.pdf);;PNG files (*.png);;SVG files (*.svg)",
        )
        if file_path is not None:
            try:
                Utils.make_screenshot(
                    self.currentView, file_path, background_transparent
                )
            except ValueError:
                log.error("Failed to generate screenshot")

    def undo_triggered(self):
        log.debug("undo")
        self.currentFlowgraphScene.undoStack.undo()
        self.currentFlowgraphScene.update()
        self.updateActions()

    def redo_triggered(self):
        log.debug("redo")
        self.currentFlowgraphScene.undoStack.redo()
        self.updateActions()

    def view_undo_stack_triggered(self):
        log.debug("view_undo_stack")
        self.undoView = QUndoView(self.currentFlowgraphScene.undoStack)
        self.undoView.setWindowTitle("Undo stack")
        self.undoView.show()

    def cut_triggered(self):
        log.debug("cut")
        self.copy_triggered()
        self.delete_triggered()
        self.updateActions()

    def copy_triggered(self):
        log.debug("copy")
        self.clipboard = self.currentFlowgraphScene.copy_to_clipboard()
        self.updateActions()

    def paste_triggered(self):
        log.debug("paste")
        if self.clipboard:
            self.currentFlowgraphScene.paste_from_clipboard(self.clipboard)
            self.currentFlowgraphScene.update()
        else:
            log.debug("clipboard is empty")

    def delete_triggered(self):
        log.debug("delete")
        self.currentFlowgraphScene.set_saved(False)
        self.tabWidget.tabBar().setTabTextColor(self.tabWidget.currentIndex(), Qt.GlobalColor.red)
        action = DeleteElementAction(self.currentFlowgraphScene)
        self.currentFlowgraphScene.undoStack.push(action)
        self.updateActions()
        self.currentFlowgraphScene.update()

    def select_all_triggered(self):
        log.debug("select_all")
        self.currentFlowgraphScene.select_all()
        self.updateActions()

    def select_none_triggered(self):
        log.debug("select_none")
        self.currentFlowgraphScene.clearSelection()
        self.updateActions()

    def rotate_ccw_triggered(self):
        # Pass to Undo/Redo
        log.debug("rotate_ccw")
        self.currentFlowgraphScene.set_saved(False)
        self.tabWidget.tabBar().setTabTextColor(self.tabWidget.currentIndex(), Qt.GlobalColor.red)
        rotateCommand = RotateAction(self.currentFlowgraphScene, -90)
        self.currentFlowgraphScene.undoStack.push(rotateCommand)
        self.updateActions()
        self.currentFlowgraphScene.update()

    def rotate_cw_triggered(self):
        # Pass to Undo/Redo
        log.debug("rotate_cw")
        self.currentFlowgraphScene.set_saved(False)
        self.tabWidget.tabBar().setTabTextColor(self.tabWidget.currentIndex(), Qt.GlobalColor.red)
        rotateCommand = RotateAction(self.currentFlowgraphScene, 90)
        self.currentFlowgraphScene.undoStack.push(rotateCommand)
        self.updateActions()
        self.currentFlowgraphScene.update()

    def toggle_source_bus_triggered(self):
        log.debug("toggle_source_bus")
        self.currentFlowgraphScene.set_saved(False)
        self.tabWidget.tabBar().setTabTextColor(self.tabWidget.currentIndex(), Qt.GlobalColor.red)
        bussifyCommand = BussifyAction(self.currentFlowgraphScene, 'source')
        self.currentFlowgraphScene.undoStack.push(bussifyCommand)
        self.updateActions()
        self.currentFlowgraphScene.update()

    def toggle_sink_bus_triggered(self):
        log.debug("toggle_sink_bus")
        self.currentFlowgraphScene.set_saved(False)
        self.tabWidget.tabBar().setTabTextColor(self.tabWidget.currentIndex(), Qt.GlobalColor.red)
        bussifyCommand = BussifyAction(self.currentFlowgraphScene, 'sink')
        self.currentFlowgraphScene.undoStack.push(bussifyCommand)
        self.updateActions()
        self.currentFlowgraphScene.update()

    def errors_triggered(self):
        log.debug("errors")
        err = ErrorsDialog(self.currentFlowgraph)
        err.exec()

    def module_browser_triggered(self):
        log.debug("oot browser")
        self.OOTBrowser.show()

    def zoom_in_triggered(self):
        log.debug("zoom in")
        self.currentView.zoom(1.1)

    def zoom_out_triggered(self):
        log.debug("zoom out")
        self.currentView.zoom(1.0 / 1.1)

    def zoom_original_triggered(self):
        log.debug("zoom to original size")
        self.currentView.zoomOriginal()

    def find_triggered(self):
        log.debug("find block")
        self._app().BlockLibrary._search_bar.clear()
        self._app().BlockLibrary._search_bar.setFocus()
        self._app().BlockLibrary.reset()

    def get_involved_triggered(self):
        log.debug("get involved")
        ad = QMessageBox()
        ad.setWindowTitle("Get Involved Instructions")
        ad.setText(
            """\
            <b>Welcome to the GNU Radio Community!</b><br/><br/>
            For more details on contributing to GNU Radio and getting engaged with our great community visit <a href="https://wiki.gnuradio.org/index.php/HowToGetInvolved">here</a>.<br/><br/>
            You can also join our <a href="https://chat.gnuradio.org/">Matrix chat server</a>, IRC Channel (#gnuradio) or contact through our <a href="https://lists.gnu.org/mailman/listinfo/discuss-gnuradio">mailing list (discuss-gnuradio)</a>.
        """
        )
        ad.exec()

    def guided_tutorials_triggered(self):
        log.debug("guided tutorials")
        QDesktopServices.openUrl(
            QUrl("https://tutorials.gnuradio.org")
        )

    def about_triggered(self):
        log.debug("about")
        config = self.platform.config
        py_version = sys.version.split()[0]
        QMessageBox.about(
            self, "About GNU Radio", f"GNU Radio {config.version} (Python {py_version})"
        )

    def about_qt_triggered(self):
        log.debug("about_qt")
        QApplication.instance().aboutQt()

    def properties_triggered(self):
        log.debug("properties")
        if len(self.currentFlowgraphScene.selected_blocks()) != 1:
            log.warning("Opening Properties even though selected_blocks() != 1 ")
        self.currentFlowgraphScene.selected_blocks()[0].open_properties()

    def enable_triggered(self):
        log.debug("enable")
        self.currentFlowgraphScene.set_saved(False)
        all_enabled = True
        for block in self.currentFlowgraphScene.selected_blocks():
            if not block.core.state == "enabled":
                all_enabled = False
                break

        if not all_enabled:
            cmd = EnableAction(self.currentFlowgraphScene)
            self.currentFlowgraphScene.undoStack.push(cmd)

        self.currentFlowgraphScene.update()
        self.updateActions()

    def disable_triggered(self):
        log.debug("disable")
        self.currentFlowgraphScene.set_saved(False)
        all_disabled = True
        for g_block in self.currentFlowgraphScene.selected_blocks():
            if not g_block.core.state == "disabled":
                all_disabled = False
                break

        if not all_disabled:
            cmd = DisableAction(self.currentFlowgraphScene)
            self.currentFlowgraphScene.undoStack.push(cmd)

        self.currentFlowgraphScene.update()
        self.updateActions()

    def bypass_triggered(self):
        log.debug("bypass")
        all_bypassed = True
        for g_block in self.currentFlowgraphScene.selected_blocks():
            if not g_block.core.state == "bypassed":
                all_bypassed = False
                break

        if not all_bypassed:
            cmd = BypassAction(self.currentFlowgraphScene)
            self.currentFlowgraphScene.undoStack.push(cmd)

        self.currentFlowgraphScene.update()
        self.updateActions()

    def block_inc_type_triggered(self):
        log.debug("block_inc_type")

    def block_dec_type_triggered(self):
        log.debug("block_dec_type")

    def generate_triggered(self, called_from_exec=False):
        log.debug("generate")
        if not self.currentFlowgraphScene.saved:
            self.save_triggered()
        if not self.currentFlowgraphScene.saved:  # The line above was cancelled
            log.error("Cannot generate a flowgraph without saving first")
            return

        filename = self.currentFlowgraphScene.filename
        self.currentFlowgraph.grc_file_path = filename

        if self.currentFlowgraph.get_option('generate_options').startswith('hb'):
            output_dir = paths.get_state_directory()
        else:
            output_dir = os.path.dirname(filename)

        generator = self.platform.Generator(
            self.currentFlowgraph, output_dir
        )
        generator.write(called_from_exec)
        self.currentView.generator = generator
        log.info(f"Generated {generator.file_path}")

    def execute_triggered(self):
        log.debug("execute")
        if self.currentView.process_is_done():
            self.generate_triggered(called_from_exec=True)
            if self.currentView.generator:
                xterm = self.app.qsettings.value("grc/xterm_executable", "")
                '''if self.config.xterm_missing() != xterm:
                    if not os.path.exists(xterm):
                        Dialogs.show_missing_xterm(main, xterm)
                    self.config.xterm_missing(xterm)'''
                if self.currentFlowgraphScene.saved and self.currentFlowgraphScene.filename:
                    # Save config before execution
                    # self.config.save()
                    ExecFlowGraphThread(
                        view=self.currentView,
                        flowgraph=self.currentFlowgraph,
                        xterm_executable=xterm,
                        update_gui_callback=self.updateActions
                    )

    def kill_triggered(self):
        log.debug("kill")

    def reload_triggered(self):
        log.debug("Reload hier blocks")
        self.app.BlockLibrary.reload_blocks()

        range_ = self.tabWidget.count()
        for idx in range(range_):
            self.rebuild_tab(idx)

        self.updateActions()

    def create_hier_triggered(self):

        selected_blocks = []

        source_fg = self.currentFlowgraphScene
        for block in source_fg.selected_blocks():
            selected_blocks.append(block.core)

        # Generate new page
        self.new_triggered()
        sink_fg = self.currentFlowgraphScene

        # Set flow graph to heir block type
        top_block = sink_fg.core.get_block(Constants.DEFAULT_FLOW_GRAPH_ID)
        # Check if hb or hb_qt is required
        gen_opts = 'hb'
        for block in selected_blocks:
            if block.label.upper().startswith('QT GUI'):
                gen_opts = 'hb_qt_gui'
                break
        top_block.params['generate_options'].set_value(gen_opts)

        # this needs to be a unique name
        top_block.params['id'].set_value('new_hier')
        # Remove default samp_rate
        remove_me = sink_fg.core.get_block("samp_rate")
        sink_fg.remove_element(remove_me.gui)

        self.clipboard = source_fg.copy_to_clipboard()
        self.paste_triggered()

        # For each variable generate a possible parameter block
        # The user has to decide if the hier block should use a variable or a paramter

        for variable in sink_fg.core.get_variables():
            id = sink_fg.add_block(
                'parameter',
                (variable.states['coordinate'][0] + variable.gui.width + 50, variable.states['coordinate'][1] + 50))
            param_block = sink_fg.core.get_block(id)
            param_block.params['id'].set_value(variable.name)

        for connection in source_fg.core.connections:
            # Get id of connected blocks
            source = connection.source_block
            sink = connection.sink_block
            if source not in selected_blocks and sink in selected_blocks:
                # Create pad source and add to canvas
                pad_source_key = int(connection.sink_port.key)
                pad_id = sink_fg.add_block(
                    'pad_source',
                    (source.states['coordinate'][0], source.states['coordinate'][1] + pad_source_key * 50))
                pad_block = sink_fg.core.get_block(pad_id)
                pad_source = pad_block.sources[0]
                sink_block = sink_fg.core.get_block(sink_fg.core.blocks[selected_blocks.index(sink) + 1].name)
                sink = sink_block.sinks[pad_source_key]
                # ensure the port types match
                if pad_source.dtype != sink.dtype:
                    if pad_source.dtype == 'complex' and sink.dtype == 'fc32':
                        pass
                    else:
                        pad_block.params['type'].value = sink.dtype
                        pad_source.dtype = sink.dtype
                new_connection = sink_fg.core.connect(pad_source, sink)
                sink_fg.addItem(new_connection.gui)

            elif sink not in selected_blocks and source in selected_blocks:
                # Create Pad Sink and add to canvas
                pad_sink_key = int(connection.source_port.key)
                pad_id = sink_fg.add_block(
                    'pad_sink', (sink.states['coordinate'][0], sink.states['coordinate'][1] + pad_sink_key * 50))
                pad_block = sink_fg.core.get_block(pad_id)
                pad_sink = pad_block.sinks[0]
                source_block = sink_fg.core.get_block(sink_fg.core.blocks[selected_blocks.index(source) + 1].name)
                source = source_block.sources[pad_sink_key]
                # ensure the port types match
                if pad_sink.dtype != source.dtype:
                    if pad_sink.dtype == 'complex' and source.dtype == 'fc32':
                        pass
                    else:
                        pad_block.params['type'].value = source.dtype
                        pad_sink.dtype = source.dtype
                new_connection = sink_fg.core.connect(source, pad_sink)
                sink_fg.addItem(new_connection.gui)

        sink_fg.clearSelection()
        sink_fg.update()
        self.updateActions()

    def open_hier_triggered(self):
        log.debug("Open hier block triggered")
        for block in self.currentFlowgraphScene.selected_blocks():
            grc_source = block.core.extra_data.get('grc_source', '')
            if grc_source:
                self.open_triggered(grc_source)

    def rebuild_tab(self, idx):
        fgscene = self.tabWidget.widget(idx).scene()
        log.info("Rebuilding flowgraph ({0})".format(fgscene.filename))
        data = fgscene.core.export_data()
        fgblocks = fgscene.core.blocks
        for block in fgblocks:
            fgscene.removeItem(block.gui)
        for conn in fgscene.core.connections:
            fgscene.removeItem(conn.gui)
        fgscene.import_data(data)

    def add_recent_file(self, file_name):
        # double check file_name
        if os.path.exists(file_name):
            recent_files = self.recent_files
            if file_name in recent_files:
                recent_files.remove(file_name)  # Attempt removal
            recent_files.insert(0, file_name)  # Insert at start
            self.recent_files = recent_files[:self.max_recent_files]

            # Now add files to menu entries
            actions_list = self.menus["open_recent"].actions()
            for i in range(len(self.recent_files)):
                actions_list[i].setText(self.recent_files[i])
                actions_list[i].setVisible(True)

    def show_help(parent):
        """Display basic usage tips."""
        message = """\
            <b>Usage Tips</b>
            \n\
            <u>Add block</u>: drag and drop or double click a block in the block
        selection window.
            <u>Rotate block</u>: Select a block, press left/right on the keyboard.
            <u>Change type</u>: Select a block, press up/down on the keyboard.
            <u>Edit parameters</u>: double click on a block in the flow graph.
            <u>Make connection</u>: click on the source port of one block, then
        click on the sink port of another block.
            <u>Remove connection</u>: select the connection and press delete, or
        drag the connection.
            \n\
            *Press Ctrl+K or see menu for Keyboard - Shortcuts
            \
        """

        ad = QMessageBox()
        ad.setWindowTitle("Help")
        ad.setText(message)
        ad.exec()

    def types_triggered(self):
        log.debug("types")
        colors = [(name, color) for name, key, sizeof, color in Constants.CORE_TYPES]

        message = """
        <table>
        <tbody>
        """

        message += "\n".join(
            '<tr bgcolor="{color}"><td><tt>{name}</tt></td></tr>'
            "".format(color=color, name=name)
            for name, color in colors
        )
        message += "</tbody></table>"
        ad = QMessageBox()
        ad.setWindowTitle("Stream Types")
        ad.setText(message)
        ad.exec()

    def keys_triggered(self):
        log.debug("keys")

        message = """\
            <b>Keyboard Shortcuts</b>
            <br><br>
            <u>Ctrl+N</u>: Create a new flowgraph.<br>
            <u>Ctrl+O</u>: Open an existing flowgraph.<br>
            <u>Ctrl+S</u>: Save the current flowgraph or save as for new.<br>
            <u>Ctrl+W</u>: Close the current flowgraph.<br>
            <u>Ctrl+Z</u>: Undo a change to the flowgraph.<br>
            <u>Ctrl+Y</u>: Redo a change to the flowgraph.<br>
            <u>Ctrl+A</u>: Selects all blocks and connections.<br>
            <u>Ctrl+P</u>: Screen Capture of the Flowgraph.<br>
            <u>Ctrl+Shift+P</u>: Save the console output to file.<br>
            <u>Ctrl+L</u>: Clear the console.<br>
            <u>Ctrl+E</u>: Show variable editor.<br>
            <u>Ctrl+F</u>: Search for a block by name.<br>
            <u>Ctrl+Q</u>: Quit.<br>
            <u>F1</u>    : Help menu.<br>
            <u>F5</u>    : Generate the Flowgraph.<br>
            <u>F6</u>    : Execute the Flowgraph.<br>
            <u>F7</u>    : Kill the Flowgraph.<br>
            <u>Ctrl+Shift+S</u>: Save as the current flowgraph.<br>
            <u>Ctrl+Shift+D</u>: Create a duplicate of current flow graph.<br>

            <u>Ctrl+X/C/V</u>: Edit-cut/copy/paste.<br>
            <u>Ctrl+D/B/R</u>: Toggle visibility of disabled blocks or
                    connections/block tree widget/console.<br>
            <u>Shift+T/M/B/L/C/R</u>: Vertical Align Top/Middle/Bottom and
                    Horizontal Align Left/Center/Right respectively of the
                    selected block.<br>
            <u>Ctrl+0</u>: Reset the zoom level<br>
            <u>Ctrl++/-</u>: Zoom in and out<br>
            \
            """

        ad = QMessageBox()
        ad.setWindowTitle("Keyboard shortcuts")
        ad.setText(message)
        ad.exec()

    def preferences_triggered(self):
        log.debug("preferences")
        prefs_dialog = PreferencesDialog(self.app.qsettings)
        if prefs_dialog.exec():  # User pressed Save
            prefs_dialog.save_all()
            self.currentFlowgraphScene.update()

    def open_recent_triggered(self):
        log.debug("open_recent")
        self.open_triggered(self.sender().text())

    def example_browser_triggered(self, key_filter: Union[str, None] = None):
        log.debug("example-browser")
        if self.examples_found:
            self.ExampleBrowser.reset()
            ex_dialog = ExampleBrowserDialog(self.ExampleBrowser)
            if len(ex_dialog.browser.examples_dict) == 0:
                ad = QMessageBox()
                ad.setWindowTitle("GRC: No examples found")
                ad.setText("GRC did not find any examples. Please ensure that the example path in grc.conf is correct.")
                ad.exec()
                return

            if isinstance(key_filter, str):
                if not ex_dialog.browser.filter_(key_filter):
                    ad = QMessageBox()
                    ad.setWindowTitle("GRC: No examples")
                    ad.setText("There are no examples for this block.")
                    ad.exec()
                    return
            else:
                ex_dialog.browser.reset()

            ex_dialog.exec()
        else:
            ad = QMessageBox()
            ad.setWindowTitle("GRC still indexing examples")
            ad.setText("GRC is still indexing examples, please try again shortly.")
            ad.exec()

    def exit_triggered(self):
        log.debug("exit")

        files_open = []
        range_ = reversed(range(self.tabWidget.count()))
        for idx in range_:  # Close the rightmost first. It'll be the first element in files_open
            tab = self.tabWidget.widget(idx)
            file_path = self.currentFlowgraphScene.filename
            if file_path:
                files_open.append(file_path)
            closed = self.close_triggered()
            if closed == False:
                # We cancelled closing a tab. We don't want to close the application
                return

        # Save the panel settings
        # Console
        self.app.qsettings.setValue('appearance/display_console', not self.app.Console.isHidden())
        # Block Library
        self.app.qsettings.setValue('appearance/display_blocklibrary', not self.app.BlockLibrary.isHidden())
        # Wiki
        self.app.qsettings.setValue('appearance/display_wiki', not self.app.WikiTab.isHidden())
        # Variable Editor
        self.app.qsettings.setValue('appearance/display_variable_editor', not self.app.VariableEditor.isHidden())

        # Write the leftmost tab to file first
        self.app.qsettings.setValue('window/files_open', reversed(files_open))
        self.app.qsettings.setValue('window/windowState', self.saveState())
        self.app.qsettings.setValue('window/geometry', self.saveGeometry())
        self.app.qsettings.sync()

        # TODO: Make sure all flowgraphs have been saved
        self.app.exit()

    def closeEvent(self, evt):
        log.debug("Close Event")
        self.exit_triggered()
        evt.ignore()

    def help_triggered(self):
        log.debug("help")
        self.show_help()

    def report_triggered(self):
        log.debug("report")

    def library_triggered(self):
        log.debug("library_triggered")

    def library_toggled(self):
        log.debug("library_toggled")

    def filter_design_tool_triggered(self):
        log.debug("filter_design_tool")
        subprocess.Popen(
            "gr_filter_design",
            shell=True,
            stdout=subprocess.PIPE,
            stderr=subprocess.STDOUT,
        )

    def start_profiler_triggered(self):
        log.info("Starting profiler")
        self.profiler.enable()

    def stop_profiler_triggered(self):
        self.profiler.disable()
        log.info("Stopping profiler")
        stats = pstats.Stats(self.profiler)
        stats.dump_stats('stats.prof')

    def get_open_flowgraphs(self):
        index = self.tabWidget.count()
        if index == 0:
            return []
        result = []
        range_ = range(index)
        for fg_nr in range_:
            self.tabWidget.setCurrentIndex(fg_nr)
            result.append(self.currentFlowgraphScene.filename)
        return result
