diff --git a/docs/source/_static/help_files.gif b/docs/source/_static/help_files.gif new file mode 100644 index 000000000..b05962cc9 Binary files /dev/null and b/docs/source/_static/help_files.gif differ diff --git a/docs/source/help_files.rst b/docs/source/help_files.rst new file mode 100644 index 000000000..ecec0fe75 --- /dev/null +++ b/docs/source/help_files.rst @@ -0,0 +1,19 @@ +================= +Adding Help Files +================= + +If you are creating a display and would like to add some documentation on how it works, PyDM provides the ability +to do this with a minimum of extra effort. By placing a .txt or .html file in the same directory as your display, +PyDM will load this file and automatically add it to both the View menu of the top menu bar, as well as the right +click menu of widgets on the display. + +In order for PyDM to associate the help file with your display, it must have the same name as your display file. For +example, let's say that we have a file called drawing_demo.ui. By adding a file called drawing_demo.txt to the same +location, PyDM will load that file along with the display. + +.. figure:: /_static/help_files.gif + :scale: 100 % + :align: center + :alt: Help files + + Where to find the automatically loaded help file diff --git a/docs/source/index.rst b/docs/source/index.rst index 97173a346..67af6f283 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -30,6 +30,7 @@ as well as a straightforward python framework to build complex applications. :caption: User & API Documentation stylesheets.rst + help_files.rst widgets/index.rst widgets/widget_rules/index.rst add_data_plugins.rst diff --git a/pydm/display.py b/pydm/display.py index 45150d835..91dcd35ca 100644 --- a/pydm/display.py +++ b/pydm/display.py @@ -16,6 +16,7 @@ from qtpy import uic from qtpy.QtWidgets import QApplication, QWidget +from .help_files import HelpWindow from .utilities import import_module_by_filename, is_pydm_app, macro @@ -63,14 +64,20 @@ def load_file(file, macros=None, args=None, target=ScreenTarget.NEW_PROCESS): app.new_pydm_process(file, macros=macros, command_line_args=args) return None - _, extension = os.path.splitext(file) + base, extension = os.path.splitext(file) loader = _extension_to_loader.get(extension, load_py_file) logger.debug("Loading %s file by way of %s...", file, loader.__name__) - w = loader(file, args=args, macros=macros) + loaded_display = loader(file, args=args, macros=macros) + + if os.path.exists(base + '.txt'): + loaded_display.load_help_file(base + '.txt') + elif os.path.exists(base + '.html'): + loaded_display.load_help_file(base + '.html') + if target == ScreenTarget.DIALOG: - w.show() + loaded_display.show() - return w + return loaded_display @lru_cache() @@ -286,6 +293,7 @@ class Display(QWidget): def __init__(self, parent=None, args=None, macros=None, ui_filename=None): super(Display, self).__init__(parent=parent) self.ui = None + self.help_window = None self._ui_filename = ui_filename self._loaded_file = None self._args = args @@ -355,6 +363,11 @@ def file_menu_items(self): """ return {} + def show_help(self) -> None: + """ Show the associated help file for this display """ + if self.help_window is not None: + self.help_window.show() + def navigate_back(self): pass @@ -401,6 +414,10 @@ def load_ui_from_file(self, ui_file_path: str, macros: Optional[Dict[str, str]] code_string, class_name = _compile_ui_file(ui_file_path) _load_compiled_ui_into_display(code_string, class_name, self, macros) + def load_help_file(self, file_path: str) -> None: + """ Loads the input help file into a window for display """ + self.help_window = HelpWindow(file_path) + def setStyleSheet(self, new_stylesheet): # Handle the case where the widget's styleSheet property contains a filename, rather than a stylesheet. possible_stylesheet_filename = os.path.expanduser(os.path.expandvars(new_stylesheet)) diff --git a/pydm/help_files/__init__.py b/pydm/help_files/__init__.py new file mode 100644 index 000000000..43c626665 --- /dev/null +++ b/pydm/help_files/__init__.py @@ -0,0 +1 @@ +from .help_window import HelpWindow diff --git a/pydm/help_files/help_window.py b/pydm/help_files/help_window.py new file mode 100644 index 000000000..d5eb43c02 --- /dev/null +++ b/pydm/help_files/help_window.py @@ -0,0 +1,33 @@ +from pathlib import Path +from qtpy.QtWidgets import QTextBrowser, QVBoxLayout, QWidget +from qtpy.QtCore import Qt +from typing import Optional + + +class HelpWindow(QWidget): + """ + A window for displaying a help file for a PyDM display + + Parameters + ---------- + help_file_path : str + The path to the help file to be displayed + """ + def __init__(self, help_file_path: str, parent: Optional[QWidget] = None): + super().__init__(parent, Qt.Window) + self.resize(500, 400) + + path = Path(help_file_path) + self.setWindowTitle(f'Help for {path.stem}') + + self.display_content = QTextBrowser() + + with open(help_file_path) as file: + if path.suffix == '.txt': + self.display_content.setText(file.read()) + else: + self.display_content.setHtml(file.read()) + + self.vBoxLayout = QVBoxLayout() + self.vBoxLayout.addWidget(self.display_content) + self.setLayout(self.vBoxLayout) diff --git a/pydm/main_window.py b/pydm/main_window.py index 83fccb125..87dc9d65f 100644 --- a/pydm/main_window.py +++ b/pydm/main_window.py @@ -80,6 +80,7 @@ def __init__(self, parent=None, hide_nav_bar=False, hide_menu_bar=False, hide_st self.ui.actionShow_Menu_Bar.triggered.connect(self.toggle_menu_bar) self.ui.actionShow_Status_Bar.triggered.connect(self.toggle_status_bar) self.ui.actionShow_Connections.triggered.connect(self.show_connections) + self.ui.actionShow_Help.triggered.connect(self.show_help) self.ui.actionAbout_PyDM.triggered.connect(self.show_about_window) self.ui.actionLoadTool.triggered.connect(self.load_tool) self.ui.actionLoadTool.setIcon(self.iconFont.icon("rocket")) @@ -486,6 +487,11 @@ def show_connections(self, checked): c = ConnectionInspector(self) c.show() + def show_help(self): + """ Show the associated help file for this window """ + if self.display_widget() is not None: + self.display_widget().show_help() + @Slot(bool) def show_about_window(self, checked): a = AboutWindow(self) @@ -530,6 +536,11 @@ def add_menu_items(self): # create the custom menu with user given items if not isinstance(self.display_widget(), Display): return + + # Only provide the view help menu option if an associated help file has been loaded + if self.display_widget().help_window is None: + self.ui.actionShow_Help.setVisible(False) + items = self.display_widget().menu_items() if len(items) == 0: self.ui.menuCustomActions.menuAction().setVisible(False) diff --git a/pydm/pydm.ui b/pydm/pydm.ui index f1943559e..af3f8385e 100644 --- a/pydm/pydm.ui +++ b/pydm/pydm.ui @@ -62,6 +62,7 @@ + diff --git a/pydm/pydm_ui.py b/pydm/pydm_ui.py index 73107d6cc..36e21ff15 100644 --- a/pydm/pydm_ui.py +++ b/pydm/pydm_ui.py @@ -93,6 +93,9 @@ def setupUi(self, MainWindow): self.actionShow_Connections = QtWidgets.QAction(MainWindow) self.actionShow_Connections.setShortcutContext(QtCore.Qt.ApplicationShortcut) self.actionShow_Connections.setObjectName("actionShow_Connections") + self.actionShow_Help = QtWidgets.QAction(MainWindow) + self.actionShow_Help.setShortcutContext(QtCore.Qt.ApplicationShortcut) + self.actionShow_Help.setObjectName("actionShow_Help") self.actionLoadTool = QtWidgets.QAction(MainWindow) self.actionLoadTool.setObjectName("actionLoadTool") self.actionEnter_Fullscreen = QtWidgets.QAction(MainWindow) @@ -135,6 +138,7 @@ def setupUi(self, MainWindow): self.menuView.addAction(self.actionShow_Menu_Bar) self.menuView.addAction(self.actionShow_Status_Bar) self.menuView.addAction(self.actionShow_Connections) + self.menuView.addAction(self.actionShow_Help) self.menuHistory.addAction(self.actionBack) self.menuHistory.addAction(self.actionForward) self.menuHistory.addAction(self.actionHome) @@ -184,6 +188,7 @@ def retranslateUi(self, MainWindow): self.actionShow_Menu_Bar.setShortcut(_translate("MainWindow", "Ctrl+M")) self.actionShow_Status_Bar.setText(_translate("MainWindow", "Show Status Bar")) self.actionShow_Connections.setText(_translate("MainWindow", "Show Connections...")) + self.actionShow_Help.setText(_translate("MainWindow", "View Help for this Display")) self.actionLoadTool.setText(_translate("MainWindow", "Load...")) self.actionEnter_Fullscreen.setText(_translate("MainWindow", "Enter Fullscreen")) self.actionEnter_Fullscreen.setShortcut(_translate("MainWindow", "F11")) diff --git a/pydm/tests/test_data/test.txt b/pydm/tests/test_data/test.txt new file mode 100644 index 000000000..18b1b0896 --- /dev/null +++ b/pydm/tests/test_data/test.txt @@ -0,0 +1 @@ +This is a test help file for the test.ui display diff --git a/pydm/tests/test_display.py b/pydm/tests/test_display.py index 8e26eed5a..139eb5f2f 100644 --- a/pydm/tests/test_display.py +++ b/pydm/tests/test_display.py @@ -1,7 +1,7 @@ import os import pytest from pydm import Display -from pydm.display import load_py_file, _compile_ui_file, _load_compiled_ui_into_display +from pydm.display import load_file, load_py_file, _compile_ui_file, _load_compiled_ui_into_display, ScreenTarget from qtpy.QtWidgets import QLabel # The path to the .ui file used in these tests @@ -155,3 +155,14 @@ def setCommands(self, commands): finally: del QLabel.setCommands + + +def test_load_file_with_help_display(qtbot): + """ + Ensure that when a file containing help information is placed in the same directory as the display to load, + that help display is loaded and available to the user. This test depends on a test.txt or test.html file + being present in the same location as the file at test_ui_path. + """ + test_display = load_file(test_ui_path, target=ScreenTarget.HOME) + assert test_display.help_window is not None + assert test_display.help_window.display_content.toPlainText() == 'This is a test help file for the test.ui display\n' diff --git a/pydm/widgets/base.py b/pydm/widgets/base.py index bacd67b21..fa8a39e75 100644 --- a/pydm/widgets/base.py +++ b/pydm/widgets/base.py @@ -690,6 +690,14 @@ def generate_context_menu(self): kwargs = {'channels': self.channels_for_tools(), 'sender': self} tools.assemble_tools_menu(menu, widget_only=True, widget=self, **kwargs) + + # Add a view help action if the parent display has an associated help file + parent_display = self.find_parent_display() + if parent_display is not None and parent_display.help_window is not None: + if len(menu.actions()) > 0: + menu.addSeparator() + menu.addAction('View Help for this Display', parent_display.show_help) + return menu def open_context_menu(self, ev):