# 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 qtpy import QtCore, QtGui, QtWidgets
from qtpy.QtCore import Qt

# 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

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

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


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

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

        # Setup the window icon
        icon = QtGui.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.BottomLeftCorner, Qt.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)
        # QtCore.QMetaObject.connectSlotsByName(self)

        # Translation support

        # self.setWindowTitle(_translate("blockLibraryDock", "Library", None))
        # library.headerItem().setText(0, _translate("blockLibraryDock", "Blocks", None))
        # QtCore.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 = QtWidgets.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):
                    self.open_triggered(file)
                    grc_file_found = 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 = QtCore.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()
    """

    @QtCore.Slot(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

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

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

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

    @QtCore.Slot(Element)
    def registerBlockPropsChange(self, elem):
        self.currentFlowgraphScene.set_saved(False)
        self.tabWidget.tabBar().setTabTextColor(self.tabWidget.currentIndex(), Qt.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.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["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["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 = QtWidgets.QFileDialog.getOpenFileName
        filename, filtr = Open(
            self,
            self.actions["open"].statusTip(),
            dir=dirname,
            filter="Flow Graph Files (*.grc);;All files (*.*)",
        )
        return filename

    def save(self):
        Save = QtWidgets.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

    @QtCore.Slot(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.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(self.palette().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 = QtWidgets.QFileDialog()
        file_dialog.setWindowTitle(self.actions["save"].statusTip())
        file_dialog.setNameFilter('Flow Graph Files (*.grc)')
        file_dialog.setDefaultSuffix('grc')
        file_dialog.setAcceptMode(QtWidgets.QFileDialog.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_() == QtWidgets.QFileDialog.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(self.palette().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 = QtWidgets.QFileDialog()
        file_dialog.setWindowTitle(self.actions["save"].statusTip())
        file_dialog.setNameFilter('Flow Graph Files (*.grc)')
        file_dialog.setDefaultSuffix('grc')
        file_dialog.setAcceptMode(QtWidgets.QFileDialog.AcceptSave)

        filename = None
        if file_dialog.exec_() == QtWidgets.QFileDialog.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=None) -> 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 is None:
            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 = QtWidgets.QMessageBox.question(
                None,
                "Save flowgraph?",
                message,
                QtWidgets.QMessageBox.Discard |
                QtWidgets.QMessageBox.Cancel |
                QtWidgets.QMessageBox.Save,
            )

            if response == QtWidgets.QMessageBox.Discard:
                file_path = self.currentFlowgraphScene.filename
                self.tabWidget.removeTab(tab_index)
            elif response == QtWidgets.QMessageBox.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 = QtWidgets.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 = QtWidgets.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.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.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.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.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.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 = QtWidgets.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 about_triggered(self):
        log.debug("about")
        config = self.platform.config
        py_version = sys.version.split()[0]
        QtWidgets.QMessageBox.about(
            self, "About GNU Radio", f"GNU Radio {config.version} (Python {py_version})"
        )

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

    def properties_triggered(self):
        log.debug("properties")
        if len(self.currentFlowgraphScene.selected_blocks()) != 1:
            log.warn("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):
        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
        generator = self.platform.Generator(
            self.currentFlowgraph, os.path.dirname(filename)
        )
        generator.write()
        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()
            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,
                        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 = QtWidgets.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 = QtWidgets.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 = QtWidgets.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 = QtWidgets.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 = QtWidgets.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 = QtWidgets.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
