Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
nstelter-slac committed Jan 10, 2025
1 parent fe794e0 commit 49d57d5
Show file tree
Hide file tree
Showing 11 changed files with 311 additions and 62 deletions.
12 changes: 9 additions & 3 deletions pydm/tests/widgets/test_enum_button.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

from ...widgets.enum_button import PyDMEnumButton, WidgetType, class_for_type
from ... import data_plugins
from ...utilities import ACTIVE_QT_WRAPPER, QtWrapperTypes


def test_construct(qtbot):
Expand Down Expand Up @@ -43,11 +44,15 @@ def test_widget_type(qtbot, widget_type):
qtbot.addWidget(widget)

assert widget.widgetType == WidgetType.PushButton
assert isinstance(widget._widgets[0], class_for_type[WidgetType.PushButton.value])
# Support both pyqt enums (inherit from 'object') and pyside6 enums (inherit from python 'Enum' and
# therefore require '.value')
index = WidgetType.PushButton if ACTIVE_QT_WRAPPER == QtWrapperTypes.PYQT5 else WidgetType.PushButton.value
assert isinstance(widget._widgets[0], class_for_type[index])

widget.widgetType = widget_type
assert widget.widgetType == widget_type
assert isinstance(widget._widgets[0], class_for_type[widget_type.value])
index = widget_type if ACTIVE_QT_WRAPPER == QtWrapperTypes.PYQT5 else widget_type.value
assert isinstance(widget._widgets[0], class_for_type[index])


@pytest.mark.parametrize("orientation", [Qt.Horizontal, Qt.Vertical])
Expand Down Expand Up @@ -82,7 +87,8 @@ def test_widget_orientation(qtbot, orientation):
w = item.widget()
qtbot.addWidget(w)
assert w is not None
assert isinstance(w, class_for_type[widget.widgetType.value])
index = widget.widgetType if ACTIVE_QT_WRAPPER == QtWrapperTypes.PYQT5 else widget.widgetType.value
assert isinstance(w, class_for_type[index])


@pytest.mark.parametrize(
Expand Down
30 changes: 29 additions & 1 deletion pydm/utilities/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
import errno

from typing import List, Optional

from enum import IntEnum
from qtpy import QtCore, QtGui, QtWidgets

from . import colors, macro, shortcuts
Expand All @@ -39,6 +39,34 @@
logger = logging.getLogger(__name__)


# The qtpy abstraction layer decides which qt python wrapper to use by the QT_API.
# Atm for pydm we only intend to support PyQt5 (legacy) and PySide6.
# ACTIVE_QT_WRAPPER is implemented basically to have easier access to the QT_API env variable,
# and we need to know the current wrapper being used to support both pyqt5 and pyside6.
class QtWrapperTypes(IntEnum):
UNSUPPORTED = 0
PYSIDE6 = 1
PYQT5 = 2


ACTIVE_QT_WRAPPER = QtWrapperTypes.UNSUPPORTED

# QT_API should be set according to the qtpy docs: https://github.com/spyder-ide/qtpy?tab=readme-ov-file#requirements
qt_api = os.getenv("QT_API", "").lower()
if qt_api == "pyside6":
ACTIVE_QT_WRAPPER = QtWrapperTypes.PYSIDE6
elif qt_api == "pyqt5":
ACTIVE_QT_WRAPPER = QtWrapperTypes.PYQT5

if ACTIVE_QT_WRAPPER == QtWrapperTypes.UNSUPPORTED:
error_message = (
"The QT_API variable is not set to a supported Qt Python wrapper "
"(PySide6 or PyQt5). Please set QT_API to 'pyside6' or 'pyqt5'."
)
logger.error(error_message)
raise RuntimeError(error_message)


def is_ssh_session():
"""
Whether or not this is a SSH session.
Expand Down
55 changes: 49 additions & 6 deletions pydm/widgets/datetime.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,32 @@
from PyQt5.QtCore import Q_ENUM

from .base import PyDMWritableWidget, PyDMWidget
from ..utilities import ACTIVE_QT_WRAPPER, QtWrapperTypes

# if ACTIVE_QT_WRAPPER == QtWrapperTypes.PYQT5:
# from PyQt5.QtCore import Q_ENUM

logger = logging.getLogger(__name__)


# Inheriting from 'Enum' class is requried for enums in pyside6
class TimeBase(Enum):
Milliseconds = 0
Seconds = 1


class PyDMDateTimeEdit(QtWidgets.QDateTimeEdit, PyDMWritableWidget):
Q_ENUM(TimeBase)
if ACTIVE_QT_WRAPPER == QtWrapperTypes.PYQT5:
# But inheriting from object class is requried for enums in pyqt5, since we need widgets to
# inherit from the enum class so the enums work in QtDesigner.
# This definition of the enum will override the previous definition.
# Doing this is pretty messy, but it seems like only way to support pyqt5 and pyside6 enums at same time,
# can be removed latter if we eventually drop pyqt5 support.
class TimeBase(object): # noqa F811
Milliseconds = 0
Seconds = 1


class PyDMDateTimeEditBase(QtWidgets.QDateTimeEdit, PyDMWritableWidget):
returnPressed = QtCore.Signal()
"""
A QDateTimeEdit with support for setting the text via a PyDM Channel, or
Expand Down Expand Up @@ -74,7 +89,7 @@ def blockPastDate(self, block):
self._block_past_date = block

def keyPressEvent(self, key_event):
ret = super(PyDMDateTimeEdit, self).keyPressEvent(key_event)
ret = super(PyDMDateTimeEditBase, self).keyPressEvent(key_event)
if key_event.key() in [QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter]:
self.returnPressed.emit()
return ret
Expand All @@ -96,7 +111,7 @@ def send_value(self):
self.send_value_signal.emit(new_value)

def value_changed(self, new_val):
super(PyDMDateTimeEdit, self).value_changed(new_val)
super(PyDMDateTimeEditBase, self).value_changed(new_val)

if self.timeBase == TimeBase.Seconds:
new_val *= 1000
Expand All @@ -109,7 +124,18 @@ def value_changed(self, new_val):
self.setDateTime(val)


class PyDMDateTimeLabel(QtWidgets.QLabel, PyDMWidget):
# works with pyside6
class PyDMDateTimeEdit(PyDMDateTimeEditBase):
pass


if ACTIVE_QT_WRAPPER == QtWrapperTypes.PYQT5:
# Overrides the previous class defintion
class PyDMDateTimeEdit(PyDMDateTimeEditBase, TimeBase): # noqa F811
pass


class PyDMDateTimeLabelBase(QtWidgets.QLabel, PyDMWidget):
Q_ENUM(TimeBase)
"""
A QLabel with support for setting the text via a PyDM Channel, or
Expand Down Expand Up @@ -168,7 +194,7 @@ def relative(self, checked):
self._relative = checked

def value_changed(self, new_val):
super(PyDMDateTimeLabel, self).value_changed(new_val)
super(PyDMDateTimeLabelBase, self).value_changed(new_val)

if self.timeBase == TimeBase.Seconds:
new_val *= 1000
Expand All @@ -179,3 +205,20 @@ def value_changed(self, new_val):
else:
val.setMSecsSinceEpoch(new_val)
self.setText(val.toString(self.textFormat))


# We define a '...Base' of the class so here we can 'conditionally define' the class as either inheriting
# from an enum or not, in order to support enums in QtDesigner for both pyqt5 and pyside6.
# We basically need to support the two ways of defining enums as explained here: https://stackoverflow.com/a/46919848.
class PyDMDateTimeLabel(PyDMDateTimeLabelBase):
pass


if ACTIVE_QT_WRAPPER == QtWrapperTypes.PYQT5:
# Having the widget inherit from the enum class is requried in pyqt5 for the enums to load in QtDesigner.
# This definition of the enum will override the previous definition.
class PyDMDateTimeLabel(PyDMDateTimeLabelBase, TimeBase): # noqa F811
Q_ENUM(TimeBase)

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
13 changes: 13 additions & 0 deletions pydm/widgets/display_format.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@
import logging
import warnings
from enum import Enum
from ..utilities import ACTIVE_QT_WRAPPER, QtWrapperTypes

logger = logging.getLogger(__name__)


# works with pyside6
class DisplayFormat(Enum):
"""Display format for showing data in a PyDM widget."""

Expand All @@ -26,6 +28,17 @@ class DisplayFormat(Enum):
Binary = 5


if ACTIVE_QT_WRAPPER == QtWrapperTypes.PYQT5:

class DisplayFormat(object): # noqa F811
Default = 0
String = 1
Decimal = 2
Exponential = 3
Hex = 4
Binary = 5


def parse_value_for_display(
value: Any,
precision: int,
Expand Down
38 changes: 31 additions & 7 deletions pydm/widgets/enum_button.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,28 @@

from .base import PyDMWritableWidget
from .. import data_plugins
from ..utilities import ACTIVE_QT_WRAPPER, QtWrapperTypes


# works with pyside6
class WidgetType(Enum):
PushButton = 0
RadioButton = 1


if ACTIVE_QT_WRAPPER == QtWrapperTypes.PYQT5:

class WidgetType(object): # noqa F811
PushButton = 0
RadioButton = 1


class_for_type = [QPushButton, QRadioButton]

logger = logging.getLogger(__name__)


class PyDMEnumButton(QWidget, PyDMWritableWidget):
class PyDMEnumButtonBase(QWidget, PyDMWritableWidget):
"""
A QWidget that renders buttons for every option of Enum Items.
For now, two types of buttons can be rendered:
Expand All @@ -40,9 +49,6 @@ class PyDMEnumButton(QWidget, PyDMWritableWidget):
Emitted when the user changes the value.
"""

Q_ENUM(WidgetType)
WidgetType = WidgetType

def __init__(self, parent=None, init_channel=None):
QWidget.__init__(self, parent)
PyDMWritableWidget.__init__(self, init_channel=init_channel)
Expand Down Expand Up @@ -401,7 +407,10 @@ def generate_widgets(items):
w.deleteLater()

for idx, entry in enumerate(items):
w = class_for_type[self._widget_type.value](parent=self)
# Support both pyqt enums (inherit from 'object') and pyside6 enums (inherit from python 'Enum' and
# therefore require '.value')
index = self._widget_type if ACTIVE_QT_WRAPPER == QtWrapperTypes.PYQT5 else self._widget_type.value
w = class_for_type[index](parent=self)
w.setCheckable(self.checkable)
w.setText(entry)
w.setVisible(False)
Expand Down Expand Up @@ -481,7 +490,7 @@ def value_changed(self, new_val):
The new value from the channel.
"""
if new_val is not None and new_val != self.value:
super(PyDMEnumButton, self).value_changed(new_val)
super(PyDMEnumButtonBase, self).value_changed(new_val)
btn = self._btn_group.button(new_val)
if btn:
btn.setChecked(True)
Expand All @@ -498,7 +507,7 @@ def enum_strings_changed(self, new_enum_strings):
The new list of values
"""
if new_enum_strings is not None and new_enum_strings != self.enum_strings:
super(PyDMEnumButton, self).enum_strings_changed(new_enum_strings)
super(PyDMEnumButtonBase, self).enum_strings_changed(new_enum_strings)
self._has_enums = True
self.check_enable_state()
self.rebuild_widgets()
Expand All @@ -522,3 +531,18 @@ def paintEvent(self, _):
opt.initFrom(self)
self.style().drawPrimitive(QStyle.PE_Widget, opt, painter, self)
painter.setRenderHint(QPainter.Antialiasing)


# works with pyside6
class PyDMEnumButton(PyDMEnumButtonBase):
pass


if ACTIVE_QT_WRAPPER == QtWrapperTypes.PYQT5:
# Overrides the previous class defintion
class PyDMEnumButton(PyDMEnumButtonBase, WidgetType): # noqa F811
WidgetType = WidgetType
Q_ENUM(WidgetType)

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
30 changes: 29 additions & 1 deletion pydm/widgets/image.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,27 @@
from .channel import PyDMChannel
from .colormaps import cmaps, cmap_names, PyDMColorMap
from .base import PyDMWidget
from ..utilities import ACTIVE_QT_WRAPPER, QtWrapperTypes

logger = logging.getLogger(__name__)


# works with pyside6
class ReadingOrder(Enum):
"""Class to build ReadingOrder ENUM property."""

Fortranlike = 0
Clike = 1


if ACTIVE_QT_WRAPPER == QtWrapperTypes.PYQT5:

class ReadingOrder(object): # noqa F811
Fortranlike = 0
Clike = 1


# works with pyside6
class DimensionOrder(Enum):
"""
Class to build DimensionOrder ENUM property.
Expand All @@ -43,6 +53,13 @@ class DimensionOrder(Enum):
WidthFirst = 1


if ACTIVE_QT_WRAPPER == QtWrapperTypes.PYQT5:

class DimensionOrder(object): # noqa F811
HeightFirst = 0
WidthFirst = 1


class ImageUpdateThread(QThread):
updateSignal = Signal(list)

Expand Down Expand Up @@ -100,7 +117,7 @@ def run(self):
self.image_view.needs_redraw = False


class PyDMImageView(
class PyDMImageViewBase(
ImageView,
PyDMWidget,
PyDMColorMap,
Expand Down Expand Up @@ -753,3 +770,14 @@ def scaleYAxis(self):
@scaleYAxis.setter
def scaleYAxis(self, new_scale):
self.getView().getAxis("left").setScale(new_scale)


# works with pyside6
class PyDMImageView(PyDMImageViewBase):
pass


if ACTIVE_QT_WRAPPER == QtWrapperTypes.PYQT5:
# Overrides the previous class defintion
class PyDMImageView(PyDMImageViewBase, ReadingOrder, DimensionOrder): # noqa F811
pass
Loading

0 comments on commit 49d57d5

Please sign in to comment.