Commit 4894fabf authored by Jameson Graef Rollins's avatar Jameson Graef Rollins
Browse files

single/dual cursor modes

Add the ability to enable either one of both T/Y cursors.

The cursor interfaces are given methods to set the individual cursor
visibilities. Cursor reset is moved to the plot interface, where it more
naturally resides. The plot menu gets simple widgets for enabling the two
cursors, and resetting their values.
parent 9f87a709
Pipeline #439221 passed with stage
in 5 minutes and 43 seconds
......@@ -11,6 +11,13 @@ from . import util
from .const import LABEL_FILL, COLOR_MODE
def _calc_reset_values(r):
return (
(3*r[0] + r[1])/4,
(r[0] + 3*r[1])/4,
)
class TCursors(QtCore.QObject):
__slots__ = [
'T1', 'T2', 'diff',
......@@ -23,8 +30,9 @@ class TCursors(QtCore.QObject):
# cursors in all the plots
cursor_moved = Signal('PyQt_PyObject')
def __init__(self):
def __init__(self, plot):
super().__init__()
self.plot = plot
pen = pg.mkPen(style=Qt.DashLine)
label_opts = {
'position': 0,
......@@ -59,13 +67,19 @@ class TCursors(QtCore.QObject):
'fill': LABEL_FILL,
},
)
# always on top
self.T1.setZValue(1)
self.T2.setZValue(1)
self.diff.setZValue(1)
self.set_visible(False, False)
def set_font(self, font):
"""Set text label font"""
"""set text label font"""
for label in [self.T1.label, self.T2.label, self.diff.label]:
label.textItem.setFont(font)
def set_color_mode(self, mode):
"""set color mode"""
fg = COLOR_MODE[mode]['fg']
bg = COLOR_MODE[mode]['bg']
for line in [self.T1, self.T2, self.diff]:
......@@ -74,18 +88,9 @@ class TCursors(QtCore.QObject):
line.pen.setColor(fg)
def update_line(self, line):
self.cursor_moved.emit((line._index, line.value()))
def get_values(self):
return self.T1.value(), self.T2.value()
def set_values(self, t1=None, t2=None):
if t1:
self.T1.setValue(t1)
self.T1.label.setText(str(util.TDStr(t1)))
if t2:
self.T2.setValue(t2)
self.T2.label.setText(str(util.TDStr(t2)))
value = line.value()
line.label.setText('{}={}'.format(line._index, util.TDStr(value)))
self.cursor_moved.emit((line._index, value))
l0 = self.T1.value()
l1 = self.T2.value()
self.diff.setValue((l0 + l1)/2)
......@@ -96,11 +101,76 @@ class TCursors(QtCore.QObject):
)
self.diff.label.setHtml(label)
def reset(self, plot):
x, y = plot.viewRange()
t1 = (2*x[0] + x[1])/3
t2 = (x[0] + 2*x[1])/3
self.set_values(t1=t1, t2=t2)
def get_values(self):
"""get cursor values as a tuple"""
return (self.T1.value(), self.T2.value())
def set_values(self, t1=None, t2=None):
"""set cursor values
Values should be floats.
"""
if t1:
self.T1.setValue(t1)
if t2:
self.T2.setValue(t2)
def reset(self):
"""reset cursor values"""
t, y = self.plot.viewRange()
self.set_values(*_calc_reset_values(t))
def set_visible(self, t1=None, t2=None):
"""set cursor visibility
Values should be True or False, or None to not change.
"""
if t1 is not None:
self.T1.setVisible(t1)
if t2 is not None:
self.T2.setVisible(t2)
self.diff.setVisible(self.T1.isVisible() and self.T2.isVisible())
def are_visible(self):
"""True if either cursor is visible"""
return self.T1.isVisible() or self.T2.isVisible()
def export(self):
"""export cursors
Value will be None or absent if the cursor is not visible.
Use load_values() to load these values.
"""
if self.T1.isVisible():
if self.T2.isVisible():
return (self.T1.value(), self.T2.value())
else:
return (self.T1.value(),)
elif self.T2.isVisible():
return (None, self.T2.value())
return ()
def load(self, cursors):
"""load cursors
Load exported cursors. If the tuple only includes one value,
only Y1 will be turned on, if the tuple includes two values,
but Y1 and Y2 will be turned on.
"""
t1 = None
t2 = None
if len(cursors) == 1:
t1 = cursors[0]
elif len(cursors) == 2:
t1, t2 = cursors
else:
raise ValueError("Only two T cursors supported.")
self.set_values(t1, t2)
self.set_visible(t1 is not None, t2 is not None)
class YCursors(QtCore.QObject):
......@@ -108,8 +178,9 @@ class YCursors(QtCore.QObject):
'Y1', 'Y2', 'diff',
]
def __init__(self):
def __init__(self, plot):
super().__init__()
self.plot = plot
pen = pg.mkPen(style=Qt.DashLine)
label_opts = {
'position': 0,
......@@ -130,6 +201,8 @@ class YCursors(QtCore.QObject):
label='Y2',
labelOpts=label_opts,
)
self.Y1._index = 'Y1'
self.Y2._index = 'Y2'
self.Y1.sigPositionChanged.connect(self.update_line)
self.Y2.sigPositionChanged.connect(self.update_line)
self.diff = pg.InfiniteLine(
......@@ -142,13 +215,19 @@ class YCursors(QtCore.QObject):
'fill': LABEL_FILL,
},
)
# always on top
self.Y1.setZValue(1)
self.Y2.setZValue(1)
self.diff.setZValue(1)
self.set_visible(False, False)
def set_font(self, font):
"""Set text label font"""
"""set text label font"""
for label in [self.Y1.label, self.Y2.label, self.diff.label]:
label.textItem.setFont(font)
def set_color_mode(self, mode):
"""set color mode"""
fg = COLOR_MODE[mode]['fg']
bg = COLOR_MODE[mode]['bg']
for line in [self.Y1, self.Y2, self.diff]:
......@@ -158,7 +237,7 @@ class YCursors(QtCore.QObject):
def update_line(self, line):
value = line.value()
line.label.setText('{:g}'.format(value))
line.label.setText('{}={:g}'.format(line._index, value))
l0 = self.Y1.value()
l1 = self.Y2.value()
self.diff.setValue((l0 + l1)/2)
......@@ -167,17 +246,72 @@ class YCursors(QtCore.QObject):
self.diff.label.setText(label)
def get_values(self):
return self.Y1.value(), self.Y2.value()
"""get cursor values as a tuple"""
return (self.Y1.value(), self.Y2.value())
def set_values(self, y1=None, y2=None):
"""set cursor values
Values should be floats.
"""
if y1:
self.Y1.setValue(y1)
if y2:
self.Y2.setValue(y2)
def reset(self, plot):
x, y = plot.viewRange()
y1 = (2*y[0] + y[1])/3
y2 = (y[0] + 2*y[1])/3
self.Y1.setValue(y1)
self.Y2.setValue(y2)
def reset(self):
"""reset cursor values"""
t, y = self.plot.viewRange()
self.set_values(*_calc_reset_values(y))
def set_visible(self, y1=None, y2=None):
"""set cursor visibility
Value should be True or False, or None to not change.
"""
if y1 is not None:
self.Y1.setVisible(y1)
if y2 is not None:
self.Y2.setVisible(y2)
self.diff.setVisible(self.Y1.isVisible() and self.Y2.isVisible())
def are_visible(self):
"""True if either cursor is visible"""
return self.Y1.isVisible() or self.Y2.isVisible()
def export(self):
"""export cursors
Value will be None or absent if the cursor is not visible.
Use load_values() to load these values.
"""
if self.Y1.isVisible():
if self.Y2.isVisible():
return (self.Y1.value(), self.Y2.value())
else:
return (self.Y1.value(),)
elif self.Y2.isVisible():
return (None, self.Y2.value())
return ()
def load(self, y_tuple):
"""load cursors
Load exported cursors. If the tuple only includes one value,
only Y1 will be turned on, if the tuple includes two values,
but Y1 and Y2 will be turned on.
"""
y1 = None
y2 = None
if len(y_tuple) == 1:
y1 = y_tuple[0]
elif len(y_tuple) == 2:
y1, y2 = y_tuple
else:
raise ValueError("Only two Y cursors supported.")
self.set_values(y1, y2)
self.set_visible(y1 is not None, y2 is not None)
import copy
import logging
import collections
import numpy as np
......@@ -17,6 +18,7 @@ from . import template
from . import legend
from . import cursors
logger = logging.getLogger('PLOT')
##################################################
......@@ -171,8 +173,9 @@ class NDScopePlot(PlotItem):
# setting the font.
self.titleLabel.opts.pop('size')
self.t_cursors = None
self.y_cursors = None
self.t_cursors = cursors.TCursors(self)
self.t_cursors.cursor_moved.connect(self._update_t_cursor)
self.y_cursors = cursors.YCursors(self)
def dropEvent(self, event):
data = event.mimeData()
......@@ -250,10 +253,8 @@ class NDScopePlot(PlotItem):
self.axes['left']['item'].label.setFont(font)
self.axes['bottom']['item'].setTickFont(font)
self.axes['left']['item'].setTickFont(font)
if self.t_cursors:
self.t_cursors.set_font(font)
if self.y_cursors:
self.y_cursors.set_font(font)
self.t_cursors.set_font(font)
self.y_cursors.set_font(font)
def set_color_mode(self, mode):
fg = const.COLOR_MODE[mode]['fg']
......@@ -263,34 +264,23 @@ class NDScopePlot(PlotItem):
self.legend.setTextColor(fg)
self.axes['left']['item'].set_color_mode(mode)
self.axes['bottom']['item'].set_color_mode(mode)
if self.t_cursors:
self.t_cursors.set_color_mode(mode)
if self.y_cursors:
self.y_cursors.set_color_mode(mode)
self._color_mode = mode
self.t_cursors.set_color_mode(mode)
self.y_cursors.set_color_mode(mode)
def _reset_t0(self, val):
self.t0_reset.emit(val)
##### cursors
def enable_t_cursors(self):
"""enable T axis cursors"""
if self.t_cursors:
return
self.t_cursors = cursors.TCursors()
self.t_cursors.cursor_moved.connect(self._update_t_cursor)
self.addItem(self.t_cursors.T1)
self.addItem(self.t_cursors.T2)
self.addItem(self.t_cursors.diff)
self.t_cursors.set_color_mode(self._color_mode)
def disable_t_cursors(self):
"""disable T axis cursors"""
if not self.t_cursors:
return
self.removeItem(self.t_cursors.T1)
self.removeItem(self.t_cursors.T2)
self.removeItem(self.t_cursors.diff)
self.t_cursors = None
"""enable T cursors and return cursor object"""
if self.t_cursors.T1 not in self.getViewBox().allChildren():
logger.debug("T cursor enable")
self.addItem(self.t_cursors.T1, ignoreBounds=True)
self.addItem(self.t_cursors.T2, ignoreBounds=True)
self.addItem(self.t_cursors.diff, ignoreBounds=True)
self.t_cursors.reset()
return self.t_cursors
def _update_t_cursors(self, enable):
self.t_cursors_enabled.emit(enable)
......@@ -299,24 +289,16 @@ class NDScopePlot(PlotItem):
self.t_cursor_moved.emit(indval)
def enable_y_cursors(self):
"""enable Y axis cursors"""
if self.y_cursors:
return
self.y_cursors = cursors.YCursors()
self.addItem(self.y_cursors.Y1)
self.addItem(self.y_cursors.Y2)
self.addItem(self.y_cursors.diff)
self.y_cursors.reset(self)
self.y_cursors.set_color_mode(self._color_mode)
def disable_y_cursors(self):
"""disable Y axis cursors"""
if not self.y_cursors:
return
self.removeItem(self.y_cursors.Y1)
self.removeItem(self.y_cursors.Y2)
self.removeItem(self.y_cursors.diff)
self.y_cursors = None
"""enable Y cursors and return cursor object"""
if self.y_cursors.Y1 not in self.getViewBox().allChildren():
logger.debug("Y cursor enable")
self.addItem(self.y_cursors.Y1, ignoreBounds=True)
self.addItem(self.y_cursors.Y2, ignoreBounds=True)
self.addItem(self.y_cursors.diff, ignoreBounds=True)
self.y_cursors.reset()
return self.y_cursors
##########
# SLOT
def _update_legend_item(self, channel):
......
......@@ -39,6 +39,24 @@ class MouseModeMenuItem(QtWidgets.QMenu):
self.rect.setActionGroup(group)
class CursorWidget(QtWidgets.QWidget):
def __init__(self, check1, check2):
super().__init__()
self._c1 = QtWidgets.QCheckBox(check1)
self._c1.setToolTip(f"enable {check1} cursor")
self._c2 = QtWidgets.QCheckBox(check2)
self._c2.setToolTip(f"enable {check2} cursor")
setattr(self, check1, self._c1)
setattr(self, check2, self._c2)
self.reset = QtWidgets.QPushButton("reset")
self.reset.setToolTip("reset cursor positions")
self.layout = QtWidgets.QHBoxLayout()
self.layout.addWidget(self._c1)
self.layout.addWidget(self._c2)
self.layout.addWidget(self.reset)
self.setLayout(self.layout)
# this is lifted from the pqtgraph.ViewBoxMenu module
class NDScopePlotMenu(QtWidgets.QMenu):
def __init__(self, plot):
......@@ -55,7 +73,6 @@ class NDScopePlotMenu(QtWidgets.QMenu):
self.addLabel(title)
self.addSeparator()
# view all data
self.viewAll = QtWidgets.QAction("view all data", self)
self.viewAll.triggered.connect(self.autoRange)
self.addAction(self.viewAll)
......@@ -72,20 +89,33 @@ class NDScopePlotMenu(QtWidgets.QMenu):
self.mouseModeUI.rect.triggered.connect(self.setMouseModeRect)
self.addMenu(self.mouseModeUI)
self.YCursorSelect = QtWidgets.QAction("Y cursors", self, checkable=True)
self.YCursorSelect.triggered.connect(self.toggle_y_cursors)
self.addAction(self.YCursorSelect)
self.TCursorSelect = QtWidgets.QAction("T cursors", self, checkable=True)
self.TCursorSelect.triggered.connect(self.toggle_t_cursors)
self.addAction(self.TCursorSelect)
self.addLabel()
self.addSection("T cursors")
self.t_cursor_widget = CursorWidget('T1', 'T2')
self.t_cursor_action = QtWidgets.QWidgetAction(self)
self.t_cursor_action.setDefaultWidget(self.t_cursor_widget)
self.t_cursor_widget.T1.stateChanged.connect(self.update_t1_cursor)
self.t_cursor_widget.T2.stateChanged.connect(self.update_t2_cursor)
self.t_cursor_widget.reset.clicked.connect(self.reset_t_cursors)
self.addAction(self.t_cursor_action)
self.addSection("Y cursors")
self.y_cursor_widget = CursorWidget('Y1', 'Y2')
self.y_cursor_action = QtWidgets.QWidgetAction(self)
self.y_cursor_action.setDefaultWidget(self.y_cursor_widget)
self.y_cursor_widget.Y1.stateChanged.connect(self.update_y1_cursor)
self.y_cursor_widget.Y2.stateChanged.connect(self.update_y2_cursor)
self.y_cursor_widget.reset.clicked.connect(self.reset_y_cursors)
self.addAction(self.y_cursor_action)
self.addLabel()
self.addSection("add/modify/remove channels")
self.addLabel()
# channel select dialog
self.openChannelSelectDialogButton = self.addButton("set channel list/parameters")
self.openChannelSelectDialogButton.clicked.connect(self.channel_select_dialog)
# add channel
self.addChannelEntry = QtWidgets.QLineEdit()
self.addChannelEntry.setMinimumSize(300, 24)
self.addChannelEntry.setPlaceholderText("enter channel to add")
......@@ -103,7 +133,6 @@ class NDScopePlotMenu(QtWidgets.QMenu):
self.addLabel()
# remove channel
self.removeChannelList = QtWidgets.QComboBox()
self.removeChannelList.setMinimumSize(200, 26)
self.removeChannelList.currentIndexChanged.connect(self.remove_channel)
......@@ -113,8 +142,7 @@ class NDScopePlotMenu(QtWidgets.QMenu):
self.addAction(rcl)
self.addLabel()
self.addSeparator()
self.addLabel("add/remove plots")
self.addSection("add/remove plots")
self.addLabel()
self.newPlotRowButton = self.addButton("add plot to row")
......@@ -184,16 +212,16 @@ class NDScopePlotMenu(QtWidgets.QMenu):
def popup(self, pos):
self.updateState()
ppos = self.plot().vb.mapSceneToView(pos)
plot = self.plot()
ppos = plot.vb.mapSceneToView(pos)
self.pos = (ppos.x(), ppos.y())
if hasattr(self.plot(), 'y_cursors'):
enabled = self.plot().y_cursors is not None
self.YCursorSelect.setChecked(enabled)
self.t_cursor_widget.T1.setChecked(plot.t_cursors.T1.isVisible())
self.t_cursor_widget.T2.setChecked(plot.t_cursors.T2.isVisible())
if hasattr(self.plot(), 't_cursors'):
enabled = self.plot().t_cursors is not None
self.TCursorSelect.setChecked(enabled)
self.y_cursor_widget.Y1.setChecked(plot.y_cursors.Y1.isVisible())
self.y_cursor_widget.Y2.setChecked(plot.y_cursors.Y2.isVisible())
# update remove channels list
self.update_channel_list()
......@@ -218,7 +246,7 @@ class NDScopePlotMenu(QtWidgets.QMenu):
# showTop=False,
# )
self.removeChannelList.setEnabled(len(self.plot().channels) > 0)
self.removeChannelList.setEnabled(len(plot.channels) > 0)
QtWidgets.QMenu.popup(self, pos)
......@@ -231,16 +259,6 @@ class NDScopePlotMenu(QtWidgets.QMenu):
def reset_t0(self):
self.plot()._reset_t0(self.pos[0])
def toggle_t_cursors(self):
self.plot()._update_t_cursors(
self.TCursorSelect.isChecked())
def toggle_y_cursors(self):
if self.YCursorSelect.isChecked():
self.plot().enable_y_cursors()
else:
self.plot().disable_y_cursors()
##########
def update_channel_list(self):
......@@ -339,3 +357,29 @@ class NDScopePlotMenu(QtWidgets.QMenu):
def yInvertToggled(self, b):
self.view().invertY(b)
def update_t1_cursor(self):
self.plot().enable_t_cursors().set_visible(
t1=self.t_cursor_widget.T1.isChecked(),
)
def update_t2_cursor(self):
self.plot().enable_t_cursors().set_visible(
t2=self.t_cursor_widget.T2.isChecked(),
)
def reset_t_cursors(self):
self.plot().t_cursors.reset()
def update_y1_cursor(self):
self.plot().enable_y_cursors().set_visible(
y1=self.y_cursor_widget.Y1.isChecked(),
)
def update_y2_cursor(self):
self.plot().enable_y_cursors().set_visible(
y2=self.y_cursor_widget.Y2.isChecked(),
)
def reset_y_cursors(self):
self.plot().y_cursors.reset()
......@@ -587,7 +587,6 @@ class NDScope(*load_ui('scope.ui')):
plot.new_plot_request.connect(self._add_plot_handler)
plot.remove_plot_request.connect(self.remove_plot)
plot.t0_reset.connect(self.reset_t0_relative)
plot.t_cursors_enabled.connect(self._update_t_cursors)
plot.t_cursor_moved.connect(self._update_t_cursor)
# FIXME: where to add if row/col not specified? need some
......@@ -624,9 +623,6 @@ class NDScope(*load_ui('scope.ui')):
ckwargs = ckwargs or {}
plot.add_channel(name, **ckwargs)
if self.plot0.t_cursors:
plot.enable_t_cursors()
plot.t_cursors.set_values(*self.plot0.t_cursors.get_values())
# set y ranges
if yrange in [None, 'auto']:
......@@ -829,13 +825,18 @@ class NDScope(*load_ui('scope.ui')):
self.remove_plot(p, _force=True)
def add_plot(p):
t_cursors = p.get('t-cursors')
y_cursors = p.get('y-cursors')
if 'y-cursors' in p:
del p['y-cursors']
for key in ['t-cursors', 'y-cursors']:
try:
del p[key]
except KeyError:
pass
plot = self.add_plot(**p)
if t_cursors:
plot.enable_t_cursors().load(t_cursors)
if y_cursors:
plot.enable_y_cursors()
plot.y_cursors.set_values(*y_cursors)
plot.enable_y_cursors().load(y_cursors)
# add the (0, 0) plot first
add_plot(plot0)
......@@ -852,8 +853,7 @@ class NDScope(*load_ui('scope.ui')):
t_cursors = template.get('t-cursors')
if t_cursors:
self.enable_t_cursors()
self.set_t_cursor_values(*t_cursors)
self.load_t_cursors(t_cursors)
time_axis_mode = template.get('time-axis-mode')