import os
from glob import glob
import signal
import json
import webbrowser
from PySide6.QtWidgets import QStyle # for PM_ScrollBarExtent
from PySide6.QtWidgets import QLabel
from PySide6.QtWidgets import QMenu
from PySide6.QtWidgets import QWidget, QSizePolicy
from PySide6.QtWidgets import QPlainTextEdit
from PySide6.QtCore import Qt
from PySide6.QtCore import QObject
from PySide6.QtCore import Slot
from PySide6.QtCore import QUrl
from PySide6.QtGui import QIcon
from PySide6.QtGui import QAction
from PySide6.QtGui import QGuiApplication
from PySide6.QtGui import QDesktopServices
from mp_main_window import MPMainWindow
from version_file import VERSION
import resources_rc
from main_splitter import MainSplitter
from mp_code_column import MPCodeColumn
from navigation_column import NavigationColumn
from graph_list_selection_spinner import GraphListSelectionSpinner
from main_graph_scene import MainGraphScene
from main_graph_view import MainGraphView
from graph_list_view import GraphListView
from graph_list_table_dialog import GraphListTableDialog
from navigation_column_width_manager import NavigationColumnWidthManager
from scope_spinner import ScopeSpinner
from graph_metadata_label import GraphMetadataLabel
from graphs_manager import GraphsManager
from graph_list_table_model import GraphListTableModel
from graph_list_sort_filter_proxy_model import GraphListSortFilterProxyModel
from graph_list_selection_model import GraphListSelectionModel
from mp_code_manager import MPCodeManager
from mp_code_view import MPCodeView
from mp_code_change_tracker import MPCodeChangeTracker
from edit_menu import EditMenu
from code_editor_settings_menu import make_code_editor_settings_menu
from gry_manager import GryManager
from settings_themes_menu import SettingsThemesMenu
from socket_client_endpoint_menu import SocketClientEndpointMenu
from graph_pane_menu import GraphPaneMenu
from export_trace_manager import ExportTraceManager
from mp_logger import set_logger_targets, log, log_to_pane, log_to_statusbar, \
                    clear_log, begin_timestamps, timestamp, show_timestamps
from mp_style import mp_menu_button, MenuBarStatusTipEventRemover
import mp_json_io_manager
from trace_generator_manager import TraceGeneratorManager, \
                                    TraceGeneratorCallback
from preferences import preferences
from preferences_manager import PreferencesManager
from settings import settings
from settings_manager import SettingsManager
from spellcheck_whitelist_manager import SpellcheckWhitelistManager
from spellcheck_whitelist_dialog_wrapper import SpellcheckWhitelistDialogWrapper
from session_persistence import SessionPersistence
from session_snapshots import SessionSnapshots
from session_snapshots_dialog_wrapper import snapshot_files
from keyboard_shortcuts_dialog import KeyboardShortcutsDialog
from about_mp_dialog_wrapper import AboutMPDialogWrapper
from search_mp_files_dialog_wrapper import SearchMPFilesDialogWrapper
from model_statistics_dialog import ModelStatisticsDialog
from tg_to_gry import tg_to_gry_graphs
from empty_gry_graph import empty_gry_graph
from paths_trace_generator import trace_generator_paths
from paths_trace_generator_manager import change_trace_generator_paths
from paths_examples import is_bundled, \
                           examples_paths, \
                           change_examples_paths, \
                           schedule_validate_examples_paths
from paths_gryphon import TRACE_GENERATED_OUTFILE_GRY
from examples_menu import fill_examples_menu
from report_an_issue import report_an_issue
from startup_options import parse_startup_options
from mp_file_dialog import get_open_filename, get_save_filename, \
                           get_existing_directory
from mp_code_parser import mp_code_schema
from mp_code_filename import active_mp_code_filename, \
                             set_active_mp_code_filename
from verbose import verbose

class GUIManager(QObject):

    """MP main window containing menu, toolbar, statusbar, and central widget.
    Central widget contains split pane areas, ref. http://firebird.nps.edu/:
      code_area
      console_area
      graph_area
      navigation_column
    """

    def __init__(self, application):
        super(GUIManager, self).__init__()
        self.application = application

        # the main window
        self.w = MPMainWindow(self)
        self.w.setWindowTitle("Monterey Phoenix v4 - Gryphon GUI %s"%VERSION)
        self.w.setWindowIcon(QIcon(":/icons/mp_logo"))

        # user preferences manager
        self.preferences_manager = PreferencesManager(self)

        # graph settings manager
        self.settings_manager = SettingsManager(self.w,
                         self.preferences_manager.signal_preferences_changed)

        # session persistence
        self.session_persistence = SessionPersistence(self)

        # session snapshots
        self.session_snapshots = SessionSnapshots(self)

        # spellcheck manager
        self.spellcheck_whitelist_manager = SpellcheckWhitelistManager()

        # spellcheck whitelist dialog wrapper
        self.spellcheck_whitelist_dialog_wrapper = \
              SpellcheckWhitelistDialogWrapper(self.w,
                                         self.spellcheck_whitelist_manager)

        # the graph list table model
        self.graph_list_table_model = GraphListTableModel()

        # the graphs manager
        self.graphs_manager = GraphsManager(self.graph_list_table_model,
                                            self.settings_manager)

        # the graph list width manager
        self.navigation_column_width_manager = NavigationColumnWidthManager(
                  self.w.style().pixelMetric(
                                   QStyle.PixelMetric.PM_ScrollBarExtent))

        # the main splitter which emits size_changed
        self.main_splitter = MainSplitter(self.navigation_column_width_manager)

        # the graph list proxy model
        self.graph_list_proxy_model = GraphListSortFilterProxyModel(
                       self.graph_list_table_model,
                       self.preferences_manager.signal_preferences_changed,
                       self.settings_manager.signal_settings_changed)

        # the graph list selection model
        self.graph_list_selection_model = GraphListSelectionModel(
                                        self.graph_list_table_model,
                                        self.graph_list_proxy_model)

        # the graph list view
        self.graph_list_view = GraphListView(
                         self.main_splitter,
                         self.graphs_manager,
                         self.graph_list_proxy_model,
                         self.graph_list_selection_model,
                         self.navigation_column_width_manager,
                         self.preferences_manager.signal_preferences_changed)

        # the graph list table dialog
        self.graph_list_table_dialog = GraphListTableDialog(self.w,
                                            self.graph_list_proxy_model,
                                            self.graph_list_selection_model)

        # the main graph scene and view
        self.main_graph_view = MainGraphView(
                                   self.graphs_manager.signal_graphs_loaded)
        self.graph_pane_menu = GraphPaneMenu(self)
        self.main_graph_scene = MainGraphScene(self.graph_list_table_model,
                                               self.graph_list_selection_model,
                                               self.settings_manager,
                                               self.graph_pane_menu)
        self.main_graph_view.setScene(self.main_graph_scene)
        self.main_graph_view.scale_view(preferences["graph_pane_scale"])

        # the schema name and scope metadata label
        self.graph_metadata_label = GraphMetadataLabel(self.graphs_manager)

        # the statusbar
        self.statusbar = self.w.statusBar()
        self.statusbar.addPermanentWidget(self.graph_metadata_label.status_text)
        self.statusbar.showMessage("Open, Import, or Compose MP Code to begin.")

        # the log pane and the logger
        self.log_pane = QPlainTextEdit()
        self.log_pane.setLineWrapMode(QPlainTextEdit.LineWrapMode.NoWrap)
        self.log_pane.setReadOnly(True)
        set_logger_targets(self.statusbar, self.log_pane)

        # the trace generator manager which runs the compiler asynchronously
        self.trace_generator_callback = TraceGeneratorCallback()
        self.trace_generator_manager = TraceGeneratorManager(
                                self.trace_generator_callback.send_signal)
        self.trace_generator_callback.signal_compile_response.connect(
                                        self.response_compile_mp_code)
        # the MP Code manager
        self.mp_code_manager = MPCodeManager(
                  self.preferences_manager.signal_preferences_changed,
                  self.settings_manager.signal_settings_changed,
                  self.spellcheck_whitelist_manager.signal_spellcheck_changed,
                  self.statusbar)

        # the MP Code change tracker
        self.mp_code_change_tracker = MPCodeChangeTracker(self)

        # persistent dialogs
        self.search_mp_files_dialog_wrapper = SearchMPFilesDialogWrapper(
                  self.w,
                  self.preferences_manager.signal_preferences_changed,
                  self.settings_manager.signal_settings_changed,
                  self.spellcheck_whitelist_manager.signal_spellcheck_changed,
                  self.open_mp_code)
        self.model_statistics_dialog = ModelStatisticsDialog(
                         self.w, self.graphs_manager,
                         self.mp_code_manager.signal_mp_code_loaded,
                         self.preferences_manager.signal_preferences_changed)

        # the two MP Code editor view windows
        self.mp_code_view_1 = MPCodeView(self.mp_code_manager,
                   self.preferences_manager,
                   self.spellcheck_whitelist_manager,
                   self.spellcheck_whitelist_dialog_wrapper,
                   self.search_mp_files_dialog_wrapper.action_search_mp_files,
                   self.application,
                   1)
        self.mp_code_view_2 = MPCodeView(self.mp_code_manager,
                   self.preferences_manager,
                   self.spellcheck_whitelist_manager,
                   self.spellcheck_whitelist_dialog_wrapper,
                   self.search_mp_files_dialog_wrapper.action_search_mp_files,
                   self.application,
                   2)

        # .gry manager
        self.gry_manager = GryManager(self.w, self.graphs_manager,
                                      self.mp_code_manager,
                                      self.graph_list_selection_model,
                                      self.settings_manager,
                                      self.mp_code_change_tracker)

        # the scope spinner containing spinner=QSpinBox
        self.scope_spinner = ScopeSpinner(
                                  self.mp_code_manager.signal_mp_code_loaded)

        # export trace manager
        self.export_trace_manager = ExportTraceManager(self.w,
                                          self.graphs_manager,
                                          self.graph_list_selection_model,
                                          application)

        # graph list selection spinner
        self.graph_list_selection_spinner = GraphListSelectionSpinner(
                                self.graph_list_proxy_model,
                                self.graph_list_selection_model)

        # the central widget containing the main split pane
        self.mp_code_column = MPCodeColumn(self)
        self.mp_code_column.set_splitter_sizes(
                  preferences["code_column_splitter_sizes"])
        self.navigation_column = NavigationColumn(self)
        self.main_splitter.addWidget(self.mp_code_column)
        self.main_splitter.addWidget(self.main_graph_view)
        self.main_splitter.addWidget(self.navigation_column)
        self.main_splitter.setSizes(preferences["main_splitter_sizes"])
        self.w.setCentralWidget(self.main_splitter)

        # actions and menus
        self.define_actions()
        self.define_menus()

        # the toolbar
        self.add_toolbar()

        # current compile state for buttons
        self.set_is_compiling(False)

        # state for preference actions
        self.preferences_manager.set_action_states()

        # do these before shutdown
        application.aboutToQuit.connect(
                      self.trace_generator_manager.mp_clean_shutdown)
        application.aboutToQuit.connect(
                      self.preferences_manager.save_preferences)
        application.aboutToQuit.connect(
                      self.session_persistence.save_this_session)
        application.aboutToQuit.connect(self._clear_clipboard)

        # get command line options
        parse_startup_options(self)

        # enqueue actions to start on Qt event loop
        if not is_bundled():
            schedule_validate_examples_paths(self.w)

        self.w.show()

        # accept Ctrl C
        signal.signal(signal.SIGINT, self.make_ctrl_c_closure_function())

    @Slot()
    def _clear_clipboard(self):
        # Qt crashes with:
        # QFontDatabase: Must construct a QGuiApplication before accessing
        # QFontDatabase
        # Aborted (core dumped)
        # so we clear it when Gryphon closes to avoid this crash.
        QGuiApplication.clipboard().clear()

    # enable graceful exit by Ctrl C
    def make_ctrl_c_closure_function(self):
        # signal handler for signal.signal
        def ctrl_c_closure_function(_signal, _frame):
            print("Ctrl C exit")
            self.w.close()
        return ctrl_c_closure_function

    # actions
    def define_actions(self):
        # action exit
        self.action_exit = QAction(QIcon(":/icons/exit"), "&Exit", self.w)
        self.action_exit.setShortcut('Ctrl+Q')
        self.action_exit.setToolTip("Exit MP")
        self.action_exit.triggered.connect(self.w.close)

        # action keyboard_shortcuts
        self.action_keyboard_shortcuts = QAction("&Keyboard Shortcuts", self.w)
        self.action_keyboard_shortcuts.setToolTip(
                                        "Keyboard shorcuts for MP controls")
        self.action_keyboard_shortcuts.triggered.connect(
                                        self.keyboard_shortcuts)

        # action about
        self.action_about = QAction(QIcon(":/icons/about_mp"),
                                   "&About MP", self)
        self.action_about.setToolTip("about MP")
        self.action_about.triggered.connect(self.about_mp)

        # action online documentation
        self.action_online_documentation = QAction(QIcon(
                   ":/icons/documentation"), "Online &documentation", self)
        self.action_online_documentation.setToolTip(
                                     "Online documentation for Gryphon")
        self.action_online_documentation.triggered.connect(
                                              self.online_documentation)

        # action report an issue
        self.action_report_an_issue = QAction("&Report an issue...", self)
        self.action_report_an_issue.setToolTip("Report an issue about MP")
        self.action_report_an_issue.triggered.connect(self._report_an_issue)

        # action run/cancel
        self.action_run_cancel = QAction(self)
        self.action_run_cancel.setToolTip("Generate traces from MP Code")
        self.action_run_cancel.triggered.connect(
                                        self._run_cancel_compile_mp_code)

        # action show model statistics
        self.action_show_model_statistics = QAction(
                             QIcon(":/icons/show_model_statistics"),
                             "&Show model statistics...", self)
        self.action_show_model_statistics.setToolTip(
                                           "Show statistics about models")
        self.action_show_model_statistics.triggered.connect(
                                          self._show_model_statistics)

        # action clear log
        self.action_clear_log = QAction(QIcon(":/icons/clear_log"),
                                       "&Clear status log", self)
        self.action_clear_log.setToolTip("Clear the status log pane")
        self.action_clear_log.triggered.connect(clear_log)

        # action open MP Code file
        self.action_open_mp_code_file = QAction(QIcon(":/icons/open"),
                                                "&Open .mp...", self)
        self.action_open_mp_code_file.setToolTip("Open MP Code File")
        self.action_open_mp_code_file.triggered.connect(
                                           self.select_and_open_mp_code_file)

        # action close MP Code file
        self.action_close_mp_code_file = QAction(QIcon(":/icons/close"),
                                                 "Close .mp", self)
        self.action_close_mp_code_file.setToolTip("Close MP Code File")
        self.action_close_mp_code_file.triggered.connect(self.close_mp_code)

        # action save MP Code file
        self.action_save_mp_code_file = QAction(QIcon(":/icons/save"),
                                                "&Save .mp", self)
        self.action_save_mp_code_file.setShortcut('Ctrl+S')
        self.action_save_mp_code_file.setToolTip("Save MP Code file")
        self.action_save_mp_code_file.triggered.connect(
                                           self.save_mp_code_file)

        # action save MP Code file as
        self.action_save_mp_code_file_as = QAction(QIcon(":/icons/save"),
                                                "Save .mp &as...", self)
        self.action_save_mp_code_file_as.setToolTip("Save MP Code file")
        self.action_save_mp_code_file_as.triggered.connect(
                                           self.save_mp_code_file_as)

        # action settings trace generator path...
        self.action_trace_generator_path = QAction(
                                     "Trace Generator Path...", self)
        self.action_trace_generator_path.setToolTip(
                                     "Set the path to the trace-generator")
        self.action_trace_generator_path.triggered.connect(
                                     self.select_trace_generator_path)

        # action settings preloaded examples path...
        self.action_examples_path = QAction(
                                     "Preloaded Examples Path...", self)
        self.action_examples_path.setToolTip(
                                     "Set the path to preloaded examples")
        self.action_examples_path.triggered.connect(
                                     self.select_examples_path)
        self.action_examples_path.setDisabled(is_bundled())

    def _configure_file_menu(self):
        # disable save when there is no active filename
        self.action_save_mp_code_file.setEnabled(bool(
                                                 active_mp_code_filename()))

        # disable actions when compiling
        is_compiling = self._is_compiling
        self.action_open_mp_code_file.setDisabled(is_compiling)
        self.action_close_mp_code_file.setDisabled(is_compiling)
        self.open_examples_menu.setDisabled(is_compiling)
        self.gry_manager.action_import_gry_file.setDisabled(is_compiling)
        self.gry_manager.action_export_gry_file.setDisabled(is_compiling)
        self.export_trace_manager.action_export_trace.setDisabled(is_compiling)
        self.export_trace_manager.action_export_all_traces.setDisabled(
                                                                  is_compiling)
        self.session_persistence.action_restore_previous_session.setDisabled(
                                                                  is_compiling)

        # disable managing snapshots when there are no snapshots
        has_snapshots = bool(len(snapshot_files()))
        self.session_snapshots.action_manage_snapshots.setDisabled(
                                                         not has_snapshots)

    def _configure_settings_menu(self):
        self.settings_manager.action_show_type_1_probability.setChecked(
                               settings["trace_show_type_1_probability"])

    def _fill_examples_menu(self):
        if os.path.exists(examples_paths["models"]):
            fill_examples_menu(self.open_examples_menu,
                               examples_paths["models"],
                               self.open_mp_code)
        else:
            print("Examples path is not configured.")

    def _configure_save_menu(self):
        # disable save in toolbar's save menu
        self.action_save_mp_code_file.setEnabled(bool(
                                                active_mp_code_filename()))

    def _enable_manage_snapshots(self):
        has_snapshots = bool(len(snapshot_files()))
        self.session_snapshots.action_manage_snapshots.setDisabled(
                                                         not has_snapshots)
    # menus
    def define_menus(self):
        menubar = self.w.menuBar()
        menubar.setNativeMenuBar(False)
        menubar.installEventFilter(MenuBarStatusTipEventRemover(self.w))

        # menu | file
        self.file_menu = QMenu("&File")
        menubar.addMenu(self.file_menu)
        self.file_menu.aboutToShow.connect(self._configure_file_menu)

        # menu | file | open
        self.file_menu.addAction(self.action_open_mp_code_file)

        # menu | file | open examples
        self.open_examples_menu = QMenu("Open Example")
        self.open_examples_menu.setToolTip("Open MP Code Example")
        self.open_examples_menu.aboutToShow.connect(
                                    self._fill_examples_menu)

        # menu | file | save MP Code
        self.file_menu.addAction(self.action_save_mp_code_file)

        # menu | file | save MP Code as
        self.file_menu.addAction(self.action_save_mp_code_file_as)

        # menu | file | close
        self.file_menu.addAction(self.action_close_mp_code_file)

        # menu | file | separator
        self.file_menu.addSeparator()

        # menu | file | open examples
        self.file_menu.addMenu(self.open_examples_menu)

        # menu | file | search .mp files
        self.file_menu.addAction(
                   self.search_mp_files_dialog_wrapper.action_search_mp_files)

        # menu | file | separator
        self.file_menu.addSeparator()

        # menu | file | Save snapshot
        self.file_menu.addAction(self.session_snapshots.action_take_snapshot)

        # menu | file | Manage snapshots
        self.file_menu.addAction(self.session_snapshots.action_manage_snapshots)

        # menu | file | session persistence
        self.file_menu.addAction(
                     self.session_persistence.action_restore_previous_session)

        # menu | file | separator
        self.file_menu.addSeparator()

        # menu | file | import Gryphon Graph file
        self.file_menu.addAction(self.gry_manager.action_import_gry_file)

        # menu | file | export Gryphon Graph file
        self.file_menu.addAction(self.gry_manager.action_export_gry_file)

        # menu | file | separator
        self.file_menu.addSeparator()

        # menu | file | export trace
        self.file_menu.addAction(self.export_trace_manager.action_export_trace)

        # menu | file | export all traces
        self.file_menu.addAction(
                       self.export_trace_manager.action_export_all_traces)

        # menu | file | separator
        self.file_menu.addSeparator()

        # menu | file | exit
        self.file_menu.addAction(self.action_exit)

        # menu | edit
        self.edit_menu = menubar.addMenu(EditMenu(self))

        # menu | actions
        actions_menu = menubar.addMenu('&Actions')

        # menu | actions | run
        actions_menu.addAction(self.action_run_cancel)

        # menu | actions | show model statistics
        actions_menu.addAction(self.action_show_model_statistics)

        # menu | actions | clear log
        actions_menu.addAction(self.action_clear_log)

        # menu | settings
        self.settings_menu = menubar.addMenu('&Settings')
        self.settings_menu.aboutToShow.connect(self._configure_settings_menu)

        # menu | settings | use dark mode
        self.settings_menu.addAction(
                            self.preferences_manager.action_use_dark_mode)

        # menu | settings | show type 1 probability
        self.settings_menu.addAction(
                    self.settings_manager.action_show_type_1_probability)

        # menu | settings | separator
        self.settings_menu.addSeparator()

        # menu | settings | themes
        self.settings_themes_menu = SettingsThemesMenu(self.w,
                                                   self.settings_manager)
        self.settings_menu.addMenu(self.settings_themes_menu)

        # menu | settings | code editor preferences
        self._code_editor_settings_menu = make_code_editor_settings_menu(self)
        self.settings_menu.addMenu(self._code_editor_settings_menu)

        # menu | settings | graph pane preferences
        self.settings_menu.addMenu(self.graph_pane_menu)

        # menu | settings | graph list navigation pane preferences
        self.settings_menu.addMenu(self.preferences_manager.graph_list_menu)

        # menu | settings | reset GUI preferences
        self.settings_menu.addAction(
                         self.preferences_manager.action_reset_preferences)

        # menu | settings | separator
        self.settings_menu.addSeparator()

        # menu | settings | ignore model statistics warnings
        self.settings_menu.addAction(self.model_statistics_dialog
                                  .action_ignore_model_statistics_warnings)

        # menu | settings | separator
        self.settings_menu.addSeparator()

        # menu | settings | Gryphon configuration
        gryphon_configuration_menu = self.settings_menu.addMenu(
                                                 'Gryphon configuration')
        gryphon_configuration_menu.addAction(self.action_trace_generator_path)
        gryphon_configuration_menu.addAction(self.action_examples_path)
        self.socket_client_endpoint_menu = SocketClientEndpointMenu()
        gryphon_configuration_menu.addMenu(self.socket_client_endpoint_menu)

        # menu | help
        help_menu = menubar.addMenu('&Help')
        help_menu.addAction(self.action_about)
        help_menu.addAction(self.action_keyboard_shortcuts)
        help_menu.addAction(self.action_online_documentation)
        help_menu.addAction(self.action_report_an_issue)

    # the toolbar
    def add_toolbar(self):
        toolbar = self.w.addToolBar("MP_Py Toolbar")
        toolbar.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonTextBesideIcon)
        self.open_menu = QMenu()
        self.open_menu.addAction(self.action_open_mp_code_file)
        self.open_menu.addMenu(self.open_examples_menu)
        self.open_menu.addAction(
                   self.search_mp_files_dialog_wrapper.action_search_mp_files)
        self.open_menu_button = mp_menu_button(self.open_menu,
                                               QIcon(":/icons/open"),
                                               "Open",
                                               "Open MP schema file")
        toolbar.addWidget(self.open_menu_button)
        self.save_menu = QMenu()
        self.save_menu.aboutToShow.connect(self._configure_save_menu)
        self.save_menu.addAction(self.action_save_mp_code_file)
        self.save_menu.addAction(self.action_save_mp_code_file_as)
        self.save_menu_button = mp_menu_button(self.save_menu,
                                               QIcon(":/icons/save"),
                                               "Save",
                                               "SaveMP schema file")
        toolbar.addWidget(self.save_menu_button)
        toolbar.addSeparator()
        toolbar.addAction(self.action_run_cancel)
        toolbar.addSeparator()
        toolbar.addWidget(QLabel("Scope"))
        toolbar.addWidget(self.scope_spinner.spinner)
        toolbar.addSeparator()
        toolbar.addWidget(self.main_graph_scene.trace_event_menu \
                                             .trace_event_menu_button)
        toolbar.addSeparator()
        toolbar.addAction(self.graph_pane_menu.action_zoom_in)
        toolbar.addAction(self.graph_pane_menu.action_zoom_out)
        toolbar.addAction(self.graph_pane_menu.action_zoom_reset)

        # far right side
        spacer = QWidget()
        spacer.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
        toolbar.addWidget(spacer)
        toolbar.addAction(self.preferences_manager.action_use_dark_mode)

    ############################################################
    # action handlers
    ############################################################

    # Keyboard Shortcuts...
    @Slot()
    def keyboard_shortcuts(self):
        dialog = KeyboardShortcutsDialog(self.w)
        dialog.show()

    # About MP...
    @Slot()
    def about_mp(self):
        wrapper = AboutMPDialogWrapper(self.w)
        wrapper.show()

    # Online documentation
    @Slot()
    def online_documentation(self):
        QDesktopServices.openUrl(QUrl("https://gitlab.nps.edu/"
                   "monterey-phoenix/user-interfaces/gryphon/-/wikis/home"))

    # Report an issue...
    @Slot()
    def _report_an_issue(self):
        report_an_issue(self.w)

    # Open MP Code file
    @Slot()
    def select_and_open_mp_code_file(self):
        filename = get_open_filename(self.w,
                        "Open MP Code file",
                        preferences["preferred_mp_code_dir"],
                        "MP Code files (*.mp);;All Files (*)")

        if filename:
            # remember the preferred path
            head, _tail = os.path.split(filename)
            preferences["preferred_mp_code_dir"] = head

            # open the file
            self.open_mp_code(filename)

        else:
            status = "Open filename not selected."
            log(status)

    # Save MP Code file
    @Slot()
    def save_mp_code_file(self):

        mp_code_text = self.mp_code_manager.text()

        # suggested filename
        filename = active_mp_code_filename()
        if not filename:
            raise RuntimeError("bad")

        # The full path is the preferred directory plus the suggested filename.
        # This allows us to use the user's folder for opened examples.
        full_mp_code_filename = os.path.join(
                              preferences["preferred_mp_code_dir"], filename)

        # save the file
        status = mp_json_io_manager.save_mp_code_file(
                         mp_code_text, full_mp_code_filename)
        if status:
            # log failure
            log(status)
        else:
            # great, exported.
            log("Saved to file %s" % full_mp_code_filename)

            # unset mp code change flag
            self.mp_code_change_tracker.unset_is_modified()

    # Save MP Code file as
    @Slot()
    def save_mp_code_file_as(self):

        mp_code_text = self.mp_code_manager.text()

        # suggested filename
        currently_active_filename = active_mp_code_filename()
        if currently_active_filename:
            # use the actively opened filename
            suggested_filename = currently_active_filename
        else:
            # use the schema name with .mp added
            schema_name = mp_code_schema(mp_code_text)
            suggested_filename = "%s.mp" % schema_name

        # The full path is the preferred directory plus the suggested filename.
        # This allows us to use the user's folder for opened examples.
        full_suggested_filename = os.path.join(
                   preferences["preferred_mp_code_dir"], suggested_filename)

        full_mp_code_filename = get_save_filename(self.w,
                   "Save MP Code file", full_suggested_filename,
                   "MP Code files (*.mp);;All Files (*)")

        if full_mp_code_filename:
            # save the file
            status = mp_json_io_manager.save_mp_code_file(
                         mp_code_text, full_mp_code_filename)
            if status:
                # log failure
                log(status)
            else:
                # great, exported.
                log("Saved to file %s" % full_mp_code_filename)

                # remember the preferred path and the active filename
                head, _tail = os.path.split(full_mp_code_filename)
                preferences["preferred_mp_code_dir"] = head
                set_active_mp_code_filename(full_mp_code_filename)

                # unset mp code change flag
                self.mp_code_change_tracker.unset_is_modified()

            # callers don't usually care about status
            return status

        else:
            status = "Save filename not selected."
            log(status)
            return status

    # Find MP code file
    @Slot()
    def find_mp_code_file(self):
        wrapper = FindMPFileDialogWrapper(self.w)
        wrapper.show()

    @Slot()
    def select_trace_generator_path(self):
        path = get_existing_directory(self.w,
                   "Select the path to the MP trace-generator",
                   trace_generator_paths["trace_generator_root"])

        if path:
            change_trace_generator_paths(self.w, path)

    @Slot()
    def select_examples_path(self):
        path = get_existing_directory(self.w,
                   "Select the path to the preloaded examples",
                   examples_paths["examples_root"])

        if path:
            change_examples_paths(self.w, path)

    ############################################################
    # primary interfaces
    ############################################################
    # open MP Code file
    def open_mp_code(self, mp_code_filename):
        # maybe abort request
        status = self.mp_code_change_tracker.maybe_save_discard_or_cancel()
        if status:
            log(status)
            return

        status, mp_code_text = mp_json_io_manager.read_mp_code_file(
                                                         mp_code_filename)

        if status:
            # log failure
            log(status)
        else:
            # accept request
            log("Opened MP Code file %s" % mp_code_filename)

            # set mp code
            self.mp_code_manager.set_text(mp_code_filename, mp_code_text)

            # clear the selection
            self.graph_list_selection_model.select_graph_index(-1)

            # clear the graph
            self.graphs_manager.clear_graphs()

    # Close MP Code file
    @Slot()
    def close_mp_code(self):
        # maybe abort request
        status = self.mp_code_change_tracker.maybe_save_discard_or_cancel()
        if status:
            log(status)
            return

        # set mp code
        self.mp_code_manager.set_text("", "")

        # clear the selection
        self.graph_list_selection_model.select_graph_index(-1)

        # clear the graph
        self.graphs_manager.clear_graphs()

    # compile MP Code
    def _request_compile_mp_code(self):
        # state at start of compilation
        self._proposed_mp_code = self.mp_code_manager.text()
        self._proposed_scope = self.scope_spinner.scope()
        self._proposed_schema_name = mp_code_schema(self._proposed_mp_code)
        self._proposed_settings = settings.copy()
        self._existing_mp_code = self.graphs_manager.mp_code
        self._existing_graphs = self.graphs_manager.graphs
        self._existing_scope = self.graphs_manager.scope
        self._existing_selected_graph_index \
                   = self.graph_list_selection_model.selected_graph_index()

        # set visual state
        self.set_is_compiling(True)

        # begin compilation
        action = "Compiling %s scope %d..."%(self._proposed_schema_name,
                                             self._proposed_scope)
        log_to_statusbar(action)
        begin_timestamps(action)
        self.trace_generator_manager.mp_compile(
                           self._proposed_schema_name,
                           self._proposed_scope,
                           self._proposed_mp_code)

        # clear the selected graph and the graphs
        self.graph_list_selection_model.select_graph_index(-1)
        self.graphs_manager.clear_graphs()

    # receive trace-generated json from the compiled MP Code
    @Slot(str, dict, str)
    def response_compile_mp_code(self, status, tg_data, _log):

        # restore visual state
        self.set_is_compiling(False)

        # disable run button while initializing response
        self.action_run_cancel.setDisabled(True)

        # log the compilation log
        log_to_pane(_log)
        timestamp("Received compilation response")

        # compensate for invalid empty JSON that the trace generator
        # returns when no traces are generated
        if status == "Error running trace-generator: Expecting" \
                     " value: line 1 column 1 (char 0)":
            if verbose():
                log(status)
            status = ""

        if status:
            # log failure
            log(status)

        else:

            # accept graphs
            gry_graphs = tg_to_gry_graphs(tg_data)
            timestamp("Created gry from tg")
            log("Compiled %s"%self._proposed_schema_name)

            # gry data
            self.graphs_manager.set_graphs(
                                           self._proposed_mp_code,
                                           gry_graphs,
                                           self._proposed_scope,
                                          )
            timestamp("Created graphs from gry")

            # the default selected graph index is 1 else 0
            if "traces" in tg_data and len(tg_data["traces"]) > 0:
                selected_graph_index = 1
            else:
                selected_graph_index = 0
            self.graph_list_selection_model.select_graph_index(
                                                   selected_graph_index)

            # for diagnostics only, and only if it is not large,
            # maybe write the generated .gry JSON, indented with sorted keys
            with open(TRACE_GENERATED_OUTFILE_GRY, "w", encoding='utf-8') as f:
                if len(gry_graphs) < 1000:
                    # prepare the generated .gry JSON, indented with sorted keys
                    gry_data = {
                           "mp_code_current":self.mp_code_manager.text(),
                           "mp_code_runtime":self._proposed_mp_code,
                           "graphs":gry_graphs,
                           "scope":self._proposed_scope,
                           "selected_graph_index":selected_graph_index,
                           "settings":self._proposed_settings,
                               }
                else:
                    # too many graphs so prepare .gry without graphs
                    print("Note: Large graph size %d, graphs not written"
                                                    %len(gry_graphs))
                    gry_data = {
                           "mp_code_current":self.mp_code_manager.text(),
                           "mp_code_runtime":self._proposed_mp_code,
                           "graphs":[empty_gry_graph()],
                           "scope":self._proposed_scope,
                           "selected_graph_index":-1,
                           "settings":self._proposed_settings,
                               }

                json.dump(gry_data, f, indent=4, sort_keys=True)

        # reenable run button after initializing response
        self.action_run_cancel.setDisabled(False)

        # set visual state
        self.set_is_compiling(False)
        show_timestamps()

    # cancel compile MP Code
    def _cancel_compile_mp_code(self):
        # cancel compilation
        log("Canceling compiling %s" % self._proposed_schema_name)
        self.trace_generator_manager.mp_cancel_compile()

        # restore graph data and the selected graph
        self.graphs_manager.restore_graphs(self._existing_mp_code,
                                           self._existing_graphs,
                                           self._existing_scope)
        self.graph_list_selection_model.select_graph_index(
                                   self._existing_selected_graph_index)

    def _run_cancel_compile_mp_code(self):
        if self._is_compiling:
            self._cancel_compile_mp_code()
        else:
            self._request_compile_mp_code()

    def _show_model_statistics(self):
        self.model_statistics_dialog.show()

    def _set_run_cancel_button_state(self):
        if self._is_compiling:
            self.action_run_cancel.setIcon(QIcon(":/icons/cancel"))
            self.action_run_cancel.setText("Stop")
        else:
            self.action_run_cancel.setIcon(QIcon(":/icons/run"))
            self.action_run_cancel.setText("Run")

    # run and cancel widget state
    def set_is_compiling(self, is_compiling):

        self._is_compiling = is_compiling

        # run/cancel button
        self._set_run_cancel_button_state()

        # disable some toolbar buttons since they are always visible
        self.open_menu_button.setDisabled(is_compiling)
        self.main_graph_scene.trace_event_menu.trace_event_menu_button \
                                                  .setDisabled(is_compiling)
        self.scope_spinner.spinner.setDisabled(is_compiling)