Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 77 additions & 7 deletions pydm/display.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@

import re
import six
from qtpy.QtWidgets import QApplication, QWidget
import xml.etree.ElementTree as ET

from qtpy.QtWidgets import QApplication, QMainWindow, QWidget

from .help_files import HelpWindow
from .utilities import import_module_by_filename, is_pydm_app, macro, ACTIVE_QT_WRAPPER, QtWrapperTypes
Expand Down Expand Up @@ -186,6 +188,26 @@ def _load_compiled_ui_into_display(
display.ui = display


def _get_ui_root_widget_class(uifile):
"""Return the class name of the root widget defined in a .ui file.

Parameters
----------
uifile : str
Path to the .ui file.

Returns
-------
str
The root widget class name (e.g. ``"QWidget"``, ``"QMainWindow"``).
"""
tree = ET.parse(uifile)
widget_elem = tree.getroot().find("widget")
if widget_elem is not None:
return widget_elem.get("class", "QWidget")
return "QWidget"


def load_ui_file(uifile, macros=None, args=None):
"""
Load a .ui file, perform macro substitution, then return the resulting QWidget.
Expand All @@ -207,7 +229,11 @@ def load_ui_file(uifile, macros=None, args=None):
QWidget
"""

display = Display(macros=macros)
root_class = _get_ui_root_widget_class(uifile)
if root_class == "QMainWindow":
display = MainWindowDisplay(macros=macros)
else:
display = Display(macros=macros)
display.load_ui_from_file(uifile, macros)
return display

Expand Down Expand Up @@ -329,9 +355,26 @@ def load_py_file(pyfile, args=None, macros=None):
}


class Display(QWidget):
def __init__(self, parent=None, args=None, macros=None, ui_filename=None):
super().__init__(parent)
class DisplayBase:
"""Shared implementation for display-like widgets.

Provides navigation, macro handling, file loading, and stylesheet
support. Mixed into both :class:`Display` (QWidget-based) and
:class:`MainWindowDisplay` (QMainWindow-based).
"""

def _init_display(self, args=None, macros=None, ui_filename=None):
"""Initialize display state. Must be called from subclass ``__init__``.

Parameters
----------
args : list, optional
Command-line arguments forwarded to the display.
macros : dict, optional
Macro substitutions.
ui_filename : str, optional
Filename of the .ui file to load.
"""
self.ui = None
self.help_window = None
self._ui_filename = ui_filename
Expand All @@ -341,8 +384,6 @@ def __init__(self, parent=None, args=None, macros=None, ui_filename=None):
self._previous_display = None
self._next_display = None
self._local_style = ""
if ui_filename or self.ui_filename():
self.load_ui(macros=macros)

def loaded_file(self):
return self._loaded_file
Expand Down Expand Up @@ -483,3 +524,32 @@ def setStyleSheet(self, new_stylesheet):
self._local_style = f.read()
logger.debug("Setting stylesheet to: %s", self._local_style)
super().setStyleSheet(self._local_style)


class Display(DisplayBase, QWidget):
def __init__(self, parent=None, args=None, macros=None, ui_filename=None):
super().__init__(parent)
self._init_display(args=args, macros=macros, ui_filename=ui_filename)
if ui_filename or self.ui_filename():
self.load_ui(macros=macros)


class MainWindowDisplay(DisplayBase, QMainWindow):
"""Display backed by QMainWindow for .ui files that use QMainWindow as
their root widget.

Inherits the full display interface from :class:`DisplayBase` so it
passes ``isinstance(widget, Display)`` checks used throughout pydm for
navigation, menu items, macro propagation, and help support.

Parameters
----------
parent : QWidget, optional
The parent widget.
macros : dict, optional
Macro substitutions for the display.
"""

def __init__(self, parent=None, macros=None):
super().__init__(parent)
self._init_display(macros=macros)
8 changes: 4 additions & 4 deletions pydm/main_window.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
close_widget_connections,
)
from .pydm_ui import Ui_MainWindow
from .display import Display, ScreenTarget, load_file, clear_compiled_ui_file_cache
from .display import Display, DisplayBase, ScreenTarget, load_file, clear_compiled_ui_file_cache
from .connection_inspector import ConnectionInspector
from .about_pydm import AboutWindow
from .show_macros import MacroWindow
Expand Down Expand Up @@ -255,7 +255,7 @@ def enable_disable_navigation(self):
self.ui.actionForward.setDisabled(True)
return

if not isinstance(w, Display):
if not isinstance(w, DisplayBase):
# We can't do much if it is not a Display and we don't have the
# previous_display and next_display properties since we don't
# have the navigation stack set.
Expand Down Expand Up @@ -327,7 +327,7 @@ def get_files_in_display(self):
if extension == ".ui":
return self.current_file(), None
else:
central_widget = self.centralWidget() if isinstance(self.centralWidget(), Display) else None
central_widget = self.centralWidget() if isinstance(self.centralWidget(), DisplayBase) else None
if central_widget is not None:
ui_file = central_widget.ui_filepath()
return ui_file, self.current_file()
Expand Down Expand Up @@ -571,7 +571,7 @@ def show_macro_window(self):

def add_menu_items(self):
# create the custom menu with user given items
if not isinstance(self.display_widget(), Display):
if not isinstance(self.display_widget(), DisplayBase):
return

# Only provide the view help menu option if an associated help file has been loaded
Expand Down
23 changes: 23 additions & 0 deletions pydm/tests/test_data/mainwindow_test.ui
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>MainWindow</class>
<widget class="QMainWindow" name="MainWindow">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>400</width>
<height>300</height>
</rect>
</property>
<widget class="QWidget" name="centralwidget">
<widget class="QLabel" name="label">
<property name="text">
<string>Test Label</string>
</property>
</widget>
</widget>
</widget>
<resources/>
<connections/>
</ui>
67 changes: 67 additions & 0 deletions pydm/tests/test_mainwindow_display.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import os
import pytest

from pydm.display import Display, MainWindowDisplay, load_ui_file, _get_ui_root_widget_class
from qtpy.QtWidgets import QMainWindow, QWidget

TEST_DATA = os.path.join(os.path.dirname(__file__), "test_data")
MAINWINDOW_UI = os.path.join(TEST_DATA, "mainwindow_test.ui")


def test_get_ui_root_widget_class_mainwindow():
"""Detect QMainWindow as root widget class in a .ui file."""
assert _get_ui_root_widget_class(MAINWINDOW_UI) == "QMainWindow"


def test_load_mainwindow_ui_returns_mainwindow_display(qtbot):
"""Loading a QMainWindow .ui file produces a MainWindowDisplay instance.

Parameters
----------
qtbot : fixture
pytest-qt fixture for widget management.
"""
display = load_ui_file(MAINWINDOW_UI)
qtbot.addWidget(display)
assert isinstance(display, MainWindowDisplay)
assert isinstance(display, QMainWindow)


def test_load_mainwindow_ui_does_not_crash(qtbot):
"""QMainWindow .ui files should load without AttributeError.

Parameters
----------
qtbot : fixture
pytest-qt fixture for widget management.
"""
display = load_ui_file(MAINWINDOW_UI)
qtbot.addWidget(display)
assert hasattr(display, "setCentralWidget")
assert display.centralWidget() is not None


def test_mainwindow_display_has_full_display_interface(qtbot):
"""MainWindowDisplay should have all Display methods so isinstance checks
and navigation/menu/macro features work correctly.

Parameters
----------
qtbot : fixture
pytest-qt fixture for widget management.
"""
display = load_ui_file(MAINWINDOW_UI)
qtbot.addWidget(display)

assert hasattr(display, "args")
assert hasattr(display, "macros")
assert hasattr(display, "loaded_file")
assert hasattr(display, "menu_items")
assert hasattr(display, "file_menu_items")
assert hasattr(display, "show_help")
assert hasattr(display, "navigate_back")
assert hasattr(display, "navigate_forward")
assert hasattr(display, "load_ui_from_file")
assert hasattr(display, "load_help_file")
assert hasattr(display, "previous_display")
assert hasattr(display, "next_display")
4 changes: 2 additions & 2 deletions pydm/widgets/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from .channel import PyDMChannel
from pydm import data_plugins, tools, config
from pydm.utilities import is_qt_designer, remove_protocol
from pydm.display import Display
from pydm.display import Display, DisplayBase
from pydm.utilities import ACTIVE_QT_WRAPPER, QtWrapperTypes

if ACTIVE_QT_WRAPPER == QtWrapperTypes.PYSIDE6:
Expand Down Expand Up @@ -368,7 +368,7 @@ def setRules(self, new_rules) -> None:
def find_parent_display(self):
widget = self.parent()
while widget is not None:
if isinstance(widget, Display):
if isinstance(widget, DisplayBase):
return widget
widget = widget.parent()
return None
Expand Down
Loading