Skip to content

Commit

Permalink
Merge branch 'master' into pyside6_upgrade_pt_1
Browse files Browse the repository at this point in the history
  • Loading branch information
nstelter-slac authored Dec 17, 2024
2 parents 3a52da7 + 136ed4a commit cf85264
Show file tree
Hide file tree
Showing 12 changed files with 147 additions and 33 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/run-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ jobs:
uses: conda-incubator/setup-miniconda@v2
with:
python-version: ${{ matrix.python-version }}
miniforge-variant: Mambaforge
miniforge-variant: Miniforge3
miniforge-version: latest
activate-environment: pydm-env
- name: Install python packages
Expand Down
16 changes: 9 additions & 7 deletions docs/source/installation.rst
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,21 @@ Please note, this guide is written with Unix in mind, so there are probably some
Installing PyDM and Prerequisites with Anaconda
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

After installing Anaconda (see https://www.anaconda.com/download/), create a new
environment for PyDM::
$ conda create -n pydm-environment python=3.8 pyqt=5 pip numpy scipy six psutil pyqtgraph pydm -c conda-forge
$ source activate pydm-environment

.. warning::
There is currently no PyQt 5.15+ build available on conda or PyPI that has
designer support for python plugins.
designer support for python plugins. PyDM widgets will not load in these designer versions.

In order to use PyDM widgets in designer, please make sure to pin the PyQt version to 5.12.3 or lower
until this is resolved.

$ conda create -n pydm-environment python=3.10 pyqt=5.12.3 pip numpy scipy six psutil pyqtgraph pydm -c conda-forge

After installing Anaconda (see https://www.anaconda.com/download/), create a new
environment for PyDM::
$ conda create -n pydm-environment python=3.10 pyqt=5 pip numpy scipy six psutil pyqtgraph pydm -c conda-forge
$ source activate pydm-environment

Once you've installed and activated the environment, you should be able to run 'pydm' to launch PyDM, or run 'designer' to launch Qt Designer. If you are on Windows, run these commands from the Anaconda Prompt.

On MacOS, launching Qt Designer is a little more annoying:
Expand Down
1 change: 1 addition & 0 deletions docs/source/tutorials/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ tasks, and data plugins for multiple control systems.
intro/macros.rst
intro/widgets.rst
intro/datasource.rst
intro/features.rst

.. toctree::
:maxdepth: 2
Expand Down
54 changes: 54 additions & 0 deletions docs/source/tutorials/intro/features.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
Features
========


Adding Menu Actions
-------------------

You can add actions to the default menu bar in 2 ways:

* Add any custom action to the "Actions" drop down
* Add a "save", "save as", and/or "load" function to the "File" drop down

To add to the menu bar, overload the ``menu_items()`` and ``file_menu_items()``
functions in your ``Display`` subclass. These functions should return dictionaries,
where the keys are the action names, and the values one of the following:

* A callable
* A two element tuple, where the first item is a callable and the second is a keyboard shortcut
* A dictionary corresponding to a sub menu, with the same key-value format so far described. This is only available for the "Actions" menu, not for the "File" menu

.. note::
The only accepted keys for the "File" menu are: "save", "save_as", and "load"


An example:

.. code:: python
from pydm import Display
class MyDisplay(Display):
def __init__(self, parent=None, args=None, macros=None):
super(MyDisplay, self).__init__(parent=parent, args=args, macros=macros)
def file_menu_items(self):
return {"save": self.save_function, "load": (self.load_function, "Ctrl+L")}
def menu_items(self):
return {"Action1": self.action1_function, "submenu": {"Action2": self.action2_function, "Action3": self.action3_function}}
def save_function(self):
# do something to save your data
def load_function(self):
# do something to load your data
def action1_function(self):
# do action 1
def action2_function(self):
# do action 2
def action3_function(self):
# do action 3
6 changes: 5 additions & 1 deletion pydm/main_window.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from os import path

from qtpy.QtWidgets import QApplication, QMainWindow, QFileDialog, QAction, QMessageBox
from qtpy.QtCore import Qt, QTimer, Slot, QSize, QLibraryInfo
from qtpy.QtCore import Qt, QTimer, Slot, QSize, QLibraryInfo, QCoreApplication
from qtpy.QtGui import QKeySequence
from .utilities import IconFont, find_file, establish_widget_connections, close_widget_connections
from .pydm_ui import Ui_MainWindow
Expand Down Expand Up @@ -479,10 +479,14 @@ def set_font_size(self, old, new):

@Slot(bool)
def enter_fullscreen(self, checked=False):
# for supporting localization (the main window menu-items all support this)
_translate = QCoreApplication.translate
if self.isFullScreen():
self.showNormal()
self.ui.actionEnter_Fullscreen.setText(_translate("MainWindow", "Enter Fullscreen"))
else:
self.showFullScreen()
self.ui.actionEnter_Fullscreen.setText(_translate("MainWindow", "Exit Fullscreen"))

@Slot(bool)
def show_connections(self, checked):
Expand Down
File renamed without changes.
17 changes: 17 additions & 0 deletions pydm/tests/test_main_window.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,20 @@ def test_reload_display(wrapped_compile_ui: MagicMock, qapp: PyDMApplication) ->
assert wrapped_compile_ui.call_count == 2
finally:
clear_compiled_ui_file_cache()


def test_menubar_text(qapp: PyDMApplication) -> None:
"""Verify main-window displays expected text in its menubar dropdown items"""
# only testing text update of "Enter/Exit Fullscreen" menu-item for now, this can be expanded later
display = Display(parent=None)

qapp.make_main_window()
qapp.main_window.set_display_widget(display)

action = qapp.main_window.ui.actionEnter_Fullscreen
# make sure we start in not fullscreen view
qapp.main_window.showNormal()
assert action.text() == "Enter Fullscreen"
# click the menu-item to go into fullscreen view
action.trigger()
assert action.text() == "Exit Fullscreen"
30 changes: 30 additions & 0 deletions pydm/tests/widgets/test_curve_editor.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@
RedrawModeColumnDelegate,
PlotStyleColumnDelegate,
)
from ...widgets import PyDMArchiverTimePlot
from ...widgets.axis_table_model import BasePlotAxesModel
from ...widgets.baseplot_table_model import BasePlotCurvesModel
from ...widgets.archiver_time_plot_editor import PyDMArchiverTimePlotCurvesModel
from ...widgets.scatterplot_curve_editor import ScatterPlotCurveEditorDialog
from ...widgets.timeplot_curve_editor import TimePlotCurveEditorDialog
from ...widgets.waveformplot import WaveformCurveItem
Expand Down Expand Up @@ -120,6 +123,33 @@ def test_axis_editor(qtbot):
assert type(axis_view.itemDelegateForColumn(axis_orientation_index)) is AxisColumnDelegate


def test_axis_table_model(qtmodeltester):
"""Check the validity of the BasePlotAxesModel with pytest-qt"""
base_plot = BasePlot()
axis_model = BasePlotAxesModel(plot=base_plot)
axis_model.append("FooBar")

qtmodeltester.check(axis_model, force_py=True)


def test_curves_table_model(qtmodeltester):
"""Check the validity of the BasePlotCurvesModel with pytest-qt"""
base_plot = BasePlot()
curves_model = BasePlotCurvesModel(plot=base_plot)
curves_model.append()

qtmodeltester.check(curves_model, force_py=True)


def test_archive_table_model(qtmodeltester):
"""Check the validity of the PyDMArchiverTimePlotCurvesModel with pytest-qt"""
archiver_plot = PyDMArchiverTimePlot()
archive_model = PyDMArchiverTimePlotCurvesModel(plot=archiver_plot)
archive_model.append()

qtmodeltester.check(archive_model, force_py=True)


def test_plot_style_column_delegate(qtbot):
"""Verify the functionality of the show/hide column feature"""

Expand Down
24 changes: 10 additions & 14 deletions pydm/widgets/archiver_time_plot.py
Original file line number Diff line number Diff line change
Expand Up @@ -476,19 +476,24 @@ def evaluate(self) -> None:
If one curve updates at a certain timestep and another does not, it uses the previously
seen data of the second curve, and assumes it is accurate at the current timestep.
"""
if not self.checkFormula():
formula = self._trueFormula
if not formula or not self.checkFormula():
logger.error("invalid formula")
self.formula_invalid_signal.emit()
return
if not self.pvs:
# If we are just a constant, then store a straight line from 1970 to ~2200
# Known Bug: Constants are hidden if the plot's x-axis range is between 30m and 1.5hr
self.archive_data_buffer = np.array([[0], [eval(self._trueFormula)]])
self.data_buffer = np.array([[APPROX_SECONDS_300_YEARS], [eval(self._trueFormula)]])
self.points_accumulated = self.archive_points_accumulated = 1
return
if not (self.connected and self.arch_connected):
return
pvArchiveData = dict()
pvLiveData = dict()
pvIndices = dict()
pvValues = dict()
formula = self._trueFormula
if not formula:
logger.error("invalid formula")
return

self.archive_data_buffer = np.zeros((2, 0), order="f", dtype=float)
self.data_buffer = np.zeros((2, 0), order="f", dtype=float)
Expand Down Expand Up @@ -604,15 +609,6 @@ def redrawCurve(self, min_x=None, max_x=None) -> None:
"""
Redraw the curve with any new data added since the last draw call.
"""
if not self.pvs:
# If we are just a constant, then forget about data
# just draw a straight line from 1970 to 300 years or so in the future
y = [eval(self._trueFormula), eval(self._trueFormula)]
x = [0, APPROX_SECONDS_300_YEARS]
# There is a known bug that this won't graph a constant with an x axis
# of between 30 minutes and 1hr 30 minutes in range. Unknown reason
self.setData(y=y, x=x)
return
self.evaluate()
try:
x = np.concatenate(
Expand Down
16 changes: 10 additions & 6 deletions pydm/widgets/archiver_time_plot_editor.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from typing import Any, Optional
from qtpy.QtCore import Qt, QModelIndex, QObject, QVariant
from qtpy.QtCore import Qt, QModelIndex, QObject
from qtpy.QtGui import QColor
from .archiver_time_plot import ArchivePlotCurveItem, FormulaCurveItem
from .baseplot import BasePlot, BasePlotCurveItem
Expand All @@ -16,15 +16,19 @@ def __init__(self, plot: BasePlot, parent: Optional[QObject] = None):

self.checkable_cols = {self.getColumnIndex("Live Data"), self.getColumnIndex("Archive Data")}

def flags(self, index):
def flags(self, index: QModelIndex) -> Qt.ItemFlags:
"""Return flags that determine how users can interact with the items in the table"""
if not index.isValid():
return Qt.NoItemFlags

flags = super().flags(index)
if index.column() in self.checkable_cols:
flags = Qt.ItemIsEnabled | Qt.ItemIsUserCheckable | Qt.ItemIsSelectable
return flags

def data(self, index, role=Qt.DisplayRole):
if not index.isValid():
return QVariant()
return None
if role == Qt.CheckStateRole and index.column() in self.checkable_cols:
value = super().data(index, Qt.DisplayRole)
return Qt.Checked if value else Qt.Unchecked
Expand All @@ -37,12 +41,12 @@ def get_data(self, column_name: str, curve: BasePlotCurveItem) -> Any:
if column_name == "Channel":
if isinstance(curve, FormulaCurveItem):
if curve.formula is None:
return QVariant()
return ""
return str(curve.formula)
# We are either a Formula or a PV (for now at leasts)
else:
if curve.address is None:
return QVariant()
return ""
return str(curve.address)

elif column_name == "Live Data":
Expand All @@ -53,7 +57,7 @@ def get_data(self, column_name: str, curve: BasePlotCurveItem) -> Any:

def setData(self, index, value, role=Qt.DisplayRole):
if not index.isValid():
return QVariant()
return None
elif role == Qt.CheckStateRole and index.column() in self.checkable_cols:
return super().setData(index, value, Qt.EditRole)
elif index.column() not in self.checkable_cols:
Expand Down
5 changes: 4 additions & 1 deletion pydm/widgets/axis_table_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,10 @@ def plot(self):
def plot(self, new_plot):
self._plot = new_plot

def flags(self, index):
def flags(self, index: QModelIndex) -> Qt.ItemFlags:
"""Return flags that determine how users can interact with the items in the table"""
if not index.isValid():
return Qt.NoItemFlags
return Qt.ItemIsSelectable | Qt.ItemIsEnabled | Qt.ItemIsEditable

def rowCount(self, parent=None):
Expand Down
9 changes: 6 additions & 3 deletions pydm/widgets/baseplot_table_model.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from qtpy.QtCore import QAbstractTableModel, Qt
from qtpy.QtCore import QAbstractTableModel, Qt, QModelIndex
from qtpy.QtGui import QBrush
from .baseplot import BasePlotCurveItem

Expand Down Expand Up @@ -42,7 +42,10 @@ def plot(self, new_plot):
def clear(self):
self.plot.clearCurves()

def flags(self, index):
def flags(self, index: QModelIndex) -> Qt.ItemFlags:
"""Return flags that determine how users can interact with the items in the table"""
if not index.isValid():
return None
column_name = self._column_names[index.column()]
if column_name == "Color" or column_name == "Limit Color":
return Qt.ItemIsSelectable | Qt.ItemIsEnabled
Expand Down Expand Up @@ -77,7 +80,7 @@ def data(self, index, role=Qt.DisplayRole):
def get_data(self, column_name, curve):
if column_name == "Label":
if curve.name() is None:
return None
return ""
return str(curve.name())
elif column_name == "Y-Axis Name":
return curve.y_axis_name
Expand Down

0 comments on commit cf85264

Please sign in to comment.