diff options
Diffstat (limited to 'silx/gui/plot/tools')
-rw-r--r-- | silx/gui/plot/tools/CurveLegendsWidget.py | 247 | ||||
-rw-r--r-- | silx/gui/plot/tools/profile/ImageProfileToolBar.py | 271 | ||||
-rw-r--r-- | silx/gui/plot/tools/test/__init__.py | 2 | ||||
-rw-r--r-- | silx/gui/plot/tools/test/testCurveLegendsWidget.py | 125 | ||||
-rw-r--r-- | silx/gui/plot/tools/test/testROI.py | 2 | ||||
-rw-r--r-- | silx/gui/plot/tools/test/testScatterProfileToolBar.py | 2 | ||||
-rw-r--r-- | silx/gui/plot/tools/test/testTools.py | 2 |
7 files changed, 377 insertions, 274 deletions
diff --git a/silx/gui/plot/tools/CurveLegendsWidget.py b/silx/gui/plot/tools/CurveLegendsWidget.py new file mode 100644 index 0000000..7b63b29 --- /dev/null +++ b/silx/gui/plot/tools/CurveLegendsWidget.py @@ -0,0 +1,247 @@ +# coding: utf-8 +# /*########################################################################## +# +# Copyright (c) 2018 European Synchrotron Radiation Facility +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# +# ###########################################################################*/ +"""This module provides a widget to display :class:`PlotWidget` curve legends. +""" + +from __future__ import division + +__authors__ = ["T. Vincent"] +__license__ = "MIT" +__date__ = "20/07/2018" + + +import logging +import weakref + + +from ... import qt +from ...widgets.FlowLayout import FlowLayout as _FlowLayout +from ..LegendSelector import LegendIcon as _LegendIcon +from .. import items + + +_logger = logging.getLogger(__name__) + + +class _LegendWidget(qt.QWidget): + """Widget displaying curve style and its legend + + :param QWidget parent: See :class:`QWidget` + :param ~silx.gui.plot.items.Curve curve: Associated curve + """ + + def __init__(self, parent, curve): + super(_LegendWidget, self).__init__(parent) + layout = qt.QHBoxLayout(self) + layout.setContentsMargins(10, 0, 10, 0) + + curve.sigItemChanged.connect(self._curveChanged) + + icon = _LegendIcon(curve=curve) + layout.addWidget(icon) + + label = qt.QLabel(curve.getLegend()) + label.setAlignment(qt.Qt.AlignLeft | qt.Qt.AlignVCenter) + layout.addWidget(label) + + self._update() + + def getCurve(self): + """Returns curve associated to this widget + + :rtype: Union[~silx.gui.plot.items.Curve,None] + """ + icon = self.findChild(_LegendIcon) + return icon.getCurve() + + def _update(self): + """Update widget according to current curve state. + """ + curve = self.getCurve() + if curve is None: + _logger.error('Curve no more exists') + self.setVisible(False) + return + + self.setEnabled(curve.isVisible()) + + label = self.findChild(qt.QLabel) + if curve.isHighlighted(): + label.setStyleSheet("border: 1px solid black") + else: + label.setStyleSheet("") + + def _curveChanged(self, event): + """Handle update of curve item + + :param event: Kind of change + """ + if event in (items.ItemChangedType.VISIBLE, + items.ItemChangedType.HIGHLIGHTED, + items.ItemChangedType.HIGHLIGHTED_STYLE): + self._update() + + +class CurveLegendsWidget(qt.QWidget): + """Widget displaying curves legends in a plot + + :param QWidget parent: See :class:`QWidget` + """ + + sigCurveClicked = qt.Signal(object) + """Signal emitted when the legend of a curve is clicked + + It provides the corresponding curve. + """ + + def __init__(self, parent=None): + super(CurveLegendsWidget, self).__init__(parent) + self._clicked = None + self._legends = {} + self._plotRef = None + + def layout(self): + layout = super(CurveLegendsWidget, self).layout() + if layout is None: + # Lazy layout initialization to allow overloading + layout = _FlowLayout() + layout.setHorizontalSpacing(0) + self.setLayout(layout) + return layout + + def getPlotWidget(self): + """Returns the associated :class:`PlotWidget` + + :rtype: Union[~silx.gui.plot.PlotWidget,None] + """ + return None if self._plotRef is None else self._plotRef() + + def setPlotWidget(self, plot): + """Set the associated :class:`PlotWidget` + + :param ~silx.gui.plot.PlotWidget plot: Plot widget to attach + """ + previousPlot = self.getPlotWidget() + if previousPlot is not None: + previousPlot.sigItemAdded.disconnect( self._itemAdded) + previousPlot.sigItemAboutToBeRemoved.disconnect(self._itemRemoved) + for legend in list(self._legends.keys()): + self._removeLegend(legend) + + self._plotRef = None if plot is None else weakref.ref(plot) + + if plot is not None: + plot.sigItemAdded.connect(self._itemAdded) + plot.sigItemAboutToBeRemoved.connect(self._itemRemoved) + + for legend in plot.getAllCurves(just_legend=True): + self._addLegend(legend) + + def curveAt(self, *args): + """Returns the curve object represented at the given position + + Either takes a QPoint or x and y as input in widget coordinates. + + :rtype: Union[~silx.gui.plot.items.Curve,None] + """ + if len(args) == 1: + point = args[0] + elif len(args) == 2: + point = qt.QPoint(*args) + else: + raise ValueError('Unsupported arguments') + assert isinstance(point, qt.QPoint) + + widget = self.childAt(point) + while widget not in (self, None): + if isinstance(widget, _LegendWidget): + return widget.getCurve() + widget = widget.parent() + return None # No widget or not in _LegendWidget + + def _itemAdded(self, item): + """Handle item added to the plot content""" + if isinstance(item, items.Curve): + self._addLegend(item.getLegend()) + + def _itemRemoved(self, item): + """Handle item removed from the plot content""" + if isinstance(item, items.Curve): + self._removeLegend(item.getLegend()) + + def _addLegend(self, legend): + """Add a curve to the legends + + :param str legend: Curve's legend + """ + if legend in self._legends: + return # Can happen when changing curve's y axis + + plot = self.getPlotWidget() + if plot is None: + return None + + curve = plot.getCurve(legend) + if curve is None: + _logger.error('Curve not found: %s' % legend) + return + + widget = _LegendWidget(parent=self, curve=curve) + self.layout().addWidget(widget) + self._legends[legend] = widget + + def _removeLegend(self, legend): + """Remove a curve from the legends if it exists + + :param str legend: The curve's legend + """ + widget = self._legends.pop(legend, None) + if widget is None: + _logger.warning('Unknown legend: %s' % legend) + else: + self.layout().removeWidget(widget) + widget.setParent(None) + + def mousePressEvent(self, event): + if event.button() == qt.Qt.LeftButton: + self._clicked = event.pos() + + _CLICK_THRESHOLD = 5 + """Threshold for clicks""" + + def mouseMoveEvent(self, event): + if self._clicked is not None: + dx = abs(self._clicked.x() - event.pos().x()) + dy = abs(self._clicked.y() - event.pos().y()) + if dx > self._CLICK_THRESHOLD or dy > self._CLICK_THRESHOLD: + self._clicked = None # Click is cancelled + + def mouseReleaseEvent(self, event): + if event.button() == qt.Qt.LeftButton and self._clicked is not None: + curve = self.curveAt(event.pos()) + if curve is not None: + self.sigCurveClicked.emit(curve) + + self._clicked = None diff --git a/silx/gui/plot/tools/profile/ImageProfileToolBar.py b/silx/gui/plot/tools/profile/ImageProfileToolBar.py deleted file mode 100644 index 207a2e2..0000000 --- a/silx/gui/plot/tools/profile/ImageProfileToolBar.py +++ /dev/null @@ -1,271 +0,0 @@ -# TODO quick & dirty proof of concept - -import numpy - -from silx.gui.plot.tools.profile.ScatterProfileToolBar import _BaseProfileToolBar -from .. import items -from ...colors import cursorColorForColormap -from ....image.bilinear import BilinearImage - - -def _alignedPartialProfile(data, rowRange, colRange, axis): - """Mean of a rectangular region (ROI) of a stack of images - along a given axis. - - Returned values and all parameters are in image coordinates. - - :param numpy.ndarray data: 3D volume (stack of 2D images) - The first dimension is the image index. - :param rowRange: [min, max[ of ROI rows (upper bound excluded). - :type rowRange: 2-tuple of int (min, max) with min < max - :param colRange: [min, max[ of ROI columns (upper bound excluded). - :type colRange: 2-tuple of int (min, max) with min < max - :param int axis: The axis along which to take the profile of the ROI. - 0: Sum rows along columns. - 1: Sum columns along rows. - :return: Profile image along the ROI as the mean of the intersection - of the ROI and the image. - """ - assert axis in (0, 1) - assert len(data.shape) == 3 - assert rowRange[0] < rowRange[1] - assert colRange[0] < colRange[1] - - nimages, height, width = data.shape - - # Range aligned with the integration direction - profileRange = colRange if axis == 0 else rowRange - - profileLength = abs(profileRange[1] - profileRange[0]) - - # Subset of the image to use as intersection of ROI and image - rowStart = min(max(0, rowRange[0]), height) - rowEnd = min(max(0, rowRange[1]), height) - colStart = min(max(0, colRange[0]), width) - colEnd = min(max(0, colRange[1]), width) - - imgProfile = numpy.mean(data[:, rowStart:rowEnd, colStart:colEnd], - axis=axis + 1, dtype=numpy.float32) - - # Profile including out of bound area - profile = numpy.zeros((nimages, profileLength), dtype=numpy.float32) - - # Place imgProfile in full profile - offset = - min(0, profileRange[0]) - profile[:, offset:offset + imgProfile.shape[1]] = imgProfile - - return profile - - -def createProfile(points, data, origin, scale, lineWidth): - """Create the profile line for the the given image. - - :param points: Coords of profile end points: (x0, y0, x1, y1) - :param numpy.ndarray data: the 2D image or the 3D stack of images - on which we compute the profile. - :param origin: (ox, oy) the offset from origin - :type origin: 2-tuple of float - :param scale: (sx, sy) the scale to use - :type scale: 2-tuple of float - :param int lineWidth: width of the profile line - :return: `profile, area`, where: - - profile is a 2D array of the profiles of the stack of images. - For a single image, the profile is a curve, so this parameter - has a shape *(1, len(curve))* - - area is a tuple of two 1D arrays with 4 values each. They represent - the effective ROI area corners in plot coords. - - :rtype: tuple(ndarray, (ndarray, ndarray), str, str) - """ - if data is None or points is None or lineWidth is None: - raise ValueError("createProfile called with invalid arguments") - - # force 3D data (stack of images) - if len(data.shape) == 2: - data3D = data.reshape((1,) + data.shape) - elif len(data.shape) == 3: - data3D = data - - roiWidth = max(1, lineWidth) - x0, y0, x1, y1 = points - - # Convert start and end points in image coords as (row, col) - startPt = ((y0 - origin[1]) / scale[1], - (x0 - origin[0]) / scale[0]) - endPt = ((y1 - origin[1]) / scale[1], - (x1 - origin[0]) / scale[0]) - - if (int(startPt[0]) == int(endPt[0]) or - int(startPt[1]) == int(endPt[1])): - # Profile is aligned with one of the axes - - # Convert to int - startPt = int(startPt[0]), int(startPt[1]) - endPt = int(endPt[0]), int(endPt[1]) - - # Ensure startPt <= endPt - if startPt[0] > endPt[0] or startPt[1] > endPt[1]: - startPt, endPt = endPt, startPt - - if startPt[0] == endPt[0]: # Row aligned - rowRange = (int(startPt[0] + 0.5 - 0.5 * roiWidth), - int(startPt[0] + 0.5 + 0.5 * roiWidth)) - colRange = startPt[1], endPt[1] + 1 - profile = _alignedPartialProfile(data3D, - rowRange, colRange, - axis=0) - - else: # Column aligned - rowRange = startPt[0], endPt[0] + 1 - colRange = (int(startPt[1] + 0.5 - 0.5 * roiWidth), - int(startPt[1] + 0.5 + 0.5 * roiWidth)) - profile = _alignedPartialProfile(data3D, - rowRange, colRange, - axis=1) - - # Convert ranges to plot coords to draw ROI area - area = ( - numpy.array( - (colRange[0], colRange[1], colRange[1], colRange[0]), - dtype=numpy.float32) * scale[0] + origin[0], - numpy.array( - (rowRange[0], rowRange[0], rowRange[1], rowRange[1]), - dtype=numpy.float32) * scale[1] + origin[1]) - - else: # General case: use bilinear interpolation - - # Ensure startPt <= endPt - if (startPt[1] > endPt[1] or ( - startPt[1] == endPt[1] and startPt[0] > endPt[0])): - startPt, endPt = endPt, startPt - - profile = [] - for slice_idx in range(data3D.shape[0]): - bilinear = BilinearImage(data3D[slice_idx, :, :]) - - profile.append(bilinear.profile_line( - (startPt[0] - 0.5, startPt[1] - 0.5), - (endPt[0] - 0.5, endPt[1] - 0.5), - roiWidth)) - profile = numpy.array(profile) - - # Extend ROI with half a pixel on each end, and - # Convert back to plot coords (x, y) - length = numpy.sqrt((endPt[0] - startPt[0]) ** 2 + - (endPt[1] - startPt[1]) ** 2) - dRow = (endPt[0] - startPt[0]) / length - dCol = (endPt[1] - startPt[1]) / length - - # Extend ROI with half a pixel on each end - startPt = startPt[0] - 0.5 * dRow, startPt[1] - 0.5 * dCol - endPt = endPt[0] + 0.5 * dRow, endPt[1] + 0.5 * dCol - - # Rotate deltas by 90 degrees to apply line width - dRow, dCol = dCol, -dRow - - area = ( - numpy.array((startPt[1] - 0.5 * roiWidth * dCol, - startPt[1] + 0.5 * roiWidth * dCol, - endPt[1] + 0.5 * roiWidth * dCol, - endPt[1] - 0.5 * roiWidth * dCol), - dtype=numpy.float32) * scale[0] + origin[0], - numpy.array((startPt[0] - 0.5 * roiWidth * dRow, - startPt[0] + 0.5 * roiWidth * dRow, - endPt[0] + 0.5 * roiWidth * dRow, - endPt[0] - 0.5 * roiWidth * dRow), - dtype=numpy.float32) * scale[1] + origin[1]) - - xProfile = numpy.arange(len(profile[0]), dtype=numpy.float64) - - return (xProfile, profile[0]), area - - -class ImageProfileToolBar(_BaseProfileToolBar): - - def __init__(self, parent=None, plot=None, title='Image Profile'): - super(ImageProfileToolBar, self).__init__(parent, plot, title) - plot.sigActiveImageChanged.connect(self.__activeImageChanged) - - roiManager = self._getRoiManager() - if roiManager is None: - _logger.error( - "Error during scatter profile toolbar initialisation") - else: - roiManager.sigInteractiveModeStarted.connect( - self.__interactionStarted) - roiManager.sigInteractiveModeFinished.connect( - self.__interactionFinished) - if roiManager.isStarted(): - self.__interactionStarted(roiManager.getRegionOfInterestKind()) - - def __interactionStarted(self, kind): - """Handle start of ROI interaction""" - plot = self.getPlotWidget() - if plot is None: - return - - plot.sigActiveImageChanged.connect(self.__activeImageChanged) - - image = plot.getActiveImage() - legend = None if image is None else image.getLegend() - self.__activeImageChanged(None, legend) - - def __interactionFinished(self, rois): - """Handle end of ROI interaction""" - plot = self.getPlotWidget() - if plot is None: - return - - plot.sigActiveImageChanged.disconnect(self.__activeImageChanged) - - image = plot.getActiveImage() - legend = None if image is None else image.getLegend() - self.__activeImageChanged(legend, None) - - def __activeImageChanged(self, previous, legend): - """Handle active image change: toggle enabled toolbar, update curve""" - plot = self.getPlotWidget() - if plot is None: - return - - activeImage = plot.getActiveImage() - if activeImage is None: - self.setEnabled(False) - else: - # Disable for empty image - self.setEnabled(activeImage.getData(copy=False).size > 0) - - # Update default profile color - if isinstance(activeImage, items.ColormapMixIn): - self.setColor(cursorColorForColormap( - activeImage.getColormap()['name'])) # TODO change thsi - else: - self.setColor('black') - - self.updateProfile() - - def computeProfile(self, x0, y0, x1, y1): - """Compute corresponding profile - - :param float x0: Profile start point X coord - :param float y0: Profile start point Y coord - :param float x1: Profile end point X coord - :param float y1: Profile end point Y coord - :return: (x, y) profile data or None - """ - plot = self.getPlotWidget() - if plot is None: - return None - - image = plot.getActiveImage() - if image is None: - return None - - profile, area = createProfile( - points=(x0, y0, x1, y1), - data=image.getData(copy=False), - origin=image.getOrigin(), - scale=image.getScale(), - lineWidth=1) # TODO - - return profile
\ No newline at end of file diff --git a/silx/gui/plot/tools/test/__init__.py b/silx/gui/plot/tools/test/__init__.py index 79301ab..9cede27 100644 --- a/silx/gui/plot/tools/test/__init__.py +++ b/silx/gui/plot/tools/test/__init__.py @@ -32,6 +32,7 @@ import unittest from . import testROI from . import testTools from . import testScatterProfileToolBar +from . import testCurveLegendsWidget def suite(): @@ -40,6 +41,7 @@ def suite(): [testROI.suite(), testTools.suite(), testScatterProfileToolBar.suite(), + testCurveLegendsWidget.suite(), ]) return test_suite diff --git a/silx/gui/plot/tools/test/testCurveLegendsWidget.py b/silx/gui/plot/tools/test/testCurveLegendsWidget.py new file mode 100644 index 0000000..4824dd7 --- /dev/null +++ b/silx/gui/plot/tools/test/testCurveLegendsWidget.py @@ -0,0 +1,125 @@ +# coding: utf-8 +# /*########################################################################## +# +# Copyright (c) 2018 European Synchrotron Radiation Facility +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# +# ###########################################################################*/ +__authors__ = ["T. Vincent"] +__license__ = "MIT" +__date__ = "02/08/2018" + + +import unittest + +from silx.gui import qt +from silx.utils.testutils import ParametricTestCase +from silx.gui.utils.testutils import TestCaseQt +from silx.gui.plot import PlotWindow +from silx.gui.plot.tools import CurveLegendsWidget + + +class TestCurveLegendsWidget(TestCaseQt, ParametricTestCase): + """Tests for CurveLegendsWidget class""" + + def setUp(self): + super(TestCurveLegendsWidget, self).setUp() + self.plot = PlotWindow() + + self.legends = CurveLegendsWidget.CurveLegendsWidget() + self.legends.setPlotWidget(self.plot) + + dock = qt.QDockWidget() + dock.setWindowTitle('Curve Legends') + dock.setWidget(self.legends) + self.plot.addTabbedDockWidget(dock) + + self.plot.show() + self.qWaitForWindowExposed(self.plot) + + def tearDown(self): + del self.legends + self.qapp.processEvents() + self.plot.setAttribute(qt.Qt.WA_DeleteOnClose) + self.plot.close() + del self.plot + super(TestCurveLegendsWidget, self).tearDown() + + def _assertNbLegends(self, count): + """Check the number of legends in the CurveLegendsWidget""" + children = self.legends.findChildren(CurveLegendsWidget._LegendWidget) + self.assertEqual(len(children), count) + + def testAddRemoveCurves(self): + """Test CurveLegendsWidget while adding/removing curves""" + self.plot.addCurve((0, 1), (1, 2), legend='a') + self._assertNbLegends(1) + self.plot.addCurve((0, 1), (2, 3), legend='b') + self._assertNbLegends(2) + + # Detached/attach + self.legends.setPlotWidget(None) + self._assertNbLegends(0) + + self.legends.setPlotWidget(self.plot) + self._assertNbLegends(2) + + self.plot.clear() + self._assertNbLegends(0) + + def testUpdateCurves(self): + """Test CurveLegendsWidget while updating curves """ + self.plot.addCurve((0, 1), (1, 2), legend='a') + self._assertNbLegends(1) + self.plot.addCurve((0, 1), (2, 3), legend='b') + self._assertNbLegends(2) + + # Activate curve + self.plot.setActiveCurve('a') + self.qapp.processEvents() + self.plot.setActiveCurve('b') + self.qapp.processEvents() + + # Change curve style + curve = self.plot.getCurve('a') + curve.setLineWidth(2) + for linestyle in (':', '', '--', '-'): + with self.subTest(linestyle=linestyle): + curve.setLineStyle(linestyle) + self.qapp.processEvents() + self.qWait(1000) + + for symbol in ('o', 'd', '', 's'): + with self.subTest(symbol=symbol): + curve.setSymbol(symbol) + self.qapp.processEvents() + self.qWait(1000) + + +def suite(): + test_suite = unittest.TestSuite() + test_suite.addTest( + unittest.defaultTestLoader.loadTestsFromTestCase( + TestCurveLegendsWidget)) + return test_suite + + +if __name__ == '__main__': + unittest.main(defaultTest='suite') diff --git a/silx/gui/plot/tools/test/testROI.py b/silx/gui/plot/tools/test/testROI.py index 5032036..8aec1d9 100644 --- a/silx/gui/plot/tools/test/testROI.py +++ b/silx/gui/plot/tools/test/testROI.py @@ -32,7 +32,7 @@ import numpy.testing from silx.gui import qt from silx.utils.testutils import ParametricTestCase -from silx.gui.test.utils import TestCaseQt, SignalListener +from silx.gui.utils.testutils import TestCaseQt, SignalListener from silx.gui.plot import PlotWindow import silx.gui.plot.items.roi as roi_items from silx.gui.plot.tools import roi diff --git a/silx/gui/plot/tools/test/testScatterProfileToolBar.py b/silx/gui/plot/tools/test/testScatterProfileToolBar.py index 16972f9..b99cac7 100644 --- a/silx/gui/plot/tools/test/testScatterProfileToolBar.py +++ b/silx/gui/plot/tools/test/testScatterProfileToolBar.py @@ -32,7 +32,7 @@ import numpy from silx.gui import qt from silx.utils.testutils import ParametricTestCase -from silx.gui.test.utils import TestCaseQt +from silx.gui.utils.testutils import TestCaseQt from silx.gui.plot import PlotWindow from silx.gui.plot.tools import profile import silx.gui.plot.items.roi as roi_items diff --git a/silx/gui/plot/tools/test/testTools.py b/silx/gui/plot/tools/test/testTools.py index 810b933..f4adda0 100644 --- a/silx/gui/plot/tools/test/testTools.py +++ b/silx/gui/plot/tools/test/testTools.py @@ -34,7 +34,7 @@ import unittest import numpy from silx.utils.testutils import TestLogging -from silx.gui.test.utils import qWaitForWindowExposedAndActivate +from silx.gui.utils.testutils import qWaitForWindowExposedAndActivate from silx.gui import qt from silx.gui.plot import PlotWindow from silx.gui.plot import tools |