summaryrefslogtreecommitdiff
path: root/silx/gui/widgets
diff options
context:
space:
mode:
Diffstat (limited to 'silx/gui/widgets')
-rw-r--r--silx/gui/widgets/FloatEdit.py4
-rw-r--r--silx/gui/widgets/FlowLayout.py177
-rw-r--r--silx/gui/widgets/PrintPreview.py4
-rw-r--r--silx/gui/widgets/RangeSlider.py627
-rw-r--r--silx/gui/widgets/__init__.py8
-rw-r--r--silx/gui/widgets/test/__init__.py4
-rw-r--r--silx/gui/widgets/test/test_boxlayoutdockwidget.py2
-rw-r--r--silx/gui/widgets/test/test_flowlayout.py77
-rw-r--r--silx/gui/widgets/test/test_framebrowser.py2
-rw-r--r--silx/gui/widgets/test/test_hierarchicaltableview.py2
-rw-r--r--silx/gui/widgets/test/test_periodictable.py2
-rw-r--r--silx/gui/widgets/test/test_printpreview.py2
-rw-r--r--silx/gui/widgets/test/test_rangeslider.py114
-rw-r--r--silx/gui/widgets/test/test_tablewidget.py2
-rw-r--r--silx/gui/widgets/test/test_threadpoolpushbutton.py4
15 files changed, 1017 insertions, 14 deletions
diff --git a/silx/gui/widgets/FloatEdit.py b/silx/gui/widgets/FloatEdit.py
index fd6d8a7..36a39a7 100644
--- a/silx/gui/widgets/FloatEdit.py
+++ b/silx/gui/widgets/FloatEdit.py
@@ -1,7 +1,7 @@
# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2004-2017 European Synchrotron Radiation Facility
+# Copyright (c) 2004-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
@@ -61,5 +61,5 @@ class FloatEdit(qt.QLineEdit):
:param float value: The value to set the QLineEdit to.
"""
- text = self.validator().locale().toString(value)
+ text = self.validator().locale().toString(float(value))
self.setText(text)
diff --git a/silx/gui/widgets/FlowLayout.py b/silx/gui/widgets/FlowLayout.py
new file mode 100644
index 0000000..14c8ab2
--- /dev/null
+++ b/silx/gui/widgets/FlowLayout.py
@@ -0,0 +1,177 @@
+# 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 flow layout for QWidget: :class:`FlowLayout`.
+"""
+
+from __future__ import division
+
+__authors__ = ["T. Vincent"]
+__license__ = "MIT"
+__date__ = "20/07/2018"
+
+
+from .. import qt
+
+
+class FlowLayout(qt.QLayout):
+ """Layout widgets on (possibly) multiple lines in the available width.
+
+ See Qt :class:`QLayout` for API documentation.
+
+ Adapted from C++ `Qt FlowLayout example
+ <http://doc.qt.io/qt-5/qtwidgets-layouts-flowlayout-example.html>`_
+
+ :param QWidget parent: See :class:`QLayout`
+ """
+
+ def __init__(self, parent=None):
+ super(FlowLayout, self).__init__(parent)
+ self._items = []
+ self._horizontalSpacing = -1
+ self._verticalSpacing = -1
+
+ def addItem(self, item):
+ self._items.append(item)
+
+ def count(self):
+ return len(self._items)
+
+ def itemAt(self, index):
+ if 0 <= index < len(self._items):
+ return self._items[index]
+ else:
+ return None
+
+ def takeAt(self, index):
+ if 0 <= index < len(self._items):
+ return self._items.pop(index)
+ else:
+ return None
+
+ def expandingDirections(self):
+ return 0
+
+ def hasHeightForWidth(self):
+ return True
+
+ def heightForWidth(self, width):
+ return self._layout(qt.QRect(0, 0, width, 0), test=True)
+
+ def setGeometry(self, rect):
+ super(FlowLayout, self).setGeometry(rect)
+ self._layout(rect)
+
+ def sizeHint(self):
+ return self.minimumSize()
+
+ def minimumSize(self):
+ size = qt.QSize()
+ for item in self._items:
+ size = size.expandedTo(item.minimumSize())
+
+ left, top, right, bottom = self.getContentsMargins()
+ size += qt.QSize(left + right, top + bottom)
+ return size
+
+ def _layout(self, rect, test=False):
+ left, top, right, bottom = self.getContentsMargins()
+ effectiveRect = rect.adjusted(left, top, -right, -bottom)
+ x, y = effectiveRect.x(), effectiveRect.y()
+ lineHeight = 0
+
+ for item in self._items:
+ widget = item.widget()
+ spaceX = self.horizontalSpacing()
+ if spaceX == -1:
+ spaceX = widget.style().layoutSpacing(
+ qt.QSizePolicy.PushButton,
+ qt.QSizePolicy.PushButton,
+ qt.Qt.Horizontal)
+ spaceY = self.verticalSpacing()
+ if spaceY == -1:
+ spaceY = widget.style().layoutSpacing(
+ qt.QSizePolicy.PushButton,
+ qt.QSizePolicy.PushButton,
+ qt.Qt.Vertical)
+
+ nextX = x + item.sizeHint().width() + spaceX
+ if (nextX - spaceX) > effectiveRect.right() and lineHeight > 0:
+ x = effectiveRect.x()
+ y += lineHeight + spaceY
+ nextX = x + item.sizeHint().width() + spaceX
+ lineHeight = 0
+
+ if not test:
+ item.setGeometry(qt.QRect(qt.QPoint(x, y), item.sizeHint()))
+
+ x = nextX
+ lineHeight = max(lineHeight, item.sizeHint().height())
+
+ return y + lineHeight - rect.y() + bottom
+
+ def setHorizontalSpacing(self, spacing):
+ """Set the horizontal spacing between widgets laid out side by side
+
+ :param int spacing:
+ """
+ self._horizontalSpacing = spacing
+ self.update()
+
+ def horizontalSpacing(self):
+ """Returns the horizontal spacing between widgets laid out side by side
+
+ :rtype: int
+ """
+ if self._horizontalSpacing >= 0:
+ return self._horizontalSpacing
+ else:
+ return self._smartSpacing(qt.QStyle.PM_LayoutHorizontalSpacing)
+
+ def setVerticalSpacing(self, spacing):
+ """Set the vertical spacing between lines
+
+ :param int spacing:
+ """
+ self._verticalSpacing = spacing
+ self.update()
+
+ def verticalSpacing(self):
+ """Returns the vertical spacing between lines
+
+ :rtype: int
+ """
+ if self._verticalSpacing >= 0:
+ return self._verticalSpacing
+ else:
+ return self._smartSpacing(qt.QStyle.PM_LayoutVerticalSpacing)
+
+ def _smartSpacing(self, pm):
+ parent = self.parent()
+ if parent is None:
+ return -1
+ if parent.isWidgetType():
+ return parent.style().pixelMetric(pm, None, parent)
+ else:
+ return parent.spacing()
diff --git a/silx/gui/widgets/PrintPreview.py b/silx/gui/widgets/PrintPreview.py
index 78d1bd7..94a8ed4 100644
--- a/silx/gui/widgets/PrintPreview.py
+++ b/silx/gui/widgets/PrintPreview.py
@@ -411,6 +411,9 @@ class PrintPreviewDialog(qt.QDialog):
"""If the printer is not already set, try to interactively
setup the printer using a QPrintDialog.
In case of failure, hide widget and log a warning.
+
+ :return: True if printer was set. False if it failed or if the
+ selection dialog was canceled.
"""
if self.printer is None:
self.setup()
@@ -418,6 +421,7 @@ class PrintPreviewDialog(qt.QDialog):
self.hide()
_logger.warning("Printer setup failed or was cancelled, " +
"but printer is required.")
+ return self.printer is not None
def setOutputFileName(self, name):
"""Set output filename.
diff --git a/silx/gui/widgets/RangeSlider.py b/silx/gui/widgets/RangeSlider.py
new file mode 100644
index 0000000..0b72e71
--- /dev/null
+++ b/silx/gui/widgets/RangeSlider.py
@@ -0,0 +1,627 @@
+# coding: utf-8
+# /*##########################################################################
+#
+# Copyright (c) 2015-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 :class:`RangeSlider` widget.
+
+.. image:: img/RangeSlider.png
+ :align: center
+"""
+from __future__ import absolute_import, division
+
+__authors__ = ["D. Naudet", "T. Vincent"]
+__license__ = "MIT"
+__date__ = "02/08/2018"
+
+
+import numpy as numpy
+
+from silx.gui import qt, icons, colors
+from silx.gui.utils.image import convertArrayToQImage
+
+
+class RangeSlider(qt.QWidget):
+ """Range slider with 2 thumbs and an optional colored groove.
+
+ The position of the slider thumbs can be retrieved either as values
+ in the slider range or as a number of steps or pixels.
+
+ :param QWidget parent: See QWidget
+ """
+
+ _SLIDER_WIDTH = 10
+ """Width of the slider rectangle"""
+
+ _PIXMAP_VOFFSET = 7
+ """Vertical groove pixmap offset"""
+
+ sigRangeChanged = qt.Signal(float, float)
+ """Signal emitted when the value range has changed.
+
+ It provides the new range (min, max).
+ """
+
+ sigValueChanged = qt.Signal(float, float)
+ """Signal emitted when the value of the sliders has changed.
+
+ It provides the slider values (first, second).
+ """
+
+ sigPositionCountChanged = qt.Signal(object)
+ """This signal is emitted when the number of steps has changed.
+
+ It provides the new position count.
+ """
+
+ sigPositionChanged = qt.Signal(int, int)
+ """Signal emitted when the position of the sliders has changed.
+
+ It provides the slider positions in steps or pixels (first, second).
+ """
+
+ def __init__(self, parent=None):
+ self.__pixmap = None
+ self.__positionCount = None
+ self.__firstValue = 0.
+ self.__secondValue = 1.
+ self.__minValue = 0.
+ self.__maxValue = 1.
+
+ self.__focus = None
+ self.__moving = None
+
+ self.__icons = {
+ 'first': icons.getQIcon('previous'),
+ 'second': icons.getQIcon('next')
+ }
+
+ # call the super constructor AFTER defining all members that
+ # are used in the "paint" method
+ super(RangeSlider, self).__init__(parent)
+
+ self.setFocusPolicy(qt.Qt.ClickFocus)
+
+ self.setMinimumSize(qt.QSize(50, 20))
+ self.setMaximumHeight(20)
+
+ # Broadcast value changed signal
+ self.sigValueChanged.connect(self.__emitPositionChanged)
+
+ # Position <-> Value conversion
+
+ def __positionToValue(self, position):
+ """Returns value corresponding to position
+
+ :param int position:
+ :rtype: float
+ """
+ min_, max_ = self.getMinimum(), self.getMaximum()
+ maxPos = self.__getCurrentPositionCount() - 1
+ return min_ + (max_ - min_) * int(position) / maxPos
+
+ def __valueToPosition(self, value):
+ """Returns closest position corresponding to value
+
+ :param float value:
+ :rtype: int
+ """
+ min_, max_ = self.getMinimum(), self.getMaximum()
+ maxPos = self.__getCurrentPositionCount() - 1
+ return int(0.5 + maxPos * (float(value) - min_) / (max_ - min_))
+
+ # Position (int) API
+
+ def __getCurrentPositionCount(self):
+ """Return current count (either position count or widget width
+
+ :rtype: int
+ """
+ count = self.getPositionCount()
+ if count is not None:
+ return count
+ else:
+ return max(2, self.width() - self._SLIDER_WIDTH)
+
+ def getPositionCount(self):
+ """Returns the number of positions.
+
+ :rtype: Union[int,None]"""
+ return self.__positionCount
+
+ def setPositionCount(self, count):
+ """Set the number of positions.
+
+ Slider values are eventually adjusted.
+
+ :param Union[int,None] count:
+ Either the number of possible positions or
+ None to allow any values.
+ :raise ValueError: If count <= 1
+ """
+ count = None if count is None else int(count)
+ if count != self.getPositionCount():
+ if count is not None and count <= 1:
+ raise ValueError("Position count must be higher than 1")
+ self.__positionCount = count
+ emit = self.__setValues(*self.getValues())
+ self.sigPositionCountChanged.emit(count)
+ if emit:
+ self.sigValueChanged.emit(*self.getValues())
+
+ def getFirstPosition(self):
+ """Returns first slider position
+
+ :rtype: int
+ """
+ return self.__valueToPosition(self.getFirstValue())
+
+ def setFirstPosition(self, position):
+ """Set the position of the first slider
+
+ The position is adjusted to valid values
+
+ :param int position:
+ """
+ self.setFirstValue(self.__positionToValue(position))
+
+ def getSecondPosition(self):
+ """Returns second slider position
+
+ :rtype: int
+ """
+ return self.__valueToPosition(self.getSecondValue())
+
+ def setSecondPosition(self, position):
+ """Set the position of the second slider
+
+ The position is adjusted to valid values
+
+ :param int position:
+ """
+ self.setSecondValue(self.__positionToValue(position))
+
+ def getPositions(self):
+ """Returns slider positions (first, second)
+
+ :rtype: List[int]
+ """
+ return self.getFirstPosition(), self.getSecondPosition()
+
+ def setPositions(self, first, second):
+ """Set the position of both sliders at once
+
+ First is clipped to the slider range: [0, max].
+ Second is clipped to valid values: [first, max]
+
+ :param int first:
+ :param int second:
+ """
+ self.setValues(self.__positionToValue(first),
+ self.__positionToValue(second))
+
+ # Value (float) API
+
+ def __emitPositionChanged(self, *args, **kwargs):
+ self.sigPositionChanged.emit(*self.getPositions())
+
+ def __rangeChanged(self):
+ """Handle change of value range"""
+ emit = self.__setValues(*self.getValues())
+ self.sigRangeChanged.emit(*self.getRange())
+ if emit:
+ self.sigValueChanged.emit(*self.getValues())
+
+ def getMinimum(self):
+ """Returns the minimum value of the slider range
+
+ :rtype: float
+ """
+ return self.__minValue
+
+ def setMinimum(self, minimum):
+ """Set the minimum value of the slider range.
+
+ It eventually adjusts maximum.
+ Slider positions remains unchanged and slider values are modified.
+
+ :param float minimum:
+ """
+ minimum = float(minimum)
+ if minimum != self.getMinimum():
+ if minimum > self.getMaximum():
+ self.__maxValue = minimum
+ self.__minValue = minimum
+ self.__rangeChanged()
+
+ def getMaximum(self):
+ """Returns the maximum value of the slider range
+
+ :rtype: float
+ """
+ return self.__maxValue
+
+ def setMaximum(self, maximum):
+ """Set the maximum value of the slider range
+
+ It eventually adjusts minimum.
+ Slider positions remains unchanged and slider values are modified.
+
+ :param float maximum:
+ """
+ maximum = float(maximum)
+ if maximum != self.getMaximum():
+ if maximum < self.getMinimum():
+ self.__minValue = maximum
+ self.__maxValue = maximum
+ self.__rangeChanged()
+
+ def getRange(self):
+ """Returns the range of values (min, max)
+
+ :rtype: List[float]
+ """
+ return self.getMinimum(), self.getMaximum()
+
+ def setRange(self, minimum, maximum):
+ """Set the range of values.
+
+ If maximum is lower than minimum, minimum is the only valid value.
+ Slider positions remains unchanged and slider values are modified.
+
+ :param float minimum:
+ :param float maximum:
+ """
+ minimum, maximum = float(minimum), float(maximum)
+ if minimum != self.getMinimum() or maximum != self.getMaximum():
+ self.__minValue = minimum
+ self.__maxValue = max(maximum, minimum)
+ self.__rangeChanged()
+
+ def getFirstValue(self):
+ """Returns the value of the first slider
+
+ :rtype: float
+ """
+ return self.__firstValue
+
+ def __clipFirstValue(self, value, max_=None):
+ """Clip first value to range and steps
+
+ :param float value:
+ :param float max_: Alternative maximum to use
+ """
+ if max_ is None:
+ max_ = self.getSecondValue()
+ value = min(max(self.getMinimum(), float(value)), max_)
+ if self.getPositionCount() is not None: # Clip to steps
+ value = self.__positionToValue(self.__valueToPosition(value))
+ return value
+
+ def setFirstValue(self, value):
+ """Set the value of the first slider
+
+ Value is clipped to valid values.
+
+ :param float value:
+ """
+ value = self.__clipFirstValue(value)
+ if value != self.getFirstValue():
+ self.__firstValue = value
+ self.update()
+ self.sigValueChanged.emit(*self.getValues())
+
+ def getSecondValue(self):
+ """Returns the value of the second slider
+
+ :rtype: float
+ """
+ return self.__secondValue
+
+ def __clipSecondValue(self, value):
+ """Clip second value to range and steps
+
+ :param float value:
+ """
+ value = min(max(self.getFirstValue(), float(value)), self.getMaximum())
+ if self.getPositionCount() is not None: # Clip to steps
+ value = self.__positionToValue(self.__valueToPosition(value))
+ return value
+
+ def setSecondValue(self, value):
+ """Set the value of the second slider
+
+ Value is clipped to valid values.
+
+ :param float value:
+ """
+ value = self.__clipSecondValue(value)
+ if value != self.getSecondValue():
+ self.__secondValue = value
+ self.update()
+ self.sigValueChanged.emit(*self.getValues())
+
+ def getValues(self):
+ """Returns value of both sliders at once
+
+ :return: (first value, second value)
+ :rtype: List[float]
+ """
+ return self.getFirstValue(), self.getSecondValue()
+
+ def setValues(self, first, second):
+ """Set values for both sliders at once
+
+ First is clipped to the slider range: [minimum, maximum].
+ Second is clipped to valid values: [first, maximum]
+
+ :param float first:
+ :param float second:
+ """
+ if self.__setValues(first, second):
+ self.sigValueChanged.emit(*self.getValues())
+
+ def __setValues(self, first, second):
+ """Set values for both sliders at once
+
+ First is clipped to the slider range: [minimum, maximum].
+ Second is clipped to valid values: [first, maximum]
+
+ :param float first:
+ :param float second:
+ :return: True if values has changed, False otherwise
+ :rtype: bool
+ """
+ first = self.__clipFirstValue(first, self.getMaximum())
+ second = self.__clipSecondValue(second)
+ values = first, second
+
+ if self.getValues() != values:
+ self.__firstValue, self.__secondValue = values
+ self.update()
+ return True
+ return False
+
+ # Groove API
+
+ def getGroovePixmap(self):
+ """Returns the pixmap displayed in the slider groove if any.
+
+ :rtype: Union[QPixmap,None]
+ """
+ return self.__pixmap
+
+ def setGroovePixmap(self, pixmap):
+ """Set the pixmap displayed in the slider groove.
+
+ :param Union[QPixmap,None] pixmap: The QPixmap to use or None to unset.
+ """
+ assert pixmap is None or isinstance(pixmap, qt.QPixmap)
+ self.__pixmap = pixmap
+ self.update()
+
+ def setGroovePixmapFromProfile(self, profile, colormap=None):
+ """Set the pixmap displayed in the slider groove from histogram values.
+
+ :param Union[numpy.ndarray,None] profile:
+ 1D array of values to display
+ :param Union[Colormap,str] colormap:
+ The colormap name or object to convert profile values to colors
+ """
+ if profile is None:
+ self.setSliderPixmap(None)
+ return
+
+ profile = numpy.array(profile, copy=False)
+
+ if profile.size == 0:
+ self.setSliderPixmap(None)
+ return
+
+ if colormap is None:
+ colormap = colors.Colormap()
+ elif isinstance(colormap, str):
+ colormap = colors.Colormap(name=colormap)
+ assert isinstance(colormap, colors.Colormap)
+
+ rgbImage = colormap.applyToData(profile.reshape(1, -1))[:, :, :3]
+ qimage = convertArrayToQImage(rgbImage)
+ qpixmap = qt.QPixmap.fromImage(qimage)
+ self.setGroovePixmap(qpixmap)
+
+ # Handle interaction
+
+ def mousePressEvent(self, event):
+ super(RangeSlider, self).mousePressEvent(event)
+
+ if event.buttons() == qt.Qt.LeftButton:
+ picked = None
+ for name in ('first', 'second'):
+ area = self.__sliderRect(name)
+ if area.contains(event.pos()):
+ picked = name
+ break
+
+ self.__moving = picked
+ self.__focus = picked
+ self.update()
+
+ def mouseMoveEvent(self, event):
+ super(RangeSlider, self).mouseMoveEvent(event)
+
+ if self.__moving is not None:
+ position = self.__xPixelToPosition(event.pos().x())
+ if self.__moving == 'first':
+ self.setFirstPosition(position)
+ else:
+ self.setSecondPosition(position)
+
+ def mouseReleaseEvent(self, event):
+ super(RangeSlider, self).mouseReleaseEvent(event)
+
+ if event.button() == qt.Qt.LeftButton and self.__moving is not None:
+ self.__moving = None
+ self.update()
+
+ def focusOutEvent(self, event):
+ if self.__focus is not None:
+ self.__focus = None
+ self.update()
+ super(RangeSlider, self).focusOutEvent(event)
+
+ def keyPressEvent(self, event):
+ key = event.key()
+ if event.modifiers() == qt.Qt.NoModifier and self.__focus is not None:
+ if key in (qt.Qt.Key_Left, qt.Qt.Key_Down):
+ if self.__focus == 'first':
+ self.setFirstPosition(self.getFirstPosition() - 1)
+ else:
+ self.setSecondPosition(self.getSecondPosition() - 1)
+ return # accept event
+ elif key in (qt.Qt.Key_Right, qt.Qt.Key_Up):
+ if self.__focus == 'first':
+ self.setFirstPosition(self.getFirstPosition() + 1)
+ else:
+ self.setSecondPosition(self.getSecondPosition() + 1)
+ return # accept event
+
+ super(RangeSlider, self).keyPressEvent(event)
+
+ # Handle resize
+
+ def resizeEvent(self, event):
+ super(RangeSlider, self).resizeEvent(event)
+
+ # If no step, signal position update when width change
+ if (self.getPositionCount() is None and
+ event.size().width() != event.oldSize().width()):
+ self.sigPositionChanged.emit(*self.getPositions())
+
+ # Handle repaint
+
+ def __xPixelToPosition(self, x):
+ """Convert position in pixel to slider position
+
+ :param int x: X in pixel coordinates
+ :rtype: int
+ """
+ sliderArea = self.__sliderAreaRect()
+ maxPos = self.__getCurrentPositionCount() - 1
+ position = maxPos * (x - sliderArea.left()) / (sliderArea.width() - 1)
+ return int(position + 0.5)
+
+ def __sliderRect(self, name):
+ """Returns rectangle corresponding to slider in pixels
+
+ :param str name: 'first' or 'second'
+ :rtype: QRect
+ :raise ValueError: If wrong name
+ """
+ assert name in ('first', 'second')
+ if name == 'first':
+ offset = - self._SLIDER_WIDTH
+ position = self.getFirstPosition()
+ elif name == 'second':
+ offset = 0
+ position = self.getSecondPosition()
+ else:
+ raise ValueError('Unknown name')
+
+ sliderArea = self.__sliderAreaRect()
+
+ maxPos = self.__getCurrentPositionCount() - 1
+ xOffset = int((sliderArea.width() - 1) * position / maxPos)
+ xPos = sliderArea.left() + xOffset + offset
+
+ return qt.QRect(xPos,
+ sliderArea.top(),
+ self._SLIDER_WIDTH,
+ sliderArea.height())
+
+ def __drawArea(self):
+ return self.rect().adjusted(self._SLIDER_WIDTH, 0,
+ -self._SLIDER_WIDTH, 0)
+
+ def __sliderAreaRect(self):
+ return self.__drawArea().adjusted(self._SLIDER_WIDTH / 2.,
+ 0,
+ -self._SLIDER_WIDTH / 2.,
+ 0)
+
+ def __pixMapRect(self):
+ return self.__sliderAreaRect().adjusted(0,
+ self._PIXMAP_VOFFSET,
+ 0,
+ -self._PIXMAP_VOFFSET)
+
+ def paintEvent(self, event):
+ painter = qt.QPainter(self)
+
+ style = qt.QApplication.style()
+
+ area = self.__drawArea()
+ pixmapRect = self.__pixMapRect()
+
+ option = qt.QStyleOptionProgressBar()
+ option.initFrom(self)
+ option.rect = area
+ option.state = ((self.isEnabled() and qt.QStyle.State_Enabled)
+ or qt.QStyle.State_None)
+ style.drawControl(qt.QStyle.CE_ProgressBarGroove,
+ option,
+ painter,
+ self)
+
+ painter.save()
+ pen = painter.pen()
+ pen.setWidth(1)
+ pen.setColor(qt.Qt.black if self.isEnabled() else qt.Qt.gray)
+ painter.setPen(pen)
+ painter.drawRect(pixmapRect.adjusted(-1, -1, 1, 1))
+ painter.restore()
+
+ if self.isEnabled() and self.__pixmap is not None:
+ painter.drawPixmap(area.adjusted(self._SLIDER_WIDTH / 2,
+ self._PIXMAP_VOFFSET,
+ -self._SLIDER_WIDTH / 2 + 1,
+ -self._PIXMAP_VOFFSET + 1),
+ self.__pixmap,
+ self.__pixmap.rect())
+
+ for name in ('first', 'second'):
+ rect = self.__sliderRect(name)
+ option = qt.QStyleOptionButton()
+ option.initFrom(self)
+ option.icon = self.__icons[name]
+ option.iconSize = rect.size() * 0.7
+ if option.state & qt.QStyle.State_MouseOver:
+ option.state ^= qt.QStyle.State_MouseOver
+ if self.__focus == name:
+ option.state |= qt.QStyle.State_HasFocus
+ elif option.state & qt.QStyle.State_HasFocus:
+ option.state ^= qt.QStyle.State_HasFocus
+ option.rect = rect
+ style.drawControl(
+ qt.QStyle.CE_PushButton, option, painter, self)
+
+ def sizeHint(self):
+ return qt.QSize(200, self.minimumHeight())
diff --git a/silx/gui/widgets/__init__.py b/silx/gui/widgets/__init__.py
index 034f4d3..9d0299d 100644
--- a/silx/gui/widgets/__init__.py
+++ b/silx/gui/widgets/__init__.py
@@ -1,7 +1,7 @@
# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2016-2017 European Synchrotron Radiation Facility
+# Copyright (c) 2016-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
@@ -22,6 +22,6 @@
# THE SOFTWARE.
#
# ###########################################################################*/
-"""This package provides a few simple Qt widgets that rely only on a Qt wrapper
-for Python (PyQt5, PyQt4 or PySide). No other optional dependencies of *silx*
-should be required."""
+"""This package provides a few simple Qt widgets that rely only on a Qt binding for Python.
+
+No other optional dependencies of *silx* should be required."""
diff --git a/silx/gui/widgets/test/__init__.py b/silx/gui/widgets/test/__init__.py
index 5e62393..8d179bc 100644
--- a/silx/gui/widgets/test/__init__.py
+++ b/silx/gui/widgets/test/__init__.py
@@ -31,6 +31,8 @@ from . import test_hierarchicaltableview
from . import test_printpreview
from . import test_framebrowser
from . import test_boxlayoutdockwidget
+from . import test_rangeslider
+from . import test_flowlayout
__authors__ = ["V. Valls", "P. Knobel"]
__license__ = "MIT"
@@ -47,5 +49,7 @@ def suite():
test_hierarchicaltableview.suite(),
test_framebrowser.suite(),
test_boxlayoutdockwidget.suite(),
+ test_rangeslider.suite(),
+ test_flowlayout.suite(),
])
return test_suite
diff --git a/silx/gui/widgets/test/test_boxlayoutdockwidget.py b/silx/gui/widgets/test/test_boxlayoutdockwidget.py
index 0df262b..9a93ca1 100644
--- a/silx/gui/widgets/test/test_boxlayoutdockwidget.py
+++ b/silx/gui/widgets/test/test_boxlayoutdockwidget.py
@@ -32,7 +32,7 @@ import unittest
from silx.gui.widgets.BoxLayoutDockWidget import BoxLayoutDockWidget
from silx.gui import qt
-from silx.gui.test.utils import TestCaseQt
+from silx.gui.utils.testutils import TestCaseQt
class TestBoxLayoutDockWidget(TestCaseQt):
diff --git a/silx/gui/widgets/test/test_flowlayout.py b/silx/gui/widgets/test/test_flowlayout.py
new file mode 100644
index 0000000..1497945
--- /dev/null
+++ b/silx/gui/widgets/test/test_flowlayout.py
@@ -0,0 +1,77 @@
+# 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.
+#
+# ###########################################################################*/
+"""Tests for FlowLayout"""
+
+__authors__ = ["T. Vincent"]
+__license__ = "MIT"
+__date__ = "02/08/2018"
+
+import unittest
+
+from silx.gui.widgets.FlowLayout import FlowLayout
+from silx.gui import qt
+from silx.gui.utils.testutils import TestCaseQt
+
+
+class TestFlowLayout(TestCaseQt):
+ """Tests for FlowLayout"""
+
+ def setUp(self):
+ """Create and show a widget"""
+ self.widget = qt.QWidget()
+ self.widget.show()
+ self.qWaitForWindowExposed(self.widget)
+
+ def tearDown(self):
+ """Delete widget"""
+ self.widget.setAttribute(qt.Qt.WA_DeleteOnClose)
+ self.widget.close()
+ del self.widget
+ self.qapp.processEvents()
+
+ def test(self):
+ """Basic tests"""
+ layout = FlowLayout()
+ self.widget.setLayout(layout)
+
+ layout.addWidget(qt.QLabel('first'))
+ layout.addWidget(qt.QLabel('second'))
+ self.assertEqual(layout.count(), 2)
+
+ layout.setHorizontalSpacing(10)
+ self.assertEqual(layout.horizontalSpacing(), 10)
+ layout.setVerticalSpacing(5)
+ self.assertEqual(layout.verticalSpacing(), 5)
+
+
+def suite():
+ loader = unittest.defaultTestLoader.loadTestsFromTestCase
+ test_suite = unittest.TestSuite()
+ test_suite.addTest(loader(TestFlowLayout))
+ return test_suite
+
+
+if __name__ == '__main__':
+ unittest.main(defaultTest='suite')
diff --git a/silx/gui/widgets/test/test_framebrowser.py b/silx/gui/widgets/test/test_framebrowser.py
index 9988d16..2dfd302 100644
--- a/silx/gui/widgets/test/test_framebrowser.py
+++ b/silx/gui/widgets/test/test_framebrowser.py
@@ -29,7 +29,7 @@ __date__ = "23/03/2018"
import unittest
-from silx.gui.test.utils import TestCaseQt
+from silx.gui.utils.testutils import TestCaseQt
from silx.gui.widgets.FrameBrowser import FrameBrowser
diff --git a/silx/gui/widgets/test/test_hierarchicaltableview.py b/silx/gui/widgets/test/test_hierarchicaltableview.py
index b3d37ed..9fad54d 100644
--- a/silx/gui/widgets/test/test_hierarchicaltableview.py
+++ b/silx/gui/widgets/test/test_hierarchicaltableview.py
@@ -29,7 +29,7 @@ __date__ = "07/04/2017"
import unittest
from .. import HierarchicalTableView
-from ...test.utils import TestCaseQt
+from silx.gui.utils.testutils import TestCaseQt
from silx.gui import qt
diff --git a/silx/gui/widgets/test/test_periodictable.py b/silx/gui/widgets/test/test_periodictable.py
index c6bed81..3e7eb16 100644
--- a/silx/gui/widgets/test/test_periodictable.py
+++ b/silx/gui/widgets/test/test_periodictable.py
@@ -29,7 +29,7 @@ __date__ = "05/12/2016"
import unittest
from .. import PeriodicTable
-from ...test.utils import TestCaseQt
+from silx.gui.utils.testutils import TestCaseQt
from silx.gui import qt
diff --git a/silx/gui/widgets/test/test_printpreview.py b/silx/gui/widgets/test/test_printpreview.py
index ecb165a..3c29171 100644
--- a/silx/gui/widgets/test/test_printpreview.py
+++ b/silx/gui/widgets/test/test_printpreview.py
@@ -30,7 +30,7 @@ __date__ = "19/07/2017"
import unittest
-from silx.gui.test.utils import TestCaseQt
+from silx.gui.utils.testutils import TestCaseQt
from silx.gui.widgets.PrintPreview import PrintPreviewDialog
from silx.gui import qt
diff --git a/silx/gui/widgets/test/test_rangeslider.py b/silx/gui/widgets/test/test_rangeslider.py
new file mode 100644
index 0000000..2829050
--- /dev/null
+++ b/silx/gui/widgets/test/test_rangeslider.py
@@ -0,0 +1,114 @@
+# 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.
+#
+# ###########################################################################*/
+"""Tests for RangeSlider"""
+
+__authors__ = ["T. Vincent"]
+__license__ = "MIT"
+__date__ = "01/08/2018"
+
+import unittest
+
+from silx.gui import qt, colors
+from silx.gui.widgets.RangeSlider import RangeSlider
+from silx.gui.utils.testutils import TestCaseQt
+from silx.utils.testutils import ParametricTestCase
+
+
+class TestRangeSlider(TestCaseQt, ParametricTestCase):
+ """Tests for TestRangeSlider"""
+
+ def setUp(self):
+ self.slider = RangeSlider()
+ self.slider.show()
+ self.qWaitForWindowExposed(self.slider)
+
+ def tearDown(self):
+ self.slider.setAttribute(qt.Qt.WA_DeleteOnClose)
+ self.slider.close()
+ del self.slider
+ self.qapp.processEvents()
+
+ def testRangeValue(self):
+ """Test slider range and values"""
+
+ # Play with range
+ self.slider.setRange(1, 2)
+ self.assertEqual(self.slider.getRange(), (1., 2.))
+ self.assertEqual(self.slider.getValues(), (1., 1.))
+
+ self.slider.setMinimum(-1)
+ self.assertEqual(self.slider.getRange(), (-1., 2.))
+ self.assertEqual(self.slider.getValues(), (1., 1.))
+
+ self.slider.setMaximum(0)
+ self.assertEqual(self.slider.getRange(), (-1., 0.))
+ self.assertEqual(self.slider.getValues(), (0., 0.))
+
+ # Play with values
+ self.slider.setFirstValue(-2.)
+ self.assertEqual(self.slider.getValues(), (-1., 0.))
+
+ self.slider.setFirstValue(-0.5)
+ self.assertEqual(self.slider.getValues(), (-0.5, 0.))
+
+ self.slider.setSecondValue(2.)
+ self.assertEqual(self.slider.getValues(), (-0.5, 0.))
+
+ self.slider.setSecondValue(-0.1)
+ self.assertEqual(self.slider.getValues(), (-0.5, -0.1))
+
+ def testStepCount(self):
+ """Test related to step count"""
+ self.slider.setPositionCount(11)
+ self.assertEqual(self.slider.getPositionCount(), 11)
+ self.slider.setFirstValue(0.32)
+ self.assertEqual(self.slider.getFirstValue(), 0.3)
+ self.assertEqual(self.slider.getFirstPosition(), 3)
+
+ self.slider.setPositionCount(3) # Value is adjusted
+ self.assertEqual(self.slider.getValues(), (0.5, 1.))
+ self.assertEqual(self.slider.getPositions(), (1, 2))
+
+ def testGroove(self):
+ """Test Groove pixmap"""
+ profile = list(range(100))
+
+ for cmap in ('jet', colors.Colormap('viridis')):
+ with self.subTest(str(cmap)):
+ self.slider.setGroovePixmapFromProfile(profile, cmap)
+ pixmap = self.slider.getGroovePixmap()
+ self.assertIsInstance(pixmap, qt.QPixmap)
+ self.assertEqual(pixmap.width(), len(profile))
+
+
+def suite():
+ loader = unittest.defaultTestLoader.loadTestsFromTestCase
+ test_suite = unittest.TestSuite()
+ test_suite.addTest(loader(TestRangeSlider))
+ return test_suite
+
+
+if __name__ == '__main__':
+ unittest.main(defaultTest='suite')
diff --git a/silx/gui/widgets/test/test_tablewidget.py b/silx/gui/widgets/test/test_tablewidget.py
index 5ad0a06..6822aef 100644
--- a/silx/gui/widgets/test/test_tablewidget.py
+++ b/silx/gui/widgets/test/test_tablewidget.py
@@ -30,7 +30,7 @@ __date__ = "05/12/2016"
import unittest
-from silx.gui.test.utils import TestCaseQt
+from silx.gui.utils.testutils import TestCaseQt
from silx.gui.widgets.TableWidget import TableWidget
diff --git a/silx/gui/widgets/test/test_threadpoolpushbutton.py b/silx/gui/widgets/test/test_threadpoolpushbutton.py
index a8618a4..e92eb02 100644
--- a/silx/gui/widgets/test/test_threadpoolpushbutton.py
+++ b/silx/gui/widgets/test/test_threadpoolpushbutton.py
@@ -32,8 +32,8 @@ __date__ = "17/01/2018"
import unittest
import time
from silx.gui import qt
-from silx.gui.test.utils import TestCaseQt
-from silx.gui.test.utils import SignalListener
+from silx.gui.utils.testutils import TestCaseQt
+from silx.gui.utils.testutils import SignalListener
from silx.gui.widgets.ThreadPoolPushButton import ThreadPoolPushButton
from silx.utils.testutils import TestLogging