-
Allen, Bruce (CIV) authoredAllen, Bruce (CIV) authored
gui_manager.py 28.66 KiB
import os
import webbrowser
from PyQt5.QtWidgets import QAction
from PyQt5.QtWidgets import QFileDialog
from PyQt5.QtWidgets import QStyle # for PM_ScrollBarExtent
from PyQt5.QtWidgets import qApp
from PyQt5.QtWidgets import QLabel
from PyQt5.QtGui import QIcon
from PyQt5.QtCore import Qt
from PyQt5.QtCore import QObject
from PyQt5.QtCore import pyqtSlot
from version_file import VERSION
from main_splitter import MainSplitter
from mp_code_column import MPCodeColumn
from navigation_column import NavigationColumn
from graph_list_selection_spinner import GraphListSelectionSpinner
from graph_main_widget import GraphMainWidget
from graph_list_table import GraphListTable
from graph_list_sort_and_filter import GraphListSortAndFilter
from navigation_column_width_manager import NavigationColumnWidthManager
from scope_spinner import ScopeSpinner
from graph_metadata_label import GraphMetadataLabel
from graphs_manager import GraphsManager
from mp_code_editor import MPCodeEditor
from logger import Logger
import mp_code_io_manager
from mp_code_syntax_checker import parse_error_line_number, MPCodeSyntaxChecker
from trace_generator_manager import TraceGeneratorManager
import export_trace_manager
from settings_manager import SettingsManager, SETTINGS_THEMES
from settings_dialog_wrapper import SettingsDialogWrapper
from keyboard_dialog_wrapper import KeyboardDialogWrapper
from about_mp_dialog_wrapper import AboutMPDialogWrapper
from event_menu import EventMenu
from path_constants import MP_CODE_EXAMPLES
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, main_window):
super(GUIManager, self).__init__()
# user's preferred MP Code path
self.preferred_mp_code_dir = os.path.expanduser("~")
# user's preferred JSON Graph Gryphon file path
self.preferred_gry_file_dir = os.path.expanduser("~")
# user's preferred JPG trace path
self.preferred_trace_dir = os.path.expanduser("~")
# the settings manager
self.settings_manager = SettingsManager()
# the graphs manager
self.graphs_manager = GraphsManager(self.settings_manager)
# the graph list width manager
self.navigation_column_width_manager = NavigationColumnWidthManager(
main_window.style().pixelMetric(QStyle.PM_ScrollBarExtent))
# state
self.w = main_window
# main window decoration
self.w.setGeometry(0, 0, 850, 800)
self.w.setWindowTitle("Monterey Phoenix v4 - Gryphon GUI %s"%VERSION)
self.w.setWindowIcon(QIcon('icons/MP-logo-small-blue.png'))
# the main splitter which emits size_changed
main_splitter = MainSplitter(self.navigation_column_width_manager)
# the scope spinner containing spinner=QSpinBox
self.scope_spinner = ScopeSpinner()
# the graph list table which responds to events
self.graph_list_table = GraphListTable(
main_splitter,
self.graphs_manager,
self.navigation_column_width_manager)
# the graph list sort and filter control widget
self.graph_list_sort_and_filter = GraphListSortAndFilter(
self.graph_list_table)
# the graph main widget
self.graph_main_widget = GraphMainWidget(self.graphs_manager,
self.graph_list_table)
# the graph list widget repaints when a node coordinate in the
# main widget changes
self.graph_main_widget.signal_graph_index_view_changed.connect(
self.graph_list_table.graph_index_view_changed)
# the event menu
self.event_menu = EventMenu(self.graphs_manager,
self.graph_list_table,
self.settings_manager)
# the graph status widget
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 logger containing log_pane=QPlainTextEdit
self.logger = Logger(self.statusbar)
# the trace generator manager which runs the compiler asynchronously
self.trace_generator_manager = TraceGeneratorManager()
self.trace_generator_manager.signal_compile_response.connect(
self.response_compile_mp_code)
# the MP Code editor
self.mp_code_editor = MPCodeEditor(self.settings_manager,
self.statusbar)
# the syntax checker
self.mp_code_syntax_checker = MPCodeSyntaxChecker(self.mp_code_editor,
self.trace_generator_manager)
self.mp_code_syntax_checker.signal_syntax_report.connect(
self.mp_code_editor.set_syntax_status)
# setup
self.define_actions()
self.define_menus()
self.define_toolbar()
# graph list selection spinner
self.graph_list_selection_spinner = GraphListSelectionSpinner(
self.graphs_manager, self.graph_list_table)
# the central widget containing the main split pane
self.mp_code_column = MPCodeColumn(self)
self.mp_code_column.set_sizes([600, 200])
self.navigation_column = NavigationColumn(self)
main_splitter.addWidget(self.mp_code_column)
main_splitter.addWidget(self.graph_main_widget.view)
main_splitter.addWidget(self.navigation_column)
main_splitter.setSizes([250, 500, 100])
self.w.setCentralWidget(main_splitter)
# clean shutdown
qApp.aboutToQuit.connect(self.trace_generator_manager.mp_clean_shutdown)
self.w.show()
# actions
def define_actions(self):
# action exit
self.action_exit = QAction(QIcon("icons/application-exit.png"),
"&Exit", self.w)
self.action_exit.setShortcut('Ctrl+Q')
self.action_exit.setStatusTip("Exit MP")
self.action_exit.triggered.connect(self.w.close)
# action help
self.action_help = QAction(QIcon("icons/help-contents-5.png"),
"&Help", self.w)
self.action_help.setShortcut('Ctrl+H')
self.action_help.setStatusTip("Help using MP")
self.action_help.triggered.connect(self.help_mp)
# action keyboard_shortcuts
self.action_keyboard_shortcuts = QAction("&Keyboard Shortcuts", self.w)
self.action_keyboard_shortcuts.setStatusTip(
"Keyboard shorcuts for MP controls")
self.action_keyboard_shortcuts.triggered.connect(
self.keyboard_shortcuts)
# action about
self.action_about = QAction(QIcon("icons/document-properties.png"),
"&About MP", self)
self.action_about.setStatusTip("about MP")
self.action_about.triggered.connect(self.about_mp)
# action run
self.action_run = QAction(QIcon("icons/run-build-install.png"),
"&Run", self)
self.action_run.setShortcut('Ctrl+R')
self.action_run.setStatusTip("Generate traces from MP Code")
self.action_run.triggered.connect(self.request_compile_mp_code)
# action cancel
self.action_cancel = QAction(QIcon("icons/dialog-cancel-3.png"),
"&Stop", self)
self.action_cancel.setStatusTip("Cancel trace generation process")
self.action_cancel.triggered.connect(self.cancel_compile_mp_code)
self.action_cancel.setDisabled(True)
# action clear log
self.action_clear_log = QAction(QIcon("icons/edit-clear-list.png"),
"&Clear MP Code Log", self)
self.action_clear_log.setShortcut('Ctrl+L')
self.action_clear_log.setStatusTip("Clear MP Code log")
self.action_clear_log.triggered.connect(self.logger.clear_log)
# action open MP Code file
self.action_open_mp_code_file = QAction(QIcon(
"icons/document-open-2.png"), "&Open...", self)
self.action_open_mp_code_file.setStatusTip("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/document-close.png"), "Close", self)
self.action_close_mp_code_file.setStatusTip("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/document-save-2.png"), "&Save...", self)
self.action_save_mp_code_file.setStatusTip("Save MP Code file")
self.action_save_mp_code_file.triggered.connect(
self.save_mp_code_file)
# action import JSON Graph Format .gry file
self.action_import_gry_file = QAction(QIcon(
"icons/document-import-2.png"), "&Import...", self)
self.action_import_gry_file.setStatusTip("Import Gryphon Graph file")
self.action_import_gry_file.triggered.connect(
self.select_and_import_gry_file)
# action export JSON Graph Format .gry file
self.action_export_gry_file = QAction(QIcon(
"icons/document-export-4.png"), "&Export...", self)
self.action_export_gry_file.setStatusTip("Export Gryphon Graph file")
self.action_export_gry_file.triggered.connect(
self.select_and_export_gry_file)
# action export trace as image file
self.action_export_trace = QAction("&Export Trace...", self)
self.action_export_trace.setStatusTip("Export trace as image file")
self.action_export_trace.triggered.connect(
self.select_and_export_trace)
# action export all traces as image files
self.action_export_all_traces = QAction("&Export All Traces...", self)
self.action_export_all_traces.setStatusTip(
"Export all traces as image files")
self.action_export_all_traces.triggered.connect(
self.select_and_export_all_traces)
# action settings custom...
self.action_settings_custom = QAction("&Custom...", self)
self.action_settings_custom.setStatusTip(
"Draw using custom settings")
self.action_settings_custom.triggered.connect(
self.set_settings_custom)
# menus
def define_menus(self):
menubar = self.w.menuBar()
menubar.setNativeMenuBar(False)
# menu | file
file_menu = menubar.addMenu('&File')
# menu | file | open
file_menu.addAction(self.action_open_mp_code_file)
# menu | file | close
file_menu.addAction(self.action_close_mp_code_file)
# menu | file | open examples
self.open_examples_menu = file_menu.addMenu("Open Example")
self.open_examples_menu.setStatusTip("Open MP Code Example")
for filename in sorted(os.listdir(MP_CODE_EXAMPLES)):
if filename.endswith(".mp"):
# add action with dedicated lambda function containing filename
action = QAction(filename, self)
action.triggered.connect(lambda checked, filename=
os.path.join(MP_CODE_EXAMPLES, filename):
self.open_mp_code(filename))
self.open_examples_menu.addAction(action)
# menu | file | save MP Code
file_menu.addAction(self.action_save_mp_code_file)
# menu | file | separator
file_menu.addSeparator()
# menu | file | import Gryphon Graph file
file_menu.addAction(self.action_import_gry_file)
# menu | file | export Gryphon Graph file
file_menu.addAction(self.action_export_gry_file)
# menu | file | separator
file_menu.addSeparator()
# menu | file | export trace
file_menu.addAction(self.action_export_trace)
# menu | file | export all traces
file_menu.addAction(self.action_export_all_traces)
# menu | file | separator
file_menu.addSeparator()
# menu | file | exit
file_menu.addAction(self.action_exit)
# menu | actions
actions_menu = menubar.addMenu('&Actions')
# menu | actions | run
actions_menu.addAction(self.action_run)
# menu | actions | cancel
actions_menu.addAction(self.action_cancel)
# menu | actions | separator
file_menu.addSeparator()
# menu | actions | clear log
actions_menu.addAction(self.action_clear_log)
# menu | preferences
preferences_menu = menubar.addMenu('&Preferences')
# menu | preferences | settings
settings_menu = preferences_menu.addMenu("Settings")
# menu | preferences | settings options
for theme_name, theme in SETTINGS_THEMES:
# add action with dedicated lambda function containing filename
action = QAction(theme_name, self)
action.triggered.connect(lambda checked, theme=theme:
self.settings_manager.change(theme))
settings_menu.addAction(action)
settings_menu.addSeparator()
settings_menu.addAction(self.action_settings_custom)
# # menu | preferences | separator
# preferences_menu.addSeparator()
# menu | help
help_menu = menubar.addMenu('&Help')
help_menu.addAction(self.action_help)
help_menu.addAction(self.action_keyboard_shortcuts)
help_menu.addAction(self.action_about)
# toolbar items
def define_toolbar(self):
toolbar = self.w.addToolBar("MP_Py Toolbar")
toolbar.setToolButtonStyle(Qt.ToolButtonTextBesideIcon)
toolbar.addAction(self.action_open_mp_code_file)
toolbar.addWidget(self.scope_spinner.spinner)
toolbar.addWidget(QLabel("Scope"))
toolbar.addAction(self.action_run)
toolbar.addAction(self.action_cancel)
toolbar.addWidget(self.event_menu.event_menu_pb)
############################################################
# action handlers
############################################################
# help...
@pyqtSlot()
def help_mp(self):
path = os.path.abspath("../doc/mp_py_um/mp_py_um.pdf")
if not os.path.exists(path):
path = os.path.abspath("pdf/mp_py_um.pdf")
if os.path.exists(path):
webbrowser.open("file://%s"%path)
else:
self.logger.log("Error: Missing help file.")
# Keyboard Shortcuts...
@pyqtSlot()
def keyboard_shortcuts(self):
wrapper = KeyboardDialogWrapper(self.w)
wrapper.show()
# About MP...
@pyqtSlot()
def about_mp(self):
wrapper = AboutMPDialogWrapper(self.w)
wrapper.show()
# Open MP Code file
@pyqtSlot()
def select_and_open_mp_code_file(self):
options = QFileDialog.Options()
options |= QFileDialog.DontUseNativeDialog
filename, _ = QFileDialog.getOpenFileName(self.w,
"Open MP Code file",
self.preferred_mp_code_dir,
"MP Code files (*.mp);;All Files (*)", options=options)
if filename:
# remember the preferred path
head, _tail = os.path.split(filename)
self.preferred_mp_code_dir = head
# open the file
self.open_mp_code(filename)
# Save MP Code file
@pyqtSlot()
def save_mp_code_file(self):
options = QFileDialog.Options()
options |= QFileDialog.DontUseNativeDialog
# suggested filename
schema_name = self.mp_code_editor.schema_name()
if not schema_name:
schema_name = "unnamed"
suggested_filename = "%s.mp" % schema_name
mp_code_filename, _ = QFileDialog.getSaveFileName(self.w,
"Save MP Code file",
os.path.join(self.preferred_mp_code_dir, suggested_filename),
"MP Code files (*.mp);;All Files (*)", options=options)
if mp_code_filename:
# remember the preferred path
head, _tail = os.path.split(mp_code_filename)
self.preferred_mp_code_dir = head
# save the file
status = mp_code_io_manager.save_mp_code_file(
self.mp_code_editor.text(), mp_code_filename)
if status:
# log failure
self.logger.log(status)
else:
# great, exported.
self.logger.log("Saved to file %s" % mp_code_filename)
# import Gryphon Graph file
@pyqtSlot()
def select_and_import_gry_file(self):
options = QFileDialog.Options()
options |= QFileDialog.DontUseNativeDialog
filename, _ = QFileDialog.getOpenFileName(self.w,
"Import Gryphon Graph file",
self.preferred_gry_file_dir,
"Gryphon graph files (*.gry);;All Files (*)", options=options)
if filename:
# remember the preferred path
head, _tail = os.path.split(filename)
self.preferred_gry_file_dir = head
# import the file
self.import_gry_file(filename)
# export Gryphon Graph file
@pyqtSlot()
def select_and_export_gry_file(self):
options = QFileDialog.Options()
options |= QFileDialog.DontUseNativeDialog
# suggested filename
schema_name = self.mp_code_editor.schema_name()
if not schema_name:
schema_name = "unnamed"
suggested_filename = "%s_scope_%d.gry" % (schema_name,
self.scope_spinner.scope())
filename, _ = QFileDialog.getSaveFileName(self.w,
"Export Gryphon Graph file",
os.path.join(self.preferred_gry_file_dir, suggested_filename),
"Gryphon graph files (*.gry);;All Files (*)", options=options)
if filename:
# remember the preferred path
head, _tail = os.path.split(filename)
self.preferred_gry_file_dir = head
# export the file
self.export_gry_file(filename)
# export trace as image file
@pyqtSlot()
def select_and_export_trace(self):
graph = self.graph_list_table.selected_graph_item()
if not graph:
self.logger.log("Error exporting trace: There are no traces")
return
options = QFileDialog.Options()
options |= QFileDialog.DontUseNativeDialog
# suggested filename
schema_name = self.mp_code_editor.schema_name()
if not schema_name:
schema_name = "unnamed"
suggested_filename = "%s_%03d.png" % (schema_name, (graph.index+1))
filename, _ = QFileDialog.getSaveFileName(self.w,
"Export trace image file",
os.path.join(self.preferred_trace_dir, suggested_filename),
"MP trace image files (*.png);;All Files (*)", options=options)
if filename:
# remember the preferred path
head, _tail = os.path.split(filename)
self.preferred_trace_dir = head
# export the trace as image file
self.export_trace(filename, graph)
# export traces as image files under path
@pyqtSlot()
def select_and_export_all_traces(self):
if not self.graphs_manager.graphs:
self.logger.log("Error exporting trace: There are no traces")
return
options = QFileDialog.Options()
options |= QFileDialog.DontUseNativeDialog
# suggested filename
schema_name = self.mp_code_editor.schema_name()
if not schema_name:
schema_name = "unnamed"
filename_prefix, _ = QFileDialog.getSaveFileName(self.w,
"Export serialized <File name>_nnn.png trace image files",
os.path.join(self.preferred_trace_dir, schema_name),
"MP trace image files (*.png);;All Files (*)", options=options)
if filename_prefix:
# remember the preferred path
head, _tail = os.path.split(filename_prefix)
self.preferred_trace_dir = head
# export the file
# export the traces as serialized image files
self.export_serialized_traces(filename_prefix)
@pyqtSlot()
def set_settings_custom(self):
wrapper = SettingsDialogWrapper(self.w, self.settings_manager)
wrapper.exec_()
############################################################
# primary interfaces
############################################################
# open MP Code file
def open_mp_code(self, mp_code_filename):
status, mp_code_text = mp_code_io_manager.read_mp_code_file(
mp_code_filename)
if status:
# log failure
self.logger.log(status)
else:
# accept request
self.logger.clear_log()
self.logger.log("Opened MP Code file %s" % mp_code_filename)
self.mp_code_editor.set_text(mp_code_text)
self.graphs_manager.clear_graphs()
self.graph_main_widget.reset_view_orientation()
# set visual state
self.set_is_compiling(False)
# Close MP Code file
@pyqtSlot()
def close_mp_code(self):
# accept request
self.logger.clear_log()
self.mp_code_editor.set_text("")
self.graphs_manager.clear_graphs()
self.graph_main_widget.reset_view_orientation()
# compile MP Code
@pyqtSlot()
def request_compile_mp_code(self):
# compile
scope = self.scope_spinner.scope()
schema_name = self.mp_code_editor.schema_name()
mp_code_text = self.mp_code_editor.text()
# set visual state
self.set_is_compiling(True)
self.logger.log("Compiling %s..." % schema_name)
self.trace_generator_manager.mp_compile(
schema_name, scope, mp_code_text)
# receive response from trace generator manager request to compile MP Code
@pyqtSlot(str, str, str)
def response_compile_mp_code(self, status, generated_json_text, log):
# log the compilation log
self.logger.log(log)
if status:
# log failure
self.logger.log(status)
# set line number
line_number = parse_error_line_number(status)
self.mp_code_editor.set_error_line_number(line_number)
else:
# accept request
status, graphs = mp_code_io_manager.read_generated_json(
generated_json_text)
if status:
# log failure
self.logger.log(status)
# clear graphs
self.graphs_manager.clear_graphs()
else:
# accept graphs
scope = self.scope_spinner.scope()
schema_name = self.mp_code_editor.schema_name()
self.graphs_manager.set_graphs(schema_name, scope, graphs)
self.logger.log("Compiled %s" % schema_name)
# select graph at first row
# ref. https://stackoverflow.com/questions/6925951/how-to-select-a-row-in-a-qlistview
if graphs:
self.graph_list_table.select_graph(0)
# set visual state
self.set_is_compiling(False)
# cancel compile MP Code
@pyqtSlot()
def cancel_compile_mp_code(self):
# cancel
schema_name = self.mp_code_editor.schema_name()
self.logger.log("Canceling compiling %s" % schema_name)
self.trace_generator_manager.mp_cancel_compile()
# import Gryphon graph file
def import_gry_file(self, gry_file_filename):
self.logger.log("Importing Gryphon Graph file %s..." %
gry_file_filename)
status, mp_code_text, scope, selected_index, \
scale, x_slider, y_slider, graphs = \
mp_code_io_manager.import_gry_file(gry_file_filename)
if status:
# log failure
self.logger.log(status)
else:
# accept import request
self.logger.clear_log()
# set view to match JSON graph specifications
self.mp_code_editor.set_text(mp_code_text)
schema_name = self.mp_code_editor.schema_name()
self.graphs_manager.set_graphs(schema_name, scope, graphs)
self.graph_list_table.select_graph(selected_index)
self.graph_main_widget.set_view_orientation(scale,
x_slider, y_slider)
self.logger.log("Imported Gryphon Graph file %s" %
gry_file_filename)
# set visual state
self.set_is_compiling(False)
# export Gryphon Graph file
def export_gry_file(self, gry_file_filename):
scale, x_slider, y_slider = \
self.graph_main_widget.get_view_orientation()
index = self.graph_list_table.selected_graph()
status = mp_code_io_manager.export_gry_file(
gry_file_filename,
self.mp_code_editor.text(),
self.scope_spinner.scope(),
index,
scale, x_slider, y_slider,
self.graphs_manager.graphs)
if status:
# log failure
self.logger.log(status)
else:
# great, exported.
self.logger.log("Exported Gryphon Graph file %s" %
gry_file_filename)
# export selected trace as image file
def export_trace(self, trace_filename, graph):
status = export_trace_manager.export_trace(trace_filename, graph)
if status:
# log failure
self.logger.log(status)
else:
# great, exported.
self.logger.log("Exported trace image file %s" % trace_filename)
# export all traces as image files
def export_serialized_traces(self, trace_filename_prefix):
for graph in self.graphs_manager.graphs:
trace_filename = "%s_%03d.png" % (trace_filename_prefix,
graph.index+1)
status = export_trace_manager.export_trace(
trace_filename, graph)
if status:
# log failure
self.logger.log("%s. Aborting." % status)
break
else:
# great, exported.
self.logger.log("Exported trace image file %s" % trace_filename)
# run and cancel button state
def set_is_compiling(self, is_compiling):
# run and cancel
self.action_run.setDisabled(is_compiling)
self.action_cancel.setDisabled(not is_compiling)
# graph list selection spinner
self.graph_list_selection_spinner.set_disabled(is_compiling)
# other actions and menus
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.action_import_gry_file.setDisabled(is_compiling)
self.action_export_gry_file.setDisabled(is_compiling)
self.action_export_trace.setDisabled(is_compiling)
self.action_export_all_traces.setDisabled(is_compiling)
# note: these two are disabled when compiling because the
# local trace generator changes the local directory, which
# makes these fail.
self.action_help.setDisabled(is_compiling)
self.action_about.setDisabled(is_compiling)
# disable the code editor so it does not fire a contentsChanged event
# which would trigger the syntax checker, which is not assynchronous.
self.mp_code_editor.code_editor.setDisabled(is_compiling)
# send a line of text to the log pane
def write_log(self, log_line):
self.logger.log(log_line)