From cebdc9244c019224846cb8d2668080fe386a6adc Mon Sep 17 00:00:00 2001 From: Alexandre Marie Date: Mon, 17 Dec 2018 12:28:24 +0100 Subject: New upstream version 0.9.0+dfsg --- silx/gui/widgets/FloatEdit.py | 4 +- silx/gui/widgets/FlowLayout.py | 177 ++++++ silx/gui/widgets/PrintPreview.py | 4 + silx/gui/widgets/RangeSlider.py | 627 +++++++++++++++++++++ silx/gui/widgets/__init__.py | 8 +- silx/gui/widgets/test/__init__.py | 4 + silx/gui/widgets/test/test_boxlayoutdockwidget.py | 2 +- silx/gui/widgets/test/test_flowlayout.py | 77 +++ silx/gui/widgets/test/test_framebrowser.py | 2 +- .../gui/widgets/test/test_hierarchicaltableview.py | 2 +- silx/gui/widgets/test/test_periodictable.py | 2 +- silx/gui/widgets/test/test_printpreview.py | 2 +- silx/gui/widgets/test/test_rangeslider.py | 114 ++++ silx/gui/widgets/test/test_tablewidget.py | 2 +- silx/gui/widgets/test/test_threadpoolpushbutton.py | 4 +- 15 files changed, 1017 insertions(+), 14 deletions(-) create mode 100644 silx/gui/widgets/FlowLayout.py create mode 100644 silx/gui/widgets/RangeSlider.py create mode 100644 silx/gui/widgets/test/test_flowlayout.py create mode 100644 silx/gui/widgets/test/test_rangeslider.py (limited to 'silx/gui/widgets') 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 + `_ + + :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 -- cgit v1.2.3