diff options
author | Picca Frédéric-Emmanuel <picca@synchrotron-soleil.fr> | 2019-12-23 13:45:09 +0100 |
---|---|---|
committer | Picca Frédéric-Emmanuel <picca@synchrotron-soleil.fr> | 2019-12-23 13:45:09 +0100 |
commit | 5d647cf9a6159afd2933da594b9c79ad93d3cd9b (patch) | |
tree | 2571025a602f68fc8933b01104dc712d41f84034 /silx/gui/widgets | |
parent | 654a6ac93513c3cc1ef97cacd782ff674c6f4559 (diff) |
New upstream version 0.12.0~b0+dfsg
Diffstat (limited to 'silx/gui/widgets')
-rw-r--r-- | silx/gui/widgets/ColormapNameComboBox.py | 166 | ||||
-rw-r--r-- | silx/gui/widgets/FrameBrowser.py | 2 | ||||
-rwxr-xr-x | silx/gui/widgets/LegendIconWidget.py | 513 | ||||
-rw-r--r-- | silx/gui/widgets/RangeSlider.py | 4 |
4 files changed, 682 insertions, 3 deletions
diff --git a/silx/gui/widgets/ColormapNameComboBox.py b/silx/gui/widgets/ColormapNameComboBox.py new file mode 100644 index 0000000..fa8faf1 --- /dev/null +++ b/silx/gui/widgets/ColormapNameComboBox.py @@ -0,0 +1,166 @@ +# coding: utf-8 +# /*########################################################################## +# +# 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 +# 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. +# +# ###########################################################################*/ +"""A QComboBox to display prefered colormaps +""" + +from __future__ import division + +__authors__ = ["V.A. Sole", "T. Vincent", "H. Payno"] +__license__ = "MIT" +__date__ = "27/11/2018" + + +import logging +import numpy + +from .. import qt +from .. import colors as colors_mdl + +_logger = logging.getLogger(__name__) + + +_colormapIconPreview = {} + + +class ColormapNameComboBox(qt.QComboBox): + def __init__(self, parent=None): + qt.QComboBox.__init__(self, parent) + self.__initItems() + + LUT_NAME = qt.Qt.UserRole + 1 + LUT_COLORS = qt.Qt.UserRole + 2 + + def __initItems(self): + for colormapName in colors_mdl.preferredColormaps(): + index = self.count() + self.addItem(str.title(colormapName)) + self.setItemIcon(index, self.getIconPreview(name=colormapName)) + self.setItemData(index, colormapName, role=self.LUT_NAME) + + def getIconPreview(self, name=None, colors=None): + """Return an icon preview from a LUT name. + + This icons are cached into a global structure. + + :param str name: Name of the LUT + :param numpy.ndarray colors: Colors identify the LUT + :rtype: qt.QIcon + """ + if name is not None: + iconKey = name + else: + iconKey = tuple(colors) + icon = _colormapIconPreview.get(iconKey, None) + if icon is None: + icon = self.createIconPreview(name, colors) + _colormapIconPreview[iconKey] = icon + return icon + + def createIconPreview(self, name=None, colors=None): + """Create and return an icon preview from a LUT name. + + This icons are cached into a global structure. + + :param str name: Name of the LUT + :param numpy.ndarray colors: Colors identify the LUT + :rtype: qt.QIcon + """ + colormap = colors_mdl.Colormap(name) + size = 32 + if name is not None: + lut = colormap.getNColors(size) + else: + lut = colors + if len(lut) > size: + # Down sample + step = int(len(lut) / size) + lut = lut[::step] + elif len(lut) < size: + # Over sample + indexes = numpy.arange(size) / float(size) * (len(lut) - 1) + indexes = indexes.astype("int") + lut = lut[indexes] + if lut is None or len(lut) == 0: + return qt.QIcon() + + pixmap = qt.QPixmap(size, size) + painter = qt.QPainter(pixmap) + for i in range(size): + rgb = lut[i] + r, g, b = rgb[0], rgb[1], rgb[2] + painter.setPen(qt.QColor(r, g, b)) + painter.drawPoint(qt.QPoint(i, 0)) + + painter.drawPixmap(0, 1, size, size - 1, pixmap, 0, 0, size, 1) + painter.end() + + return qt.QIcon(pixmap) + + def getCurrentName(self): + return self.itemData(self.currentIndex(), self.LUT_NAME) + + def getCurrentColors(self): + return self.itemData(self.currentIndex(), self.LUT_COLORS) + + def findLutName(self, name): + return self.findData(name, role=self.LUT_NAME) + + def findLutColors(self, lut): + for index in range(self.count()): + if self.itemData(index, role=self.LUT_NAME) is not None: + continue + colors = self.itemData(index, role=self.LUT_COLORS) + if colors is None: + continue + if numpy.array_equal(colors, lut): + return index + return -1 + + def setCurrentLut(self, colormap): + name = colormap.getName() + if name is not None: + self._setCurrentName(name) + else: + lut = colormap.getColormapLUT() + self._setCurrentLut(lut) + + def _setCurrentLut(self, lut): + index = self.findLutColors(lut) + if index == -1: + index = self.count() + self.addItem("Custom") + self.setItemIcon(index, self.getIconPreview(colors=lut)) + self.setItemData(index, None, role=self.LUT_NAME) + self.setItemData(index, lut, role=self.LUT_COLORS) + self.setCurrentIndex(index) + + def _setCurrentName(self, name): + index = self.findLutName(name) + if index < 0: + index = self.count() + self.addItem(str.title(name)) + self.setItemIcon(index, self.getIconPreview(name=name)) + self.setItemData(index, name, role=self.LUT_NAME) + self.setCurrentIndex(index) diff --git a/silx/gui/widgets/FrameBrowser.py b/silx/gui/widgets/FrameBrowser.py index b4f88fc..671991f 100644 --- a/silx/gui/widgets/FrameBrowser.py +++ b/silx/gui/widgets/FrameBrowser.py @@ -95,7 +95,7 @@ class FrameBrowser(qt.QWidget): else: first, last = 0, n - self._lineEdit.setFixedWidth(self._lineEdit.fontMetrics().width('%05d' % last)) + self._lineEdit.setFixedWidth(self._lineEdit.fontMetrics().boundingRect('%05d' % last).width()) validator = qt.QIntValidator(first, last, self._lineEdit) self._lineEdit.setValidator(validator) self._lineEdit.setText("%d" % first) diff --git a/silx/gui/widgets/LegendIconWidget.py b/silx/gui/widgets/LegendIconWidget.py new file mode 100755 index 0000000..1a403cb --- /dev/null +++ b/silx/gui/widgets/LegendIconWidget.py @@ -0,0 +1,513 @@ +# coding: utf-8 +# /*########################################################################## +# +# 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 +# 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. +# +# ###########################################################################*/ +"""Widget displaying a symbol (marker symbol, line style and color) to identify +an item displayed by a plot. +""" + +__authors__ = ["V.A. Sole", "T. Rueter", "T. Vincent"] +__license__ = "MIT" +__data__ = "11/11/2019" + + +import logging + +import numpy + +from .. import qt, colors + + +_logger = logging.getLogger(__name__) + + +# Build all symbols +# Courtesy of the pyqtgraph project + +_Symbols = None +""""Cache supported symbols as Qt paths""" + + +_NoSymbols = (None, 'None', 'none', '', ' ') +"""List of values resulting in no symbol being displayed for a curve""" + + +_LineStyles = { + None: qt.Qt.NoPen, + 'None': qt.Qt.NoPen, + 'none': qt.Qt.NoPen, + '': qt.Qt.NoPen, + ' ': qt.Qt.NoPen, + '-': qt.Qt.SolidLine, + '--': qt.Qt.DashLine, + ':': qt.Qt.DotLine, + '-.': qt.Qt.DashDotLine +} +"""Conversion from matplotlib-like linestyle to Qt""" + +_NoLineStyle = (None, 'None', 'none', '', ' ') +"""List of style values resulting in no line being displayed for a curve""" + + +_colormapImage = {} +"""Store cached pixmap""" +# FIXME: Could be better to use a LRU dictionary + +_COLORMAP_PIXMAP_SIZE = 32 +"""Size of the cached pixmaps for the colormaps""" + + +def _initSymbols(): + """Init the cached symbol structure if not yet done.""" + global _Symbols + if _Symbols is not None: + return + + symbols = dict([(name, qt.QPainterPath()) + for name in ['o', 's', 't', 'd', '+', 'x', '.', ',']]) + symbols['o'].addEllipse(qt.QRectF(.1, .1, .8, .8)) + symbols['.'].addEllipse(qt.QRectF(.3, .3, .4, .4)) + symbols[','].addEllipse(qt.QRectF(.4, .4, .2, .2)) + symbols['s'].addRect(qt.QRectF(.1, .1, .8, .8)) + + coords = { + 't': [(0.5, 0.), (.1, .8), (.9, .8)], + 'd': [(0.1, 0.5), (0.5, 0.), (0.9, 0.5), (0.5, 1.)], + '+': [(0.0, 0.40), (0.40, 0.40), (0.40, 0.), (0.60, 0.), + (0.60, 0.40), (1., 0.40), (1., 0.60), (0.60, 0.60), + (0.60, 1.), (0.40, 1.), (0.40, 0.60), (0., 0.60)], + 'x': [(0.0, 0.40), (0.40, 0.40), (0.40, 0.), (0.60, 0.), + (0.60, 0.40), (1., 0.40), (1., 0.60), (0.60, 0.60), + (0.60, 1.), (0.40, 1.), (0.40, 0.60), (0., 0.60)] + } + for s, c in coords.items(): + symbols[s].moveTo(*c[0]) + for x, y in c[1:]: + symbols[s].lineTo(x, y) + symbols[s].closeSubpath() + tr = qt.QTransform() + tr.rotate(45) + symbols['x'].translate(qt.QPointF(-0.5, -0.5)) + symbols['x'] = tr.map(symbols['x']) + symbols['x'].translate(qt.QPointF(0.5, 0.5)) + + _Symbols = symbols + + +class LegendIconWidget(qt.QWidget): + """Object displaying linestyle and symbol of plots. + + :param QWidget parent: See :class:`QWidget` + """ + + def __init__(self, parent=None): + super(LegendIconWidget, self).__init__(parent) + _initSymbols() + + # Visibilities + self.showLine = True + self.showSymbol = True + self.showColormap = True + + # Line attributes + self.lineStyle = qt.Qt.NoPen + self.lineWidth = 1. + self.lineColor = qt.Qt.green + + self.symbol = '' + # Symbol attributes + self.symbolStyle = qt.Qt.SolidPattern + self.symbolColor = qt.Qt.green + self.symbolOutlineBrush = qt.QBrush(qt.Qt.white) + self.symbolColormap = None + """Name or array of colors""" + + self.colormap = None + """Name or array of colors""" + + # Control widget size: sizeHint "is the only acceptable + # alternative, so the widget can never grow or shrink" + # (c.f. Qt Doc, enum QSizePolicy::Policy) + self.setSizePolicy(qt.QSizePolicy.Fixed, + qt.QSizePolicy.Fixed) + + def sizeHint(self): + return qt.QSize(50, 15) + + def setSymbol(self, symbol): + """Set the symbol""" + symbol = str(symbol) + if symbol not in _NoSymbols: + if symbol not in _Symbols: + raise ValueError("Unknown symbol: <%s>" % symbol) + self.symbol = symbol + self.update() + + def setSymbolColor(self, color): + """ + :param color: determines the symbol color + :type style: qt.QColor + """ + self.symbolColor = qt.QColor(color) + self.update() + + # Modify Line + + def setLineColor(self, color): + self.lineColor = qt.QColor(color) + self.update() + + def setLineWidth(self, width): + self.lineWidth = float(width) + self.update() + + def setLineStyle(self, style): + """Set the linestyle. + + Possible line styles: + + - '', ' ', 'None': No line + - '-': solid + - '--': dashed + - ':': dotted + - '-.': dash and dot + + :param str style: The linestyle to use + """ + if style not in _LineStyles: + raise ValueError('Unknown style: %s', style) + self.lineStyle = _LineStyles[style] + self.update() + + def _toLut(self, colormap): + """Returns an internal LUT object used by this widget to manage + a colormap LUT. + + If the argument is a `Colormap` object, only the current state will be + displayed. The object itself will not be stored, and further changes + of this `Colormap` will not update this widget. + + :param Union[str,numpy.ndarray,Colormap] colormap: The colormap to + display + :rtype: Union[None,str,numpy.ndarray] + """ + if isinstance(colormap, colors.Colormap): + # Helper to allow to support Colormap objects + c = colormap.getName() + if c is None: + c = colormap.getNColors() + colormap = c + + return colormap + + def setColormap(self, colormap): + """Set the colormap to display + + If the argument is a `Colormap` object, only the current state will be + displayed. The object itself will not be stored, and further changes + of this `Colormap` will not update this widget. + + :param Union[str,numpy.ndarray,Colormap] colormap: The colormap to + display + """ + colormap = self._toLut(colormap) + + if colormap is None: + if self.colormap is None: + return + self.colormap = None + self.update() + return + + if numpy.array_equal(self.colormap, colormap): + # This also works with strings + return + + self.colormap = colormap + self.update() + + def getColormap(self): + """Returns the used colormap. + + If the argument was set with a `Colormap` object, this function will + returns the LUT, represented by a string name or by an array or colors. + + :returns: Union[None,str,numpy.ndarray,Colormap] + """ + return self.colormap + + def setSymbolColormap(self, colormap): + """Set the colormap to display a symbol + + If the argument is a `Colormap` object, only the current state will be + displayed. The object itself will not be stored, and further changes + of this `Colormap` will not update this widget. + + :param Union[str,numpy.ndarray,Colormap] colormap: The colormap to + display + """ + colormap = self._toLut(colormap) + + if colormap is None: + if self.colormap is None: + return + self.symbolColormap = None + self.update() + return + + if numpy.array_equal(self.symbolColormap, colormap): + # This also works with strings + return + + self.symbolColormap = colormap + self.update() + + def getSymbolColormap(self): + """Returns the used symbol colormap. + + If the argument was set with a `Colormap` object, this function will + returns the LUT, represented by a string name or by an array or colors. + + :returns: Union[None,str,numpy.ndarray,Colormap] + """ + return self.colormap + + # Paint + + def paintEvent(self, event): + """ + :param event: event + :type event: QPaintEvent + """ + painter = qt.QPainter(self) + self.paint(painter, event.rect(), self.palette()) + + def paint(self, painter, rect, palette): + painter.save() + painter.setRenderHint(qt.QPainter.Antialiasing) + # Scale painter to the icon height + # current -> width = 2.5, height = 1.0 + scale = float(self.height()) + ratio = float(self.width()) / scale + symbolOffset = qt.QPointF(.5 * (ratio - 1.), 0.) + # Determine and scale offset + offset = qt.QPointF(float(rect.left()) / scale, float(rect.top()) / scale) + + # Override color when disabled + if self.isEnabled(): + overrideColor = None + else: + overrideColor = palette.color(qt.QPalette.Disabled, + qt.QPalette.WindowText) + + # Draw BG rectangle (for debugging) + # bottomRight = qt.QPointF( + # float(rect.right())/scale, + # float(rect.bottom())/scale) + # painter.fillRect(qt.QRectF(offset, bottomRight), + # qt.QBrush(qt.Qt.green)) + + if self.showColormap: + if self.colormap is not None: + if self.isEnabled(): + image = self.getColormapImage(self.colormap) + else: + image = self.getGrayedColormapImage(self.colormap) + pixmapRect = qt.QRect(0, 0, _COLORMAP_PIXMAP_SIZE, 1) + widthMargin = 0 + halfHeight = 4 + dest = qt.QRect( + rect.left() + widthMargin, + rect.center().y() - halfHeight + 1, + rect.width() - widthMargin * 2, + halfHeight * 2, + ) + painter.drawImage(dest, image, pixmapRect) + + painter.scale(scale, scale) + + llist = [] + if self.showLine: + linePath = qt.QPainterPath() + linePath.moveTo(0., 0.5) + linePath.lineTo(ratio, 0.5) + # linePath.lineTo(2.5, 0.5) + lineBrush = qt.QBrush( + self.lineColor if overrideColor is None else overrideColor) + linePen = qt.QPen( + lineBrush, + (self.lineWidth / self.height()), + self.lineStyle, + qt.Qt.FlatCap + ) + llist.append((linePath, linePen, lineBrush)) + + isValidSymbol = (len(self.symbol) and + self.symbol not in _NoSymbols) + if self.showSymbol and isValidSymbol: + if self.symbolColormap is None: + # PITFALL ahead: Let this be a warning to others + # symbolPath = Symbols[self.symbol] + # Copy before translate! Dict is a mutable type + symbolPath = qt.QPainterPath(_Symbols[self.symbol]) + symbolPath.translate(symbolOffset) + symbolBrush = qt.QBrush( + self.symbolColor if overrideColor is None else overrideColor, + self.symbolStyle) + symbolPen = qt.QPen( + self.symbolOutlineBrush, # Brush + 1. / self.height(), # Width + qt.Qt.SolidLine # Style + ) + llist.append((symbolPath, + symbolPen, + symbolBrush)) + else: + nbSymbols = int(ratio + 2) + for i in range(nbSymbols): + if self.isEnabled(): + image = self.getColormapImage(self.symbolColormap) + else: + image = self.getGrayedColormapImage(self.symbolColormap) + pos = int((_COLORMAP_PIXMAP_SIZE / nbSymbols) * i) + pos = numpy.clip(pos, 0, _COLORMAP_PIXMAP_SIZE-1) + color = image.pixelColor(pos, 0) + delta = qt.QPointF(ratio * ((i - (nbSymbols-1)/2) / nbSymbols), 0) + + symbolPath = qt.QPainterPath(_Symbols[self.symbol]) + symbolPath.translate(symbolOffset + delta) + symbolBrush = qt.QBrush(color, self.symbolStyle) + symbolPen = qt.QPen( + self.symbolOutlineBrush, # Brush + 1. / self.height(), # Width + qt.Qt.SolidLine # Style + ) + llist.append((symbolPath, + symbolPen, + symbolBrush)) + + # Draw + for path, pen, brush in llist: + path.translate(offset) + painter.setPen(pen) + painter.setBrush(brush) + painter.drawPath(path) + + painter.restore() + + # Helpers + + @staticmethod + def isEmptySymbol(symbol): + """Returns True if this symbol description will result in an empty + symbol.""" + return symbol in _NoSymbols + + @staticmethod + def isEmptyLineStyle(lineStyle): + """Returns True if this line style description will result in an empty + line.""" + return lineStyle in _NoLineStyle + + @staticmethod + def _getColormapKey(colormap): + """ + Returns the key used to store the image in the data storage + """ + if isinstance(colormap, numpy.ndarray): + key = tuple(colormap) + else: + key = colormap + return key + + @staticmethod + def getGrayedColormapImage(colormap): + """Return a grayed version image preview from a LUT name. + + This images are cached into a global structure. + + :param Union[str,numpy.ndarray] colormap: Description of the LUT + :rtype: qt.QImage + """ + key = LegendIconWidget._getColormapKey(colormap) + grayKey = (key, "gray") + image = _colormapImage.get(grayKey, None) + if image is None: + image = LegendIconWidget.getColormapImage(colormap) + image = image.convertToFormat(qt.QImage.Format_Grayscale8) + _colormapImage[grayKey] = image + return image + + @staticmethod + def getColormapImage(colormap): + """Return an image preview from a LUT name. + + This images are cached into a global structure. + + :param Union[str,numpy.ndarray] colormap: Description of the LUT + :rtype: qt.QImage + """ + key = LegendIconWidget._getColormapKey(colormap) + image = _colormapImage.get(key, None) + if image is None: + image = LegendIconWidget.createColormapImage(colormap) + _colormapImage[key] = image + return image + + @staticmethod + def createColormapImage(colormap): + """Create and return an icon preview from a LUT name. + + This icons are cached into a global structure. + + :param Union[str,numpy.ndarray] colormap: Description of the LUT + :rtype: qt.QImage + """ + size = _COLORMAP_PIXMAP_SIZE + if isinstance(colormap, numpy.ndarray): + lut = colormap + if len(lut) > size: + # Down sample + step = int(len(lut) / size) + lut = lut[::step] + elif len(lut) < size: + # Over sample + indexes = numpy.arange(size) / float(size) * (len(lut) - 1) + indexes = indexes.astype("int") + lut = lut[indexes] + else: + colormap = colors.Colormap(colormap) + lut = colormap.getNColors(size) + + if lut is None or len(lut) == 0: + return qt.QIcon() + + pixmap = qt.QPixmap(size, 1) + painter = qt.QPainter(pixmap) + for i in range(size): + rgb = lut[i] + r, g, b = rgb[0], rgb[1], rgb[2] + painter.setPen(qt.QColor(r, g, b)) + painter.drawPoint(qt.QPoint(i, 0)) + painter.end() + return pixmap.toImage() diff --git a/silx/gui/widgets/RangeSlider.py b/silx/gui/widgets/RangeSlider.py index 0cf195c..c352147 100644 --- a/silx/gui/widgets/RangeSlider.py +++ b/silx/gui/widgets/RangeSlider.py @@ -1,7 +1,7 @@ # coding: utf-8 # /*########################################################################## # -# Copyright (c) 2015-2018 European Synchrotron Radiation Facility +# Copyright (c) 2015-2019 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 @@ -466,7 +466,7 @@ class RangeSlider(qt.QWidget): :param Union[numpy.ndarray,None] profile: 1D array of values to display - :param Union[Colormap,str] colormap: + :param Union[~silx.gui.colors.Colormap,str] colormap: The colormap name or object to convert profile values to colors """ if profile is None: |